├── .circleci └── config.yml ├── .clang-format ├── .codeclimate.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dangerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── contributing └── code-of-conduct.md ├── install_requirements.txt ├── pbPlist ├── StrParse.py ├── Switch.py ├── __init__.py ├── pbItem.py ├── pbParser.py ├── pbPlist.py ├── pbRoot.py └── pbSerializer.py ├── pylintrc ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── pbPlist-test-data │ ├── array_encoding_hint │ │ └── test.plist │ ├── array_mixed │ │ └── test.plist │ ├── array_qstrings │ │ └── test.plist │ ├── array_strings │ │ └── test.plist │ ├── bom │ │ └── utf-16 │ │ │ └── Localizable.strings │ ├── data_encoding_hint │ │ └── test.plist │ ├── dict_encoding_hint │ │ └── test.plist │ ├── dict_string_values │ │ └── test.plist │ ├── empty_encoding_hint │ │ └── test.plist │ ├── empty_plist │ │ └── test.plist │ ├── qstring_encoding_hint │ │ └── test.plist │ ├── raw_array │ │ └── test.plist │ ├── raw_data │ │ └── test.plist │ ├── raw_dict │ │ └── test.plist │ ├── raw_qstring │ │ └── test.plist │ ├── raw_string │ │ └── test.plist │ ├── string_encoding_hint │ │ └── test.plist │ └── xcode_proj │ │ └── test.plist └── pbPlist_test.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/python:3.7.2 7 | steps: 8 | - checkout 9 | - run: sudo make build 10 | - store_artifacts: 11 | path: installed_files.txt 12 | test: 13 | docker: 14 | - image: circleci/python:3.7.2 15 | steps: 16 | - checkout 17 | - run: sudo make install-deps 18 | - run: make ci 19 | - store_test_results: 20 | path: htmlcov/ 21 | - store_test_results: 22 | path: test-reports/ 23 | - store_artifacts: 24 | path: test-reports/ 25 | - store_artifacts: 26 | path: htmlcov/ 27 | - store_artifacts: 28 | path: lint_output.txt 29 | - save_cache: 30 | key: pbPlist-{{ .Revision }} 31 | paths: 32 | - test-reports/ 33 | danger: 34 | docker: 35 | - image: circleci/ruby 36 | steps: 37 | - checkout 38 | - run: bundle install 39 | - restore_cache: 40 | key: pbPlist-{{ .Revision }} 41 | - run: bundle exec danger --verbose 42 | 43 | workflows: 44 | version: 2 45 | primary: 46 | jobs: 47 | - build 48 | - test: 49 | requires: 50 | - build 51 | - danger: 52 | requires: 53 | - test 54 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AlignAfterOpenBracket: false 3 | AlignOperands: false 4 | AlignTrailingComments: true 5 | 6 | AllowAllParametersOfDeclarationOnNextLine: false 7 | 8 | AllowShortBlocksOnASingleLine: false 9 | AllowShortCaseLabelsOnASingleLine: false 10 | AllowShortFunctionsOnASingleLine: None 11 | AllowShortIfStatementsOnASingleLine: false 12 | AllowShortLoopsOnASingleLine: false 13 | 14 | AlwaysBreakAfterDefinitionReturnType: false 15 | AlwaysBreakBeforeMultilineStrings: false 16 | 17 | BinPackArguments: false 18 | BinPackParameters: false 19 | 20 | BreakBeforeBinaryOperators: NonAssignment 21 | BreakBeforeBraces: Stroustrup 22 | BreakBeforeTernaryOperators: false 23 | 24 | ColumnLimit: 0 25 | IndentCaseLabels: true 26 | IndentWidth: 4 27 | TabWidth: 4 28 | UseTab: Always 29 | 30 | KeepEmptyLinesAtTheStartOfBlocks: true 31 | MaxEmptyLinesToKeep: 1 32 | 33 | SpaceBeforeAssignmentOperators: true 34 | SpaceBeforeParens: ControlStatements 35 | SpaceInEmptyParentheses: false 36 | SpacesInParentheses: false 37 | SpacesInSquareBrackets: false 38 | --- 39 | Language: Cpp 40 | ObjCSpaceAfterProperty: true 41 | ObjCSpaceBeforeProtocolList: true 42 | --- 43 | Language: Proto 44 | DisableFormat: true 45 | --- 46 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | radon: 3 | enabled: true 4 | 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - python 10 | exclude_fingerprints: 11 | FIXME: 12 | enabled: true 13 | 14 | ratings: 15 | paths: 16 | - pbPlist/ 17 | 18 | exclude_paths: 19 | - tests/ 20 | - tools/ 21 | - "*.md" 22 | - "*.yml" 23 | - Rakefile 24 | - Dangerfile 25 | - Gemfile 26 | - LICENSE 27 | - pyc.py 28 | - contributing/ 29 | - docs/ 30 | - setup.py 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### OS X ### 2 | .DS_Store 3 | 4 | ### C ### 5 | *.o 6 | 7 | ### Python ### 8 | *.pyc 9 | dist/ 10 | *.egg-info 11 | 12 | ### Xcode ### 13 | build/ 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata 23 | *.xccheckout 24 | *.moved-aside 25 | DerivedData 26 | *.xcuserstate 27 | 28 | ### Hopper ### 29 | *.hop 30 | 31 | 32 | ### Test Output ### 33 | output.plist 34 | /htmlcov 35 | /.tox 36 | /.coverage 37 | /lint_output.txt 38 | /installed_files.txt 39 | 40 | .python-version 41 | .coverage* 42 | .eggs/ 43 | coverage.xml 44 | test-reports/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # [Code of Conduct](./contributing/code-of-conduct.md) 2 | This project has and enforces a Code of Conduct. This must be adhered to at all times when engaging with maintainers not only on Github but also all other mediums (including social networks). The maintainers reserve the right to remove you from being able to contribute or comment on this project. 3 | 4 | --- 5 | 6 | # [Issues](../../wiki/github_issues) 7 | When a new issue is created it will use a template that will ask for all the relevant information. If there is something missing or confusing, please look at the contributing guidelines for Issues on the wiki. 8 | 9 | --- 10 | 11 | # [Pull Requests](../../wiki/github_prs) 12 | On each pull request, a new CI build will be dispatched. This will run the test suite and upload the results of the coverage report. This project uses a tool called Danger to analyze many aspects of a PR and report if anything about the PR breaks expectations that the repo has. If you are interested in what will be analyzed, then please look at the contributing guildelines for PRs on the wiki. 13 | 14 | --- 15 | 16 | You made it! If you have finished reading the contributing guide then you can put 🌈 in your issue or pull request to acknowledge that you have read this document and have done your best to adhere -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # this dangerfile sets values that will be consumed by the global danger 2 | # file. The global dangerfile is run automatically after this repo-specific 3 | # file is run. The global dangerfile is located at: https://github.com/samdmarshall/danger 4 | 5 | # set the number of lines that must be changed before this classifies as a 'Big PR' 6 | @SDM_DANGER_BIG_PR_LINES = 25 7 | 8 | # set the files to watch and warn about if there are changes made 9 | @SDM_DANGER_BUILD_FILES = ['Rakefile', 'Gemfile', 'Dangerfile', 'circle.yml', '.codeclimate.yml', 'tox.ini', 'pylintrc', 'install_requirements.txt'] 10 | 11 | # set the files to watch and warn about if there are 12 | @SDM_DANGER_INSTALL_REQUIREMENTS_FILES = ['requirements.txt', 'setup.py'] 13 | 14 | # set the files to watch and fail if there are changes 15 | @SDM_DANGER_IMMUTABLE_FILES = ['LICENSE', 'contributing.md', 'contributing/code-of-conduct.md'] 16 | 17 | # mark the paths that should be reported as part of the circle ci build artifacts 18 | @SDM_DANGER_REPORT_CIRCLE_CI_ARTIFACTS = Array[ 19 | { 20 | 'message'=> 'html coverage report', 21 | 'path'=> 'htmlcov/index.html' 22 | }, 23 | { 24 | 'message'=> 'linter report', 25 | 'path'=> 'lint_output.txt' 26 | } 27 | ] 28 | 29 | # Run the shared Dangerfile with these settings 30 | danger.import_dangerfile(github: 'samdmarshall/danger') 31 | 32 | junit.parse Dir["test-reports/*-py27.xml"][0] 33 | junit.report 34 | 35 | junit.parse Dir["test-reports/*-py35.xml"][0] 36 | junit.report 37 | 38 | junit.parse Dir["test-reports/*-py372.xml"][0] 39 | junit.report -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pyconfig 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | source 'https://rubygems.org' 32 | 33 | gem 'danger' 34 | gem 'danger-junit', :git => 'git@github.com:orta/danger-junit.git' 35 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git@github.com:orta/danger-junit.git 3 | revision: 32fc5d2c026604a0880a3901783628dab06e4877 4 | specs: 5 | danger-junit (1.0.0) 6 | danger (> 2.0) 7 | ox (~> 2.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | addressable (2.5.0) 13 | public_suffix (~> 2.0, >= 2.0.2) 14 | claide (1.0.1) 15 | claide-plugins (0.9.2) 16 | cork 17 | nap 18 | open4 (~> 1.3) 19 | colored (1.2) 20 | cork (0.2.0) 21 | colored (~> 1.2) 22 | danger (4.0.3) 23 | claide (~> 1.0) 24 | claide-plugins (>= 0.9.2) 25 | colored (~> 1.2) 26 | cork (~> 0.1) 27 | faraday (~> 0.9) 28 | faraday-http-cache (~> 1.0) 29 | git (~> 1) 30 | kramdown (~> 1.5) 31 | octokit (~> 4.2) 32 | terminal-table (~> 1) 33 | faraday (0.10.0) 34 | multipart-post (>= 1.2, < 3) 35 | faraday-http-cache (1.3.1) 36 | faraday (~> 0.8) 37 | git (1.3.0) 38 | kramdown (1.13.1) 39 | multipart-post (2.0.0) 40 | nap (1.1.0) 41 | octokit (4.6.2) 42 | sawyer (~> 0.8.0, >= 0.5.3) 43 | open4 (1.3.4) 44 | ox (2.10.0) 45 | public_suffix (2.0.4) 46 | sawyer (0.8.1) 47 | addressable (>= 2.3.5, < 2.6) 48 | faraday (~> 0.8, < 1.0) 49 | terminal-table (1.7.3) 50 | unicode-display_width (~> 1.1.1) 51 | unicode-display_width (1.1.2) 52 | 53 | PLATFORMS 54 | ruby 55 | 56 | DEPENDENCIES 57 | danger 58 | danger-junit! 59 | 60 | BUNDLED WITH 61 | 1.17.2 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Samantha Marshall 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Sam Marshall nor the names of its contributors may 15 | be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 26 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | # Variables 32 | 33 | # path to installation record that gets written when performing: 34 | # - make build2 35 | # - make build3 36 | 37 | INSTALLED_FILES_RECORD := ./installed_files.txt 38 | 39 | # names of the executables that are used as a part of this project 40 | 41 | PYTHON3_CMD := python3 42 | TOX_CMD := tox 43 | COVERAGE_CMD := coverage 44 | DANGER_CMD := danger 45 | GEM_CMD := gem 46 | FIND_CMD := find 47 | RM_CMD := rm 48 | WHICH_CMD := which 49 | XARGS_CMD := xargs 50 | PRINTF_CMD := printf 51 | TOUCH_CMD := touch 52 | CP_CMD := cp 53 | CAT_CMD := cat 54 | PIP_CMD := pip2 55 | PIP3_CMD := pip3 56 | CCTREPORTER_CMD := codeclimate-test-reporter 57 | UNAME_CMD := uname 58 | EXIT_CMD := exit 59 | TPUT_CMD := tput 60 | TR_CMD := tr 61 | PYLINT_CMD := pylint 62 | PYENV_CMD := pyenv 63 | GEM_CMD := gem 64 | TOX_PYENV := tox-pyenv 65 | 66 | 67 | # invoke the specific executable command 68 | 69 | PYTHON3 = $(shell command -v $(PYTHON3_CMD) 2> /dev/null) 70 | TOX = $(shell command -v $(TOX_CMD) 2> /dev/null) 71 | COVERAGE = $(shell command -v $(COVERAGE_CMD) 2> /dev/null) 72 | DANGER = $(shell command -v $(DANGER_CMD) 2> /dev/null) 73 | GEM = $(shell command -v $(GEM_CMD) 2> /dev/null) 74 | FIND = $(shell command -v $(FIND_CMD) 2> /dev/null) 75 | RM = $(shell command -v $(RM_CMD) 2> /dev/null) 76 | WHICH = $(shell command -v $(WHICH_CMD) 2> /dev/null) 77 | XARGS = $(shell command -v $(XARGS_CMD) 2> /dev/null) 78 | PRINTF = $(shell command -v $(PRINTF_CMD) 2> /dev/null) 79 | TOUCH = $(shell command -v $(TOUCH_CMD) 2> /dev/null) 80 | CP = $(shell command -v $(CP_CMD) 2> /dev/null) 81 | CAT = $(shell command -v $(CAT_CMD) 2> /dev/null) 82 | PIP = $(shell command -v $(PIP_CMD) 2> /dev/null) 83 | PIP3 = $(shell command -v $(PIP3_CMD) 2> /dev/null) 84 | CCTREPORTER = $(shell command -v $(CCTREPORTER_CMD) 2> /dev/null) 85 | UNAME = $(shell command -v $(UNAME_CMD) 2> /dev/null) 86 | EXIT = $(shell command -v $(EXIT_CMD) 2> /dev/null) 87 | TPUT = $(shell command -v $(TPUT_CMD) 2> /dev/null) 88 | TR = $(shell command -v $(TR_CMD) 2> /dev/null) 89 | PYLINT = $(shell command -v $(PYLINT_CMD) 2> /dev/null) 90 | PYENV = $(shell command -v $(PYENV_CMD) 2> /dev/null) 91 | GEM = $(shell command -v $(GEM_CMD) 2> /dev/null) 92 | 93 | SYSTEM := $(shell $(UNAME) -s) 94 | ifeq ($(SYSTEM),Darwin) 95 | USER_FLAG := --user 96 | else 97 | USER_FLAG := 98 | endif 99 | 100 | DISPLAY_SEPARATOR := $(PRINTF) "%*.s\n" 80 " " | $(TR) ' ' '=' 101 | 102 | # --- 103 | 104 | pipinstall = @$(PIP) install $1 $(USER_FLAG) 105 | pipthreeinstall = @$(PIP3_CMD) install $1 106 | geminstall = @$(GEM) install $1 107 | 108 | pyenv_exec = @$(PYENV_CMD) $1 $2 109 | 110 | install-deps: 111 | $(call pipthreeinstall,-r install_requirements.txt) 112 | @$(DISPLAY_SEPARATOR) 113 | 114 | install-gems: 115 | $(call geminstall,"bundler") 116 | bundle install 117 | @$(DISPLAY_SEPARATOR) 118 | 119 | # --- 120 | 121 | # this is for installing any tools that we don't already have 122 | 123 | install-tools: 124 | @$(PRINTF) "Installing git hooks..." 125 | @$(PYTHON2) ./tools/hooks-config.py 126 | @$(PRINTF) " done!\n" 127 | @$(DISPLAY_SEPARATOR) 128 | 129 | # --- 130 | 131 | removeall = $(RM) -rRf 132 | cleanlocation = @$(FIND) $1 $2 -print0 | $(XARGS) -0 $(removeall) 133 | 134 | clean: 135 | @$(PRINTF) "Removing existing installation... " 136 | @$(TOUCH) $(INSTALLED_FILES_RECORD) 137 | @$(CAT) $(INSTALLED_FILES_RECORD) | $(XARGS) $(removeall) 138 | @$(removeall) ./nslocalizer.egg-info 139 | @$(removeall) ./build 140 | @$(removeall) ./dist 141 | @$(removeall) ./.tox 142 | @$(removeall) .coverage 143 | @$(removeall) ./htmlcov 144 | @$(removeall) ./.eggs 145 | $(call cleanlocation, ., -name ".DS_Store") 146 | $(call cleanlocation, ., -name "*.pyc") 147 | $(call cleanlocation, ., -name "__pycache__" -type d) 148 | $(call cleanlocation, ., -name "*_output.plist") 149 | @$(PRINTF) "done!\n" 150 | @$(DISPLAY_SEPARATOR) 151 | 152 | 153 | # --- 154 | 155 | build: clean 156 | $(PYTHON3) ./setup.py install --record $(INSTALLED_FILES_RECORD) 157 | @$(DISPLAY_SEPARATOR) 158 | 159 | # --- 160 | 161 | test: clean 162 | $(TOX) || true 163 | @$(DISPLAY_SEPARATOR) 164 | 165 | # --- 166 | 167 | run_cctreporter = @$(PRINTF) "Checking CI branch to upload coverage results... " ; \ 168 | if [ "$(CIRCLE_BRANCH)" = "develop" ]; then \ 169 | $(PRINTF) "OK.\n"; \ 170 | $(CCTREPORTER) --token $(value CIRCLECI_CODECLIMATE_TOKEN) ; \ 171 | else \ 172 | $(PRINTF) "skipping.\n"; \ 173 | fi 174 | 175 | checktest = @$(PRINTF) "Checking that coverage data exists... " ; \ 176 | if [ -e ./.coverage ] ; then \ 177 | $(PRINTF) "ok!\n" ; \ 178 | else \ 179 | $(PRINTF) "not found!\n" ; \ 180 | $(PRINTF) "Please run 'make test' before running 'make report'\n" ; \ 181 | exit 1 ; \ 182 | fi \ 183 | 184 | report: 185 | $(COVERAGE) combine 186 | @$(call checktest) 187 | $(COVERAGE) report 188 | @$(DISPLAY_SEPARATOR) 189 | @$(PRINTF) "Generating html report... " 190 | @$(COVERAGE) html 191 | @$(PRINTF) "done!\n" 192 | @$(PRINTF) "Generated html report is located at: ./htmlcov/index.html\n" 193 | @$(DISPLAY_SEPARATOR) 194 | $(call run_cctreporter) 195 | @$(DISPLAY_SEPARATOR) 196 | 197 | # --- 198 | 199 | danger: 200 | @$(PRINTF) "Running danger " 201 | ifdef DANGER_GITHUB_API_TOKEN 202 | @$(PRINTF) "(PR)... \n" 203 | @bundle exec danger --verbose 204 | else 205 | @$(PRINTF) "(local)... \n" 206 | @bundle exec danger local --verbose 207 | endif 208 | @$(DISPLAY_SEPARATOR) 209 | 210 | # --- 211 | 212 | ci: test lint report 213 | 214 | # --- 215 | 216 | lint: 217 | @$(TOUCH) lint_output.txt 218 | @$(PRINTF) "Running linter... " 219 | @$(PYLINT) --rcfile=pylintrc pbPlist > lint_output.txt || true 220 | @$(PRINTF) " done!\n" 221 | @$(PRINTF) "Generated linter report: lint_output.txt\n" 222 | @$(DISPLAY_SEPARATOR) 223 | 224 | # --- 225 | 226 | .PHONY: danger lint ci report test build clean install-tools install-deps install-gems 227 | -------------------------------------------------------------------------------- /contributing/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /install_requirements.txt: -------------------------------------------------------------------------------- 1 | biplist == 1.0.1 2 | 3 | coverage 4 | tox 5 | codeclimate-test-reporter 6 | pylint 7 | unittest-xml-reporting 8 | -------------------------------------------------------------------------------- /pbPlist/StrParse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import sys 32 | import string 33 | 34 | if sys.version_info >= (3, 0): 35 | def unichr(character): # pylint: disable=redefined-builtin 36 | return chr(character) 37 | 38 | def ConvertNEXTSTEPToUnicode(hex_digits): 39 | # taken from http://ftp.unicode.org/Public/MAPPINGS/VENDORS/NEXT/NEXTSTEP.TXT 40 | conversion = { 41 | "80": "a0", # NO-BREAK SPACE 42 | "81": "c0", # LATIN CAPITAL LETTER A WITH GRAVE 43 | "82": "c1", # LATIN CAPITAL LETTER A WITH ACUTE 44 | "83": "c2", # LATIN CAPITAL LETTER A WITH CIRCUMFLEX 45 | "84": "c3", # LATIN CAPITAL LETTER A WITH TILDE 46 | "85": "c4", # LATIN CAPITAL LETTER A WITH DIAERESIS 47 | "86": "c5", # LATIN CAPITAL LETTER A WITH RING 48 | "87": "c7", # LATIN CAPITAL LETTER C WITH CEDILLA 49 | "88": "c8", # LATIN CAPITAL LETTER E WITH GRAVE 50 | "89": "c9", # LATIN CAPITAL LETTER E WITH ACUTE 51 | "8a": "ca", # LATIN CAPITAL LETTER E WITH CIRCUMFLEX 52 | "8b": "cb", # LATIN CAPITAL LETTER E WITH DIAERESIS 53 | "8c": "cc", # LATIN CAPITAL LETTER I WITH GRAVE 54 | "8d": "cd", # LATIN CAPITAL LETTER I WITH ACUTE 55 | "8e": "ce", # LATIN CAPITAL LETTER I WITH CIRCUMFLEX 56 | "8f": "cf", # LATIN CAPITAL LETTER I WITH DIAERESIS 57 | "90": "d0", # LATIN CAPITAL LETTER ETH 58 | "91": "d1", # LATIN CAPITAL LETTER N WITH TILDE 59 | "92": "d2", # LATIN CAPITAL LETTER O WITH GRAVE 60 | "93": "d3", # LATIN CAPITAL LETTER O WITH ACUTE 61 | "94": "d4", # LATIN CAPITAL LETTER O WITH CIRCUMFLEX 62 | "95": "d5", # LATIN CAPITAL LETTER O WITH TILDE 63 | "96": "d6", # LATIN CAPITAL LETTER O WITH DIAERESIS 64 | "97": "d9", # LATIN CAPITAL LETTER U WITH GRAVE 65 | "98": "da", # LATIN CAPITAL LETTER U WITH ACUTE 66 | "99": "db", # LATIN CAPITAL LETTER U WITH CIRCUMFLEX 67 | "9a": "dc", # LATIN CAPITAL LETTER U WITH DIAERESIS 68 | "9b": "dd", # LATIN CAPITAL LETTER Y WITH ACUTE 69 | "9c": "de", # LATIN CAPITAL LETTER THORN 70 | "9d": "b5", # MICRO SIGN 71 | "9e": "d7", # MULTIPLICATION SIGN 72 | "9f": "f7", # DIVISION SIGN 73 | "a0": "a9", # COPYRIGHT SIGN 74 | "a1": "a1", # INVERTED EXCLAMATION MARK 75 | "a2": "a2", # CENT SIGN 76 | "a3": "a3", # POUND SIGN 77 | "a4": "44", # FRACTION SLASH 78 | "a5": "a5", # YEN SIGN 79 | "a6": "92", # LATIN SMALL LETTER F WITH HOOK 80 | "a7": "a7", # SECTION SIGN 81 | "a8": "a4", # CURRENCY SIGN 82 | "a9": "19", # RIGHT SINGLE QUOTATION MARK 83 | "aa": "1c", # LEFT DOUBLE QUOTATION MARK 84 | "ab": "ab", # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK 85 | "ac": "39", # SINGLE LEFT-POINTING ANGLE QUOTATION MARK 86 | "ad": "3a", # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK 87 | "ae": "01", # LATIN SMALL LIGATURE FI 88 | "af": "02", # LATIN SMALL LIGATURE FL 89 | "b0": "ae", # REGISTERED SIGN 90 | "b1": "13", # EN DASH 91 | "b2": "20", # DAGGER 92 | "b3": "21", # DOUBLE DAGGER 93 | "b4": "b7", # MIDDLE DOT 94 | "b5": "a6", # BROKEN BAR 95 | "b6": "b6", # PILCROW SIGN 96 | "b7": "22", # BULLET 97 | "b8": "1a", # SINGLE LOW-9 QUOTATION MARK 98 | "b9": "1e", # DOUBLE LOW-9 QUOTATION MARK 99 | "ba": "1d", # RIGHT DOUBLE QUOTATION MARK 100 | "bb": "bb", # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK 101 | "bc": "26", # HORIZONTAL ELLIPSIS 102 | "bd": "30", # PER MILLE SIGN 103 | "be": "ac", # NOT SIGN 104 | "bf": "bf", # INVERTED QUESTION MARK 105 | "c0": "b9", # SUPERSCRIPT ONE 106 | "c1": "cb", # MODIFIER LETTER GRAVE ACCENT 107 | "c2": "b4", # ACUTE ACCENT 108 | "c3": "c6", # MODIFIER LETTER CIRCUMFLEX ACCENT 109 | "c4": "dc", # SMALL TILDE 110 | "c5": "af", # MACRON 111 | "c6": "d8", # BREVE 112 | "c7": "d9", # DOT ABOVE 113 | "c8": "a8", # DIAERESIS 114 | "c9": "b2", # SUPERSCRIPT TWO 115 | "ca": "da", # RING ABOVE 116 | "cb": "b8", # CEDILLA 117 | "cc": "b3", # SUPERSCRIPT THREE 118 | "cd": "dd", # DOUBLE ACUTE ACCENT 119 | "ce": "db", # OGONEK 120 | "cf": "c7", # CARON 121 | "d0": "14", # EM DASH 122 | "d1": "b1", # PLUS-MINUS SIGN 123 | "d2": "bc", # VULGAR FRACTION ONE QUARTER 124 | "d3": "bd", # VULGAR FRACTION ONE HALF 125 | "d4": "be", # VULGAR FRACTION THREE QUARTERS 126 | "d5": "e0", # LATIN SMALL LETTER A WITH GRAVE 127 | "d6": "e1", # LATIN SMALL LETTER A WITH ACUTE 128 | "d7": "e2", # LATIN SMALL LETTER A WITH CIRCUMFLEX 129 | "d8": "e3", # LATIN SMALL LETTER A WITH TILDE 130 | "d9": "e4", # LATIN SMALL LETTER A WITH DIAERESIS 131 | "da": "e5", # LATIN SMALL LETTER A WITH RING ABOVE 132 | "db": "e7", # LATIN SMALL LETTER C WITH CEDILLA 133 | "dc": "e8", # LATIN SMALL LETTER E WITH GRAVE 134 | "dd": "e9", # LATIN SMALL LETTER E WITH ACUTE 135 | "de": "ea", # LATIN SMALL LETTER E WITH CIRCUMFLEX 136 | "df": "eb", # LATIN SMALL LETTER E WITH DIAERESIS 137 | "e0": "ec", # LATIN SMALL LETTER I WITH GRAVE 138 | "e1": "c6", # LATIN CAPITAL LETTER AE 139 | "e2": "ed", # LATIN SMALL LETTER I WITH ACUTE 140 | "e3": "aa", # FEMININE ORDINAL INDICATOR 141 | "e4": "ee", # LATIN SMALL LETTER I WITH CIRCUMFLEX 142 | "e5": "ef", # LATIN SMALL LETTER I WITH DIAERESIS 143 | "e6": "f0", # LATIN SMALL LETTER ETH 144 | "e7": "f1", # LATIN SMALL LETTER N WITH TILDE 145 | "e8": "41", # LATIN CAPITAL LETTER L WITH STROKE 146 | "e9": "d8", # LATIN CAPITAL LETTER O WITH STROKE 147 | "ea": "52", # LATIN CAPITAL LIGATURE OE 148 | "eb": "ba", # MASCULINE ORDINAL INDICATOR 149 | "ec": "f2", # LATIN SMALL LETTER O WITH GRAVE 150 | "ed": "f3", # LATIN SMALL LETTER O WITH ACUTE 151 | "ee": "f4", # LATIN SMALL LETTER O WITH CIRCUMFLEX 152 | "ef": "f5", # LATIN SMALL LETTER O WITH TILDE 153 | "f0": "f6", # LATIN SMALL LETTER O WITH DIAERESIS 154 | "f1": "e6", # LATIN SMALL LETTER AE 155 | "f2": "f9", # LATIN SMALL LETTER U WITH GRAVE 156 | "f3": "fa", # LATIN SMALL LETTER U WITH ACUTE 157 | "f4": "fb", # LATIN SMALL LETTER U WITH CIRCUMFLEX 158 | "f5": "31", # LATIN SMALL LETTER DOTLESS I 159 | "f6": "fc", # LATIN SMALL LETTER U WITH DIAERESIS 160 | "f7": "fd", # LATIN SMALL LETTER Y WITH ACUTE 161 | "f8": "42", # LATIN SMALL LETTER L WITH STROKE 162 | "f9": "f8", # LATIN SMALL LETTER O WITH STROKE 163 | "fa": "53", # LATIN SMALL LIGATURE OE 164 | "fb": "df", # LATIN SMALL LETTER SHARP S 165 | "fc": "fe", # LATIN SMALL LETTER THORN 166 | "fd": "ff", # LATIN SMALL LETTER Y WITH DIAERESIS 167 | "fe": "fd", # .notdef, REPLACEMENT CHARACTER 168 | "ff": "fd", # .notdef, REPLACEMENT CHARACTER 169 | } 170 | return conversion[hex_digits] 171 | 172 | def IsOctalNumber(character): 173 | oct_digits = set(string.octdigits) 174 | return set(character).issubset(oct_digits) 175 | 176 | def IsHexNumber(character): 177 | hex_digits = set(string.hexdigits) 178 | return set(character).issubset(hex_digits) 179 | 180 | def SanitizeCharacter(character): 181 | char = character 182 | escaped_characters = { 183 | '\a': '\\a', 184 | '\b': '\\b', 185 | '\f': '\\f', 186 | '\n': '\\n', 187 | '\r': '\\r', 188 | '\t': '\\t', 189 | '\v': '\\v', 190 | '\"': '\\"', 191 | } 192 | if character in escaped_characters.keys(): 193 | char = escaped_characters[character] 194 | return char 195 | 196 | # http://www.opensource.apple.com/source/CF/CF-744.19/CFOldStylePList.c See `getSlashedChar()` 197 | def UnQuotifyString(string_data, start_index, end_index): # pylint: disable=too-many-locals,too-many-branches,too-many-statements 198 | formatted_string = '' 199 | extracted_string = string_data[start_index:end_index] 200 | string_length = len(extracted_string) 201 | all_cases = ['0', '1', '2', '3', '4', '5', '6', '7', 'a', 'b', 'f', 'n', 'r', 't', 'v', '\"', '\n', 'U'] 202 | index = 0 203 | while index < string_length: # pylint: disable=too-many-nested-blocks 204 | current_char = extracted_string[index] 205 | if current_char == '\\': 206 | next_char = extracted_string[index+1] 207 | if next_char in all_cases: 208 | index += 1 209 | if next_char == 'a': 210 | formatted_string += '\a' 211 | if next_char == 'b': 212 | formatted_string += '\b' 213 | if next_char == 'f': 214 | formatted_string += '\f' 215 | if next_char == 'n': 216 | formatted_string += '\n' 217 | if next_char == 'r': 218 | formatted_string += '\r' 219 | if next_char == 't': 220 | formatted_string += '\t' 221 | if next_char == 'v': 222 | formatted_string += '\v' 223 | if next_char == '"': 224 | formatted_string += '\"' 225 | if next_char == '\n': 226 | formatted_string += '\n' 227 | if next_char == 'U': 228 | starting_index = index + 1 229 | ending_index = starting_index + 4 230 | unicode_numbers = extracted_string[starting_index:ending_index] 231 | for number in unicode_numbers: 232 | index += 1 233 | if IsHexNumber(number) is False: # pragma: no cover 234 | message = 'Invalid unicode sequence on line '+str(LineNumberForIndex(string_data, start_index+index)) 235 | raise Exception(message) 236 | formatted_string += unichr(int(unicode_numbers, 16)) 237 | if IsOctalNumber(next_char) is True: # https://twitter.com/Catfish_Man/status/658014170055507968 238 | starting_index = index 239 | ending_index = starting_index + 1 240 | for oct_index in range(3): 241 | test_index = starting_index + oct_index 242 | test_oct = extracted_string[test_index] 243 | if IsOctalNumber(test_oct) is True: 244 | ending_index += 1 245 | octal_numbers = extracted_string[starting_index:ending_index] 246 | hex_number = int(octal_numbers, 8) 247 | hex_str = format(hex_number, 'x') 248 | if hex_number >= 0x80: 249 | hex_str = ConvertNEXTSTEPToUnicode(hex_str) 250 | formatted_string += unichr(int('00'+hex_str, 16)) 251 | else: 252 | formatted_string += current_char 253 | index += 1 254 | formatted_string += next_char 255 | else: 256 | formatted_string += current_char 257 | index += 1 258 | return formatted_string 259 | 260 | def LineNumberForIndex(string_data, current_index): 261 | line_number = 1 262 | index = 0 263 | string_length = len(string_data) 264 | while (index < current_index) and (index < string_length): 265 | current_char = string_data[index] 266 | if IsNewline(current_char) is True: 267 | line_number += 1 268 | index += 1 269 | return line_number 270 | 271 | def IsValidUnquotedStringCharacter(character): 272 | if len(character) == 1: 273 | valid_characters = set(string.ascii_letters+string.digits+'_$/:.-') 274 | return set(character).issubset(valid_characters) 275 | else: # pragma: no cover 276 | message = 'The function "IsValidUnquotedStringCharacter()" can only take single characters!' 277 | raise ValueError(message) 278 | 279 | def IsSpecialWhitespace(character): 280 | value = ord(character) 281 | result = (value >= 9 and value <= 13) # tab, newline, vt, form feed, carriage return 282 | return result 283 | 284 | def IsUnicodeSeparator(character): 285 | value = ord(character) 286 | result = (value == 8232 or value == 8233) 287 | return result 288 | 289 | def IsRegularWhitespace(character): 290 | value = ord(character) 291 | result = (value == 32 or IsUnicodeSeparator(character)) # space and Unicode line sep, para sep 292 | return result 293 | 294 | def IsDataFormattingWhitespace(character): 295 | value = ord(character) 296 | result = (IsNewline(character) or IsRegularWhitespace(character) or value == 9) 297 | return result 298 | 299 | def IsNewline(character): 300 | value = ord(character) 301 | result = (value == 13 or value == 10) 302 | return result 303 | 304 | def IsEndOfLine(character): 305 | result = (IsNewline(character) or IsUnicodeSeparator(character)) 306 | return result 307 | 308 | def IndexOfNextNonSpace(string_data, current_index): # pylint: disable=too-many-branches,too-many-statements 309 | successful = False 310 | found_index = current_index 311 | string_length = len(string_data) 312 | annotation_string = '' 313 | while found_index < string_length: # pylint: disable=too-many-nested-blocks 314 | current_char = string_data[found_index] 315 | if IsSpecialWhitespace(current_char) is True: 316 | found_index += 1 317 | continue 318 | if IsRegularWhitespace(current_char) is True: 319 | found_index += 1 320 | continue 321 | if current_char == '/': 322 | next_index = found_index + 1 323 | if next_index >= string_length: 324 | successful = True 325 | break 326 | else: 327 | next_character = string_data[next_index] 328 | if next_character == '/': # found a line comment "//" 329 | found_index += 1 330 | next_index = found_index 331 | first_pass = True 332 | while next_index < string_length: 333 | test_char = string_data[next_index] 334 | if IsEndOfLine(test_char) is True: 335 | break 336 | else: 337 | if first_pass is False: 338 | annotation_string += test_char 339 | else: 340 | first_pass = False 341 | next_index += 1 342 | found_index = next_index 343 | elif next_character == '*': # found a block comment "/* ... */" 344 | found_index += 1 345 | next_index = found_index 346 | first_pass = True 347 | while next_index < string_length: 348 | test_char = string_data[next_index] 349 | if test_char == '*' and (next_index+1 < string_length) and string_data[next_index+1] == '/': 350 | next_index += 2 351 | break 352 | else: 353 | if first_pass != True: 354 | annotation_string += test_char 355 | else: 356 | first_pass = False 357 | next_index += 1 358 | found_index = next_index 359 | else: 360 | successful = True 361 | break 362 | else: 363 | successful = True 364 | break 365 | result = (successful, found_index, annotation_string) 366 | return result 367 | -------------------------------------------------------------------------------- /pbPlist/Switch.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pylocalizer 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | # Original code taken from http://code.activestate.com/recipes/410692/ 32 | 33 | class Switch(object): 34 | def __init__(self, value): 35 | self.value = value 36 | self.fall = False 37 | 38 | def __iter__(self): 39 | """Return the match method once, then stop""" 40 | yield self.match 41 | 42 | def match(self, *args): 43 | """Indicate whether or not to enter a case suite""" 44 | result = False 45 | if self.fall or not args: 46 | result = True 47 | elif self.value in args: # changed for v1.5, see below 48 | self.fall = True 49 | result = True 50 | return result 51 | -------------------------------------------------------------------------------- /pbPlist/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from . import pbPlist 32 | __version__ = '1.0.4' -------------------------------------------------------------------------------- /pbPlist/pbItem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from . import StrParse 32 | 33 | def PushIndent(indent_level): 34 | return indent_level + 1 35 | 36 | def PopIndent(indent_level): 37 | return indent_level - 1 38 | 39 | def WriteIndent(level=0): 40 | output = '' 41 | for _index in range(level): 42 | output += '\t' 43 | return output 44 | 45 | def WriteNewline(level=0, indent=True): 46 | output = '\n' 47 | if indent: 48 | output += WriteIndent(level) 49 | return output 50 | 51 | class pbItem(object): 52 | def __init__(self, value=None, type_name=None, annotation=None): 53 | if value != None and type_name != None: 54 | self.value = value 55 | if type_name not in KnownTypes.keys(): # pragma: no cover 56 | message = 'Unknown type "'+type_name+'" passed to '+self.__class__.__name__+' initializer!' 57 | raise TypeError(message) 58 | self.type_name = type_name 59 | self.annotation = annotation 60 | else: # pragma: no cover 61 | message = 'The class "'+self.__class__.__name__+'" must be initialized with a non-None value' 62 | raise ValueError(message) 63 | 64 | def __eq__(self, other): 65 | is_equal = False 66 | if isinstance(other, pbItem): 67 | other = other.value 68 | if type(other) is type(self.value): 69 | is_equal = self.value.__eq__(other) 70 | return is_equal 71 | 72 | def __hash__(self): 73 | return self.value.__hash__() 74 | 75 | def __repr__(self): 76 | return self.value.__repr__() 77 | 78 | def __iter__(self): 79 | return self.value.__iter__() 80 | 81 | def __getattr__(self, attrib): 82 | return self.value.__getattr__(attrib) 83 | 84 | def __str__(self): 85 | return self.writeStringRep(0, False)[0] 86 | 87 | def __getitem__(self, key): 88 | return self.value.__getitem__(key) 89 | 90 | def __setitem__(self, key, value): 91 | self.value.__setitem__(key, value) 92 | 93 | def __len__(self): 94 | return self.value.__len__() 95 | 96 | def __contains__(self, item): 97 | return self.value.__contains__(item) 98 | 99 | def __get__(self, obj, objtype): 100 | return self.value.__get__(obj, objtype) 101 | 102 | def writeStringRep(self, indent_level=0, pretty=True): 103 | return self.writeString(indent_level, pretty) 104 | 105 | def writeString(self, indent_level=0, pretty=True): # pylint: disable=no-self-use,unused-variable,unused-argument ; # pragma: no cover 106 | message = 'This is a base class, it cannot write!' 107 | raise Exception(message) 108 | 109 | def nativeType(self): 110 | return self.value 111 | 112 | def writeAnnotation(self): 113 | output_string = '' 114 | if self.annotation != None and len(self.annotation) > 0: 115 | output_string += ' ' 116 | output_string += '/*' 117 | output_string += self.annotation 118 | output_string += '*/' 119 | return output_string 120 | 121 | class pbString(pbItem): 122 | def writeString(self, indent_level=0, pretty=True): 123 | string_string = '' 124 | string_string += self.value 125 | if pretty is True: 126 | string_string += self.writeAnnotation() 127 | return (string_string, indent_level) 128 | 129 | class pbQString(pbItem): 130 | def writeStringRep(self, indent_level=0, pretty=True): 131 | qstring_string = '' 132 | for character in self.value: 133 | qstring_string += StrParse.SanitizeCharacter(character) 134 | return (qstring_string, indent_level) 135 | 136 | def writeString(self, indent_level=0, pretty=True): 137 | qstring_string = '' 138 | qstring_string += '"' 139 | string_rep, indent_level = self.writeStringRep(indent_level, pretty) 140 | qstring_string += string_rep 141 | qstring_string += '"' 142 | if pretty is True: 143 | qstring_string += self.writeAnnotation() 144 | return (qstring_string, indent_level) 145 | 146 | class pbData(pbItem): 147 | def writeString(self, indent_level=0, pretty=True): 148 | data_string = '' 149 | indent_level = PushIndent(indent_level) 150 | data_string += '<' 151 | grouping_byte_counter = 0 152 | grouping_line_counter = 0 153 | for hex_byte in map(ord, self.value.decode()): 154 | data_string += format(hex_byte, 'x') 155 | grouping_byte_counter += 1 156 | # write a space every 4th byte 157 | if grouping_byte_counter == 4: 158 | data_string += ' ' 159 | grouping_byte_counter = 0 160 | grouping_line_counter += 1 161 | # write a newline every 4th grouping of bytes 162 | if grouping_line_counter == 4: 163 | data_string += WriteNewline(indent_level) 164 | data_string += ' ' # indent an additional space to make the byte groupings line up 165 | grouping_line_counter = 0 166 | data_string += '>' 167 | if pretty is True: 168 | data_string += self.writeAnnotation() 169 | indent_level = PopIndent(indent_level) 170 | return (data_string, indent_level) 171 | 172 | class pbDictionary(pbItem): 173 | def nativeType(self): 174 | new_value = dict() 175 | for key in self.keys(): 176 | value = self[key] 177 | new_value[str(key)] = value.nativeType() 178 | return new_value 179 | def writeString(self, indent_level=0, pretty=True): 180 | dictionary_string = '' 181 | dictionary_string += '{' 182 | has_sorted_keys, keys_array = self.value.sortedKeys() 183 | dictionary_string += WriteNewline(indent_level, not has_sorted_keys) 184 | indent_level = PushIndent(indent_level) 185 | previous_value_type = None 186 | if len(keys_array) == 0: 187 | indent_level = PopIndent(indent_level) 188 | else: 189 | if not has_sorted_keys: 190 | dictionary_string += '\t' 191 | for key in keys_array: 192 | if has_sorted_keys: 193 | current_value_type = str(self.value[key]['isa']) 194 | if previous_value_type != current_value_type: 195 | if previous_value_type != None: 196 | dictionary_string += '/* End '+previous_value_type+' section */' 197 | dictionary_string += WriteNewline(indent_level, False) 198 | previous_value_type = current_value_type 199 | dictionary_string += '\n/* Begin '+current_value_type+' section */' 200 | dictionary_string += WriteNewline(indent_level) 201 | else: 202 | dictionary_string += WriteIndent(indent_level) 203 | write_string, indent_level = key.writeString(indent_level, pretty) 204 | dictionary_string += write_string 205 | dictionary_string += ' = ' 206 | write_string, indent_level = self.value[key].writeString(indent_level, pretty) 207 | dictionary_string += write_string 208 | dictionary_string += ';' 209 | should_indent = True 210 | is_last_key = (key == keys_array[-1]) 211 | if is_last_key: 212 | if has_sorted_keys: 213 | dictionary_string += WriteNewline(indent_level, False) 214 | dictionary_string += '/* End '+previous_value_type+' section */' 215 | indent_level = PopIndent(indent_level) 216 | else: 217 | if has_sorted_keys: 218 | should_indent = False 219 | dictionary_string += WriteNewline(indent_level, should_indent) 220 | dictionary_string += '}' 221 | return (dictionary_string, indent_level) 222 | 223 | class pbArray(pbItem): 224 | def nativeType(self): 225 | new_value = [item.nativeType() for item in self.value] 226 | return new_value 227 | def writeString(self, indent_level=0, pretty=True): 228 | array_string = '' 229 | array_string += '(' 230 | array_string += WriteNewline(indent_level) 231 | indent_level = PushIndent(indent_level) 232 | values_array = list(self.value) 233 | if len(values_array) == 0: 234 | indent_level = PopIndent(indent_level) 235 | else: 236 | array_string += '\t' 237 | for value in values_array: 238 | write_string, indent_level = value.writeString(indent_level, pretty) 239 | array_string += write_string 240 | if value != values_array[-1]: 241 | array_string += ',' 242 | else: 243 | indent_level = PopIndent(indent_level) 244 | array_string += WriteNewline(indent_level) 245 | array_string += ')' 246 | return (array_string, indent_level) 247 | 248 | KnownTypes = { 249 | 'string': pbString, 250 | 'qstring': pbQString, 251 | 'data': pbData, 252 | 'dictionary': pbDictionary, 253 | 'array': pbArray, 254 | } 255 | 256 | def pbItemResolver(obj, type_name): 257 | initializer = KnownTypes[type_name] 258 | if initializer: 259 | return initializer(obj, type_name) 260 | else: # pragma: no cover 261 | message = 'Unknown type "'+type_name+'" passed to pbItemResolver!' 262 | raise TypeError(message) 263 | -------------------------------------------------------------------------------- /pbPlist/pbParser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from __future__ import print_function 32 | import os 33 | import sys 34 | import codecs 35 | from . import StrParse 36 | from . import pbRoot 37 | from . import pbItem 38 | from .Switch import Switch 39 | 40 | def GetFileEncoding(path): 41 | encoding = 'utf-8-sig' 42 | 43 | size = os.path.getsize(path) 44 | if size > 2: 45 | file_descriptor = OpenFile(path) 46 | first_two_bytes = file_descriptor.read(2) 47 | file_descriptor.close() 48 | 49 | for case in Switch(first_two_bytes): 50 | if case(codecs.BOM_UTF16): 51 | encoding = 'utf-16' 52 | break 53 | if case(codecs.BOM_UTF16_LE): 54 | encoding = 'utf-16-le' 55 | break 56 | if case(codecs.BOM_UTF16_BE): 57 | encoding = 'utf-16-be' 58 | break 59 | if case(): 60 | break # pragma: no cover 61 | 62 | return encoding 63 | 64 | def OpenFileWithEncoding(file_path, encoding): 65 | return codecs.open(file_path, 'r', encoding=encoding, errors='ignore') 66 | 67 | if sys.version_info < (3, 0): 68 | def OpenFile(file_path): 69 | return open(file_path, 'rb') 70 | else: 71 | def OpenFile(file_path): 72 | return open(file_path, 'br') 73 | 74 | class PBParser(object): 75 | 76 | def __init__(self, file_path=None): 77 | self.index = 0 78 | self.string_encoding = None 79 | self.file_path = file_path 80 | self.file_type = None 81 | try: 82 | encoding = GetFileEncoding(self.file_path) 83 | file_descriptor = OpenFileWithEncoding(self.file_path, encoding) 84 | self.data = file_descriptor.read() 85 | if self.file_path.endswith('.strings'): 86 | self.data = '{'+self.data+'}' 87 | file_descriptor.close() 88 | except IOError as exception: # pragma: no cover 89 | print('I/O error({0}): {1}'.format(exception.errno, exception.strerror)) 90 | except: # pragma: no cover 91 | print('Unexpected error:'+str(sys.exc_info()[0])) 92 | raise 93 | 94 | def read(self): 95 | parsed_plist = None 96 | prefix = self.data[0:6] 97 | for case in Switch(prefix): 98 | if case('bplist'): 99 | self.file_type = 'binary' 100 | import biplist 101 | parsed_plist = biplist.readPlist(self.file_path) 102 | break 103 | if case('= (3, 9): 107 | with OpenFile(self.file_path) as fd: 108 | parsed_plist = plistlib.load(fd) 109 | else: 110 | parsed_plist = plistlib.readPlist(self.file_path) 111 | break 112 | if case(): 113 | self.file_type = 'ascii' 114 | # test for encoding hint 115 | if self.data[0:2] == '//': 116 | # this is to try to see if we can locate the desired string encoding of the file 117 | import re 118 | result = re.search('^// !\$\*(.+?)\*\$!', self.data) # pylint: disable=anomalous-backslash-in-string 119 | if result: 120 | self.string_encoding = result.group(1) 121 | #now return the parse 122 | parsed_plist = self.__readTest(True) 123 | break 124 | return parsed_plist 125 | 126 | def __readTest(self, requires_object=True): 127 | read_result = None 128 | # can we parse this? 129 | can_parse, self.index, _annotation = StrParse.IndexOfNextNonSpace(self.data, self.index) 130 | # we can ignore the annotation value here 131 | if not can_parse: 132 | if self.index != len(self.data): 133 | if requires_object is True: # pragma: no cover 134 | message = 'Invalid plist file!' 135 | raise Exception(message) 136 | else: 137 | read_result = self.__parse(requires_object) 138 | return read_result 139 | 140 | def __parse(self, requires_object=True): 141 | parsed_item = None 142 | starting_character = self.data[self.index] 143 | for case in Switch(starting_character): 144 | if case('{'): 145 | # parse dictionary 146 | parsed_item = pbItem.pbItemResolver(self.__parseDict(), 'dictionary') # pylint: disable=redefined-variable-type 147 | break 148 | if case('('): 149 | # parse array 150 | parsed_item = pbItem.pbItemResolver(self.__parseArray(), 'array') # pylint: disable=redefined-variable-type 151 | break 152 | if case('<'): 153 | # parse data 154 | parsed_item = pbItem.pbItemResolver(self.__parseData(), 'data') # pylint: disable=redefined-variable-type 155 | break 156 | if case('\''): 157 | pass 158 | if case('\"'): 159 | # parse quoted string 160 | parsed_item = pbItem.pbItemResolver(self.__parseQuotedString(), 'qstring') # pylint: disable=redefined-variable-type 161 | break 162 | if case(): 163 | if StrParse.IsValidUnquotedStringCharacter(starting_character) is True: 164 | # parse unquoted string 165 | parsed_item = pbItem.pbItemResolver(self.__parseUnquotedString(), 'string') # pylint: disable=redefined-variable-type 166 | else: 167 | if requires_object is True: # pragma: no cover 168 | message = 'Unexpected character "0x%s" at line %i of file %s' % (str(format(ord(starting_character), 'x')), StrParse.LineNumberForIndex(self.data, self.index), self.file_path) 169 | raise Exception(message) 170 | return parsed_item 171 | 172 | def __parseUnquotedString(self): 173 | string_length = len(self.data) 174 | start_index = self.index 175 | while self.index < string_length: 176 | current_char = self.data[self.index] 177 | if StrParse.IsValidUnquotedStringCharacter(current_char) is True: 178 | self.index += 1 179 | else: 180 | break 181 | if start_index != self.index: 182 | return self.data[start_index:self.index] 183 | else: # pragma: no cover 184 | message = 'Unexpected EOF in file %s' % self.file_path 185 | raise Exception(message) 186 | 187 | def __parseQuotedString(self): 188 | quote = self.data[self.index] 189 | string_length = len(self.data) 190 | self.index += 1 # skip over the first quote 191 | start_index = self.index 192 | while self.index < string_length: 193 | current_char = self.data[self.index] 194 | if current_char == quote: 195 | break 196 | if current_char == '\\': 197 | self.index += 2 198 | else: 199 | self.index += 1 200 | if self.index >= string_length: # pragma: no cover 201 | message = 'Unterminated quoted string starting on line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) 202 | raise Exception(message) 203 | else: 204 | string_without_quotes = StrParse.UnQuotifyString(self.data, start_index, self.index) 205 | self.index += 1 # advance past quote character 206 | return string_without_quotes 207 | 208 | def __parseData(self): 209 | string_length = len(self.data) 210 | self.index += 1 # skip over "<" 211 | start_index = self.index 212 | end_index = 0 213 | byte_stream = '' 214 | while self.index < string_length: 215 | current_char = self.data[self.index] 216 | if current_char == '>': 217 | self.index += 1 # move past the ">" 218 | end_index = self.index 219 | break 220 | if StrParse.IsHexNumber(current_char) is True: 221 | byte_stream += current_char 222 | else: 223 | if not StrParse.IsDataFormattingWhitespace(current_char): # pragma: no cover 224 | message = 'Malformed data byte group (invalid hex) at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) 225 | raise Exception(message) 226 | self.index += 1 227 | if (len(byte_stream) % 2) == 1: # pragma: no cover 228 | message = 'Malformed data byte group (uneven length) at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) 229 | raise Exception(message) 230 | if end_index == 0: # pragma: no cover 231 | message = 'Expected terminating >" for data at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) 232 | raise Exception(message) 233 | data_object = bytearray.fromhex(byte_stream) 234 | return data_object 235 | 236 | def __parseArray(self): 237 | array_objects = list() 238 | self.index += 1 # move past the "(" 239 | start_index = self.index 240 | new_object = self.__readTest(False) 241 | while new_object is not None: 242 | can_parse, self.index, new_object.annotation = StrParse.IndexOfNextNonSpace(self.data, self.index) 243 | _can_parse = can_parse # pylint: disable=unused-variable 244 | array_objects.append(new_object) 245 | current_char = self.data[self.index] 246 | if current_char == ',': 247 | self.index += 1 248 | new_object = self.__readTest(False) 249 | current_char = self.data[self.index] 250 | if current_char != ')': # pragma: no cover 251 | message = 'Expected terminating ")" for array at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) 252 | raise Exception(message) 253 | self.index += 1 # skip over ending ")" 254 | return array_objects 255 | 256 | def __parseDict(self): 257 | dictionary = pbRoot.pbRoot() 258 | self.index += 1 # move past the "{" 259 | start_index = self.index 260 | new_object = self.__readTest(False) 261 | while new_object is not None: 262 | can_parse, self.index, new_object.annotation = StrParse.IndexOfNextNonSpace(self.data, self.index) 263 | _can_parse = can_parse # pylint: disable=unused-variable 264 | key_object = new_object 265 | current_char = self.data[self.index] 266 | value_object = None 267 | for case in Switch(current_char): 268 | if case('='): 269 | self.index += 1 270 | value_object = self.__readTest(True) 271 | break 272 | if case(';'): 273 | # this is for strings files where the key and the value may be the same thing 274 | self.index += 1 275 | value_object = pbItem.pbItemResolver(new_object.value, new_object.type_name) 276 | value_object.annotation = new_object.annotation 277 | break 278 | if case(): # pragma: no cover 279 | message = 'Missing ";" or "=" on line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) 280 | raise Exception(message) 281 | can_parse, self.index, annotation = StrParse.IndexOfNextNonSpace(self.data, self.index) 282 | _can_parse = can_parse # pylint: disable=unused-variable 283 | if value_object.annotation is None: # this is to prevent losing the annotation of the key when parsing strings dicts 284 | value_object.annotation = annotation 285 | dictionary[key_object] = value_object 286 | current_char = self.data[self.index] 287 | if current_char == ';': 288 | self.index += 1 # advancing to the next key 289 | new_object = self.__readTest(False) 290 | current_char = self.data[self.index] 291 | if current_char != '}': # pragma: no cover 292 | message = 'Expected terminating "}" for dictionary at line %s in file %s' % (str(StrParse.LineNumberForIndex(self.data, start_index)), self.file_path) 293 | raise Exception(message) 294 | self.index += 1 # skip over ending "}" 295 | return dictionary 296 | -------------------------------------------------------------------------------- /pbPlist/pbPlist.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import os 32 | from .pbParser import PBParser 33 | from .pbSerializer import PBSerializer 34 | 35 | class PBPlist(object): 36 | 37 | def __init__(self, file_path): 38 | self.root = None 39 | if self.__checkFile(file_path) is True: 40 | parser = PBParser(self.file_path) 41 | self.root = parser.read() 42 | self.string_encoding = parser.string_encoding 43 | self.file_type = parser.file_type 44 | 45 | def write(self, file_path=None): 46 | if file_path is None: 47 | file_path = self.file_path 48 | serializer = PBSerializer(file_path, self.string_encoding, self.file_type) 49 | serializer.write(self.root) 50 | 51 | def __checkFile(self, file_path): 52 | can_access_file = os.path.exists(file_path) 53 | if can_access_file is True: 54 | self.file_path = file_path 55 | return can_access_file 56 | -------------------------------------------------------------------------------- /pbPlist/pbRoot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from functools import cmp_to_key 32 | import sys 33 | if sys.version_info >= (3, 10): 34 | from collections.abc import MutableMapping 35 | else: 36 | from collections import MutableMapping 37 | from . import pbItem 38 | 39 | def StringCmp(obj1, obj2): 40 | result = -1 41 | if obj1 > obj2: 42 | result = 1 43 | elif obj1 == obj2: 44 | result = 0 45 | return result 46 | 47 | def KeySorter(obj1, obj2): 48 | result = 0 49 | if str(obj1) == 'isa': 50 | result = -1 51 | elif str(obj2) == 'isa': 52 | result = 1 53 | else: 54 | result = StringCmp(str(obj1), str(obj2)) 55 | return result 56 | 57 | class pbRoot(MutableMapping): 58 | 59 | def __init__(self, *args, **kwargs): 60 | self.store = dict() 61 | self.key_storage = list() 62 | self.update(dict(*args, **kwargs)) # use the free update to set keys 63 | 64 | def __internalKeyCheck(self, key): # pylint: disable=no-self-use 65 | safe_key = key 66 | if isinstance(safe_key, str): 67 | safe_key = pbItem.pbItemResolver(safe_key, 'qstring') 68 | return safe_key 69 | 70 | def __getitem__(self, key): 71 | return self.store[key] 72 | 73 | def __setitem__(self, key, value): 74 | if key not in self.key_storage: 75 | self.key_storage.append(self.__internalKeyCheck(key)) 76 | self.store[key] = value 77 | 78 | def __delitem__(self, key): 79 | if key in self.key_storage: 80 | self.key_storage.remove(key) 81 | del self.store[key] 82 | 83 | def __iter__(self): 84 | return self.key_storage.__iter__() 85 | 86 | def __len__(self): 87 | return self.key_storage.__len__() 88 | 89 | def __str__(self): 90 | return self.store.__str__() 91 | 92 | def __contains__(self, item): 93 | return item in self.key_storage 94 | 95 | def __getattr__(self, attrib): 96 | return getattr(self.store, attrib) 97 | 98 | def __keytransform__(self, key): # pylint: disable=no-self-use 99 | result = key 100 | if isinstance(key, pbItem.pbItem): 101 | result = key.value 102 | return result 103 | 104 | def sortedKeys(self): 105 | unsorted_keys = self.key_storage 106 | sorted_keys = sorted(unsorted_keys, key=cmp_to_key(KeySorter)) 107 | can_sort = False 108 | if len(sorted_keys) > 0: 109 | all_dictionaries = all((isinstance(self[key].value, dict) or isinstance(self[key].value, pbRoot)) for key in unsorted_keys) 110 | if all_dictionaries: 111 | can_sort = all(self[key].get('isa', None) is not None for key in unsorted_keys) 112 | if can_sort: 113 | sorted_keys = sorted(unsorted_keys, key=lambda k: str(self[k]['isa'])) 114 | return (can_sort, sorted_keys) 115 | -------------------------------------------------------------------------------- /pbPlist/pbSerializer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from __future__ import print_function 32 | import sys 33 | from .Switch import Switch 34 | 35 | class PBSerializer(object): 36 | 37 | def __init__(self, file_path=None, encoding=None, file_type=None): 38 | self.string_encoding = encoding 39 | self.file_path = file_path 40 | self.file_type = file_type 41 | 42 | def write(self, obj=None): 43 | for case in Switch(self.file_type): 44 | if case('ascii'): 45 | try: 46 | file_descriptor = open(self.file_path, 'w') 47 | self.__writeObject(file_descriptor, obj) 48 | file_descriptor.close() 49 | except IOError as exception: # pragma: no cover 50 | print('I/O error({0}): {1}'.format(exception.errno, exception.strerror)) 51 | except: # pragma: no cover 52 | print('Unexpected error:'+str(sys.exc_info()[0])) 53 | raise 54 | break 55 | if case('binary'): 56 | import biplist 57 | biplist.writePlist(obj, self.file_path) 58 | break 59 | if case('xml'): 60 | import plistlib 61 | plistlib.writePlist(obj, self.file_path) 62 | break 63 | if case(): 64 | break 65 | 66 | def __writeObject(self, file_descriptor=None, obj=None): 67 | if file_descriptor is None: # pragma: no cover 68 | message = 'Fatal error, file descriptor is None' 69 | raise TypeError(message) 70 | if self.string_encoding is not None: 71 | file_descriptor.write('// !$*'+self.string_encoding+'*$!\n') 72 | if obj is not None: 73 | write_string, indent_level = obj.writeString() 74 | _ = indent_level 75 | file_descriptor.write(write_string) 76 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Add files or directories to the blacklist. They should be base names, not 4 | # paths. 5 | ignore= 6 | 7 | # Add files or directories matching the regex patterns to the blacklist. The 8 | # regex matches against base names, not paths. 9 | ignore-patterns= 10 | 11 | # Pickle collected data for later comparisons. 12 | persistent=yes 13 | 14 | # List of plugins (as comma separated values of python modules names) to load, 15 | # usually to register additional checkers. 16 | load-plugins= 17 | 18 | # Use multiple processes to speed up Pylint. 19 | jobs=1 20 | 21 | # Allow loading of arbitrary C extensions. Extensions are imported into the 22 | # active Python interpreter and may run arbitrary code. 23 | unsafe-load-any-extension=no 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code 28 | extension-pkg-whitelist= 29 | 30 | # Allow optimization of some AST trees. This will activate a peephole AST 31 | # optimizer, which will apply various small optimizations. For instance, it can 32 | # be used to obtain the result of joining multiple strings with the addition 33 | # operator. Joining a lot of strings can lead to a maximum recursion error in 34 | # Pylint and this flag can prevent that. It has one side effect, the resulting 35 | # AST will be different than the one from reality. This option is deprecated 36 | # and it will be removed in Pylint 2.0. 37 | optimize-ast=no 38 | 39 | 40 | [MESSAGES CONTROL] 41 | 42 | # Only show warnings with the listed confidence levels. Leave empty to show 43 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 44 | confidence= 45 | 46 | # Enable the message, report, category or checker with the given id(s). You can 47 | # either give multiple identifier separated by comma (,) or put this option 48 | # multiple time (only on the command line, not in the configuration file where 49 | # it should appear only once). See also the "--disable" option for examples. 50 | #enable= 51 | 52 | # Disable the message, report, category or checker with the given id(s). You 53 | # can either give multiple identifiers separated by comma (,) or put this 54 | # option multiple times (only on the command line, not in the configuration 55 | # file where it should appear only once).You can also use "--disable=all" to 56 | # disable everything first and then reenable specific checks. For example, if 57 | # you want to run only the similarities checker, you can use "--disable=all 58 | # --enable=similarities". If you want to run only the classes checker, but have 59 | # no Warning level messages displayed, use"--disable=all --enable=classes 60 | # --disable=W" 61 | disable=missing-docstring,too-many-lines,locally-disabled,too-many-ancestors 62 | 63 | 64 | [REPORTS] 65 | 66 | # Set the output format. Available formats are text, parseable, colorized, msvs 67 | # (visual studio) and html. You can also give a reporter class, eg 68 | # mypackage.mymodule.MyReporterClass. 69 | output-format=text 70 | 71 | # Put messages in a separate file for each module / package specified on the 72 | # command line instead of printing them on stdout. Reports (if any) will be 73 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 74 | # and it will be removed in Pylint 2.0. 75 | files-output=no 76 | 77 | # Tells whether to display a full report or only the messages 78 | reports=yes 79 | 80 | # Python expression which should return a note less than 10 (10 is the highest 81 | # note). You have access to the variables errors warning, statement which 82 | # respectively contain the number of errors / warnings messages and the total 83 | # number of statements analyzed. This is used by the global evaluation report 84 | # (RP0004). 85 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 86 | 87 | # Template used to display messages. This is a python new-style format string 88 | # used to format the message information. See doc for all details 89 | #msg-template= 90 | 91 | 92 | [BASIC] 93 | 94 | # Good variable names which should always be accepted, separated by a comma 95 | good-names=Run,_ 96 | 97 | # Bad variable names which should always be refused, separated by a comma 98 | bad-names=foo,bar,baz,toto,tutu,tata,i,j,k,idx 99 | 100 | # Colon-delimited sets of names that determine each other's naming style when 101 | # the name regexes allow several styles. 102 | name-group= 103 | 104 | # Include a hint for the correct naming format with invalid-name 105 | include-naming-hint=no 106 | 107 | # List of decorators that produce properties, such as abc.abstractproperty. Add 108 | # to this list to register other decorators that produce valid properties. 109 | property-classes=abc.abstractproperty 110 | 111 | # Regular expression matching correct function names 112 | function-rgx=[A-Za-z_][a-zA-Z0-9_]{2,40}$ 113 | 114 | # Naming hint for function names 115 | function-name-hint=[A-Za-z_][a-zA-Z0-9_]{2,40}$ 116 | 117 | # Regular expression matching correct variable names 118 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 119 | 120 | # Naming hint for variable names 121 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 122 | 123 | # Regular expression matching correct constant names 124 | const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ 125 | 126 | # Naming hint for constant names 127 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 128 | 129 | # Regular expression matching correct attribute names 130 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 131 | 132 | # Naming hint for attribute names 133 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 134 | 135 | # Regular expression matching correct argument names 136 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 137 | 138 | # Naming hint for argument names 139 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 140 | 141 | # Regular expression matching correct class attribute names 142 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 143 | 144 | # Naming hint for class attribute names 145 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 146 | 147 | # Regular expression matching correct inline iteration names 148 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 149 | 150 | # Naming hint for inline iteration names 151 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 152 | 153 | # Regular expression matching correct class names 154 | class-rgx=[a-zA-Z0-9_]+$ 155 | 156 | # Naming hint for class names 157 | class-name-hint=[a-zA-Z0-9_]+$ 158 | 159 | # Regular expression matching correct module names 160 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Za-z][a-zA-Z0-9_]+))$ 161 | 162 | # Naming hint for module names 163 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Za-z][a-zA-Z0-9]+))$ 164 | 165 | # Regular expression matching correct method names 166 | method-rgx=[a-z_][a-zA-Z0-9_]{2,40}$ 167 | 168 | # Naming hint for method names 169 | method-name-hint=[a-zA-Z0-9_]{2,40}$ 170 | 171 | # Regular expression which should only match function or class names that do 172 | # not require a docstring. 173 | no-docstring-rgx=^_ 174 | 175 | # Minimum line length for functions/classes that require docstrings, shorter 176 | # ones are exempt. 177 | docstring-min-length=0 178 | 179 | 180 | [ELIF] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | 186 | [FORMAT] 187 | 188 | # Maximum number of characters on a single line. 189 | max-line-length=100000 190 | 191 | # Regexp for a line that is allowed to be longer than the limit. 192 | ignore-long-lines=^\s*(# )??$ 193 | 194 | # Allow the body of an if to be on the same line as the test if there is no 195 | # else. 196 | single-line-if-stmt=no 197 | 198 | # List of optional constructs for which whitespace checking is disabled. `dict- 199 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 200 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 201 | # `empty-line` allows space-only lines. 202 | no-space-check=trailing-comma,dict-separator 203 | 204 | # Maximum number of lines in a module 205 | max-module-lines=0 206 | 207 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 208 | # tab). 209 | indent-string=' ' 210 | 211 | # Number of spaces of indent required inside a hanging or continued line. 212 | indent-after-paren=4 213 | 214 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 215 | expected-line-ending-format= 216 | 217 | 218 | [LOGGING] 219 | 220 | # Logging modules to check that the string format arguments are in logging 221 | # function parameter format 222 | logging-modules=logging 223 | 224 | 225 | [MISCELLANEOUS] 226 | 227 | # List of note tags to take in consideration, separated by a comma. 228 | notes=FIXME,XXX,TODO 229 | 230 | 231 | [SIMILARITIES] 232 | 233 | # Minimum lines number of a similarity. 234 | min-similarity-lines=10 235 | 236 | # Ignore comments when computing similarities. 237 | ignore-comments=yes 238 | 239 | # Ignore docstrings when computing similarities. 240 | ignore-docstrings=yes 241 | 242 | # Ignore imports when computing similarities. 243 | ignore-imports=yes 244 | 245 | 246 | [SPELLING] 247 | 248 | # Spelling dictionary name. Available dictionaries: none. To make it working 249 | # install python-enchant package. 250 | spelling-dict= 251 | 252 | # List of comma separated words that should not be checked. 253 | spelling-ignore-words= 254 | 255 | # A path to a file that contains private dictionary; one word per line. 256 | spelling-private-dict-file= 257 | 258 | # Tells whether to store unknown words to indicated private dictionary in 259 | # --spelling-private-dict-file option instead of raising a message. 260 | spelling-store-unknown-words=no 261 | 262 | 263 | [TYPECHECK] 264 | 265 | # Tells whether missing members accessed in mixin class should be ignored. A 266 | # mixin class is detected if its name ends with "mixin" (case insensitive). 267 | ignore-mixin-members=yes 268 | 269 | # List of module names for which member attributes should not be checked 270 | # (useful for modules/projects where namespaces are manipulated during runtime 271 | # and thus existing member attributes cannot be deduced by static analysis. It 272 | # supports qualified module names, as well as Unix pattern matching. 273 | ignored-modules= 274 | 275 | # List of class names for which member attributes should not be checked (useful 276 | # for classes with dynamically set attributes). This supports the use of 277 | # qualified names. 278 | ignored-classes=optparse.Values,thread._local,_thread._local 279 | 280 | # List of members which are set dynamically and missed by pylint inference 281 | # system, and so shouldn't trigger E1101 when accessed. Python regular 282 | # expressions are accepted. 283 | generated-members= 284 | 285 | # List of decorators that produce context managers, such as 286 | # contextlib.contextmanager. Add to this list to register other decorators that 287 | # produce valid context managers. 288 | contextmanager-decorators=contextlib.contextmanager 289 | 290 | 291 | [VARIABLES] 292 | 293 | # Tells whether we should check for unused import in __init__ files. 294 | init-import=no 295 | 296 | # A regular expression matching the name of dummy variables (i.e. expectedly 297 | # not used). 298 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 299 | 300 | # List of additional names supposed to be defined in builtins. Remember that 301 | # you should avoid to define new builtins when possible. 302 | additional-builtins= 303 | 304 | # List of strings which can identify a callback function by name. A callback 305 | # name must start or end with one of those strings. 306 | callbacks=cb_,_cb 307 | 308 | # List of qualified module names which can have objects that can redefine 309 | # builtins. 310 | redefining-builtins-modules=six.moves,future.builtins 311 | 312 | 313 | [CLASSES] 314 | 315 | # List of method names used to declare (i.e. assign) instance attributes. 316 | defining-attr-methods=__init__,__new__,setUp 317 | 318 | # List of valid names for the first argument in a class method. 319 | valid-classmethod-first-arg=cls 320 | 321 | # List of valid names for the first argument in a metaclass class method. 322 | valid-metaclass-classmethod-first-arg=mcs 323 | 324 | # List of member names, which should be excluded from the protected access 325 | # warning. 326 | exclude-protected=_asdict,_fields,_replace,_source,_make 327 | 328 | 329 | [DESIGN] 330 | 331 | # Maximum number of arguments for function / method 332 | max-args=5 333 | 334 | # Argument names that match this expression will be ignored. Default to name 335 | # with leading underscore 336 | ignored-argument-names=_.* 337 | 338 | # Maximum number of locals for function / method body 339 | max-locals=15 340 | 341 | # Maximum number of return / yield for function / method body 342 | max-returns=1 343 | 344 | # Maximum number of branch for function / method body 345 | max-branches=12 346 | 347 | # Maximum number of statements in function / method body 348 | max-statements=50 349 | 350 | # Maximum number of parents for a class (see R0901). 351 | max-parents=7 352 | 353 | # Maximum number of attributes for a class (see R0902). 354 | max-attributes=10 355 | 356 | # Minimum number of public methods for a class (see R0903). 357 | min-public-methods=0 358 | 359 | # Maximum number of public methods for a class (see R0904). 360 | max-public-methods=20 361 | 362 | # Maximum number of boolean expressions in a if statement 363 | max-bool-expr=3 364 | 365 | 366 | [IMPORTS] 367 | 368 | # Deprecated modules which should not be used, separated by a comma 369 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 370 | 371 | # Create a graph of every (i.e. internal and external) dependencies in the 372 | # given file (report RP0402 must not be disabled) 373 | import-graph= 374 | 375 | # Create a graph of external dependencies in the given file (report RP0402 must 376 | # not be disabled) 377 | ext-import-graph= 378 | 379 | # Create a graph of internal dependencies in the given file (report RP0402 must 380 | # not be disabled) 381 | int-import-graph= 382 | 383 | # Force import order to recognize a module as part of the standard 384 | # compatibility libraries. 385 | known-standard-library=os,re,sys,argparse,logging 386 | 387 | # Force import order to recognize a module as part of a third party library. 388 | known-third-party= 389 | 390 | # Analyse import fallback blocks. This can be used to support both Python 2 and 391 | # 3 compatible code, which means that the block might have code that exists 392 | # only in one or another interpreter, leading to false positives when analysed. 393 | analyse-fallback-blocks=no 394 | 395 | 396 | [EXCEPTIONS] 397 | 398 | # Exceptions that will emit a warning when being caught. Defaults to 399 | # "Exception" 400 | overgeneral-exceptions=Exception 401 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | biplist == 1.0.1 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from setuptools import setup 32 | 33 | setup( 34 | name = 'pbPlist', 35 | version = '1.0.4', 36 | description = 'Property List Parser and Serializer. Supports XML, Binary, and NeXTSTEP Property Lists.', 37 | url = 'https://github.com/samdmarshall/pbPlist', 38 | author = 'Samantha Marshall', 39 | author_email = 'hello@pewpewthespells.com', 40 | license = 'BSD 3-Clause', 41 | packages = [ 42 | 'pbPlist' 43 | ], 44 | test_suite = 'tests', 45 | tests_require = [ 46 | 'unittest-xml-reporting', 47 | ], 48 | zip_safe = False, 49 | install_requires = [ 50 | 'biplist', 51 | ] 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import xmlrunner 32 | from . import pbPlist_test 33 | -------------------------------------------------------------------------------- /tests/pbPlist-test-data/array_encoding_hint/test.plist: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | () -------------------------------------------------------------------------------- /tests/pbPlist-test-data/array_mixed/test.plist: -------------------------------------------------------------------------------- 1 | ( 2 | "", 3 | testing 4 | ) -------------------------------------------------------------------------------- /tests/pbPlist-test-data/array_qstrings/test.plist: -------------------------------------------------------------------------------- 1 | ( 2 | "hello world!", "this is a test" , 'Of quoted strings' 3 | ) -------------------------------------------------------------------------------- /tests/pbPlist-test-data/array_strings/test.plist: -------------------------------------------------------------------------------- 1 | ( 2 | obj1, 3 | obj2, obj3, 4 | ) -------------------------------------------------------------------------------- /tests/pbPlist-test-data/bom/utf-16/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdmarshall/pbPlist/c553c8d80930999425388d9848155da9761f854d/tests/pbPlist-test-data/bom/utf-16/Localizable.strings -------------------------------------------------------------------------------- /tests/pbPlist-test-data/data_encoding_hint/test.plist: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | <42> -------------------------------------------------------------------------------- /tests/pbPlist-test-data/dict_encoding_hint/test.plist: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | {} -------------------------------------------------------------------------------- /tests/pbPlist-test-data/dict_string_values/test.plist: -------------------------------------------------------------------------------- 1 | { 2 | a = "a"; 3 | "b" = b; 4 | "c" = "c"; 5 | d = d; 6 | } -------------------------------------------------------------------------------- /tests/pbPlist-test-data/empty_encoding_hint/test.plist: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! -------------------------------------------------------------------------------- /tests/pbPlist-test-data/empty_plist/test.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdmarshall/pbPlist/c553c8d80930999425388d9848155da9761f854d/tests/pbPlist-test-data/empty_plist/test.plist -------------------------------------------------------------------------------- /tests/pbPlist-test-data/qstring_encoding_hint/test.plist: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | "quoted string with encoding hint" -------------------------------------------------------------------------------- /tests/pbPlist-test-data/raw_array/test.plist: -------------------------------------------------------------------------------- 1 | () -------------------------------------------------------------------------------- /tests/pbPlist-test-data/raw_data/test.plist: -------------------------------------------------------------------------------- 1 | <42> -------------------------------------------------------------------------------- /tests/pbPlist-test-data/raw_dict/test.plist: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tests/pbPlist-test-data/raw_qstring/test.plist: -------------------------------------------------------------------------------- 1 | "this is a quoted string" -------------------------------------------------------------------------------- /tests/pbPlist-test-data/raw_string/test.plist: -------------------------------------------------------------------------------- 1 | unquoted_string -------------------------------------------------------------------------------- /tests/pbPlist-test-data/string_encoding_hint/test.plist: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | unquoted_string_with_encoding_hint -------------------------------------------------------------------------------- /tests/pbPlist-test-data/xcode_proj/test.plist: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | 15D488AC1BD3D6C700EC46B1 /* Lemon Setup */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = 15D488AD1BD3D6C700EC46B1 /* Build configuration list for PBXAggregateTarget "Lemon Setup" */; 13 | buildPhases = ( 14 | 15D488B01BD3D6D000EC46B1 /* make distclean ; ./autogen.sh ; ./configure */, 15 | ); 16 | dependencies = ( 17 | ); 18 | name = "Lemon Setup"; 19 | productName = "Lemon Setup"; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | 15D488BB1BD3D7A900EC46B1 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15D488BA1BD3D7A900EC46B1 /* AppKit.framework */; }; 25 | 15D488BD1BD3D7A900EC46B1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15D488BC1BD3D7A900EC46B1 /* Foundation.framework */; }; 26 | 15D488C41BD3D7A900EC46B1 /* CitrusPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 15D488C31BD3D7A900EC46B1 /* CitrusPlugin.m */; }; 27 | 15D488C71BD3D7A900EC46B1 /* NSObject_Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 15D488C61BD3D7A900EC46B1 /* NSObject_Extension.m */; }; 28 | 15D488D01BD3D9CD00EC46B1 /* Lemon.pbfilespec in Resources */ = {isa = PBXBuildFile; fileRef = 15D488CC1BD3D9CD00EC46B1 /* Lemon.pbfilespec */; }; 29 | 15D488D11BD3D9CD00EC46B1 /* Lemon.strings in Resources */ = {isa = PBXBuildFile; fileRef = 15D488CD1BD3D9CD00EC46B1 /* Lemon.strings */; }; 30 | 15D488D21BD3D9CD00EC46B1 /* Lemon.xcspec in Resources */ = {isa = PBXBuildFile; fileRef = 15D488CE1BD3D9CD00EC46B1 /* Lemon.xcspec */; }; 31 | 15D488D71BD3DA8800EC46B1 /* lemon in Resources */ = {isa = PBXBuildFile; fileRef = 15D488D61BD3DA8800EC46B1 /* lemon */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 15D488B11BD3D6F400EC46B1 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 15D488951BD3D59200EC46B1 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 15D488AC1BD3D6C700EC46B1; 40 | remoteInfo = "Lemon Setup"; 41 | }; 42 | 15D488D81BD3DA9C00EC46B1 /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 15D488951BD3D59200EC46B1 /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = 15D488991BD3D59200EC46B1; 47 | remoteInfo = Lemon; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXFileReference section */ 52 | 158E53E61BD3E6A600F75AAD /* PluginUsage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = PluginUsage.md; sourceTree = ""; }; 53 | 15D488A01BD3D62C00EC46B1 /* autogen.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = autogen.sh; sourceTree = ""; }; 54 | 15D488A11BD3D62C00EC46B1 /* citrus.pc.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = citrus.pc.in; sourceTree = ""; }; 55 | 15D488A21BD3D62C00EC46B1 /* configure.ac */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = configure.ac; sourceTree = ""; }; 56 | 15D488A31BD3D62C00EC46B1 /* lemon.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lemon.c; sourceTree = ""; }; 57 | 15D488A41BD3D62C00EC46B1 /* lempar.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lempar.c; sourceTree = ""; }; 58 | 15D488A51BD3D62C00EC46B1 /* lempar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lempar.h; sourceTree = ""; }; 59 | 15D488A61BD3D62C00EC46B1 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 60 | 15D488A71BD3D62C00EC46B1 /* Makefile.am */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Makefile.am; sourceTree = ""; }; 61 | 15D488A81BD3D62C00EC46B1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 62 | 15D488A91BD3D62C00EC46B1 /* VERSION */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = VERSION; sourceTree = ""; }; 63 | 15D488AA1BD3D62C00EC46B1 /* version.h.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = version.h.in; sourceTree = ""; }; 64 | 15D488B71BD3D7A900EC46B1 /* CitrusPlugin.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CitrusPlugin.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 15D488BA1BD3D7A900EC46B1 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 66 | 15D488BC1BD3D7A900EC46B1 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 67 | 15D488C01BD3D7A900EC46B1 /* CitrusPlugin.xcscheme */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = CitrusPlugin.xcscheme; path = CitrusPlugin.xcodeproj/xcshareddata/xcschemes/CitrusPlugin.xcscheme; sourceTree = SOURCE_ROOT; }; 68 | 15D488C21BD3D7A900EC46B1 /* CitrusPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CitrusPlugin.h; sourceTree = ""; }; 69 | 15D488C31BD3D7A900EC46B1 /* CitrusPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CitrusPlugin.m; sourceTree = ""; }; 70 | 15D488C51BD3D7A900EC46B1 /* NSObject_Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSObject_Extension.h; sourceTree = ""; }; 71 | 15D488C61BD3D7A900EC46B1 /* NSObject_Extension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSObject_Extension.m; sourceTree = ""; }; 72 | 15D488C81BD3D7A900EC46B1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 15D488CC1BD3D9CD00EC46B1 /* Lemon.pbfilespec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.pbfilespec; path = Lemon.pbfilespec; sourceTree = ""; }; 74 | 15D488CD1BD3D9CD00EC46B1 /* Lemon.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Lemon.strings; sourceTree = ""; }; 75 | 15D488CE1BD3D9CD00EC46B1 /* Lemon.xcspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xcspec; path = Lemon.xcspec; sourceTree = ""; }; 76 | 15D488D61BD3DA8800EC46B1 /* lemon */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = lemon; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | 15D488B51BD3D7A900EC46B1 /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | 15D488BB1BD3D7A900EC46B1 /* AppKit.framework in Frameworks */, 85 | 15D488BD1BD3D7A900EC46B1 /* Foundation.framework in Frameworks */, 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | /* End PBXFrameworksBuildPhase section */ 90 | 91 | /* Begin PBXGroup section */ 92 | 15D488941BD3D59200EC46B1 = { 93 | isa = PBXGroup; 94 | children = ( 95 | 158E53E61BD3E6A600F75AAD /* PluginUsage.md */, 96 | 15D4889F1BD3D5D500EC46B1 /* lemon */, 97 | 15D488BE1BD3D7A900EC46B1 /* CitrusPlugin */, 98 | 15D488B91BD3D7A900EC46B1 /* Frameworks */, 99 | 15D488B81BD3D7A900EC46B1 /* Products */, 100 | 15D488D61BD3DA8800EC46B1 /* lemon */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 15D4889F1BD3D5D500EC46B1 /* lemon */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 15D488A01BD3D62C00EC46B1 /* autogen.sh */, 108 | 15D488A11BD3D62C00EC46B1 /* citrus.pc.in */, 109 | 15D488A21BD3D62C00EC46B1 /* configure.ac */, 110 | 15D488A31BD3D62C00EC46B1 /* lemon.c */, 111 | 15D488A41BD3D62C00EC46B1 /* lempar.c */, 112 | 15D488A51BD3D62C00EC46B1 /* lempar.h */, 113 | 15D488A61BD3D62C00EC46B1 /* LICENSE */, 114 | 15D488A71BD3D62C00EC46B1 /* Makefile.am */, 115 | 15D488A81BD3D62C00EC46B1 /* README.md */, 116 | 15D488A91BD3D62C00EC46B1 /* VERSION */, 117 | 15D488AA1BD3D62C00EC46B1 /* version.h.in */, 118 | ); 119 | name = lemon; 120 | sourceTree = ""; 121 | }; 122 | 15D488B81BD3D7A900EC46B1 /* Products */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 15D488B71BD3D7A900EC46B1 /* CitrusPlugin.xcplugin */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | 15D488B91BD3D7A900EC46B1 /* Frameworks */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 15D488BA1BD3D7A900EC46B1 /* AppKit.framework */, 134 | 15D488BC1BD3D7A900EC46B1 /* Foundation.framework */, 135 | ); 136 | name = Frameworks; 137 | sourceTree = ""; 138 | }; 139 | 15D488BE1BD3D7A900EC46B1 /* CitrusPlugin */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 15D488D41BD3D9D000EC46B1 /* Resources */, 143 | 15D488C21BD3D7A900EC46B1 /* CitrusPlugin.h */, 144 | 15D488C31BD3D7A900EC46B1 /* CitrusPlugin.m */, 145 | 15D488C51BD3D7A900EC46B1 /* NSObject_Extension.h */, 146 | 15D488C61BD3D7A900EC46B1 /* NSObject_Extension.m */, 147 | 15D488C81BD3D7A900EC46B1 /* Info.plist */, 148 | 15D488BF1BD3D7A900EC46B1 /* Supporting Files */, 149 | ); 150 | path = CitrusPlugin; 151 | sourceTree = ""; 152 | }; 153 | 15D488BF1BD3D7A900EC46B1 /* Supporting Files */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 15D488C01BD3D7A900EC46B1 /* CitrusPlugin.xcscheme */, 157 | ); 158 | name = "Supporting Files"; 159 | sourceTree = ""; 160 | }; 161 | 15D488D41BD3D9D000EC46B1 /* Resources */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 15D488CC1BD3D9CD00EC46B1 /* Lemon.pbfilespec */, 165 | 15D488CD1BD3D9CD00EC46B1 /* Lemon.strings */, 166 | 15D488CE1BD3D9CD00EC46B1 /* Lemon.xcspec */, 167 | ); 168 | name = Resources; 169 | sourceTree = ""; 170 | }; 171 | /* End PBXGroup section */ 172 | 173 | /* Begin PBXLegacyTarget section */ 174 | 15D488991BD3D59200EC46B1 /* Lemon */ = { 175 | isa = PBXLegacyTarget; 176 | buildArgumentsString = "${ACTION}"; 177 | buildConfigurationList = 15D4889C1BD3D59200EC46B1 /* Build configuration list for PBXLegacyTarget "Lemon" */; 178 | buildPhases = ( 179 | ); 180 | buildToolPath = /usr/bin/make; 181 | buildWorkingDirectory = "$(PROJECT_DIR)"; 182 | dependencies = ( 183 | 15D488B21BD3D6F400EC46B1 /* PBXTargetDependency */, 184 | ); 185 | name = Lemon; 186 | passBuildSettingsInEnvironment = 1; 187 | productName = CitrusPlugin; 188 | }; 189 | /* End PBXLegacyTarget section */ 190 | 191 | /* Begin PBXNativeTarget section */ 192 | 15D488B61BD3D7A900EC46B1 /* CitrusPlugin */ = { 193 | isa = PBXNativeTarget; 194 | buildConfigurationList = 15D488C91BD3D7A900EC46B1 /* Build configuration list for PBXNativeTarget "CitrusPlugin" */; 195 | buildPhases = ( 196 | 15D488B31BD3D7A900EC46B1 /* Sources */, 197 | 15D488B41BD3D7A900EC46B1 /* Resources */, 198 | 15D488B51BD3D7A900EC46B1 /* Frameworks */, 199 | 15D488DC1BD3DBBB00EC46B1 /* Copying default template into bundle because Xcode has a bug */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | 15D488D91BD3DA9C00EC46B1 /* PBXTargetDependency */, 205 | ); 206 | name = CitrusPlugin; 207 | productName = CitrusPlugin; 208 | productReference = 15D488B71BD3D7A900EC46B1 /* CitrusPlugin.xcplugin */; 209 | productType = "com.apple.product-type.bundle"; 210 | }; 211 | /* End PBXNativeTarget section */ 212 | 213 | /* Begin PBXProject section */ 214 | 15D488951BD3D59200EC46B1 /* Project object */ = { 215 | isa = PBXProject; 216 | attributes = { 217 | LastUpgradeCheck = 0700; 218 | ORGANIZATIONNAME = "Samantha Marshall"; 219 | TargetAttributes = { 220 | 15D488991BD3D59200EC46B1 = { 221 | CreatedOnToolsVersion = 7.0.1; 222 | }; 223 | 15D488AC1BD3D6C700EC46B1 = { 224 | CreatedOnToolsVersion = 7.0.1; 225 | }; 226 | 15D488B61BD3D7A900EC46B1 = { 227 | CreatedOnToolsVersion = 7.0.1; 228 | }; 229 | }; 230 | }; 231 | buildConfigurationList = 15D488981BD3D59200EC46B1 /* Build configuration list for PBXProject "CitrusPlugin" */; 232 | compatibilityVersion = "Xcode 3.2"; 233 | developmentRegion = English; 234 | hasScannedForEncodings = 0; 235 | knownRegions = ( 236 | en, 237 | ); 238 | mainGroup = 15D488941BD3D59200EC46B1; 239 | productRefGroup = 15D488B81BD3D7A900EC46B1 /* Products */; 240 | projectDirPath = ""; 241 | projectRoot = ""; 242 | targets = ( 243 | 15D488AC1BD3D6C700EC46B1 /* Lemon Setup */, 244 | 15D488991BD3D59200EC46B1 /* Lemon */, 245 | 15D488B61BD3D7A900EC46B1 /* CitrusPlugin */, 246 | ); 247 | }; 248 | /* End PBXProject section */ 249 | 250 | /* Begin PBXResourcesBuildPhase section */ 251 | 15D488B41BD3D7A900EC46B1 /* Resources */ = { 252 | isa = PBXResourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | 15D488D71BD3DA8800EC46B1 /* lemon in Resources */, 256 | 15D488D01BD3D9CD00EC46B1 /* Lemon.pbfilespec in Resources */, 257 | 15D488D11BD3D9CD00EC46B1 /* Lemon.strings in Resources */, 258 | 15D488D21BD3D9CD00EC46B1 /* Lemon.xcspec in Resources */, 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | /* End PBXResourcesBuildPhase section */ 263 | 264 | /* Begin PBXShellScriptBuildPhase section */ 265 | 15D488B01BD3D6D000EC46B1 /* make distclean ; ./autogen.sh ; ./configure */ = { 266 | isa = PBXShellScriptBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | ); 270 | inputPaths = ( 271 | ); 272 | name = "make distclean ; ./autogen.sh ; ./configure"; 273 | outputPaths = ( 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | shellPath = /bin/sh; 277 | shellScript = "cd ${PROJECT_DIR}\n\nif [ -e Makefile ]; then\n\tmake distclean\nfi\n\n./autogen.sh\n./configure"; 278 | }; 279 | 15D488DC1BD3DBBB00EC46B1 /* Copying default template into bundle because Xcode has a bug */ = { 280 | isa = PBXShellScriptBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | ); 284 | inputPaths = ( 285 | ); 286 | name = "Copying default template into bundle because Xcode has a bug"; 287 | outputPaths = ( 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | shellPath = /bin/sh; 291 | 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"; 292 | }; 293 | /* End PBXShellScriptBuildPhase section */ 294 | 295 | /* Begin PBXSourcesBuildPhase section */ 296 | 15D488B31BD3D7A900EC46B1 /* Sources */ = { 297 | isa = PBXSourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | 15D488C41BD3D7A900EC46B1 /* CitrusPlugin.m in Sources */, 301 | 15D488C71BD3D7A900EC46B1 /* NSObject_Extension.m in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | /* End PBXSourcesBuildPhase section */ 306 | 307 | /* Begin PBXTargetDependency section */ 308 | 15D488B21BD3D6F400EC46B1 /* PBXTargetDependency */ = { 309 | isa = PBXTargetDependency; 310 | target = 15D488AC1BD3D6C700EC46B1 /* Lemon Setup */; 311 | targetProxy = 15D488B11BD3D6F400EC46B1 /* PBXContainerItemProxy */; 312 | }; 313 | 15D488D91BD3DA9C00EC46B1 /* PBXTargetDependency */ = { 314 | isa = PBXTargetDependency; 315 | target = 15D488991BD3D59200EC46B1 /* Lemon */; 316 | targetProxy = 15D488D81BD3DA9C00EC46B1 /* PBXContainerItemProxy */; 317 | }; 318 | /* End PBXTargetDependency section */ 319 | 320 | /* Begin XCBuildConfiguration section */ 321 | 15D4889A1BD3D59200EC46B1 /* Debug */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 326 | CLANG_CXX_LIBRARY = "libc++"; 327 | CLANG_ENABLE_MODULES = YES; 328 | CLANG_ENABLE_OBJC_ARC = YES; 329 | CLANG_WARN_BOOL_CONVERSION = YES; 330 | CLANG_WARN_CONSTANT_CONVERSION = YES; 331 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 332 | CLANG_WARN_EMPTY_BODY = YES; 333 | CLANG_WARN_ENUM_CONVERSION = YES; 334 | CLANG_WARN_INT_CONVERSION = YES; 335 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 336 | CLANG_WARN_UNREACHABLE_CODE = YES; 337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 338 | COPY_PHASE_STRIP = NO; 339 | DEBUG_INFORMATION_FORMAT = dwarf; 340 | ENABLE_STRICT_OBJC_MSGSEND = YES; 341 | ENABLE_TESTABILITY = YES; 342 | GCC_C_LANGUAGE_STANDARD = gnu99; 343 | GCC_DYNAMIC_NO_PIC = NO; 344 | GCC_NO_COMMON_BLOCKS = YES; 345 | GCC_OPTIMIZATION_LEVEL = 0; 346 | GCC_PREPROCESSOR_DEFINITIONS = ( 347 | "DEBUG=1", 348 | "$(inherited)", 349 | ); 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | MTL_ENABLE_DEBUG_INFO = YES; 357 | ONLY_ACTIVE_ARCH = NO; 358 | SDKROOT = macosx; 359 | }; 360 | name = Debug; 361 | }; 362 | 15D4889B1BD3D59200EC46B1 /* Release */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | ALWAYS_SEARCH_USER_PATHS = NO; 366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 367 | CLANG_CXX_LIBRARY = "libc++"; 368 | CLANG_ENABLE_MODULES = YES; 369 | CLANG_ENABLE_OBJC_ARC = YES; 370 | CLANG_WARN_BOOL_CONVERSION = YES; 371 | CLANG_WARN_CONSTANT_CONVERSION = YES; 372 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 373 | CLANG_WARN_EMPTY_BODY = YES; 374 | CLANG_WARN_ENUM_CONVERSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 377 | CLANG_WARN_UNREACHABLE_CODE = YES; 378 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 379 | COPY_PHASE_STRIP = NO; 380 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 381 | ENABLE_NS_ASSERTIONS = NO; 382 | ENABLE_STRICT_OBJC_MSGSEND = YES; 383 | GCC_C_LANGUAGE_STANDARD = gnu99; 384 | GCC_NO_COMMON_BLOCKS = YES; 385 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 386 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 387 | GCC_WARN_UNDECLARED_SELECTOR = YES; 388 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 389 | GCC_WARN_UNUSED_FUNCTION = YES; 390 | GCC_WARN_UNUSED_VARIABLE = YES; 391 | MTL_ENABLE_DEBUG_INFO = NO; 392 | ONLY_ACTIVE_ARCH = NO; 393 | SDKROOT = macosx; 394 | }; 395 | name = Release; 396 | }; 397 | 15D4889D1BD3D59200EC46B1 /* Debug */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | DEBUGGING_SYMBOLS = YES; 401 | DEBUG_INFORMATION_FORMAT = dwarf; 402 | GCC_GENERATE_DEBUGGING_SYMBOLS = YES; 403 | GCC_OPTIMIZATION_LEVEL = 0; 404 | OTHER_CFLAGS = ""; 405 | OTHER_LDFLAGS = ""; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | }; 408 | name = Debug; 409 | }; 410 | 15D4889E1BD3D59200EC46B1 /* Release */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 414 | OTHER_CFLAGS = ""; 415 | OTHER_LDFLAGS = ""; 416 | PRODUCT_NAME = "$(TARGET_NAME)"; 417 | }; 418 | name = Release; 419 | }; 420 | 15D488AE1BD3D6C700EC46B1 /* Debug */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | }; 425 | name = Debug; 426 | }; 427 | 15D488AF1BD3D6C700EC46B1 /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | PRODUCT_NAME = "$(TARGET_NAME)"; 431 | }; 432 | name = Release; 433 | }; 434 | 15D488CA1BD3D7A900EC46B1 /* Debug */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | CODE_SIGN_IDENTITY = "Developer ID Application: Samantha Marshall (329DAD2G44)"; 438 | COMBINE_HIDPI_IMAGES = YES; 439 | DEPLOYMENT_LOCATION = YES; 440 | DSTROOT = "$(HOME)"; 441 | INFOPLIST_FILE = CitrusPlugin/Info.plist; 442 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 443 | MACOSX_DEPLOYMENT_TARGET = 10.9; 444 | PRODUCT_BUNDLE_IDENTIFIER = com.samdmarshall.CitrusPlugin; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | WRAPPER_EXTENSION = xcplugin; 447 | }; 448 | name = Debug; 449 | }; 450 | 15D488CB1BD3D7A900EC46B1 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | CODE_SIGN_IDENTITY = "Developer ID Application: Samantha Marshall (329DAD2G44)"; 454 | COMBINE_HIDPI_IMAGES = YES; 455 | DEPLOYMENT_LOCATION = YES; 456 | DSTROOT = "$(HOME)"; 457 | INFOPLIST_FILE = CitrusPlugin/Info.plist; 458 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 459 | MACOSX_DEPLOYMENT_TARGET = 10.9; 460 | PRODUCT_BUNDLE_IDENTIFIER = com.samdmarshall.CitrusPlugin; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | WRAPPER_EXTENSION = xcplugin; 463 | }; 464 | name = Release; 465 | }; 466 | /* End XCBuildConfiguration section */ 467 | 468 | /* Begin XCConfigurationList section */ 469 | 15D488981BD3D59200EC46B1 /* Build configuration list for PBXProject "CitrusPlugin" */ = { 470 | isa = XCConfigurationList; 471 | buildConfigurations = ( 472 | 15D4889A1BD3D59200EC46B1 /* Debug */, 473 | 15D4889B1BD3D59200EC46B1 /* Release */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | 15D4889C1BD3D59200EC46B1 /* Build configuration list for PBXLegacyTarget "Lemon" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | 15D4889D1BD3D59200EC46B1 /* Debug */, 482 | 15D4889E1BD3D59200EC46B1 /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | 15D488AD1BD3D6C700EC46B1 /* Build configuration list for PBXAggregateTarget "Lemon Setup" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 15D488AE1BD3D6C700EC46B1 /* Debug */, 491 | 15D488AF1BD3D6C700EC46B1 /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | 15D488C91BD3D7A900EC46B1 /* Build configuration list for PBXNativeTarget "CitrusPlugin" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | 15D488CA1BD3D7A900EC46B1 /* Debug */, 500 | 15D488CB1BD3D7A900EC46B1 /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | /* End XCConfigurationList section */ 506 | }; 507 | rootObject = 15D488951BD3D59200EC46B1 /* Project object */; 508 | } 509 | -------------------------------------------------------------------------------- /tests/pbPlist_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Samantha Marshall (http://pewpewthespells.com) 2 | # All rights reserved. 3 | # 4 | # https://github.com/samdmarshall/pbPlist 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of Samantha Marshall nor the names of its contributors may 17 | # be used to endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | # OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import os 32 | import sys 33 | import unittest 34 | from pbPlist import pbPlist 35 | from pbPlist.pbRoot import pbRoot 36 | 37 | import xmlrunner 38 | 39 | if sys.version_info < (3, 0): 40 | def StringType(): 41 | return unicode 42 | else: 43 | def StringType(): 44 | return str 45 | 46 | test_directory_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pbPlist-test-data') 47 | 48 | def readAndWritefile(test_directory): 49 | test_directory_path = os.path.join(test_directory_root, test_directory) 50 | 51 | test_path = os.path.join(test_directory_path, 'test.plist') 52 | output_path = os.path.join(test_directory_path, 'output.plist') 53 | 54 | test_input = pbPlist.PBPlist(test_path) 55 | test_input.write(output_path) 56 | test_output = pbPlist.PBPlist(output_path) 57 | 58 | return (test_input, test_output) 59 | 60 | class pbPlistTestCases(unittest.TestCase): 61 | 62 | def test_array_encoding_hint(self): 63 | test_input, test_output = readAndWritefile('array_encoding_hint') 64 | self.assertEqual(test_input.string_encoding, 'UTF8') 65 | self.assertEqual(test_output.string_encoding, 'UTF8') 66 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 67 | self.assertNotEqual(len(str(test_input.root)), 0) 68 | self.assertNotEqual(len(str(test_output.root)), 0) 69 | self.assertEqual(len(str(test_input.root)), len(str(test_output.root))) 70 | 71 | def test_array_qstrings(self): 72 | test_input, test_output = readAndWritefile('array_qstrings') 73 | 74 | item_one = u'hello world!' 75 | item_two = u'this is a test' 76 | item_three = u'Of quoted strings' 77 | 78 | input_items = test_input.root 79 | self.assertEqual(input_items[0], item_one) 80 | self.assertEqual(input_items[1], item_two) 81 | self.assertEqual(input_items[2], item_three) 82 | 83 | output_items = test_output.root 84 | self.assertEqual(output_items[0], item_one) 85 | self.assertEqual(output_items[1], item_two) 86 | self.assertEqual(output_items[2], item_three) 87 | 88 | self.assertEqual(input_items[0], output_items[0]) 89 | self.assertEqual(input_items[1], output_items[1]) 90 | self.assertEqual(input_items[2], output_items[2]) 91 | 92 | self.assertEqual(input_items, output_items) 93 | 94 | def test_array_strings(self): 95 | test_input, test_output = readAndWritefile('array_strings') 96 | 97 | item_one = u'obj1' 98 | item_two = u'obj2' 99 | item_three = u'obj3' 100 | 101 | input_items = test_input.root 102 | self.assertEqual(input_items[0], item_one) 103 | self.assertEqual(input_items[1], item_two) 104 | self.assertEqual(input_items[2], item_three) 105 | 106 | output_items = test_output.root 107 | self.assertEqual(output_items[0], item_one) 108 | self.assertEqual(output_items[1], item_two) 109 | self.assertEqual(output_items[2], item_three) 110 | 111 | self.assertEqual(input_items[0], output_items[0]) 112 | self.assertEqual(input_items[1], output_items[1]) 113 | self.assertEqual(input_items[2], output_items[2]) 114 | 115 | self.assertEqual(input_items, output_items) 116 | 117 | def test_array_mixed(self): 118 | test_input, test_output = readAndWritefile('array_mixed') 119 | 120 | item_one = '' 121 | item_two = u'testing' 122 | 123 | input_items = test_input.root 124 | self.assertEqual(input_items[0], item_one) 125 | self.assertEqual(input_items[1], item_two) 126 | 127 | output_items = test_output.root 128 | self.assertEqual(output_items[0], item_one) 129 | self.assertEqual(output_items[1], item_two) 130 | 131 | self.assertEqual(input_items[0], output_items[0]) 132 | self.assertEqual(input_items[1], output_items[1]) 133 | 134 | self.assertEqual(input_items, output_items) 135 | 136 | def test_data_encoding_hint(self): 137 | test_input, test_output = readAndWritefile('data_encoding_hint') 138 | 139 | self.assertEqual(test_input.string_encoding, 'UTF8') 140 | self.assertEqual(test_output.string_encoding, 'UTF8') 141 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 142 | 143 | self.assertEqual(test_input.root, test_output.root) 144 | 145 | def test_dict_encoding_hint(self): 146 | test_input, test_output = readAndWritefile('dict_encoding_hint') 147 | 148 | self.assertEqual(test_input.string_encoding, 'UTF8') 149 | self.assertEqual(test_output.string_encoding, 'UTF8') 150 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 151 | 152 | self.assertTrue(isinstance(list(test_input.root.keys()), list)) 153 | self.assertTrue(len(test_input.root.keys()) == 0) 154 | 155 | self.assertTrue(isinstance(list(test_output.root.keys()), list)) 156 | self.assertTrue(len(test_output.root.keys()) == 0) 157 | 158 | def test_dict_string_values(self): 159 | test_input, test_output = readAndWritefile('dict_string_values') 160 | 161 | test_key_list = [u'a', u'b', u'c', u'd'] 162 | 163 | self.assertTrue(isinstance(list(test_input.root.keys()), list)) 164 | self.assertTrue(len(test_input.root.keys()) == 4) 165 | self.assertEqual(set(test_input.root.keys()), set(test_key_list)) 166 | 167 | self.assertTrue(isinstance(list(test_output.root.keys()), list)) 168 | self.assertTrue(len(test_output.root.keys()) == 4) 169 | self.assertEqual(set(test_output.root.keys()), set(test_key_list)) 170 | 171 | for key in test_input.root.keys(): 172 | self.assertEqual(test_input.root[key], test_output.root[key]) 173 | 174 | for key in test_output.root.keys(): 175 | self.assertEqual(test_output.root[key], test_input.root[key]) 176 | 177 | def test_empty_encoding_hint(self): 178 | test_input, test_output = readAndWritefile('empty_encoding_hint') 179 | 180 | self.assertEqual(test_input.string_encoding, 'UTF8') 181 | self.assertEqual(test_output.string_encoding, 'UTF8') 182 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 183 | 184 | self.assertIsNone(test_input.root) 185 | self.assertIsNone(test_output.root) 186 | self.assertEqual(test_input.root, test_output.root) 187 | 188 | def test_empty_plist(self): 189 | test_input, test_output = readAndWritefile('empty_plist') 190 | 191 | self.assertIsNone(test_input.string_encoding) 192 | self.assertIsNone(test_output.string_encoding) 193 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 194 | 195 | self.assertIsNone(test_input.root) 196 | self.assertIsNone(test_output.root) 197 | self.assertEqual(test_input.root, test_output.root) 198 | 199 | def test_qstring_encoding_hint(self): 200 | test_input, test_output = readAndWritefile('qstring_encoding_hint') 201 | 202 | self.assertEqual(test_input.string_encoding, 'UTF8') 203 | self.assertEqual(test_output.string_encoding, 'UTF8') 204 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 205 | 206 | self.assertEqual(test_input.root, test_output.root) 207 | 208 | def test_raw_array(self): 209 | test_input, test_output = readAndWritefile('raw_array') 210 | 211 | self.assertTrue(isinstance(test_input.root.value, list)) 212 | self.assertTrue(isinstance(test_output.root.value, list)) 213 | 214 | def test_raw_data(self): 215 | test_input, test_output = readAndWritefile('raw_data') 216 | 217 | self.assertTrue(isinstance(test_input.root.value, bytearray)) 218 | self.assertTrue(isinstance(test_output.root.value, bytearray)) 219 | 220 | def test_raw_dict(self): 221 | test_input, test_output = readAndWritefile('raw_dict') 222 | 223 | self.assertTrue(isinstance(test_input.root.value, pbRoot)) 224 | self.assertTrue(isinstance(test_output.root.value, pbRoot)) 225 | 226 | def test_raw_qstring(self): 227 | test_input, test_output = readAndWritefile('raw_qstring') 228 | 229 | self.assertTrue(isinstance(test_input.root.value, StringType())) 230 | self.assertTrue(isinstance(test_output.root.value, StringType())) 231 | 232 | def test_raw_string(self): 233 | test_input, test_output = readAndWritefile('raw_string') 234 | 235 | self.assertTrue(isinstance(test_input.root.value, StringType())) 236 | self.assertTrue(isinstance(test_output.root.value, StringType())) 237 | 238 | def test_string_encoding_hint(self): 239 | test_input, test_output = readAndWritefile('string_encoding_hint') 240 | 241 | self.assertEqual(test_input.string_encoding, 'UTF8') 242 | self.assertEqual(test_output.string_encoding, 'UTF8') 243 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 244 | 245 | self.assertEqual(test_input.root, test_output.root) 246 | 247 | def test_xcode_proj(self): 248 | test_input, test_output = readAndWritefile('xcode_proj') 249 | 250 | self.assertEqual(test_input.string_encoding, 'UTF8') 251 | self.assertEqual(test_output.string_encoding, 'UTF8') 252 | self.assertEqual(test_input.string_encoding, test_output.string_encoding) 253 | 254 | def test_bom_utf16(self): 255 | test_directory_path = os.path.join(test_directory_root, 'bom/utf-16') 256 | test_strings_path = os.path.join(test_directory_path, 'Localizable.strings') 257 | 258 | test_input = pbPlist.PBPlist(test_strings_path) 259 | self.assertEqual(len(test_input.root.keys()), 109) 260 | 261 | if __name__ == '__main__': 262 | unittest.main( 263 | testRunner=xmlrunner.XMLTestRunner(output='test-reports', outsuffix=os.environ['TOX_ENV_NAME']), 264 | failfast=False, 265 | buffer=False, 266 | catchbreak=False, 267 | exit=False 268 | ) 269 | 270 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | platform = linux2|darwin 3 | 4 | envlist = py27,py35,py372 5 | 6 | [testenv] 7 | commands = 8 | coverage run --parallel-mode tests/pbPlist_test.py 9 | coverage run --parallel-mode setup.py test 10 | 11 | deps = 12 | coverage 13 | unittest-xml-reporting 14 | --------------------------------------------------------------------------------