diff --git a/CHANGELOG b/CHANGELOG index 92f14387..c8572ba6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,11 @@ jc changelog -20221229 v1.23.4 +20221229 v1.22.4 - Add `iwconfig` command parser +- Add NeXTSTEP format support to the PLIST file parser - Fix `proc` parser magic signature detection for `/proc/pid/stat` hacks - Fix `x509-cert` parser for string serial numbers -- Add category tags to parser metadata +- Add category tags to parser metadata: generic, standard, file, string, binary, command - Add "list parsers by category" view to help - Fix python 3.6-related issues - Add python 3.6 to automated tests diff --git a/docs/parsers/plist.md b/docs/parsers/plist.md index 14b34c78..d8515e27 100644 --- a/docs/parsers/plist.md +++ b/docs/parsers/plist.md @@ -5,7 +5,7 @@ jc - JSON Convert PLIST file parser -Converts binary and XML PLIST files. +Converts binary, XML, and NeXTSTEP PLIST files. Binary values are converted into an ASCII hex representation. @@ -74,4 +74,4 @@ Returns: ### Parser Information Compatibility: linux, darwin, cygwin, win32, aix, freebsd -Version 1.0 by Kelly Brazil (kellyjonbrazil@gmail.com) +Version 1.1 by Kelly Brazil (kellyjonbrazil@gmail.com) diff --git a/jc/parsers/pbPlist/StrParse.py b/jc/parsers/pbPlist/StrParse.py new file mode 100644 index 00000000..5c981bfb --- /dev/null +++ b/jc/parsers/pbPlist/StrParse.py @@ -0,0 +1,366 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pbPlist +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys +import string + +if sys.version_info >= (3, 0): + def unichr(character): # pylint: disable=redefined-builtin + return chr(character) + +def ConvertNEXTSTEPToUnicode(hex_digits): + # taken from http://ftp.unicode.org/Public/MAPPINGS/VENDORS/NEXT/NEXTSTEP.TXT + conversion = { + "80": "a0", # NO-BREAK SPACE + "81": "c0", # LATIN CAPITAL LETTER A WITH GRAVE + "82": "c1", # LATIN CAPITAL LETTER A WITH ACUTE + "83": "c2", # LATIN CAPITAL LETTER A WITH CIRCUMFLEX + "84": "c3", # LATIN CAPITAL LETTER A WITH TILDE + "85": "c4", # LATIN CAPITAL LETTER A WITH DIAERESIS + "86": "c5", # LATIN CAPITAL LETTER A WITH RING + "87": "c7", # LATIN CAPITAL LETTER C WITH CEDILLA + "88": "c8", # LATIN CAPITAL LETTER E WITH GRAVE + "89": "c9", # LATIN CAPITAL LETTER E WITH ACUTE + "8a": "ca", # LATIN CAPITAL LETTER E WITH CIRCUMFLEX + "8b": "cb", # LATIN CAPITAL LETTER E WITH DIAERESIS + "8c": "cc", # LATIN CAPITAL LETTER I WITH GRAVE + "8d": "cd", # LATIN CAPITAL LETTER I WITH ACUTE + "8e": "ce", # LATIN CAPITAL LETTER I WITH CIRCUMFLEX + "8f": "cf", # LATIN CAPITAL LETTER I WITH DIAERESIS + "90": "d0", # LATIN CAPITAL LETTER ETH + "91": "d1", # LATIN CAPITAL LETTER N WITH TILDE + "92": "d2", # LATIN CAPITAL LETTER O WITH GRAVE + "93": "d3", # LATIN CAPITAL LETTER O WITH ACUTE + "94": "d4", # LATIN CAPITAL LETTER O WITH CIRCUMFLEX + "95": "d5", # LATIN CAPITAL LETTER O WITH TILDE + "96": "d6", # LATIN CAPITAL LETTER O WITH DIAERESIS + "97": "d9", # LATIN CAPITAL LETTER U WITH GRAVE + "98": "da", # LATIN CAPITAL LETTER U WITH ACUTE + "99": "db", # LATIN CAPITAL LETTER U WITH CIRCUMFLEX + "9a": "dc", # LATIN CAPITAL LETTER U WITH DIAERESIS + "9b": "dd", # LATIN CAPITAL LETTER Y WITH ACUTE + "9c": "de", # LATIN CAPITAL LETTER THORN + "9d": "b5", # MICRO SIGN + "9e": "d7", # MULTIPLICATION SIGN + "9f": "f7", # DIVISION SIGN + "a0": "a9", # COPYRIGHT SIGN + "a1": "a1", # INVERTED EXCLAMATION MARK + "a2": "a2", # CENT SIGN + "a3": "a3", # POUND SIGN + "a4": "44", # FRACTION SLASH + "a5": "a5", # YEN SIGN + "a6": "92", # LATIN SMALL LETTER F WITH HOOK + "a7": "a7", # SECTION SIGN + "a8": "a4", # CURRENCY SIGN + "a9": "19", # RIGHT SINGLE QUOTATION MARK + "aa": "1c", # LEFT DOUBLE QUOTATION MARK + "ab": "ab", # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + "ac": "39", # SINGLE LEFT-POINTING ANGLE QUOTATION MARK + "ad": "3a", # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + "ae": "01", # LATIN SMALL LIGATURE FI + "af": "02", # LATIN SMALL LIGATURE FL + "b0": "ae", # REGISTERED SIGN + "b1": "13", # EN DASH + "b2": "20", # DAGGER + "b3": "21", # DOUBLE DAGGER + "b4": "b7", # MIDDLE DOT + "b5": "a6", # BROKEN BAR + "b6": "b6", # PILCROW SIGN + "b7": "22", # BULLET + "b8": "1a", # SINGLE LOW-9 QUOTATION MARK + "b9": "1e", # DOUBLE LOW-9 QUOTATION MARK + "ba": "1d", # RIGHT DOUBLE QUOTATION MARK + "bb": "bb", # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + "bc": "26", # HORIZONTAL ELLIPSIS + "bd": "30", # PER MILLE SIGN + "be": "ac", # NOT SIGN + "bf": "bf", # INVERTED QUESTION MARK + "c0": "b9", # SUPERSCRIPT ONE + "c1": "cb", # MODIFIER LETTER GRAVE ACCENT + "c2": "b4", # ACUTE ACCENT + "c3": "c6", # MODIFIER LETTER CIRCUMFLEX ACCENT + "c4": "dc", # SMALL TILDE + "c5": "af", # MACRON + "c6": "d8", # BREVE + "c7": "d9", # DOT ABOVE + "c8": "a8", # DIAERESIS + "c9": "b2", # SUPERSCRIPT TWO + "ca": "da", # RING ABOVE + "cb": "b8", # CEDILLA + "cc": "b3", # SUPERSCRIPT THREE + "cd": "dd", # DOUBLE ACUTE ACCENT + "ce": "db", # OGONEK + "cf": "c7", # CARON + "d0": "14", # EM DASH + "d1": "b1", # PLUS-MINUS SIGN + "d2": "bc", # VULGAR FRACTION ONE QUARTER + "d3": "bd", # VULGAR FRACTION ONE HALF + "d4": "be", # VULGAR FRACTION THREE QUARTERS + "d5": "e0", # LATIN SMALL LETTER A WITH GRAVE + "d6": "e1", # LATIN SMALL LETTER A WITH ACUTE + "d7": "e2", # LATIN SMALL LETTER A WITH CIRCUMFLEX + "d8": "e3", # LATIN SMALL LETTER A WITH TILDE + "d9": "e4", # LATIN SMALL LETTER A WITH DIAERESIS + "da": "e5", # LATIN SMALL LETTER A WITH RING ABOVE + "db": "e7", # LATIN SMALL LETTER C WITH CEDILLA + "dc": "e8", # LATIN SMALL LETTER E WITH GRAVE + "dd": "e9", # LATIN SMALL LETTER E WITH ACUTE + "de": "ea", # LATIN SMALL LETTER E WITH CIRCUMFLEX + "df": "eb", # LATIN SMALL LETTER E WITH DIAERESIS + "e0": "ec", # LATIN SMALL LETTER I WITH GRAVE + "e1": "c6", # LATIN CAPITAL LETTER AE + "e2": "ed", # LATIN SMALL LETTER I WITH ACUTE + "e3": "aa", # FEMININE ORDINAL INDICATOR + "e4": "ee", # LATIN SMALL LETTER I WITH CIRCUMFLEX + "e5": "ef", # LATIN SMALL LETTER I WITH DIAERESIS + "e6": "f0", # LATIN SMALL LETTER ETH + "e7": "f1", # LATIN SMALL LETTER N WITH TILDE + "e8": "41", # LATIN CAPITAL LETTER L WITH STROKE + "e9": "d8", # LATIN CAPITAL LETTER O WITH STROKE + "ea": "52", # LATIN CAPITAL LIGATURE OE + "eb": "ba", # MASCULINE ORDINAL INDICATOR + "ec": "f2", # LATIN SMALL LETTER O WITH GRAVE + "ed": "f3", # LATIN SMALL LETTER O WITH ACUTE + "ee": "f4", # LATIN SMALL LETTER O WITH CIRCUMFLEX + "ef": "f5", # LATIN SMALL LETTER O WITH TILDE + "f0": "f6", # LATIN SMALL LETTER O WITH DIAERESIS + "f1": "e6", # LATIN SMALL LETTER AE + "f2": "f9", # LATIN SMALL LETTER U WITH GRAVE + "f3": "fa", # LATIN SMALL LETTER U WITH ACUTE + "f4": "fb", # LATIN SMALL LETTER U WITH CIRCUMFLEX + "f5": "31", # LATIN SMALL LETTER DOTLESS I + "f6": "fc", # LATIN SMALL LETTER U WITH DIAERESIS + "f7": "fd", # LATIN SMALL LETTER Y WITH ACUTE + "f8": "42", # LATIN SMALL LETTER L WITH STROKE + "f9": "f8", # LATIN SMALL LETTER O WITH STROKE + "fa": "53", # LATIN SMALL LIGATURE OE + "fb": "df", # LATIN SMALL LETTER SHARP S + "fc": "fe", # LATIN SMALL LETTER THORN + "fd": "ff", # LATIN SMALL LETTER Y WITH DIAERESIS + "fe": "fd", # .notdef, REPLACEMENT CHARACTER + "ff": "fd", # .notdef, REPLACEMENT CHARACTER + } + return conversion[hex_digits] + +def IsOctalNumber(character): + oct_digits = set(string.octdigits) + return set(character).issubset(oct_digits) + +def IsHexNumber(character): + hex_digits = set(string.hexdigits) + return set(character).issubset(hex_digits) + +def SanitizeCharacter(character): + char = character + escaped_characters = { + '\a': '\\a', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\v': '\\v', + '\"': '\\"', + } + if character in escaped_characters.keys(): + char = escaped_characters[character] + return char + +# http://www.opensource.apple.com/source/CF/CF-744.19/CFOldStylePList.c See `getSlashedChar()` +def UnQuotifyString(string_data, start_index, end_index): # pylint: disable=too-many-locals,too-many-branches,too-many-statements + formatted_string = '' + extracted_string = string_data[start_index:end_index] + string_length = len(extracted_string) + all_cases = ['0', '1', '2', '3', '4', '5', '6', '7', 'a', 'b', 'f', 'n', 'r', 't', 'v', '\"', '\n', 'U'] + index = 0 + while index < string_length: # pylint: disable=too-many-nested-blocks + current_char = extracted_string[index] + if current_char == '\\': + next_char = extracted_string[index+1] + if next_char in all_cases: + index += 1 + if next_char == 'a': + formatted_string += '\a' + if next_char == 'b': + formatted_string += '\b' + if next_char == 'f': + formatted_string += '\f' + if next_char == 'n': + formatted_string += '\n' + if next_char == 'r': + formatted_string += '\r' + if next_char == 't': + formatted_string += '\t' + if next_char == 'v': + formatted_string += '\v' + if next_char == '"': + formatted_string += '\"' + if next_char == '\n': + formatted_string += '\n' + if next_char == 'U': + starting_index = index + 1 + ending_index = starting_index + 4 + unicode_numbers = extracted_string[starting_index:ending_index] + for number in unicode_numbers: + index += 1 + if IsHexNumber(number) is False: # pragma: no cover + message = 'Invalid unicode sequence on line '+str(LineNumberForIndex(string_data, start_index+index)) + raise Exception(message) + formatted_string += unichr(int(unicode_numbers, 16)) + if IsOctalNumber(next_char) is True: # https://twitter.com/Catfish_Man/status/658014170055507968 + starting_index = index + ending_index = starting_index + 1 + for oct_index in range(3): + test_index = starting_index + oct_index + test_oct = extracted_string[test_index] + if IsOctalNumber(test_oct) is True: + ending_index += 1 + octal_numbers = extracted_string[starting_index:ending_index] + hex_number = int(octal_numbers, 8) + hex_str = format(hex_number, 'x') + if hex_number >= 0x80: + hex_str = ConvertNEXTSTEPToUnicode(hex_str) + formatted_string += unichr(int('00'+hex_str, 16)) + else: + formatted_string += current_char + index += 1 + formatted_string += next_char + else: + formatted_string += current_char + index += 1 + return formatted_string + +def LineNumberForIndex(string_data, current_index): + line_number = 1 + index = 0 + string_length = len(string_data) + while (index < current_index) and (index < string_length): + current_char = string_data[index] + if IsNewline(current_char) is True: + line_number += 1 + index += 1 + return line_number + +def IsValidUnquotedStringCharacter(character): + if len(character) == 1: + valid_characters = set(string.ascii_letters+string.digits+'_$/:.-') + return set(character).issubset(valid_characters) + else: # pragma: no cover + message = 'The function "IsValidUnquotedStringCharacter()" can only take single characters!' + raise ValueError(message) + +def IsSpecialWhitespace(character): + value = ord(character) + result = (value >= 9 and value <= 13) # tab, newline, vt, form feed, carriage return + return result + +def IsUnicodeSeparator(character): + value = ord(character) + result = (value == 8232 or value == 8233) + return result + +def IsRegularWhitespace(character): + value = ord(character) + result = (value == 32 or IsUnicodeSeparator(character)) # space and Unicode line sep, para sep + return result + +def IsDataFormattingWhitespace(character): + value = ord(character) + result = (IsNewline(character) or IsRegularWhitespace(character) or value == 9) + return result + +def IsNewline(character): + value = ord(character) + result = (value == 13 or value == 10) + return result + +def IsEndOfLine(character): + result = (IsNewline(character) or IsUnicodeSeparator(character)) + return result + +def IndexOfNextNonSpace(string_data, current_index): # pylint: disable=too-many-branches,too-many-statements + successful = False + found_index = current_index + string_length = len(string_data) + annotation_string = '' + while found_index < string_length: # pylint: disable=too-many-nested-blocks + current_char = string_data[found_index] + if IsSpecialWhitespace(current_char) is True: + found_index += 1 + continue + if IsRegularWhitespace(current_char) is True: + found_index += 1 + continue + if current_char == '/': + next_index = found_index + 1 + if next_index >= string_length: + successful = True + break + else: + next_character = string_data[next_index] + if next_character == '/': # found a line comment "//" + found_index += 1 + next_index = found_index + first_pass = True + while next_index < string_length: + test_char = string_data[next_index] + if IsEndOfLine(test_char) is True: + break + else: + if first_pass is False: + annotation_string += test_char + else: + first_pass = False + next_index += 1 + found_index = next_index + elif next_character == '*': # found a block comment "/* ... */" + found_index += 1 + next_index = found_index + first_pass = True + while next_index < string_length: + test_char = string_data[next_index] + if test_char == '*' and (next_index+1 < string_length) and string_data[next_index+1] == '/': + next_index += 2 + break + else: + if first_pass != True: + annotation_string += test_char + else: + first_pass = False + next_index += 1 + found_index = next_index + else: + successful = True + break + else: + successful = True + break + result = (successful, found_index, annotation_string) + return result diff --git a/jc/parsers/pbPlist/Switch.py b/jc/parsers/pbPlist/Switch.py new file mode 100644 index 00000000..7547c7e7 --- /dev/null +++ b/jc/parsers/pbPlist/Switch.py @@ -0,0 +1,50 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pylocalizer +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +# Original code taken from http://code.activestate.com/recipes/410692/ + +class Switch(object): + def __init__(self, value): + self.value = value + self.fall = False + + def __iter__(self): + """Return the match method once, then stop""" + yield self.match + + def match(self, *args): + """Indicate whether or not to enter a case suite""" + result = False + if self.fall or not args: + result = True + elif self.value in args: # changed for v1.5, see below + self.fall = True + result = True + return result diff --git a/jc/parsers/pbPlist/__init__.py b/jc/parsers/pbPlist/__init__.py new file mode 100644 index 00000000..4b03e3a6 --- /dev/null +++ b/jc/parsers/pbPlist/__init__.py @@ -0,0 +1,32 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pbPlist +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +from . import pbPlist +__version__ = '1.0.4' \ No newline at end of file diff --git a/jc/parsers/pbPlist/pbItem.py b/jc/parsers/pbPlist/pbItem.py new file mode 100644 index 00000000..f1003462 --- /dev/null +++ b/jc/parsers/pbPlist/pbItem.py @@ -0,0 +1,262 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pbPlist +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +from . import StrParse + +def PushIndent(indent_level): + return indent_level + 1 + +def PopIndent(indent_level): + return indent_level - 1 + +def WriteIndent(level=0): + output = '' + for _index in range(level): + output += '\t' + return output + +def WriteNewline(level=0, indent=True): + output = '\n' + if indent: + output += WriteIndent(level) + return output + +class pbItem(object): + def __init__(self, value=None, type_name=None, annotation=None): + if value != None and type_name != None: + self.value = value + if type_name not in KnownTypes.keys(): # pragma: no cover + message = 'Unknown type "'+type_name+'" passed to '+self.__class__.__name__+' initializer!' + raise TypeError(message) + self.type_name = type_name + self.annotation = annotation + else: # pragma: no cover + message = 'The class "'+self.__class__.__name__+'" must be initialized with a non-None value' + raise ValueError(message) + + def __eq__(self, other): + is_equal = False + if isinstance(other, pbItem): + other = other.value + if type(other) is type(self.value): + is_equal = self.value.__eq__(other) + return is_equal + + def __hash__(self): + return self.value.__hash__() + + def __repr__(self): + return self.value.__repr__() + + def __iter__(self): + return self.value.__iter__() + + def __getattr__(self, attrib): + return self.value.__getattr__(attrib) + + def __str__(self): + return self.writeStringRep(0, False)[0] + + def __getitem__(self, key): + return self.value.__getitem__(key) + + def __setitem__(self, key, value): + self.value.__setitem__(key, value) + + def __len__(self): + return self.value.__len__() + + def __contains__(self, item): + return self.value.__contains__(item) + + def __get__(self, obj, objtype): + return self.value.__get__(obj, objtype) + + def writeStringRep(self, indent_level=0, pretty=True): + return self.writeString(indent_level, pretty) + + def writeString(self, indent_level=0, pretty=True): # pylint: disable=no-self-use,unused-variable,unused-argument ; # pragma: no cover + message = 'This is a base class, it cannot write!' + raise Exception(message) + + def nativeType(self): + return self.value + + def writeAnnotation(self): + output_string = '' + if self.annotation != None and len(self.annotation) > 0: + output_string += ' ' + output_string += '/*' + output_string += self.annotation + output_string += '*/' + return output_string + +class pbString(pbItem): + def writeString(self, indent_level=0, pretty=True): + string_string = '' + string_string += self.value + if pretty is True: + string_string += self.writeAnnotation() + return (string_string, indent_level) + +class pbQString(pbItem): + def writeStringRep(self, indent_level=0, pretty=True): + qstring_string = '' + for character in self.value: + qstring_string += StrParse.SanitizeCharacter(character) + return (qstring_string, indent_level) + + def writeString(self, indent_level=0, pretty=True): + qstring_string = '' + qstring_string += '"' + string_rep, indent_level = self.writeStringRep(indent_level, pretty) + qstring_string += string_rep + qstring_string += '"' + if pretty is True: + qstring_string += self.writeAnnotation() + return (qstring_string, indent_level) + +class pbData(pbItem): + def writeString(self, indent_level=0, pretty=True): + data_string = '' + indent_level = PushIndent(indent_level) + data_string += '<' + grouping_byte_counter = 0 + grouping_line_counter = 0 + for hex_byte in map(ord, self.value.decode()): + data_string += format(hex_byte, 'x') + grouping_byte_counter += 1 + # write a space every 4th byte + if grouping_byte_counter == 4: + data_string += ' ' + grouping_byte_counter = 0 + grouping_line_counter += 1 + # write a newline every 4th grouping of bytes + if grouping_line_counter == 4: + data_string += WriteNewline(indent_level) + data_string += ' ' # indent an additional space to make the byte groupings line up + grouping_line_counter = 0 + data_string += '>' + if pretty is True: + data_string += self.writeAnnotation() + indent_level = PopIndent(indent_level) + return (data_string, indent_level) + +class pbDictionary(pbItem): + def nativeType(self): + new_value = dict() + for key in self.keys(): + value = self[key] + new_value[str(key)] = value.nativeType() + return new_value + def writeString(self, indent_level=0, pretty=True): + dictionary_string = '' + dictionary_string += '{' + has_sorted_keys, keys_array = self.value.sortedKeys() + dictionary_string += WriteNewline(indent_level, not has_sorted_keys) + indent_level = PushIndent(indent_level) + previous_value_type = None + if len(keys_array) == 0: + indent_level = PopIndent(indent_level) + else: + if not has_sorted_keys: + dictionary_string += '\t' + for key in keys_array: + if has_sorted_keys: + current_value_type = str(self.value[key]['isa']) + if previous_value_type != current_value_type: + if previous_value_type != None: + dictionary_string += '/* End '+previous_value_type+' section */' + dictionary_string += WriteNewline(indent_level, False) + previous_value_type = current_value_type + dictionary_string += '\n/* Begin '+current_value_type+' section */' + dictionary_string += WriteNewline(indent_level) + else: + dictionary_string += WriteIndent(indent_level) + write_string, indent_level = key.writeString(indent_level, pretty) + dictionary_string += write_string + dictionary_string += ' = ' + write_string, indent_level = self.value[key].writeString(indent_level, pretty) + dictionary_string += write_string + dictionary_string += ';' + should_indent = True + is_last_key = (key == keys_array[-1]) + if is_last_key: + if has_sorted_keys: + dictionary_string += WriteNewline(indent_level, False) + dictionary_string += '/* End '+previous_value_type+' section */' + indent_level = PopIndent(indent_level) + else: + if has_sorted_keys: + should_indent = False + dictionary_string += WriteNewline(indent_level, should_indent) + dictionary_string += '}' + return (dictionary_string, indent_level) + +class pbArray(pbItem): + def nativeType(self): + new_value = [item.nativeType() for item in self.value] + return new_value + def writeString(self, indent_level=0, pretty=True): + array_string = '' + array_string += '(' + array_string += WriteNewline(indent_level) + indent_level = PushIndent(indent_level) + values_array = list(self.value) + if len(values_array) == 0: + indent_level = PopIndent(indent_level) + else: + array_string += '\t' + for value in values_array: + write_string, indent_level = value.writeString(indent_level, pretty) + array_string += write_string + if value != values_array[-1]: + array_string += ',' + else: + indent_level = PopIndent(indent_level) + array_string += WriteNewline(indent_level) + array_string += ')' + return (array_string, indent_level) + +KnownTypes = { + 'string': pbString, + 'qstring': pbQString, + 'data': pbData, + 'dictionary': pbDictionary, + 'array': pbArray, +} + +def pbItemResolver(obj, type_name): + initializer = KnownTypes[type_name] + if initializer: + return initializer(obj, type_name) + else: # pragma: no cover + message = 'Unknown type "'+type_name+'" passed to pbItemResolver!' + raise TypeError(message) diff --git a/jc/parsers/pbPlist/pbParser.py b/jc/parsers/pbPlist/pbParser.py new file mode 100644 index 00000000..ab209a2a --- /dev/null +++ b/jc/parsers/pbPlist/pbParser.py @@ -0,0 +1,291 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pbPlist +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import print_function +import os +import sys +import codecs +from . import StrParse +from . import pbRoot +from . import pbItem +from .Switch import Switch + +def GetFileEncoding(path): + encoding = 'utf-8-sig' + + size = os.path.getsize(path) + if size > 2: + file_descriptor = OpenFile(path) + first_two_bytes = file_descriptor.read(2) + file_descriptor.close() + + for case in Switch(first_two_bytes): + if case(codecs.BOM_UTF16): + encoding = 'utf-16' + break + if case(codecs.BOM_UTF16_LE): + encoding = 'utf-16-le' + break + if case(codecs.BOM_UTF16_BE): + encoding = 'utf-16-be' + break + if case(): + break # pragma: no cover + + return encoding + +def OpenFileWithEncoding(file_path, encoding): + return codecs.open(file_path, 'r', encoding=encoding, errors='ignore') + +if sys.version_info < (3, 0): + def OpenFile(file_path): + return open(file_path, 'rb') +else: + def OpenFile(file_path): + return open(file_path, 'br') + +class PBParser(object): + + def __init__(self, file_path=None): + self.index = 0 + self.string_encoding = None + self.file_path = file_path + self.file_type = None + try: + encoding = GetFileEncoding(self.file_path) + file_descriptor = OpenFileWithEncoding(self.file_path, encoding) + self.data = file_descriptor.read() + if self.file_path.endswith('.strings'): + self.data = '{'+self.data+'}' + file_descriptor.close() + except IOError as exception: # pragma: no cover + print('I/O error({0}): {1}'.format(exception.errno, exception.strerror)) + except: # pragma: no cover + print('Unexpected error:'+str(sys.exc_info()[0])) + raise + + def read(self): + parsed_plist = None + prefix = self.data[0:6] + for case in Switch(prefix): + if case('bplist'): + self.file_type = 'binary' + import biplist + parsed_plist = biplist.readPlist(self.file_path) + break + if case('= string_length: # pragma: no cover + message = 'Unterminated quoted string starting on line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) + raise Exception(message) + else: + string_without_quotes = StrParse.UnQuotifyString(self.data, start_index, self.index) + self.index += 1 # advance past quote character + return string_without_quotes + + def __parseData(self): + string_length = len(self.data) + self.index += 1 # skip over "<" + start_index = self.index + end_index = 0 + byte_stream = '' + while self.index < string_length: + current_char = self.data[self.index] + if current_char == '>': + self.index += 1 # move past the ">" + end_index = self.index + break + if StrParse.IsHexNumber(current_char) is True: + byte_stream += current_char + else: + if not StrParse.IsDataFormattingWhitespace(current_char): # pragma: no cover + message = 'Malformed data byte group (invalid hex) at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) + raise Exception(message) + self.index += 1 + if (len(byte_stream) % 2) == 1: # pragma: no cover + message = 'Malformed data byte group (uneven length) at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) + raise Exception(message) + if end_index == 0: # pragma: no cover + message = 'Expected terminating >" for data at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) + raise Exception(message) + data_object = bytearray.fromhex(byte_stream) + return data_object + + def __parseArray(self): + array_objects = list() + self.index += 1 # move past the "(" + start_index = self.index + new_object = self.__readTest(False) + while new_object is not None: + can_parse, self.index, new_object.annotation = StrParse.IndexOfNextNonSpace(self.data, self.index) + _can_parse = can_parse # pylint: disable=unused-variable + array_objects.append(new_object) + current_char = self.data[self.index] + if current_char == ',': + self.index += 1 + new_object = self.__readTest(False) + current_char = self.data[self.index] + if current_char != ')': # pragma: no cover + message = 'Expected terminating ")" for array at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) + raise Exception(message) + self.index += 1 # skip over ending ")" + return array_objects + + def __parseDict(self): + dictionary = pbRoot.pbRoot() + self.index += 1 # move past the "{" + start_index = self.index + new_object = self.__readTest(False) + while new_object is not None: + can_parse, self.index, new_object.annotation = StrParse.IndexOfNextNonSpace(self.data, self.index) + _can_parse = can_parse # pylint: disable=unused-variable + key_object = new_object + current_char = self.data[self.index] + value_object = None + for case in Switch(current_char): + if case('='): + self.index += 1 + value_object = self.__readTest(True) + break + if case(';'): + # this is for strings files where the key and the value may be the same thing + self.index += 1 + value_object = pbItem.pbItemResolver(new_object.value, new_object.type_name) + value_object.annotation = new_object.annotation + break + if case(): # pragma: no cover + message = 'Missing ";" or "=" on line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) + raise Exception(message) + can_parse, self.index, annotation = StrParse.IndexOfNextNonSpace(self.data, self.index) + _can_parse = can_parse # pylint: disable=unused-variable + if value_object.annotation is None: # this is to prevent losing the annotation of the key when parsing strings dicts + value_object.annotation = annotation + dictionary[key_object] = value_object + current_char = self.data[self.index] + if current_char == ';': + self.index += 1 # advancing to the next key + new_object = self.__readTest(False) + current_char = self.data[self.index] + if current_char != '}': # pragma: no cover + message = 'Expected terminating "}" for dictionary at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) + raise Exception(message) + self.index += 1 # skip over ending "}" + return dictionary diff --git a/jc/parsers/pbPlist/pbPlist.py b/jc/parsers/pbPlist/pbPlist.py new file mode 100644 index 00000000..0239631f --- /dev/null +++ b/jc/parsers/pbPlist/pbPlist.py @@ -0,0 +1,55 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pbPlist +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +from .pbParser import PBParser +from .pbSerializer import PBSerializer + +class PBPlist(object): + + def __init__(self, file_path): + self.root = None + if self.__checkFile(file_path) is True: + parser = PBParser(self.file_path) + self.root = parser.read() + self.string_encoding = parser.string_encoding + self.file_type = parser.file_type + + def write(self, file_path=None): + if file_path is None: + file_path = self.file_path + serializer = PBSerializer(file_path, self.string_encoding, self.file_type) + serializer.write(self.root) + + def __checkFile(self, file_path): + can_access_file = os.path.exists(file_path) + if can_access_file is True: + self.file_path = file_path + return can_access_file diff --git a/jc/parsers/pbPlist/pbRoot.py b/jc/parsers/pbPlist/pbRoot.py new file mode 100644 index 00000000..972a7d86 --- /dev/null +++ b/jc/parsers/pbPlist/pbRoot.py @@ -0,0 +1,110 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pbPlist +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +from functools import cmp_to_key +import collections +from . import pbItem + +def StringCmp(obj1, obj2): + result = -1 + if obj1 > obj2: + result = 1 + elif obj1 == obj2: + result = 0 + return result + +def KeySorter(obj1, obj2): + result = 0 + if str(obj1) == 'isa': + result = -1 + elif str(obj2) == 'isa': + result = 1 + else: + result = StringCmp(str(obj1), str(obj2)) + return result + +class pbRoot(collections.MutableMapping): + + def __init__(self, *args, **kwargs): + self.store = dict() + self.key_storage = list() + self.update(dict(*args, **kwargs)) # use the free update to set keys + + def __internalKeyCheck(self, key): # pylint: disable=no-self-use + safe_key = key + if isinstance(safe_key, str): + safe_key = pbItem.pbItemResolver(safe_key, 'qstring') + return safe_key + + def __getitem__(self, key): + return self.store[key] + + def __setitem__(self, key, value): + if key not in self.key_storage: + self.key_storage.append(self.__internalKeyCheck(key)) + self.store[key] = value + + def __delitem__(self, key): + if key in self.key_storage: + self.key_storage.remove(key) + del self.store[key] + + def __iter__(self): + return self.key_storage.__iter__() + + def __len__(self): + return self.key_storage.__len__() + + def __str__(self): + return self.store.__str__() + + def __contains__(self, item): + return item in self.key_storage + + def __getattr__(self, attrib): + return getattr(self.store, attrib) + + def __keytransform__(self, key): # pylint: disable=no-self-use + result = key + if isinstance(key, pbItem.pbItem): + result = key.value + return result + + def sortedKeys(self): + unsorted_keys = self.key_storage + sorted_keys = sorted(unsorted_keys, key=cmp_to_key(KeySorter)) + can_sort = False + if len(sorted_keys) > 0: + all_dictionaries = all((isinstance(self[key].value, dict) or isinstance(self[key].value, pbRoot)) for key in unsorted_keys) + if all_dictionaries: + can_sort = all(self[key].get('isa', None) is not None for key in unsorted_keys) + if can_sort: + sorted_keys = sorted(unsorted_keys, key=lambda k: str(self[k]['isa'])) + return (can_sort, sorted_keys) diff --git a/jc/parsers/pbPlist/pbSerializer.py b/jc/parsers/pbPlist/pbSerializer.py new file mode 100644 index 00000000..47b35b4e --- /dev/null +++ b/jc/parsers/pbPlist/pbSerializer.py @@ -0,0 +1,75 @@ +# Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) +# All rights reserved. +# +# https://github.com/samdmarshall/pbPlist +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of Samantha Marshall nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import print_function +import sys +from .Switch import Switch + +class PBSerializer(object): + + def __init__(self, file_path=None, encoding=None, file_type=None): + self.string_encoding = encoding + self.file_path = file_path + self.file_type = file_type + + def write(self, obj=None): + for case in Switch(self.file_type): + if case('ascii'): + try: + file_descriptor = open(self.file_path, 'w') + self.__writeObject(file_descriptor, obj) + file_descriptor.close() + except IOError as exception: # pragma: no cover + print('I/O error({0}): {1}'.format(exception.errno, exception.strerror)) + except: # pragma: no cover + print('Unexpected error:'+str(sys.exc_info()[0])) + raise + break + if case('binary'): + import biplist + biplist.writePlist(obj, self.file_path) + break + if case('xml'): + import plistlib + plistlib.writePlist(obj, self.file_path) + break + if case(): + break + + def __writeObject(self, file_descriptor=None, obj=None): + if file_descriptor is None: # pragma: no cover + message = 'Fatal error, file descriptor is None' + raise TypeError(message) + if self.string_encoding is not None: + file_descriptor.write('// !$*'+self.string_encoding+'*$!\n') + if obj is not None: + write_string, indent_level = obj.writeString() + _ = indent_level + file_descriptor.write(write_string) diff --git a/jc/parsers/plist.py b/jc/parsers/plist.py index e4e9da62..f39eb8b9 100644 --- a/jc/parsers/plist.py +++ b/jc/parsers/plist.py @@ -1,6 +1,6 @@ """jc - JSON Convert PLIST file parser -Converts binary and XML PLIST files. +Converts binary, XML, and NeXTSTEP PLIST files. Binary values are converted into an ASCII hex representation. @@ -48,15 +48,17 @@ from typing import Dict, Union import plistlib import binascii from datetime import datetime +from jc.parsers.pbPlist.pbPlist import PBPlist import jc.utils class info(): """Provides parser metadata (version, author, etc.)""" - version = '1.0' + version = '1.1' description = 'PLIST file parser' author = 'Kelly Brazil' author_email = 'kellyjonbrazil@gmail.com' + details = 'Using the pbPlist library from https://github.com/samdmarshall/pbPlist/releases/tag/v1.0.4 for NeXTSTEP support' compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd'] tags = ['standard', 'file', 'string', 'binary'] @@ -157,7 +159,20 @@ def parse( if isinstance(data, str): data = bytes(data, 'utf-8') - raw_output = plistlib.loads(data) + try: + raw_output = plistlib.loads(data) + + except Exception: + # Try parsing as an old-style NeXTSTEP Plist format + # pbPlist library only works on file paths, not strings :( + import tempfile + with tempfile.NamedTemporaryFile(mode='w+') as plist_file: + data = data.decode() + plist_file.write(data) + plist_file.seek(0) + parsed_plist = PBPlist(plist_file.name) + raw_output = parsed_plist.root.nativeType() + raw_output = _fix_objects(raw_output) return raw_output if raw else _process(raw_output) diff --git a/man/jc.1 b/man/jc.1 index f418aa46..38a59f03 100644 --- a/man/jc.1 +++ b/man/jc.1 @@ -1,4 +1,4 @@ -.TH jc 1 2022-12-29 1.22.4 "JSON Convert" +.TH jc 1 2022-12-30 1.22.4 "JSON Convert" .SH NAME \fBjc\fP \- JSON Convert JSONifies the output of many CLI tools, file-types, and strings .SH SYNOPSIS diff --git a/tests/fixtures/generic/plist-nextstep.json b/tests/fixtures/generic/plist-nextstep.json new file mode 100644 index 00000000..ac6c3886 --- /dev/null +++ b/tests/fixtures/generic/plist-nextstep.json @@ -0,0 +1 @@ +{"archiveVersion":"1","classes":{},"objectVersion":"46","objects":{"15D488AC1BD3D6C700EC46B1":{"isa":"PBXAggregateTarget","buildConfigurationList":"15D488AD1BD3D6C700EC46B1","buildPhases":["15D488B01BD3D6D000EC46B1"],"dependencies":[],"name":"Lemon Setup","productName":"Lemon Setup"},"15D488BB1BD3D7A900EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488BA1BD3D7A900EC46B1"},"15D488BD1BD3D7A900EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488BC1BD3D7A900EC46B1"},"15D488C41BD3D7A900EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488C31BD3D7A900EC46B1"},"15D488C71BD3D7A900EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488C61BD3D7A900EC46B1"},"15D488D01BD3D9CD00EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488CC1BD3D9CD00EC46B1"},"15D488D11BD3D9CD00EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488CD1BD3D9CD00EC46B1"},"15D488D21BD3D9CD00EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488CE1BD3D9CD00EC46B1"},"15D488D71BD3DA8800EC46B1":{"isa":"PBXBuildFile","fileRef":"15D488D61BD3DA8800EC46B1"},"15D488B11BD3D6F400EC46B1":{"isa":"PBXContainerItemProxy","containerPortal":"15D488951BD3D59200EC46B1","proxyType":"1","remoteGlobalIDString":"15D488AC1BD3D6C700EC46B1","remoteInfo":"Lemon Setup"},"15D488D81BD3DA9C00EC46B1":{"isa":"PBXContainerItemProxy","containerPortal":"15D488951BD3D59200EC46B1","proxyType":"1","remoteGlobalIDString":"15D488991BD3D59200EC46B1","remoteInfo":"Lemon"},"158E53E61BD3E6A600F75AAD":{"isa":"PBXFileReference","lastKnownFileType":"net.daringfireball.markdown","path":"PluginUsage.md","sourceTree":""},"15D488A01BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text.script.sh","path":"autogen.sh","sourceTree":""},"15D488A11BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text","path":"citrus.pc.in","sourceTree":""},"15D488A21BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text","path":"configure.ac","sourceTree":""},"15D488A31BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"sourcecode.c.c","path":"lemon.c","sourceTree":""},"15D488A41BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"sourcecode.c.c","path":"lempar.c","sourceTree":""},"15D488A51BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"sourcecode.c.h","path":"lempar.h","sourceTree":""},"15D488A61BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text","path":"LICENSE","sourceTree":""},"15D488A71BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text","path":"Makefile.am","sourceTree":""},"15D488A81BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"net.daringfireball.markdown","path":"README.md","sourceTree":""},"15D488A91BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text","path":"VERSION","sourceTree":""},"15D488AA1BD3D62C00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text","path":"version.h.in","sourceTree":""},"15D488B71BD3D7A900EC46B1":{"isa":"PBXFileReference","explicitFileType":"wrapper.cfbundle","includeInIndex":"0","path":"CitrusPlugin.xcplugin","sourceTree":"BUILT_PRODUCTS_DIR"},"15D488BA1BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"wrapper.framework","name":"AppKit.framework","path":"/System/Library/Frameworks/AppKit.framework","sourceTree":""},"15D488BC1BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"wrapper.framework","name":"Foundation.framework","path":"/System/Library/Frameworks/Foundation.framework","sourceTree":""},"15D488C01BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"text.xml","name":"CitrusPlugin.xcscheme","path":"CitrusPlugin.xcodeproj/xcshareddata/xcschemes/CitrusPlugin.xcscheme","sourceTree":"SOURCE_ROOT"},"15D488C21BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"sourcecode.c.h","path":"CitrusPlugin.h","sourceTree":""},"15D488C31BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"sourcecode.c.objc","path":"CitrusPlugin.m","sourceTree":""},"15D488C51BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"sourcecode.c.h","path":"NSObject_Extension.h","sourceTree":""},"15D488C61BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"sourcecode.c.objc","path":"NSObject_Extension.m","sourceTree":""},"15D488C81BD3D7A900EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"text.plist.xml","path":"Info.plist","sourceTree":""},"15D488CC1BD3D9CD00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text.plist.pbfilespec","path":"Lemon.pbfilespec","sourceTree":""},"15D488CD1BD3D9CD00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text.plist.strings","path":"Lemon.strings","sourceTree":""},"15D488CE1BD3D9CD00EC46B1":{"isa":"PBXFileReference","fileEncoding":"4","lastKnownFileType":"text.plist.xcspec","path":"Lemon.xcspec","sourceTree":""},"15D488D61BD3DA8800EC46B1":{"isa":"PBXFileReference","lastKnownFileType":"compiled.mach-o.executable","path":"lemon","sourceTree":""},"15D488B51BD3D7A900EC46B1":{"isa":"PBXFrameworksBuildPhase","buildActionMask":"2147483647","files":["15D488BB1BD3D7A900EC46B1","15D488BD1BD3D7A900EC46B1"],"runOnlyForDeploymentPostprocessing":"0"},"15D488941BD3D59200EC46B1":{"isa":"PBXGroup","children":["158E53E61BD3E6A600F75AAD","15D4889F1BD3D5D500EC46B1","15D488BE1BD3D7A900EC46B1","15D488B91BD3D7A900EC46B1","15D488B81BD3D7A900EC46B1","15D488D61BD3DA8800EC46B1"],"sourceTree":""},"15D4889F1BD3D5D500EC46B1":{"isa":"PBXGroup","children":["15D488A01BD3D62C00EC46B1","15D488A11BD3D62C00EC46B1","15D488A21BD3D62C00EC46B1","15D488A31BD3D62C00EC46B1","15D488A41BD3D62C00EC46B1","15D488A51BD3D62C00EC46B1","15D488A61BD3D62C00EC46B1","15D488A71BD3D62C00EC46B1","15D488A81BD3D62C00EC46B1","15D488A91BD3D62C00EC46B1","15D488AA1BD3D62C00EC46B1"],"name":"lemon","sourceTree":""},"15D488B81BD3D7A900EC46B1":{"isa":"PBXGroup","children":["15D488B71BD3D7A900EC46B1"],"name":"Products","sourceTree":""},"15D488B91BD3D7A900EC46B1":{"isa":"PBXGroup","children":["15D488BA1BD3D7A900EC46B1","15D488BC1BD3D7A900EC46B1"],"name":"Frameworks","sourceTree":""},"15D488BE1BD3D7A900EC46B1":{"isa":"PBXGroup","children":["15D488D41BD3D9D000EC46B1","15D488C21BD3D7A900EC46B1","15D488C31BD3D7A900EC46B1","15D488C51BD3D7A900EC46B1","15D488C61BD3D7A900EC46B1","15D488C81BD3D7A900EC46B1","15D488BF1BD3D7A900EC46B1"],"path":"CitrusPlugin","sourceTree":""},"15D488BF1BD3D7A900EC46B1":{"isa":"PBXGroup","children":["15D488C01BD3D7A900EC46B1"],"name":"Supporting Files","sourceTree":""},"15D488D41BD3D9D000EC46B1":{"isa":"PBXGroup","children":["15D488CC1BD3D9CD00EC46B1","15D488CD1BD3D9CD00EC46B1","15D488CE1BD3D9CD00EC46B1"],"name":"Resources","sourceTree":""},"15D488991BD3D59200EC46B1":{"isa":"PBXLegacyTarget","buildArgumentsString":"${ACTION}","buildConfigurationList":"15D4889C1BD3D59200EC46B1","buildPhases":[],"buildToolPath":"/usr/bin/make","buildWorkingDirectory":"$(PROJECT_DIR)","dependencies":["15D488B21BD3D6F400EC46B1"],"name":"Lemon","passBuildSettingsInEnvironment":"1","productName":"CitrusPlugin"},"15D488B61BD3D7A900EC46B1":{"isa":"PBXNativeTarget","buildConfigurationList":"15D488C91BD3D7A900EC46B1","buildPhases":["15D488B31BD3D7A900EC46B1","15D488B41BD3D7A900EC46B1","15D488B51BD3D7A900EC46B1","15D488DC1BD3DBBB00EC46B1"],"buildRules":[],"dependencies":["15D488D91BD3DA9C00EC46B1"],"name":"CitrusPlugin","productName":"CitrusPlugin","productReference":"15D488B71BD3D7A900EC46B1","productType":"com.apple.product-type.bundle"},"15D488951BD3D59200EC46B1":{"isa":"PBXProject","attributes":{"LastUpgradeCheck":"0700","ORGANIZATIONNAME":"Samantha Marshall","TargetAttributes":{"15D488991BD3D59200EC46B1":{"CreatedOnToolsVersion":"7.0.1"},"15D488AC1BD3D6C700EC46B1":{"CreatedOnToolsVersion":"7.0.1"},"15D488B61BD3D7A900EC46B1":{"CreatedOnToolsVersion":"7.0.1"}}},"buildConfigurationList":"15D488981BD3D59200EC46B1","compatibilityVersion":"Xcode 3.2","developmentRegion":"English","hasScannedForEncodings":"0","knownRegions":["en"],"mainGroup":"15D488941BD3D59200EC46B1","productRefGroup":"15D488B81BD3D7A900EC46B1","projectDirPath":"","projectRoot":"","targets":["15D488AC1BD3D6C700EC46B1","15D488991BD3D59200EC46B1","15D488B61BD3D7A900EC46B1"]},"15D488B41BD3D7A900EC46B1":{"isa":"PBXResourcesBuildPhase","buildActionMask":"2147483647","files":["15D488D71BD3DA8800EC46B1","15D488D01BD3D9CD00EC46B1","15D488D11BD3D9CD00EC46B1","15D488D21BD3D9CD00EC46B1"],"runOnlyForDeploymentPostprocessing":"0"},"15D488B01BD3D6D000EC46B1":{"isa":"PBXShellScriptBuildPhase","buildActionMask":"2147483647","files":[],"inputPaths":[],"name":"make distclean ; ./autogen.sh ; ./configure","outputPaths":[],"runOnlyForDeploymentPostprocessing":"0","shellPath":"/bin/sh","shellScript":"cd ${PROJECT_DIR}\n\nif [ -e Makefile ]; then\n\tmake distclean\nfi\n\n./autogen.sh\n./configure"},"15D488DC1BD3DBBB00EC46B1":{"isa":"PBXShellScriptBuildPhase","buildActionMask":"2147483647","files":[],"inputPaths":[],"name":"Copying default template into bundle because Xcode has a bug","outputPaths":[],"runOnlyForDeploymentPostprocessing":"0","shellPath":"/bin/sh","shellScript":"cp ${PROJECT_DIR}/lempar.c ${CONFIGURATION_BUILD_DIR}/${PRODUCT_NAME}.${WRAPPER_EXTENSION}/Contents/Resources\ncp ${PROJECT_DIR}/lempar.h ${CONFIGURATION_BUILD_DIR}/${PRODUCT_NAME}.${WRAPPER_EXTENSION}/Contents/Resources"},"15D488B31BD3D7A900EC46B1":{"isa":"PBXSourcesBuildPhase","buildActionMask":"2147483647","files":["15D488C41BD3D7A900EC46B1","15D488C71BD3D7A900EC46B1"],"runOnlyForDeploymentPostprocessing":"0"},"15D488B21BD3D6F400EC46B1":{"isa":"PBXTargetDependency","target":"15D488AC1BD3D6C700EC46B1","targetProxy":"15D488B11BD3D6F400EC46B1"},"15D488D91BD3DA9C00EC46B1":{"isa":"PBXTargetDependency","target":"15D488991BD3D59200EC46B1","targetProxy":"15D488D81BD3DA9C00EC46B1"},"15D4889A1BD3D59200EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_CXX_LANGUAGE_STANDARD":"gnu++0x","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf","ENABLE_STRICT_OBJC_MSGSEND":"YES","ENABLE_TESTABILITY":"YES","GCC_C_LANGUAGE_STANDARD":"gnu99","GCC_DYNAMIC_NO_PIC":"NO","GCC_NO_COMMON_BLOCKS":"YES","GCC_OPTIMIZATION_LEVEL":"0","GCC_PREPROCESSOR_DEFINITIONS":["DEBUG=1","$(inherited)"],"GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","MTL_ENABLE_DEBUG_INFO":"YES","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"macosx"},"name":"Debug"},"15D4889B1BD3D59200EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_CXX_LANGUAGE_STANDARD":"gnu++0x","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf-with-dsym","ENABLE_NS_ASSERTIONS":"NO","ENABLE_STRICT_OBJC_MSGSEND":"YES","GCC_C_LANGUAGE_STANDARD":"gnu99","GCC_NO_COMMON_BLOCKS":"YES","GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","MTL_ENABLE_DEBUG_INFO":"NO","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"macosx"},"name":"Release"},"15D4889D1BD3D59200EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"DEBUGGING_SYMBOLS":"YES","DEBUG_INFORMATION_FORMAT":"dwarf","GCC_GENERATE_DEBUGGING_SYMBOLS":"YES","GCC_OPTIMIZATION_LEVEL":"0","OTHER_CFLAGS":"","OTHER_LDFLAGS":"","PRODUCT_NAME":"$(TARGET_NAME)"},"name":"Debug"},"15D4889E1BD3D59200EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"DEBUG_INFORMATION_FORMAT":"dwarf-with-dsym","OTHER_CFLAGS":"","OTHER_LDFLAGS":"","PRODUCT_NAME":"$(TARGET_NAME)"},"name":"Release"},"15D488AE1BD3D6C700EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"PRODUCT_NAME":"$(TARGET_NAME)"},"name":"Debug"},"15D488AF1BD3D6C700EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"PRODUCT_NAME":"$(TARGET_NAME)"},"name":"Release"},"15D488CA1BD3D7A900EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"CODE_SIGN_IDENTITY":"Developer ID Application: Samantha Marshall (329DAD2G44)","COMBINE_HIDPI_IMAGES":"YES","DEPLOYMENT_LOCATION":"YES","DSTROOT":"$(HOME)","INFOPLIST_FILE":"CitrusPlugin/Info.plist","INSTALL_PATH":"/Library/Application Support/Developer/Shared/Xcode/Plug-ins","MACOSX_DEPLOYMENT_TARGET":"10.9","PRODUCT_BUNDLE_IDENTIFIER":"com.samdmarshall.CitrusPlugin","PRODUCT_NAME":"$(TARGET_NAME)","WRAPPER_EXTENSION":"xcplugin"},"name":"Debug"},"15D488CB1BD3D7A900EC46B1":{"isa":"XCBuildConfiguration","buildSettings":{"CODE_SIGN_IDENTITY":"Developer ID Application: Samantha Marshall (329DAD2G44)","COMBINE_HIDPI_IMAGES":"YES","DEPLOYMENT_LOCATION":"YES","DSTROOT":"$(HOME)","INFOPLIST_FILE":"CitrusPlugin/Info.plist","INSTALL_PATH":"/Library/Application Support/Developer/Shared/Xcode/Plug-ins","MACOSX_DEPLOYMENT_TARGET":"10.9","PRODUCT_BUNDLE_IDENTIFIER":"com.samdmarshall.CitrusPlugin","PRODUCT_NAME":"$(TARGET_NAME)","WRAPPER_EXTENSION":"xcplugin"},"name":"Release"},"15D488981BD3D59200EC46B1":{"isa":"XCConfigurationList","buildConfigurations":["15D4889A1BD3D59200EC46B1","15D4889B1BD3D59200EC46B1"],"defaultConfigurationIsVisible":"0","defaultConfigurationName":"Release"},"15D4889C1BD3D59200EC46B1":{"isa":"XCConfigurationList","buildConfigurations":["15D4889D1BD3D59200EC46B1","15D4889E1BD3D59200EC46B1"],"defaultConfigurationIsVisible":"0","defaultConfigurationName":"Release"},"15D488AD1BD3D6C700EC46B1":{"isa":"XCConfigurationList","buildConfigurations":["15D488AE1BD3D6C700EC46B1","15D488AF1BD3D6C700EC46B1"],"defaultConfigurationIsVisible":"0","defaultConfigurationName":"Release"},"15D488C91BD3D7A900EC46B1":{"isa":"XCConfigurationList","buildConfigurations":["15D488CA1BD3D7A900EC46B1","15D488CB1BD3D7A900EC46B1"],"defaultConfigurationIsVisible":"0","defaultConfigurationName":"Release"}},"rootObject":"15D488951BD3D59200EC46B1"} diff --git a/tests/fixtures/generic/plist-nextstep.plist b/tests/fixtures/generic/plist-nextstep.plist new file mode 100644 index 00000000..40c285ed --- /dev/null +++ b/tests/fixtures/generic/plist-nextstep.plist @@ -0,0 +1,508 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + 15D488AC1BD3D6C700EC46B1 /* Lemon Setup */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 15D488AD1BD3D6C700EC46B1 /* Build configuration list for PBXAggregateTarget "Lemon Setup" */; + buildPhases = ( + 15D488B01BD3D6D000EC46B1 /* make distclean ; ./autogen.sh ; ./configure */, + ); + dependencies = ( + ); + name = "Lemon Setup"; + productName = "Lemon Setup"; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 15D488BB1BD3D7A900EC46B1 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15D488BA1BD3D7A900EC46B1 /* AppKit.framework */; }; + 15D488BD1BD3D7A900EC46B1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15D488BC1BD3D7A900EC46B1 /* Foundation.framework */; }; + 15D488C41BD3D7A900EC46B1 /* CitrusPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 15D488C31BD3D7A900EC46B1 /* CitrusPlugin.m */; }; + 15D488C71BD3D7A900EC46B1 /* NSObject_Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 15D488C61BD3D7A900EC46B1 /* NSObject_Extension.m */; }; + 15D488D01BD3D9CD00EC46B1 /* Lemon.pbfilespec in Resources */ = {isa = PBXBuildFile; fileRef = 15D488CC1BD3D9CD00EC46B1 /* Lemon.pbfilespec */; }; + 15D488D11BD3D9CD00EC46B1 /* Lemon.strings in Resources */ = {isa = PBXBuildFile; fileRef = 15D488CD1BD3D9CD00EC46B1 /* Lemon.strings */; }; + 15D488D21BD3D9CD00EC46B1 /* Lemon.xcspec in Resources */ = {isa = PBXBuildFile; fileRef = 15D488CE1BD3D9CD00EC46B1 /* Lemon.xcspec */; }; + 15D488D71BD3DA8800EC46B1 /* lemon in Resources */ = {isa = PBXBuildFile; fileRef = 15D488D61BD3DA8800EC46B1 /* lemon */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 15D488B11BD3D6F400EC46B1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 15D488951BD3D59200EC46B1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 15D488AC1BD3D6C700EC46B1; + remoteInfo = "Lemon Setup"; + }; + 15D488D81BD3DA9C00EC46B1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 15D488951BD3D59200EC46B1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 15D488991BD3D59200EC46B1; + remoteInfo = Lemon; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 158E53E61BD3E6A600F75AAD /* PluginUsage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = PluginUsage.md; sourceTree = ""; }; + 15D488A01BD3D62C00EC46B1 /* autogen.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = autogen.sh; sourceTree = ""; }; + 15D488A11BD3D62C00EC46B1 /* citrus.pc.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = citrus.pc.in; sourceTree = ""; }; + 15D488A21BD3D62C00EC46B1 /* configure.ac */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = configure.ac; sourceTree = ""; }; + 15D488A31BD3D62C00EC46B1 /* lemon.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lemon.c; sourceTree = ""; }; + 15D488A41BD3D62C00EC46B1 /* lempar.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lempar.c; sourceTree = ""; }; + 15D488A51BD3D62C00EC46B1 /* lempar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lempar.h; sourceTree = ""; }; + 15D488A61BD3D62C00EC46B1 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 15D488A71BD3D62C00EC46B1 /* Makefile.am */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Makefile.am; sourceTree = ""; }; + 15D488A81BD3D62C00EC46B1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 15D488A91BD3D62C00EC46B1 /* VERSION */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = VERSION; sourceTree = ""; }; + 15D488AA1BD3D62C00EC46B1 /* version.h.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = version.h.in; sourceTree = ""; }; + 15D488B71BD3D7A900EC46B1 /* CitrusPlugin.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CitrusPlugin.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; + 15D488BA1BD3D7A900EC46B1 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 15D488BC1BD3D7A900EC46B1 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 15D488C01BD3D7A900EC46B1 /* CitrusPlugin.xcscheme */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = CitrusPlugin.xcscheme; path = CitrusPlugin.xcodeproj/xcshareddata/xcschemes/CitrusPlugin.xcscheme; sourceTree = SOURCE_ROOT; }; + 15D488C21BD3D7A900EC46B1 /* CitrusPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CitrusPlugin.h; sourceTree = ""; }; + 15D488C31BD3D7A900EC46B1 /* CitrusPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CitrusPlugin.m; sourceTree = ""; }; + 15D488C51BD3D7A900EC46B1 /* NSObject_Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSObject_Extension.h; sourceTree = ""; }; + 15D488C61BD3D7A900EC46B1 /* NSObject_Extension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSObject_Extension.m; sourceTree = ""; }; + 15D488C81BD3D7A900EC46B1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 15D488CC1BD3D9CD00EC46B1 /* Lemon.pbfilespec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.pbfilespec; path = Lemon.pbfilespec; sourceTree = ""; }; + 15D488CD1BD3D9CD00EC46B1 /* Lemon.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Lemon.strings; sourceTree = ""; }; + 15D488CE1BD3D9CD00EC46B1 /* Lemon.xcspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xcspec; path = Lemon.xcspec; sourceTree = ""; }; + 15D488D61BD3DA8800EC46B1 /* lemon */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = lemon; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 15D488B51BD3D7A900EC46B1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 15D488BB1BD3D7A900EC46B1 /* AppKit.framework in Frameworks */, + 15D488BD1BD3D7A900EC46B1 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 15D488941BD3D59200EC46B1 = { + isa = PBXGroup; + children = ( + 158E53E61BD3E6A600F75AAD /* PluginUsage.md */, + 15D4889F1BD3D5D500EC46B1 /* lemon */, + 15D488BE1BD3D7A900EC46B1 /* CitrusPlugin */, + 15D488B91BD3D7A900EC46B1 /* Frameworks */, + 15D488B81BD3D7A900EC46B1 /* Products */, + 15D488D61BD3DA8800EC46B1 /* lemon */, + ); + sourceTree = ""; + }; + 15D4889F1BD3D5D500EC46B1 /* lemon */ = { + isa = PBXGroup; + children = ( + 15D488A01BD3D62C00EC46B1 /* autogen.sh */, + 15D488A11BD3D62C00EC46B1 /* citrus.pc.in */, + 15D488A21BD3D62C00EC46B1 /* configure.ac */, + 15D488A31BD3D62C00EC46B1 /* lemon.c */, + 15D488A41BD3D62C00EC46B1 /* lempar.c */, + 15D488A51BD3D62C00EC46B1 /* lempar.h */, + 15D488A61BD3D62C00EC46B1 /* LICENSE */, + 15D488A71BD3D62C00EC46B1 /* Makefile.am */, + 15D488A81BD3D62C00EC46B1 /* README.md */, + 15D488A91BD3D62C00EC46B1 /* VERSION */, + 15D488AA1BD3D62C00EC46B1 /* version.h.in */, + ); + name = lemon; + sourceTree = ""; + }; + 15D488B81BD3D7A900EC46B1 /* Products */ = { + isa = PBXGroup; + children = ( + 15D488B71BD3D7A900EC46B1 /* CitrusPlugin.xcplugin */, + ); + name = Products; + sourceTree = ""; + }; + 15D488B91BD3D7A900EC46B1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 15D488BA1BD3D7A900EC46B1 /* AppKit.framework */, + 15D488BC1BD3D7A900EC46B1 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 15D488BE1BD3D7A900EC46B1 /* CitrusPlugin */ = { + isa = PBXGroup; + children = ( + 15D488D41BD3D9D000EC46B1 /* Resources */, + 15D488C21BD3D7A900EC46B1 /* CitrusPlugin.h */, + 15D488C31BD3D7A900EC46B1 /* CitrusPlugin.m */, + 15D488C51BD3D7A900EC46B1 /* NSObject_Extension.h */, + 15D488C61BD3D7A900EC46B1 /* NSObject_Extension.m */, + 15D488C81BD3D7A900EC46B1 /* Info.plist */, + 15D488BF1BD3D7A900EC46B1 /* Supporting Files */, + ); + path = CitrusPlugin; + sourceTree = ""; + }; + 15D488BF1BD3D7A900EC46B1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 15D488C01BD3D7A900EC46B1 /* CitrusPlugin.xcscheme */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 15D488D41BD3D9D000EC46B1 /* Resources */ = { + isa = PBXGroup; + children = ( + 15D488CC1BD3D9CD00EC46B1 /* Lemon.pbfilespec */, + 15D488CD1BD3D9CD00EC46B1 /* Lemon.strings */, + 15D488CE1BD3D9CD00EC46B1 /* Lemon.xcspec */, + ); + name = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXLegacyTarget section */ + 15D488991BD3D59200EC46B1 /* Lemon */ = { + isa = PBXLegacyTarget; + buildArgumentsString = "${ACTION}"; + buildConfigurationList = 15D4889C1BD3D59200EC46B1 /* Build configuration list for PBXLegacyTarget "Lemon" */; + buildPhases = ( + ); + buildToolPath = /usr/bin/make; + buildWorkingDirectory = "$(PROJECT_DIR)"; + dependencies = ( + 15D488B21BD3D6F400EC46B1 /* PBXTargetDependency */, + ); + name = Lemon; + passBuildSettingsInEnvironment = 1; + productName = CitrusPlugin; + }; +/* End PBXLegacyTarget section */ + +/* Begin PBXNativeTarget section */ + 15D488B61BD3D7A900EC46B1 /* CitrusPlugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 15D488C91BD3D7A900EC46B1 /* Build configuration list for PBXNativeTarget "CitrusPlugin" */; + buildPhases = ( + 15D488B31BD3D7A900EC46B1 /* Sources */, + 15D488B41BD3D7A900EC46B1 /* Resources */, + 15D488B51BD3D7A900EC46B1 /* Frameworks */, + 15D488DC1BD3DBBB00EC46B1 /* Copying default template into bundle because Xcode has a bug */, + ); + buildRules = ( + ); + dependencies = ( + 15D488D91BD3DA9C00EC46B1 /* PBXTargetDependency */, + ); + name = CitrusPlugin; + productName = CitrusPlugin; + productReference = 15D488B71BD3D7A900EC46B1 /* CitrusPlugin.xcplugin */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 15D488951BD3D59200EC46B1 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Samantha Marshall"; + TargetAttributes = { + 15D488991BD3D59200EC46B1 = { + CreatedOnToolsVersion = 7.0.1; + }; + 15D488AC1BD3D6C700EC46B1 = { + CreatedOnToolsVersion = 7.0.1; + }; + 15D488B61BD3D7A900EC46B1 = { + CreatedOnToolsVersion = 7.0.1; + }; + }; + }; + buildConfigurationList = 15D488981BD3D59200EC46B1 /* Build configuration list for PBXProject "CitrusPlugin" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 15D488941BD3D59200EC46B1; + productRefGroup = 15D488B81BD3D7A900EC46B1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 15D488AC1BD3D6C700EC46B1 /* Lemon Setup */, + 15D488991BD3D59200EC46B1 /* Lemon */, + 15D488B61BD3D7A900EC46B1 /* CitrusPlugin */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 15D488B41BD3D7A900EC46B1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 15D488D71BD3DA8800EC46B1 /* lemon in Resources */, + 15D488D01BD3D9CD00EC46B1 /* Lemon.pbfilespec in Resources */, + 15D488D11BD3D9CD00EC46B1 /* Lemon.strings in Resources */, + 15D488D21BD3D9CD00EC46B1 /* Lemon.xcspec in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 15D488B01BD3D6D000EC46B1 /* make distclean ; ./autogen.sh ; ./configure */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "make distclean ; ./autogen.sh ; ./configure"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd ${PROJECT_DIR}\n\nif [ -e Makefile ]; then\n\tmake distclean\nfi\n\n./autogen.sh\n./configure"; + }; + 15D488DC1BD3DBBB00EC46B1 /* Copying default template into bundle because Xcode has a bug */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copying default template into bundle because Xcode has a bug"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cp ${PROJECT_DIR}/lempar.c ${CONFIGURATION_BUILD_DIR}/${PRODUCT_NAME}.${WRAPPER_EXTENSION}/Contents/Resources\ncp ${PROJECT_DIR}/lempar.h ${CONFIGURATION_BUILD_DIR}/${PRODUCT_NAME}.${WRAPPER_EXTENSION}/Contents/Resources"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 15D488B31BD3D7A900EC46B1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 15D488C41BD3D7A900EC46B1 /* CitrusPlugin.m in Sources */, + 15D488C71BD3D7A900EC46B1 /* NSObject_Extension.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 15D488B21BD3D6F400EC46B1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 15D488AC1BD3D6C700EC46B1 /* Lemon Setup */; + targetProxy = 15D488B11BD3D6F400EC46B1 /* PBXContainerItemProxy */; + }; + 15D488D91BD3DA9C00EC46B1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 15D488991BD3D59200EC46B1 /* Lemon */; + targetProxy = 15D488D81BD3DA9C00EC46B1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 15D4889A1BD3D59200EC46B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = NO; + SDKROOT = macosx; + }; + name = Debug; + }; + 15D4889B1BD3D59200EC46B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + SDKROOT = macosx; + }; + name = Release; + }; + 15D4889D1BD3D59200EC46B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUGGING_SYMBOLS = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 15D4889E1BD3D59200EC46B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 15D488AE1BD3D6C700EC46B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 15D488AF1BD3D6C700EC46B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 15D488CA1BD3D7A900EC46B1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "Developer ID Application: Samantha Marshall (329DAD2G44)"; + COMBINE_HIDPI_IMAGES = YES; + DEPLOYMENT_LOCATION = YES; + DSTROOT = "$(HOME)"; + INFOPLIST_FILE = CitrusPlugin/Info.plist; + INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = com.samdmarshall.CitrusPlugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xcplugin; + }; + name = Debug; + }; + 15D488CB1BD3D7A900EC46B1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "Developer ID Application: Samantha Marshall (329DAD2G44)"; + COMBINE_HIDPI_IMAGES = YES; + DEPLOYMENT_LOCATION = YES; + DSTROOT = "$(HOME)"; + INFOPLIST_FILE = CitrusPlugin/Info.plist; + INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = com.samdmarshall.CitrusPlugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xcplugin; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 15D488981BD3D59200EC46B1 /* Build configuration list for PBXProject "CitrusPlugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 15D4889A1BD3D59200EC46B1 /* Debug */, + 15D4889B1BD3D59200EC46B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 15D4889C1BD3D59200EC46B1 /* Build configuration list for PBXLegacyTarget "Lemon" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 15D4889D1BD3D59200EC46B1 /* Debug */, + 15D4889E1BD3D59200EC46B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 15D488AD1BD3D6C700EC46B1 /* Build configuration list for PBXAggregateTarget "Lemon Setup" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 15D488AE1BD3D6C700EC46B1 /* Debug */, + 15D488AF1BD3D6C700EC46B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 15D488C91BD3D7A900EC46B1 /* Build configuration list for PBXNativeTarget "CitrusPlugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 15D488CA1BD3D7A900EC46B1 /* Debug */, + 15D488CB1BD3D7A900EC46B1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 15D488951BD3D59200EC46B1 /* Project object */; +} diff --git a/tests/fixtures/generic/plist-nextstep2.json b/tests/fixtures/generic/plist-nextstep2.json new file mode 100644 index 00000000..8cbfd5f9 --- /dev/null +++ b/tests/fixtures/generic/plist-nextstep2.json @@ -0,0 +1 @@ +{"key1":"Value1","key2":"value3","DateOfStateChange":"2022-12-18 05:51:30 +0000","Destination":"F134545","Progress":{"Percent":"0.4563777755358589"}} diff --git a/tests/fixtures/generic/plist-nextstep2.plist b/tests/fixtures/generic/plist-nextstep2.plist new file mode 100644 index 00000000..8708ff6c --- /dev/null +++ b/tests/fixtures/generic/plist-nextstep2.plist @@ -0,0 +1,9 @@ +{ + key1 = Value1; + key2 = "value3"; + DateOfStateChange = "2022-12-18 05:51:30 +0000"; + Destination = "F134545"; + Progress = { + Percent = "0.4563777755358589"; + }; +} diff --git a/tests/test_plist.py b/tests/test_plist.py index ce38856e..99831798 100644 --- a/tests/test_plist.py +++ b/tests/test_plist.py @@ -21,6 +21,12 @@ class MyTests(unittest.TestCase): with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/plist-alltypes-bin.plist'), 'rb') as f: generic_alltypes_bin = f.read() + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/plist-nextstep.plist'), 'rb') as f: + nextstep = f.read() + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/plist-nextstep2.plist'), 'rb') as f: + nextstep2 = f.read() + # output with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/plist-garageband-info.json'), 'r', encoding='utf-8') as f: generic_garageband_json = json.loads(f.read()) @@ -34,6 +40,12 @@ class MyTests(unittest.TestCase): with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/plist-alltypes-bin.json'), 'r', encoding='utf-8') as f: generic_alltypes_bin_json = json.loads(f.read()) + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/plist-nextstep.json'), 'r', encoding='utf-8') as f: + nextstep_json = json.loads(f.read()) + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/plist-nextstep2.json'), 'r', encoding='utf-8') as f: + nextstep2_json = json.loads(f.read()) + def test_plist_nodata(self): """ @@ -70,5 +82,18 @@ class MyTests(unittest.TestCase): self.assertEqual(jc.parsers.plist.parse(self.generic_alltypes_bin, quiet=True), self.generic_alltypes_bin_json) + def test_plist_nextstep(self): + """ + Test NeXTSTEP style plist file + """ + self.assertEqual(jc.parsers.plist.parse(self.nextstep, quiet=True), self.nextstep_json) + + def test_plist_nextstep2(self): + """ + Test NeXTSTEP style plist file simple + """ + self.assertEqual(jc.parsers.plist.parse(self.nextstep2, quiet=True), self.nextstep2_json) + + if __name__ == '__main__': unittest.main()