├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── codecov.yml ├── docs ├── Makefile ├── conf.py ├── contributing.rst ├── extending.rst ├── images │ ├── ndeflib.ico │ └── ndeflib.png ├── index.rst ├── license.rst ├── ndef.rst ├── records.rst └── records │ ├── bluetooth.rst │ ├── deviceinfo.rst │ ├── handover.rst │ ├── signature.rst │ ├── smartposter.rst │ ├── text.rst │ ├── uri.rst │ └── wifi.rst ├── requirements-dev.txt ├── requirements-pypi.txt ├── setup.cfg ├── setup.py ├── src └── ndef │ ├── __init__.py │ ├── bluetooth.py │ ├── deviceinfo.py │ ├── handover.py │ ├── message.py │ ├── record.py │ ├── signature.py │ ├── smartposter.py │ ├── text.py │ ├── uri.py │ └── wifi.py ├── tests ├── _test_record_base.py ├── test_bluetooth.py ├── test_deviceinfo.py ├── test_handover.py ├── test_message.py ├── test_record.py ├── test_signature.py ├── test_smartposter.py ├── test_text.py ├── test_uri.py └── test_wifi.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = ndef 4 | 5 | [paths] 6 | source = 7 | src/ndef 8 | .tox/*/lib/python*/site-packages/ndef 9 | .tox/pypy/site-packages/ndef 10 | 11 | [report] 12 | show_missing = True 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.egg-info 4 | .tox 5 | .cache 6 | .coverage* 7 | __pycache__/ 8 | docs/_build/ 9 | htmlcov 10 | dist 11 | python-2 12 | python-3 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: false 3 | 4 | cache: 5 | directories: 6 | - $HOME/.cache/pip 7 | 8 | language: python 9 | 10 | 11 | matrix: 12 | include: 13 | - python: "2.7" 14 | env: TOXENV=py2 15 | - python: "3.5" 16 | env: TOXENV=py3 17 | - python: "3.6" 18 | env: TOXENV=py3 19 | - python: "3.7" 20 | env: TOXENV=py3 21 | - python: "3.8" 22 | env: TOXENV=py3 23 | 24 | # Meta 25 | - python: "3.8" 26 | env: TOXENV=flake8 27 | - python: "3.8" 28 | env: TOXENV=manifest 29 | - python: "3.8" 30 | env: TOXENV=docs 31 | - python: "3.8" 32 | env: TOXENV=readme 33 | 34 | 35 | install: 36 | - pip install tox 37 | 38 | 39 | script: 40 | - tox 41 | 42 | 43 | before_install: 44 | - pip install codecov 45 | 46 | 47 | after_success: 48 | - tox -e coverage-report 49 | - codecov 50 | 51 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | ``ndeflib`` is written and maintained by `Stephen Tiedemann `_. 5 | 6 | The development is kindly supported by `Sony Stuttgart Technology Center `_. 7 | 8 | A full list of contributors can be found in `GitHub's overview `_. 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Thank you for considering contributing to **ndeflib**. There are many 8 | ways to help and any help is welcome. 9 | 10 | 11 | Reporting issues 12 | ================ 13 | 14 | - Under which versions of Python does this happen? This is especially 15 | important if your issue is encoding related. 16 | 17 | - Under which version of **ndeflib** does this happen? Check if this 18 | issue is fixed in the repository. 19 | 20 | 21 | Submitting patches 22 | ================== 23 | 24 | - Include tests if your patch is supposed to solve a bug, and explain 25 | clearly under which circumstances the bug happens. Make sure the 26 | test fails without your patch. 27 | 28 | - Include or update tests and documentation if your patch is supposed 29 | to add a new feature. Note that documentation is in two places, the 30 | code itself for rendering help pages and in the docs folder for the 31 | online documentation. 32 | 33 | - Follow `PEP 8 `_ and 34 | `PEP 257 `_. 35 | 36 | 37 | Development tips 38 | ================ 39 | 40 | - `Fork `_ the 41 | repository and clone it locally:: 42 | 43 | git clone git@github.com:your-username/ndeflib.git 44 | cd ndeflib 45 | 46 | - Create virtual environments for Python 2 an Python 3, setup the 47 | ndeftool package in develop mode, and install required development 48 | packages:: 49 | 50 | virtualenv python-2 51 | python3 -m venv python-3 52 | source python-2/bin/activate 53 | python setup.py develop 54 | pip install -r requirements-dev.txt 55 | source python-3/bin/activate 56 | python setup.py develop 57 | pip install -r requirements-dev.txt 58 | 59 | - Verify that all tests pass and the documentation is build:: 60 | 61 | tox 62 | 63 | - Preferably develop in the Python 3 virtual environment. Running 64 | ``tox`` ensures tests are run with both the Python 2 and Python 3 65 | interpreter but it takes some time to complete. Alternatively switch 66 | back and forth between versions and just run the tests:: 67 | 68 | source python-2/bin/activate 69 | py.test 70 | source python-3/bin/activate 71 | py.test 72 | 73 | - Test coverage should be close to 100 percent. A great help is the 74 | HTML output produced by coverage.py:: 75 | 76 | py.test --cov ndef --cov-report html 77 | firefox htmlcov/index.html 78 | 79 | - The documentation can be created and viewed loacally:: 80 | 81 | (cd docs && make html) 82 | firefox docs/_build/html/index.html 83 | 84 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.3.3 (2019-05-27) 6 | ------------------ 7 | 8 | * Support Python 3.7. 9 | 10 | * Python 3.7 mandates PEP 479 which made StopIteration within a 11 | generator raise RuntimeException. 12 | 13 | * The urllib.parse.quote function now follows RFC 3986 and does no 14 | longer convert the `~` character. 15 | 16 | * Importing ABCs from `collections` will no longer be possible with 17 | Python 3.8, must then import from `collections.abc`. 18 | 19 | * WiFi and Bluetooth Records print attribute keys in sorted order to 20 | have consitent output in documentation embedded tests (doctest). 21 | 22 | 0.3.2 (2018-02-05) 23 | ------------------ 24 | 25 | * Bugfix to not encode the Certificate URI if it is None or empty, 26 | contributed by `Nick Knudson `_. 27 | 28 | 0.3.1 (2017-11-21) 29 | ------------------ 30 | 31 | * Fixes the signature record to encode lengths as 16-bits. 32 | 33 | 0.3.0 (2017-11-17) 34 | ------------------ 35 | 36 | * Support for decoding and encoding of the NFC Forum Signature Record, 37 | contributed by `Nick Knudson `_. 38 | 39 | 0.2.0 (2016-11-16) 40 | ------------------ 41 | 42 | * Wi-Fi Simple Configuration (WSC) and P2P records and attributes 43 | decode and encode added. 44 | 45 | 0.1.1 (2016-07-14) 46 | ------------------ 47 | 48 | * Development status set to Stable, required new release level as PyPI 49 | doesn't allow changing. 50 | 51 | 0.1.0 (2016-07-14) 52 | ------------------ 53 | 54 | * First release with complete documentation and pushed to PyPI. 55 | * Fully implements decoding and encoding of generic records. 56 | * Implements specific record decode/encode for NFC Forum Text, Uri, 57 | Smartposter, Device Information, Handover Request, Handover Select, 58 | Handover Mediation, Handover Initiate, and Handover Carrier Record. 59 | * Tested to work with Python 2.7 and 3.5 with 100 % test coverage. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Stephen Tiedemann 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.rst *.yml LICENSE tox.ini .coveragerc docs/Makefile 2 | recursive-include tests *.py 3 | recursive-include docs *.ico 4 | recursive-include docs *.png 5 | recursive-include docs *.py 6 | recursive-include docs *.rst 7 | prune docs/_build 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Parse or generate NDEF messages 3 | =============================== 4 | 5 | .. image:: https://badge.fury.io/py/ndeflib.svg 6 | :target: https://pypi.python.org/pypi/ndeflib 7 | :alt: Python Package 8 | 9 | .. image:: https://readthedocs.org/projects/ndeflib/badge/?version=latest 10 | :target: http://ndeflib.readthedocs.io/en/latest/?badge=latest 11 | :alt: Latest Documentation 12 | 13 | .. image:: https://travis-ci.org/nfcpy/ndeflib.svg?branch=master 14 | :target: https://travis-ci.org/nfcpy/ndeflib 15 | :alt: Build Status 16 | 17 | .. image:: https://codecov.io/gh/nfcpy/ndeflib/branch/master/graph/badge.svg 18 | :target: https://codecov.io/gh/nfcpy/ndeflib 19 | :alt: Code Coverage 20 | 21 | The ``ndeflib`` is an `ISC `_-licensed Python package for parsing and generating NFC Data Exchange Format (NDEF) messages: 22 | 23 | .. code-block:: pycon 24 | 25 | >>> import ndef 26 | >>> hexstr = '9101085402656e48656c6c6f5101085402656e576f726c64' 27 | >>> octets = bytearray.fromhex(hexstr) 28 | >>> for record in ndef.message_decoder(octets): print(record) 29 | NDEF Text Record ID '' Text 'Hello' Language 'en' Encoding 'UTF-8' 30 | NDEF Text Record ID '' Text 'World' Language 'en' Encoding 'UTF-8' 31 | >>> message = [ndef.TextRecord("Hello"), ndef.TextRecord("World")] 32 | >>> b''.join(ndef.message_encoder(message)) == octets 33 | True 34 | 35 | The ``ndeflib`` documentation can be found on `Read the Docs `_, the code on `GitHub `_. It is `continously tested `_ for Python 2.7 and 3.5 with pretty complete `test coverage `_. 36 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: true 4 | comment: 5 | behavior: default 6 | layout: header, diff 7 | require_changes: false 8 | coverage: 9 | precision: 2 10 | range: 11 | - 70.0 12 | - 100.0 13 | round: down 14 | status: 15 | changes: false 16 | patch: true 17 | project: true 18 | parsers: 19 | gcov: 20 | branch_detection: 21 | conditional: true 22 | loop: true 23 | macro: false 24 | method: false 25 | javascript: 26 | enable_partials: false 27 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -v 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/nfcpy.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/nfcpy.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/nfcpy" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/nfcpy" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # nfcpy documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Sep 19 18:10:55 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import codecs 15 | import datetime 16 | import os 17 | import re 18 | 19 | try: 20 | import sphinx_rtd_theme 21 | except ImportError: 22 | sphinx_rtd_theme = None 23 | 24 | 25 | def read(*parts): 26 | """ 27 | Build an absolute path from *parts* and and return the contents of the 28 | resulting file. Assume UTF-8 encoding. 29 | """ 30 | here = os.path.abspath(os.path.dirname(__file__)) 31 | with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: 32 | return f.read() 33 | 34 | 35 | def find_version(*file_paths): 36 | """ 37 | Build a path from *file_paths* and search for a ``__version__`` 38 | string inside. 39 | """ 40 | version_file = read(*file_paths) 41 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 42 | version_file, re.M) 43 | if version_match: 44 | return version_match.group(1) 45 | raise RuntimeError("Unable to find version string.") 46 | 47 | 48 | # If extensions (or modules to document with autodoc) are in another directory, 49 | # add these directories to sys.path here. If the directory is relative to the 50 | # documentation root, use os.path.abspath to make it absolute, like shown here. 51 | #sys.path.insert(0, os.path.abspath('.')) 52 | 53 | # -- General configuration ------------------------------------------------ 54 | 55 | # If your documentation needs a minimal Sphinx version, state it here. 56 | needs_sphinx = '1.0' 57 | 58 | # Add any Sphinx extension module names here, as strings. They can be 59 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 60 | # ones. 61 | extensions = [ 62 | 'sphinx.ext.autodoc', 63 | 'sphinx.ext.doctest', 64 | 'sphinx.ext.intersphinx', 65 | 'sphinx.ext.todo', 66 | ] 67 | intersphinx_mapping = { 68 | 'python': ('https://docs.python.org/3', None) 69 | } 70 | autodoc_member_order = 'bysource' 71 | autodoc_default_options = { 72 | 'members': True, 73 | 'show-inheritance': True 74 | } 75 | 76 | # Add any paths that contain templates here, relative to this directory. 77 | templates_path = ['_templates'] 78 | 79 | # The suffix of source filenames. 80 | source_suffix = '.rst' 81 | 82 | # The encoding of source files. 83 | source_encoding = 'utf-8-sig' 84 | 85 | # The master toctree document. 86 | master_doc = 'index' 87 | 88 | # General information about the project. 89 | project = u'ndeflib' 90 | year = datetime.date.today().year 91 | copyright = u'2016{0}, Stephen Tiedemann'.format( 92 | u'-{0}'.format(year) if year > 2016 else u"" 93 | ) 94 | 95 | # A string of reStructuredText that will be included at the end of 96 | # every source file that is read. This is the right place to add 97 | # substitutions that should be available in every file. 98 | rst_epilog = """ 99 | .. _NFC Forum: http://nfc-forum.org/ 100 | """ 101 | 102 | # A string of reStructuredText that will be included at the beginning 103 | # of every source file that is read. 104 | rst_prolog = """ 105 | """ 106 | 107 | # The version info for the project you're documenting, acts as replacement for 108 | # |version| and |release|, also used in various other places throughout the 109 | # built documents. 110 | # 111 | # The full version, including alpha/beta/rc tags. 112 | release = find_version("../src/ndef/__init__.py") 113 | # The short X.Y version. 114 | version = release.rsplit(u".", 1)[0] 115 | 116 | # The language for content autogenerated by Sphinx. Refer to documentation 117 | # for a list of supported languages. 118 | language = None 119 | 120 | # There are two options for replacing |today|: either, you set today to some 121 | # non-false value, then it is used: 122 | #today = '' 123 | # Else, today_fmt is used as the format for a strftime call. 124 | #today_fmt = '%B %d, %Y' 125 | 126 | # List of patterns, relative to source directory, that match files and 127 | # directories to ignore when looking for source files. 128 | exclude_patterns = ['_build'] 129 | 130 | # The reST default role (used for this markup: `text`) to use for all 131 | # documents. 132 | default_role = 'py:obj' 133 | 134 | # If true, '()' will be appended to :func: etc. cross-reference text. 135 | add_function_parentheses = True 136 | 137 | # If true, the current module name will be prepended to all description 138 | # unit titles (such as .. function::). 139 | add_module_names = True 140 | 141 | # If true, sectionauthor and moduleauthor directives will be shown in the 142 | # output. They are ignored by default. 143 | show_authors = False 144 | 145 | # The name of the Pygments (syntax highlighting) style to use. 146 | pygments_style = 'sphinx' 147 | 148 | # A list of ignored prefixes for module index sorting. 149 | #modindex_common_prefix = [] 150 | 151 | # If true, keep warnings as "system message" paragraphs in the built documents. 152 | #keep_warnings = False 153 | 154 | 155 | # -- Options for HTML output --------------------------------------------------- 156 | 157 | # The theme to use for HTML and HTML Help pages. See the documentation for 158 | # a list of builtin themes. 159 | 160 | if sphinx_rtd_theme: 161 | html_theme = "sphinx_rtd_theme" 162 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 163 | else: 164 | html_theme = "default" 165 | 166 | #html_title = ' '.join([project, version, "documentation"]) 167 | #html_short_title = ' '.join([project, version]) 168 | #html_last_updated_fmt = '%b %d, %Y' 169 | #html_show_sourcelink = True 170 | 171 | # Theme options are theme-specific and customize the look and feel of a theme 172 | # further. For a list of options available for each theme, see the 173 | # documentation. 174 | #html_theme_options = {} 175 | 176 | # Add any paths that contain custom themes here, relative to this directory. 177 | #html_theme_path = [] 178 | 179 | # The name for this set of Sphinx documents. If None, it defaults to 180 | # " v documentation". 181 | #html_title = None 182 | 183 | # A shorter title for the navigation bar. Default is the same as html_title. 184 | #html_short_title = None 185 | 186 | # The name of an image file (relative to this directory) to place at the top 187 | # of the sidebar. 188 | html_logo = "images/ndeflib.png" 189 | 190 | # The name of an image file (within the static path) to use as favicon of the 191 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 192 | # pixels large. 193 | html_favicon = "images/ndeflib.ico" 194 | 195 | # Add any paths that contain custom static files (such as style sheets) here, 196 | # relative to this directory. They are copied after the builtin static files, 197 | # so a file named "default.css" will overwrite the builtin "default.css". 198 | # html_static_path = ['_static'] 199 | 200 | # Add any extra paths that contain custom files (such as robots.txt or 201 | # .htaccess) here, relative to this directory. These files are copied 202 | # directly to the root of the documentation. 203 | #html_extra_path = [] 204 | 205 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 206 | # using the given strftime format. 207 | #html_last_updated_fmt = '%b %d, %Y' 208 | 209 | # If true, SmartyPants will be used to convert quotes and dashes to 210 | # typographically correct entities. 211 | #html_use_smartypants = True 212 | 213 | # Custom sidebar templates, maps document names to template names. 214 | #html_sidebars = {} 215 | 216 | # Additional templates that should be rendered to pages, maps page names to 217 | # template names. 218 | #html_additional_pages = {} 219 | 220 | # If false, no module index is generated. 221 | #html_domain_indices = True 222 | 223 | # If false, no index is generated. 224 | #html_use_index = True 225 | 226 | # If true, the index is split into individual pages for each letter. 227 | #html_split_index = False 228 | 229 | # If true, links to the reST sources are added to the pages. 230 | #html_show_sourcelink = True 231 | 232 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 233 | #html_show_sphinx = True 234 | 235 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 236 | #html_show_copyright = True 237 | 238 | # If true, an OpenSearch description file will be output, and all pages will 239 | # contain a tag referring to it. The value of this option must be the 240 | # base URL from which the finished HTML is served. 241 | #html_use_opensearch = '' 242 | 243 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 244 | #html_file_suffix = None 245 | 246 | # Output file base name for HTML help builder. 247 | htmlhelp_basename = 'ndeflibdoc' 248 | 249 | 250 | # -- Options for LaTeX output --------------------------------------------- 251 | 252 | latex_elements = { 253 | # The paper size ('letterpaper' or 'a4paper'). 254 | #'papersize': 'letterpaper', 255 | 256 | # The font size ('10pt', '11pt' or '12pt'). 257 | #'pointsize': '10pt', 258 | 259 | # Additional stuff for the LaTeX preamble. 260 | #'preamble': '', 261 | } 262 | 263 | # Grouping the document tree into LaTeX files. List of tuples 264 | # (source start file, target name, title, 265 | # author, documentclass [howto, manual, or own class]). 266 | latex_documents = [ 267 | ('index', 'ndeflib.tex', u'ndeflib documentation', 268 | u'Stephen Tiedemann', 'manual'), 269 | ] 270 | 271 | # The name of an image file (relative to this directory) to place at the top of 272 | # the title page. 273 | #latex_logo = None 274 | 275 | # For "manual" documents, if this is true, then toplevel headings are parts, 276 | # not chapters. 277 | #latex_use_parts = False 278 | 279 | # If true, show page references after internal links. 280 | #latex_show_pagerefs = False 281 | 282 | # If true, show URL addresses after external links. 283 | #latex_show_urls = False 284 | 285 | # Documents to append as an appendix to all manuals. 286 | #latex_appendices = [] 287 | 288 | # If false, no module index is generated. 289 | #latex_domain_indices = True 290 | 291 | 292 | # -- Options for manual page output --------------------------------------- 293 | 294 | # One entry per manual page. List of tuples 295 | # (source start file, name, description, authors, manual section). 296 | man_pages = [ 297 | ('index', 'ndeflib', u'ndeflib Documentation', 298 | [u'Stephen Tiedemann'], 1) 299 | ] 300 | 301 | # If true, show URL addresses after external links. 302 | #man_show_urls = False 303 | 304 | # -- Options for Texinfo output ------------------------------------------- 305 | 306 | # Grouping the document tree into Texinfo files. List of tuples 307 | # (source start file, target name, title, author, 308 | # dir menu entry, description, category) 309 | texinfo_documents = [ 310 | ('index', 'ndeflib', u'ndeflib Documentation', 311 | u'Stephen Tiedemann', 'ndeflib', 'Parse or generate NDEF messages.', 312 | 'Miscellaneous'), 313 | ] 314 | 315 | # Documents to append as an appendix to all manuals. 316 | #texinfo_appendices = [] 317 | 318 | # If false, no module index is generated. 319 | #texinfo_domain_indices = True 320 | 321 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 322 | #texinfo_show_urls = 'footnote' 323 | 324 | # If true, do not generate a @detailmenu in the "Top" node's menu. 325 | #texinfo_no_detailmenu = False 326 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. include:: ../CONTRIBUTING.rst 4 | -------------------------------------------------------------------------------- /docs/extending.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _extending: 4 | 5 | Adding Private Records 6 | ====================== 7 | 8 | Private (or experimental) NDEF Record decoding and encoding can be easily made 9 | recognized by the :func:`message_decoder` and :func:`message_encoder`. It just 10 | requires a record class that inherits from ``ndef.record.GlobalRecord`` and 11 | provides the desired record type value as well as the payload decode and encode 12 | methods. The following sections document the decode/encode interface by way of 13 | example, with increasing complexity. 14 | 15 | Record with no Payload 16 | ---------------------- 17 | 18 | This is the most simple yet fully functional record class. It inherits from the 19 | abstract class ``ndef.record.GlobalRecord`` (which is actually just an abstract 20 | version of `Record` to make sure the dervied class implements the payload 21 | decode and encode methods. The record type string is set via the ``_type`` class 22 | attribute. The ``_encode_payload`` method must return the `bytes` for the NDEF 23 | Record PAYLOAD field, usually encoded from other record attributes but here it's 24 | just empty. The ``_decode_payload`` classmethod receives the NDEF Record PAYLOAD 25 | field the `bytes` type *octets* and returns a record object populated with the 26 | decoded PAYLOAD data, again nothing for the record with no payload. The 27 | ``_decode_min_payload_length`` and ``_decode_max_payload_length`` class 28 | attributes (put at the end of the class definition only to align with the 29 | explanation) inform the record decoder about the minmum required and maximum 30 | acceptable PAYLOAD size, thus the *octets* argument will never have less or more 31 | data. If a class does not set those values, the default min value is 0 and the 32 | default max value is `Record.MAX_PAYLOAD_SIZE`. 33 | 34 | .. testcode:: 35 | 36 | import ndef 37 | 38 | class ExampleRecordWithNoPayload(ndef.record.GlobalRecord): 39 | """An NDEF Record with no payload.""" 40 | 41 | _type = 'urn:nfc:ext:nfcpy.org:x-empty' 42 | 43 | def _encode_payload(self): 44 | # This record does not have any payload to encode. 45 | return b'' 46 | 47 | @classmethod 48 | def _decode_payload(cls, octets, errors): 49 | # This record does not have any payload to decode. 50 | return cls() 51 | 52 | _decode_min_payload_length = 0 53 | _decode_max_payload_length = 0 54 | 55 | ndef.Record.register_type(ExampleRecordWithNoPayload) 56 | 57 | record = ExampleRecordWithNoPayload() 58 | octets = b''.join(ndef.message_encoder([record])) 59 | print("encoded: {}".format(octets)) 60 | 61 | message = list(ndef.message_decoder(octets)) 62 | print("decoded: {}".format(message[0])) 63 | 64 | .. testoutput:: 65 | 66 | encoded: b'\xd4\x11\x00nfcpy.org:x-empty' 67 | decoded: NDEF Example Record With No Payload ID '' PAYLOAD 0 byte 68 | 69 | 70 | Example Temperature Record 71 | -------------------------- 72 | 73 | This record carries an unsigned 32-bit integer timestamp that is the seconds 74 | since 1.1.1970 (and will overflow on February 7, 2106 !) and a signed 16-bit 75 | integer with a temperature. The payload is thus a fixed structure with exactly 6 76 | octets for which the inherited ``_decode_struct`` and ``_encode_struct`` methods 77 | are perfectly suited. They are quite the same as using `struct.unpack_from` and 78 | `struct.pack` but return a single value directly and not as a (value, ) tuple. 79 | 80 | This example also shows how the ``__format__`` method is used to provide an 81 | arguments and a data view for the `str() ` and :func:`repr` functions. 82 | 83 | .. testcode:: 84 | 85 | import ndef 86 | import time 87 | 88 | class ExampleTemperatureRecord(ndef.record.GlobalRecord): 89 | """An NDEF Record that carries a temperature and a timestamp.""" 90 | 91 | _type = 'urn:nfc:ext:nfcpy.org:x-temp' 92 | 93 | def __init__(self, timestamp, temperature): 94 | self._time = timestamp 95 | self._temp = temperature 96 | 97 | def __format__(self, format_spec): 98 | if format_spec == 'args': 99 | # Return the init args for repr() but w/o class name and brackets 100 | return "{r._time}, {r._temp}".format(r=self) 101 | if format_spec == 'data': 102 | # Return a nicely formatted content string for str() 103 | data_str = time.strftime('%d.%m.%Y', time.gmtime(self._time)) 104 | time_str = time.strftime('%H:%M:%S', time.gmtime(self._time)) 105 | return "{}°C on {} at {}".format(self._temp, data_str, time_str) 106 | return super(ExampleTemperatureRecord, self).__format__(format_spec) 107 | 108 | def _encode_payload(self): 109 | return self._encode_struct('>Lh', self._time, self._temp) 110 | 111 | @classmethod 112 | def _decode_payload(cls, octets, errors): 113 | timestamp, temperature = cls._decode_struct('>Lh', octets) 114 | return cls(timestamp, temperature) 115 | 116 | # Make sure that _decode_payload gets only called with 6 octets 117 | _decode_min_payload_length = 6 118 | _decode_max_payload_length = 6 119 | 120 | ndef.Record.register_type(ExampleTemperatureRecord) 121 | 122 | record = ExampleTemperatureRecord(1468410873, 25) 123 | octets = b''.join(ndef.message_encoder([record])) 124 | print("encoded: {}".format(octets)) 125 | 126 | message = list(ndef.message_decoder(octets)) 127 | print("decoded: {}".format(message[0])) 128 | 129 | .. testoutput:: 130 | 131 | encoded: b'\xd4\x10\x06nfcpy.org:x-tempW\x86+\xf9\x00\x19' 132 | decoded: NDEF Example Temperature Record ID '' 25°C on 13.07.2016 at 11:54:33 133 | 134 | 135 | 136 | Type Length Value Record 137 | ------------------------ 138 | 139 | This record class demonstrates how ``_decode_struct`` and ``_encode_struct`` can 140 | be used for typical Type-Length-Value constructs. The notion 'BB+' is a slight 141 | extension of the `struct` module's format string syntax and means to decode or 142 | encode a 1 byte Type field, a 1 byte Length field and Length number of octets as 143 | Value. The ``_decode_struct`` method then returns just the Type and Value. The 144 | ``_encode_struct`` needs only the Type and Value arguments and takes the Length 145 | from Value. Another format string syntax extension, but not not used in the 146 | example, is a trailing '*' character. That just means that all remaining octets 147 | are returned as `bytes`. 148 | 149 | This example also demonstrates how decode and encode error exceptions are 150 | generated with the ``_decode_error`` and ``_encode_error`` methods. These 151 | methods return an instance of ``ndef.DecodeError`` and ``ndef.EncodeError`` with 152 | the fully qualified class name followed by the expanded format string. Two 153 | similar methods, ``_type_error`` and ``_value_error`` may be used whenever a 154 | `TypeError` or `ValueError` shall be reported with the full classname in its 155 | error string. They do also check if the first word in the format string matches 156 | a data attribute name, and if, the string is joined with a '.' to the classname. 157 | 158 | The ``_decode_payload`` method also shows the use of the errors argument. With 159 | 'strict' interpretation of errors the payload is expected to have the Type 1 TLV 160 | encoded in first place (although not a recommended design for TLV loops). The 161 | errors argument may also say 'relax' and then the order won't matter. 162 | 163 | .. testcode:: 164 | 165 | import ndef 166 | 167 | class ExampleTypeLengthValueRecord(ndef.record.GlobalRecord): 168 | """An NDEF Record with carries a temperature and a timestamp.""" 169 | 170 | _type = 'urn:nfc:ext:nfcpy.org:x-tlvs' 171 | 172 | def __init__(self, *args): 173 | # We expect each argument to be a tuple of (Type, Value) where Type 174 | # is int and Value is bytes. So *args* will be a tuple of tuples. 175 | self._tlvs = args 176 | 177 | def _encode_payload(self): 178 | if sum([t for t, v in self._tlvs if t == 1]) != 1: 179 | raise self._encode_error("exactly one Type 1 TLV is required") 180 | tlv_octets = [] 181 | for t, v in self._tlvs: 182 | tlv_octets.append(self._encode_struct('>BB+', t, v)) 183 | return b''.join(tlv_octets) 184 | 185 | @classmethod 186 | def _decode_payload(cls, octets, errors): 187 | tlvs = [] 188 | offset = 0 189 | while offset < len(octets): 190 | t, v = cls._decode_struct('>BB+', octets, offset) 191 | offset = offset + 2 + len(v) 192 | tlvs.append((t, v)) 193 | if sum([t for t, v in tlvs if t == 1]) != 1: 194 | raise cls._encode_error("missing the mandatory Type 1 TLV") 195 | if errors == 'strict' and len(tlvs) > 0 and tlvs[0][0] != 1: 196 | errstr = 'first TLV must be Type 1, not Type {}' 197 | raise cls._encode_error(errstr, tlvs[0][0]) 198 | return cls(*tlvs) 199 | 200 | # We need at least the 2 octets Type, Length for the first TLV. 201 | _decode_min_payload_length = 2 202 | 203 | ndef.Record.register_type(ExampleTypeLengthValueRecord) 204 | 205 | record = ExampleTypeLengthValueRecord((1, b'abc'), (5, b'xyz')) 206 | octets = b''.join(ndef.message_encoder([record])) 207 | print("encoded: {}".format(octets)) 208 | 209 | message = list(ndef.message_decoder(octets)) 210 | print("decoded: {}".format(message[0])) 211 | 212 | .. testoutput:: 213 | 214 | encoded: b'\xd4\x10\nnfcpy.org:x-tlvs\x01\x03abc\x05\x03xyz' 215 | decoded: NDEF Example Type Length Value Record ID '' PAYLOAD 10 byte '0103616263050378797a' 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /docs/images/ndeflib.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcpy/ndeflib/2afb429d7f8cd98d9f673f4ea9f2cbbc8b5795c7/docs/images/ndeflib.ico -------------------------------------------------------------------------------- /docs/images/ndeflib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcpy/ndeflib/2afb429d7f8cd98d9f673f4ea9f2cbbc8b5795c7/docs/images/ndeflib.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | =================================== 4 | NDEF Decoder and Encoder for Python 5 | =================================== 6 | 7 | .. _ISCL: http://choosealicense.com/licenses/isc/ 8 | .. _GitHub: https://github.com/nfcpy/ndeflib 9 | .. _PyPI: https://pypi.python.org/pypi/ndeflib 10 | 11 | The ``ndeflib`` is a Python package for parsing and generating NFC Data Exchange 12 | Format (NDEF) messages. It is licensed under the `ISCL`_, hosted on `GitHub`_ 13 | and can be installed from `PyPI`_. 14 | 15 | >>> import ndef 16 | >>> hexstr = '9101085402656e48656c6c6f5101085402656e576f726c64' 17 | >>> octets = bytearray.fromhex(hexstr) 18 | >>> for record in ndef.message_decoder(octets): print(record) 19 | NDEF Text Record ID '' Text 'Hello' Language 'en' Encoding 'UTF-8' 20 | NDEF Text Record ID '' Text 'World' Language 'en' Encoding 'UTF-8' 21 | >>> message = [ndef.TextRecord("Hello"), ndef.TextRecord("World")] 22 | >>> b''.join(ndef.message_encoder(message)) == octets 23 | True 24 | 25 | .. toctree:: 26 | :caption: Documentation 27 | :maxdepth: 2 28 | 29 | Decoding and Encoding 30 | Known Record Types 31 | Adding Private Records 32 | contributing 33 | license 34 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | ======= 4 | License 5 | ======= 6 | 7 | The **ndeflib** is licensed under the Internet Systems Consortium ISC 8 | license. This is a permissive free software license functionally equivalent to 9 | the simplified BSD and the MIT license. 10 | 11 | License text 12 | ------------ 13 | 14 | .. include:: ../LICENSE 15 | -------------------------------------------------------------------------------- /docs/ndef.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | NDEF Decoding and Encoding 4 | ========================== 5 | 6 | NDEF (NFC Data Exchange Format), specified by the `NFC Forum`_, is a binary 7 | message format used to encapsulate application-defined payloads exchanged 8 | between NFC Devices and Tags. Each payload is encoded as an NDEF Record with 9 | fields that specify the payload size, payload type, an optional payload 10 | identifier, and flags for indicating the first and last record of an NDEF 11 | Message or tagging record chunks. An NDEF Message is simply a sequence of one or 12 | more NDEF Records where the first and last record are marked by the Message 13 | Begin and End flags. 14 | 15 | The ``ndef`` package interface for decoding and encoding of NDEF Messages 16 | consists of the :func:`message_decoder` and :func:`message_encoder` functions 17 | that both return generators for decoding octets into :class:`ndef.Record` 18 | instances or encoding :class:`ndef.Record` instances into octets. :ref:`Known 19 | record types ` are decoded into instances of their implementation 20 | class and can be directly encoded as part of a message. 21 | 22 | Message Decoder 23 | --------------- 24 | 25 | .. function:: message_decoder(stream_or_bytes, errors='strict', \ 26 | known_types=Record._known_types) 27 | 28 | Returns a generator function that decodes NDEF Records from a file-like, 29 | byte-oriented stream or a bytes object given by the *stream_or_bytes* 30 | argument. When the *errors* argument is set to 'strict' (the default), the 31 | decoder expects a valid NDEF Message with Message Begin and End flags set for 32 | the first and last record and decoding of known record types will fail for 33 | any format errors. Minor format errors are accepted when *errors* is set to 34 | 'relax'. With *errors* set to 'ignore' the decoder silently stops when a 35 | non-correctable error is encountered. The *known_types* argument provides 36 | the mapping of record type strings to class implementations. It defaults to 37 | all global records implemented by `ndeflib` or additionally registered from 38 | user code. It's main use would probably be to force decoding into only 39 | generic records with `known_types={}`. 40 | 41 | :param stream_or_bytes: message data octets 42 | :type stream_or_bytes: byte stream or bytes object 43 | :param str errors: error handling strategy, may be 'strict', 'relax' or 'ignore' 44 | :param dict known_types: mapping of known record types to implementation classes 45 | :raises ndef.DecodeError: for data format errors (unless *errors* is set to 'ignore') 46 | 47 | >>> import ndef 48 | >>> octets = bytearray.fromhex('910303414243616263 5903030144454630646566') 49 | >>> decoder = ndef.message_decoder(octets) 50 | >>> next(decoder) 51 | ndef.record.Record('urn:nfc:wkt:ABC', '', bytearray(b'abc')) 52 | >>> next(decoder) 53 | ndef.record.Record('urn:nfc:wkt:DEF', '0', bytearray(b'def')) 54 | >>> next(decoder) 55 | Traceback (most recent call last): 56 | File "", line 1, in 57 | StopIteration 58 | >>> message = list(ndef.message_decoder(octets)) 59 | >>> len(message) 60 | 2 61 | 62 | 63 | Message Encoder 64 | --------------- 65 | 66 | .. function:: message_encoder(message=None, stream=None) 67 | 68 | Returns a generator function that encodes :class:`ndef.Record` objects into 69 | an NDEF Message octet sequence. The *message* argument is either an iterable 70 | of records or None, if *message* is None the records must be sequentially 71 | send to the encoder (as for any generator the first send value must be None, 72 | specific to the message encoder is that octets are generated for the previous 73 | record and a final None value must be send for the last record octets). The 74 | *stream* argument controls the output of the generator function. If *stream* 75 | is None, the generator yields a bytes object for each encoded record. 76 | Otherwise, it must be a file-like, byte-oriented stream that receives the 77 | encoded octets and the generator yields the number of octets written per 78 | record. 79 | 80 | :param message: sequence of records to encode 81 | :type message: iterable or None 82 | :param stream: file-like output stream 83 | :type stream: byte stream or None 84 | :raises ndef.EncodeError: for invalid record parameter values or types 85 | 86 | >>> import ndef 87 | >>> record1 = ndef.Record('urn:nfc:wkt:ABC', '1', b'abc') 88 | >>> record2 = ndef.Record('urn:nfc:wkt:DEF', '2', b'def') 89 | >>> encoder = ndef.message_encoder() 90 | >>> encoder.send(None) 91 | >>> encoder.send(record1) 92 | >>> encoder.send(record2) 93 | b'\x99\x03\x03\x01ABC1abc' 94 | >>> encoder.send(None) 95 | b'Y\x03\x03\x01DEF2def' 96 | >>> message = [record1, record2] 97 | >>> b''.join((ndef.message_encoder(message))) 98 | b'\x99\x03\x03\x01ABC1abcY\x03\x03\x01DEF2def' 99 | >>> list((ndef.message_encoder(message, open('/dev/null', 'wb')))) 100 | [11, 11] 101 | 102 | 103 | 104 | Record Class 105 | ------------ 106 | 107 | .. class:: Record(type='', name='', data=b'') 108 | 109 | This class implements generic decoding and encoding of an NDEF Record and is 110 | the base for all specialized record type classes. The NDEF Record Payload 111 | Type encoded by the TNF (Type Name Format) and TYPE field is represented by a 112 | single *type* string argument: 113 | 114 | *Empty (TNF 0)* 115 | 116 | An *Empty* record has no TYPE, ID, and PAYLOAD fields. This is set if the 117 | *type* argument is absent, None, or an empty string. Encoding ignores 118 | whatever is set as *name* and *data*, producing just the short length 119 | record ``b'\x10\x00\x00'``. 120 | 121 | *NFC Forum Well Known Type (TNF 1)* 122 | 123 | An *NFC Forum Well Known Type* is a URN (:rfc:`2141`) with namespace 124 | identifier (NID) ``nfc`` and the namespace specific string (NSS) prefixed 125 | with ``wkt:``. When encoding, the type is written as a relative-URI 126 | (cf. :rfc:`3986`), omitting the NID and the prefix. For example, the type 127 | ``urn:nfc:wkt:T`` is encoded as TNF 1, TYPE ``T``. 128 | 129 | *Media-type as defined in RFC 2046 (TNF 2)* 130 | 131 | A *media-type* follows the media-type grammar defined in :rfc:`2046`. 132 | Records that carry a payload with an existing, registered media type should 133 | use this record type. Note that the record type indicates the type of the 134 | payload; it does not refer to a MIME message that contains an entity of the 135 | given type. For example, the media type 'image/jpeg' indicates that the 136 | payload is an image in JPEG format using JFIF encoding as defined by 137 | :rfc:`2046`. 138 | 139 | *Absolute URI as defined in RFC 3986 (TNF 3)* 140 | 141 | An *absolute-URI* follows the absolute-URI BNF construct defined by 142 | :rfc:`3986`. This type can be used for payloads that are defined by 143 | URIs. For example, records that carry a payload with an XML-based message 144 | type may use the XML namespace identifier of the root element as the record 145 | type, like a SOAP/1.1 message may be 146 | ``http://schemas.xmlsoap.org/soap/envelope/``. 147 | 148 | *NFC Forum External Type (TNF 4)* 149 | 150 | An *NFC Forum External Type* is a URN (:rfc:`2141`) with namespace 151 | identifier (NID) ``nfc`` and the namespace specific string (NSS) prefixed 152 | with ``ext:``. When encoding, the type is written as a relative-URI 153 | (cf. :rfc:`3986`), omitting the NID and the prefix. For example, the type 154 | ``urn:nfc:ext:nfcpy.org:T`` will be encoded as TNF 4, TYPE ``nfcpy.org:T``. 155 | 156 | *Unknown (TNF 5)* 157 | 158 | The *Unknown* record type indicates that the type of the payload is 159 | unknown, similar to the ``application/octet-stream`` media type. It is set 160 | with the *type* argument ``unknown`` and encoded with an empty TYPE field. 161 | 162 | *Unchanged (TNF 6)* 163 | 164 | The *Unchanged* record type is used for all except the first record in a 165 | chunked payload. It is set with the *type* argument ``unchanged`` and 166 | encoded with an empty TYPE field. 167 | 168 | The *type* argument sets the final value of the :attr:`type` attribute, which 169 | provides the value only for reading. The *name* and *data* argument set the 170 | initial values of the :attr:`name` and :attr:`data` attributes. They can both 171 | be changed later. 172 | 173 | :param str type: final value for the :attr:`type` attribute 174 | :param str name: initial value for the see :attr:`name` attribute 175 | :param bytes data: initial value for the :attr:`data` attribute 176 | 177 | 178 | .. attribute:: type 179 | 180 | The record type is a read-only text string set either by decoding or 181 | through initialization. 182 | 183 | .. attribute:: name 184 | 185 | The record name is a text string that corresponds to the NDEF Record ID 186 | field. The maximum capacity is 255 8-bit characters, converted in and out 187 | as latin-1. 188 | 189 | .. attribute:: data 190 | 191 | The record data is a bytearray with the sequence of octets that correspond 192 | to the NDEF Record PAYLOAD field. The attribute itself is readonly but the 193 | bytearray content can be changed. Note that for derived record classes 194 | this becomes a read-only bytes object with the content encoded from the 195 | record's attributes. 196 | 197 | .. attribute:: MAX_PAYLOAD_SIZE 198 | 199 | This is a class data attribute that restricts the decodable and encodable 200 | maximum NDEF Record PAYLOAD size from the theoretical value of up to 4GB 201 | to 1MB. If needed, a different value can be assigned to the record class: 202 | ``ndef.Record.MAX_PAYLOAD_SIZE = 100*1024`` 203 | 204 | .. classmethod:: register_type(record_class) 205 | 206 | Register a derived record class as a known type for decoding. This creates 207 | an entry for the record_class type string to be decoded as a record_class 208 | instance. Beyond internal use this is needed for :ref:`adding private 209 | records `. 210 | -------------------------------------------------------------------------------- /docs/records.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _known-types: 4 | 5 | Known Record Types 6 | ================== 7 | 8 | The ``ndef`` package implements special decoding and encoding for a number of 9 | known record types. 10 | 11 | .. toctree:: 12 | 13 | records/text 14 | records/uri 15 | records/smartposter 16 | records/deviceinfo 17 | records/handover 18 | records/bluetooth 19 | records/wifi 20 | records/signature 21 | -------------------------------------------------------------------------------- /docs/records/deviceinfo.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | Device Information Record 4 | ------------------------- 5 | 6 | The NDEF Device Information Record is a well-known record type defined by the 7 | `NFC Forum`_. It carries a number of Type-Length-Value data elements that 8 | provide information about the device, such as the manufacturer and device model 9 | name. 10 | 11 | .. class:: DeviceInformationRecord(vendor_name, model_name, \ 12 | unique_name=None, uuid_string=None, version_string=None) 13 | 14 | Initialize the record with required and optional device information. The 15 | vendor_name and model_name arguments are required, all other arguments are 16 | optional information. 17 | 18 | :param str vendor_name: sets the :attr:`vendor_name` attribute 19 | :param str model_name: sets the :attr:`model_name` attribute 20 | :param str unique_name: sets the :attr:`unique_name` attribute 21 | :param str uuid_string: sets the :attr:`uuid_string` attribute 22 | :param str version_string: sets the :attr:`version_string` attribute 23 | 24 | .. attribute:: type 25 | 26 | The Device Information Record type is ``urn:nfc:wkt:Di``. 27 | 28 | .. attribute:: name 29 | 30 | Value of the NDEF Record ID field, an empty `str` if not set. 31 | 32 | .. attribute:: data 33 | 34 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 35 | current attributes. 36 | 37 | .. attribute:: vendor_name 38 | 39 | Get or set the device vendor name `str`. 40 | 41 | .. attribute:: model_name 42 | 43 | Get or set the device model name `str`. 44 | 45 | .. attribute:: unique_name 46 | 47 | Get or set the device unique name `str`. 48 | 49 | .. attribute:: uuid_string 50 | 51 | Get or set the universially unique identifier `str`. 52 | 53 | .. attribute:: version_string 54 | 55 | Get or set the device firmware version `str`. 56 | 57 | .. attribute:: undefined_data_elements 58 | 59 | A list of undefined data elements as named tuples with data_type and 60 | data_bytes attributes. This is a reference to the internal list and may 61 | thus be updated in-place but it is strongly recommended to use the 62 | add_undefined_data_element method with data_type and data_bytes 63 | validation. It would also not be safe to rely on such implementation 64 | detail. 65 | 66 | .. method:: add_undefined_data_element(data_type, data_bytes) 67 | 68 | Add an undefined (reserved future use) device information data 69 | element. The data_type must be an an integer in range(5, 256). The 70 | data_bytes argument provides the up to 255 octets to transmit. 71 | 72 | Undefined data elements should not normally be added. This method is 73 | primarily here to allow data elements defined by future revisions of the 74 | specification before this implementation is updated. 75 | 76 | >>> import ndef 77 | >>> record = ndef.DeviceInformationRecord('Sony', 'RC-S380') 78 | >>> record.unique_name = 'Black NFC Reader connected to PC' 79 | >>> record.uuid_string = '123e4567-e89b-12d3-a456-426655440000' 80 | >>> record.version_string = 'NFC Port-100 v1.02' 81 | >>> len(b''.join(ndef.message_encoder([record]))) 82 | 92 83 | 84 | -------------------------------------------------------------------------------- /docs/records/handover.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _connection-handover: 4 | 5 | Connection Handover 6 | =================== 7 | 8 | The `NFC Forum`_ Connection Handover specification defines a number of Record 9 | structures that are used to exchange messages between Handover Requester, 10 | Selector and Mediator devices to eventually establish alternative carrier 11 | connections for additional data exchange. Generally, a requester device sends a 12 | Handover Request Message to announce supported alternative carriers and expects 13 | the selector device to return a Handover Select Message with a selection of 14 | alternative carriers supported by both devices. If the two devices are not close 15 | enough for NFC communication, a third device may use the Handover Mediation and 16 | Handover Initiate Messages to relay information between the two. 17 | 18 | Any of above mentioned Handover Messages is constructed as an NDEF Message where 19 | the first record associates the processing context. The Handover Request, 20 | Select, Mediation, and Initiate Record classes implement the appropriate 21 | context, i.e. record types known by context are decoded by associated record 22 | type classes while others are decoded as generic NDEF Records. 23 | 24 | Handover Request Record 25 | ----------------------- 26 | 27 | The Handover Request Record is the first record of a connection handover request 28 | message. Information enclosed within the payload of a handover request record 29 | includes the handover version number, a random number for resolving a handover 30 | request collision (when both peer devices simultaenously send a handover request 31 | message) and a number of references to alternative carrier information records 32 | subsequently encoded in the same message. 33 | 34 | >>> import ndef 35 | >>> from os import urandom 36 | >>> wsc = 'application/vnd.wfa.wsc' 37 | >>> message = [ndef.HandoverRequestRecord('1.3', urandom(2))] 38 | >>> message.append(ndef.HandoverCarrierRecord(wsc, None, 'wifi')) 39 | >>> message[0].add_alternative_carrier('active', message[1].name) 40 | 41 | .. class:: HandoverRequestRecord(version='1.3', crn=None, *alternative_carrier) 42 | 43 | Initialize the record with a *version* number, a collision resolution random 44 | number *crn* and zero or more *alternative_carrier*. The version number can 45 | be set as an 8-bit integer (with 4-bit major and minor part), or as a 46 | ``'{major}.{minor}'`` version string. An alternative carrier is given by a 47 | tuple with *carrier power state*, *carrier data reference* and zero or more 48 | *auxiliary data references*. The collision resolution number (crn) argument 49 | is the unsigned 16-bit random integer for connection handover version '1.2' 50 | or later, for any prior version number it must be None. 51 | 52 | :param version: handover version number 53 | :type version: int or str 54 | :param int crn: collision resolution random number 55 | :param tuple alternative_carrier: alternative carrier entry 56 | 57 | .. attribute:: type 58 | 59 | The Handover Request Record type is ``urn:nfc:wkt:Hr``. 60 | 61 | .. attribute:: name 62 | 63 | Value of the NDEF Record ID field, an empty `str` if not set. 64 | 65 | .. attribute:: data 66 | 67 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 68 | current attributes. 69 | 70 | .. attribute:: hexversion 71 | 72 | The version as an 8-bit integer with 4-bit major and minor part. This is a 73 | read-only attribute. 74 | 75 | .. attribute:: version_info 76 | 77 | The version as a named tuple with major and minor version number 78 | attributes. This is a read-only attribute. 79 | 80 | .. attribute:: version_string 81 | 82 | The version as the '{major}.{minor}' formatted string. This is a read-only 83 | attribute. 84 | 85 | .. attribute:: collision_resolution_number 86 | 87 | Get or set the random number for handover request message collision 88 | resolution. May be None if the random number was neither decoded or set. 89 | 90 | .. attribute:: alternative_carriers 91 | 92 | A `list` of alternative carriers with attributes carrier_power_state, 93 | carrier_data_reference, and auxiliary_data_reference list. 94 | 95 | 96 | .. method:: add_alternative_carrier(cps, cdr, *adr): 97 | 98 | Add a reference to a carrier data record within the handover request 99 | message. The carrier data reference *cdr* is the name (NDEF Record ID) of 100 | the carrier data record. The carrier power state *cps* is either 101 | 'inactive', 'active', 'activating', or 'unknown'. Any number of auxiliary 102 | data references *adr* may be added to link with other records in the 103 | message that carry information related to the carrier. 104 | 105 | 106 | Handover Select Record 107 | ---------------------- 108 | 109 | The Handover Select Record is the first record of a connection handover select 110 | message. Information enclosed within the payload of a handover select record 111 | includes the handover version number, error reason and associated error data 112 | when processing of the previously received handover request message failed, and 113 | a number of references to alternative carrier information records subsequently 114 | encoded in the same message. 115 | 116 | >>> import ndef 117 | >>> carrier = ndef.Record('mimetype/subtype', 'ref', b'1234') 118 | >>> message = [ndef.HandoverSelectRecord('1.3'), carrier] 119 | >>> message[0].add_alternative_carrier('active', carrier.name) 120 | 121 | .. class:: HandoverSelectRecord(version='1.3', error=None, *alternative_carrier) 122 | 123 | Initialize the record with a *version* number, an *error* information tuple, 124 | and zero or more *alternative_carrier*. The version number can be either an 125 | 8-bit integer (4-bit major, 4-bit minor), or a ``'{major}.{minor}'`` version 126 | string. An alternative carrier is given by a tuple with *carrier power 127 | state*, *carrier data reference* and zero or more *auxiliary data 128 | references*. The *error* argument is a tuple with error reason and error 129 | data. Error information, if not None, is encoded as the local Error Record 130 | after all given alternative carriers. 131 | 132 | :param version: handover version number 133 | :type version: int or str 134 | :param tuple error: error reason and data 135 | :param tuple alternative_carrier: alternative carrier entry 136 | 137 | .. attribute:: type 138 | 139 | The Handover Select Record type is ``urn:nfc:wkt:Hs``. 140 | 141 | .. attribute:: name 142 | 143 | Value of the NDEF Record ID field, an empty `str` if not set. 144 | 145 | .. attribute:: data 146 | 147 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 148 | current attributes. 149 | 150 | .. attribute:: hexversion 151 | 152 | The version as an 8-bit integer with 4-bit major and minor part. This is a 153 | read-only attribute. 154 | 155 | .. attribute:: version_info 156 | 157 | The version as a named tuple with major and minor version number 158 | attributes. This is a read-only attribute. 159 | 160 | .. attribute:: version_string 161 | 162 | The version as the '{major}.{minor}' formatted string. This is a read-only 163 | attribute. 164 | 165 | .. attribute:: error 166 | 167 | Either error information or None. Error details can be accessed with 168 | ``error.error_reason`` and ``error.error_data``. Formatted error 169 | information is provided with ``error.error_reason_string``. 170 | 171 | .. method:: set_error(error_reason, error_data): 172 | 173 | Set error information. The *error_reason* argument is an 8-bit integer 174 | value but only values 1, 2 and 3 are defined in the specification. For 175 | defined error reasons the *error_data* argument is the associated value 176 | (which is a number in all cases). For undefined error reason values the 177 | *error_data* argument is `bytes`. Error reason value 0 is strictly 178 | reserved and never encoded or decoded. 179 | 180 | .. attribute:: alternative_carriers 181 | 182 | A `list` of alternative carriers with attributes carrier_power_state, 183 | carrier_data_reference, and auxiliary_data_reference list. 184 | 185 | 186 | .. method:: add_alternative_carrier(cps, cdr, *adr): 187 | 188 | Add a reference to a carrier data record within the handover select 189 | message. The carrier data reference *cdr* is the name (NDEF Record ID) of 190 | the carrier data record. The carrier power state *cps* is either 191 | 'inactive', 'active', 'activating', or 'unknown'. Any number of auxiliary 192 | data references *adr* may be added to link with other records in the 193 | message that carry information related to the carrier. 194 | 195 | 196 | Handover Mediation Record 197 | ------------------------- 198 | 199 | The Handover Mediation Record is the first record of a connection handover 200 | mediation message. Information enclosed within the payload of a handover 201 | mediation record includes the version number and zero or more references to 202 | alternative carrier information records subsequently encoded in the same 203 | message. 204 | 205 | >>> import ndef 206 | >>> carrier = ndef.Record('mimetype/subtype', 'ref', b'1234') 207 | >>> message = [ndef.HandoverMediationRecord('1.3'), carrier] 208 | >>> message[0].add_alternative_carrier('active', carrier.name) 209 | 210 | .. class:: HandoverMediationRecord(version='1.3', *alternative_carrier) 211 | 212 | Initialize the record with *version* number and zero or more 213 | *alternative_carrier*. The version number can be either an 8-bit integer 214 | (4-bit major, 4-bit minor), or a ``'{major}.{minor}'`` version string. An 215 | alternative carrier is given by a tuple with *carrier power state*, *carrier 216 | data reference* and zero or more *auxiliary data references*. 217 | 218 | :param version: handover version number 219 | :type version: int or str 220 | :param tuple alternative_carrier: alternative carrier entry 221 | 222 | .. attribute:: type 223 | 224 | The Handover Select Record type is ``urn:nfc:wkt:Hm``. 225 | 226 | .. attribute:: name 227 | 228 | Value of the NDEF Record ID field, an empty `str` if not set. 229 | 230 | .. attribute:: data 231 | 232 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 233 | current attributes. 234 | 235 | .. attribute:: hexversion 236 | 237 | The version as an 8-bit integer with 4-bit major and minor part. This is a 238 | read-only attribute. 239 | 240 | .. attribute:: version_info 241 | 242 | The version as a named tuple with major and minor version number 243 | attributes. This is a read-only attribute. 244 | 245 | .. attribute:: version_string 246 | 247 | The version as the '{major}.{minor}' formatted string. This is a read-only 248 | attribute. 249 | 250 | .. attribute:: alternative_carriers 251 | 252 | A `list` of alternative carriers with attributes carrier_power_state, 253 | carrier_data_reference, and auxiliary_data_reference list. 254 | 255 | 256 | .. method:: add_alternative_carrier(cps, cdr, *adr): 257 | 258 | Add a reference to a carrier data record within the handover mediation 259 | message. The carrier data reference *cdr* is the name (NDEF Record ID) of 260 | the carrier data record. The carrier power state *cps* is either 261 | 'inactive', 'active', 'activating', or 'unknown'. Any number of auxiliary 262 | data references *adr* may be added to link with other records in the 263 | message that carry information related to the carrier. 264 | 265 | 266 | Handover Initiate Record 267 | ------------------------ 268 | 269 | The Handover Initiate Record is the first record of a connection handover initiate 270 | message. Information enclosed within the payload of a handover initiate record 271 | includes the version number and zero or more references to alternative carrier 272 | information records subsequently encoded in the same message. 273 | 274 | >>> import ndef 275 | >>> carrier = ndef.Record('mimetype/subtype', 'ref', b'1234') 276 | >>> message = [ndef.HandoverInitiateRecord('1.3'), carrier] 277 | >>> message[0].add_alternative_carrier('active', carrier.name) 278 | 279 | .. class:: HandoverInitiateRecord(version='1.3', *alternative_carrier) 280 | 281 | Initialize the record with *version* number and zero or more 282 | *alternative_carrier*. The version number can be either an 8-bit integer 283 | (4-bit major, 4-bit minor), or a ``'{major}.{minor}'`` version string. An 284 | alternative carrier is given by a tuple with *carrier power state*, *carrier 285 | data reference* and zero or more *auxiliary data references*. 286 | 287 | :param version: handover version number 288 | :type version: int or str 289 | :param tuple alternative_carrier: alternative carrier entry 290 | 291 | .. attribute:: type 292 | 293 | The Handover Select Record type is ``urn:nfc:wkt:Hi``. 294 | 295 | .. attribute:: name 296 | 297 | Value of the NDEF Record ID field, an empty `str` if not set. 298 | 299 | .. attribute:: data 300 | 301 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 302 | current attributes. 303 | 304 | .. attribute:: hexversion 305 | 306 | The version as an 8-bit integer with 4-bit major and minor part. This is a 307 | read-only attribute. 308 | 309 | .. attribute:: version_info 310 | 311 | The version as a named tuple with major and minor version number 312 | attributes. This is a read-only attribute. 313 | 314 | .. attribute:: version_string 315 | 316 | The version as the '{major}.{minor}' formatted string. This is a read-only 317 | attribute. 318 | 319 | .. attribute:: alternative_carriers 320 | 321 | A `list` of alternative carriers with attributes carrier_power_state, 322 | carrier_data_reference, and auxiliary_data_reference list. 323 | 324 | 325 | .. method:: add_alternative_carrier(cps, cdr, *adr): 326 | 327 | Add a reference to a carrier data record within the handover initiate 328 | message. The carrier data reference *cdr* is the name (NDEF Record ID) of 329 | the carrier data record. The carrier power state *cps* is either 330 | 'inactive', 'active', 'activating', or 'unknown'. Any number of auxiliary 331 | data references *adr* may be added to link with other records in the 332 | message that carry information related to the carrier. 333 | 334 | 335 | Handover Carrier Record 336 | ----------------------- 337 | 338 | The Handover Carrier Record allows a unique identification of an alternative 339 | carrier technology in a handover request message when no carrier configuration 340 | data is to be provided. If the handover selector device has the same carrier 341 | technology available, it would respond with a carrier configuration record with 342 | payload type equal to the carrier type (that is, the triples (TNF, TYPE_LENGTH, 343 | TYPE) and (CTF, CARRIER_TYPE_LENGTH, CARRIER_TYPE) match exactly). 344 | 345 | >>> import ndef 346 | >>> record = ndef.HandoverCarrierRecord('application/vnd.wfa.wsc') 347 | >>> record.name = 'wlan' 348 | >>> print(record) 349 | NDEF Handover Carrier Record ID 'wlan' CARRIER 'application/vnd.wfa.wsc' DATA 0 byte 350 | 351 | .. class:: HandoverCarrierRecord(carrier_type, carrier_data=None, reference=None) 352 | 353 | Initialize the HandoverCarrierRecord with *carrier_type*, *carrier_data*, and 354 | a *reference* that sets the `Record.name` attribute. The carrier type has the 355 | same format as a record type name, i.e. the combination of NDEF Record TNF 356 | and TYPE that is used by the `Record.type` attribute. The carrier_data 357 | argument must be a valid `bytearray` initializer, or None. 358 | 359 | :param str carrier_type: initial value of the `carrier_type` attribute 360 | :param sequence carrier_data: initial value of the `carrier_data` attribute 361 | :param str reference: initial value of the the `name` attribute 362 | 363 | .. attribute:: type 364 | 365 | The Handover Select Record type is ``urn:nfc:wkt:Hc``. 366 | 367 | .. attribute:: name 368 | 369 | Value of the NDEF Record ID field, an empty `str` if not set. The 370 | *reference* init argument can also be used to set this value. 371 | 372 | .. attribute:: data 373 | 374 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 375 | current attributes. 376 | 377 | .. attribute:: carrier_type 378 | 379 | Get or set the carrier type as a `Record.type` formatted representation of 380 | the Handover Carrier Record CTF and CARRIER_TYPE fields. 381 | 382 | .. attribute:: carrier_data 383 | 384 | Contents of the Handover Carrier Record CARRIER_DATA field as a 385 | `bytearray`. The attribute itself is read-only but the content may be 386 | modified or expanded. 387 | 388 | 389 | -------------------------------------------------------------------------------- /docs/records/signature.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | Signature Record 4 | ---------------- 5 | 6 | The NDEF Signature Record is a well-known record type defined by the 7 | `NFC Forum`_. It contains three fields: a version field, a signature field and 8 | a certificate field. 9 | 10 | The version field is static. Currently this implementation only supports v2.0 11 | of the NDEF Signature Record. 12 | 13 | The signature field contains the signature type, the message hash type and 14 | either the signature itself or a URI to the signature. 15 | 16 | The certificate field contains the certificate format type, a certificate chain 17 | store and an option URI to the next certificate in the chain. 18 | 19 | .. class:: SignatureRecord(signature_type=None, hash_type='SHA-256', signature=b'', signature_uri='', certificate_format='X.509', certificate_store=[], certificate_uri='') 20 | 21 | The `SignatureRecord` class decodes or encodes an NDEF Signature Record. 22 | 23 | :param str signature_type: initial value for the `signature_type` attribute, default None 24 | :param str hash_type: initial value for the `hash_type` attribute, default 'SHA-256' 25 | :param bytes signature: initial value for the `signature` attribute, default b'' 26 | :param str signature_uri: initial value for the `signature_uri` attribute, default '' 27 | :param str certificate_format: initial value for the `certificate_format` attribute, default 'X.509' 28 | :param list certificate_store: initial value for the `certificate_store` attribute, default [] 29 | :param str certificate_uri: initial value for the `certificate_uri` attribute, default '' 30 | 31 | .. attribute:: type 32 | 33 | The Signature Record type is ``urn:nfc:wkt:Sig``. 34 | 35 | .. attribute:: name 36 | 37 | Value of the NDEF Record ID field, an empty `str` if not set. 38 | 39 | .. attribute:: data 40 | 41 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 42 | current attributes. 43 | 44 | .. attribute:: version 45 | 46 | The version of the NDEF Signature Record. 47 | 48 | .. attribute:: signature_type 49 | 50 | The signature type used in the signature algorithm. 51 | 52 | >>> import ndef 53 | >>> print('\n'.join([str(x[1]) for x in ndef.signature.SignatureRecord()._mapping_signature_type])) 54 | None 55 | RSASSA-PSS-1024 56 | RSASSA-PKCS1-v1_5-1024 57 | DSA-1024 58 | ECDSA-P192 59 | RSASSA-PSS-2048 60 | RSASSA-PKCS1-v1_5-2048 61 | DSA-2048 62 | ECDSA-P224 63 | ECDSA-K233 64 | ECDSA-B233 65 | ECDSA-P256 66 | 67 | .. attribute:: hash_type 68 | 69 | The hash type used in the signature algorithm. 70 | 71 | >>> import ndef 72 | >>> print("\n".join([str(x[1]) for x in ndef.signature.SignatureRecord()._mapping_hash_type])) 73 | SHA-256 74 | 75 | .. attribute:: signature 76 | 77 | The signature (if not specified by `signature_uri`). 78 | 79 | .. attribute:: signature_uri 80 | 81 | The uniform resource identifier for the signature (if not specified by 82 | `signature`). 83 | 84 | .. attribute:: certificate_format 85 | 86 | The format of the certificates in the chain. 87 | 88 | >>> import ndef 89 | >>> print("\n".join([str(x[1]) for x in ndef.signature.SignatureRecord()._mapping_certificate_format])) 90 | X.509 91 | M2M 92 | 93 | .. attribute:: certificate_store 94 | 95 | A list of certificates in the certificate chain. 96 | 97 | .. attribute:: certificate_uri 98 | 99 | The uniform resource identifier for the next certificate in the 100 | certificate chain. 101 | 102 | This is default usage: 103 | 104 | >>> import ndef 105 | >>> signature_record = ndef.SignatureRecord(None, 'SHA-256', b'', '', 'X.509', [], '') 106 | 107 | This is a full example creating records, signing them and verifying them: 108 | 109 | >>> import ndef 110 | >>> import io 111 | >>> from cryptography.hazmat.backends import default_backend 112 | >>> from cryptography.hazmat.primitives import hashes 113 | >>> from cryptography.hazmat.primitives.asymmetric import ec 114 | >>> from cryptography.hazmat.primitives.asymmetric import utils 115 | >>> from cryptography.exceptions import InvalidSignature 116 | >>> from asn1crypto.algos import DSASignature 117 | 118 | >>> private_key = ec.generate_private_key(ec.SECP256K1(), default_backend()) 119 | >>> public_key = private_key.public_key() 120 | 121 | >>> r1 = ndef.UriRecord("https://example.com") 122 | >>> r2 = ndef.TextRecord("TEST") 123 | 124 | >>> stream = io.BytesIO() 125 | >>> records = [r1, r2, ndef.SignatureRecord("ECDSA-P256", "SHA-256")] 126 | >>> encoder = ndef.message_encoder(records, stream) 127 | >>> for _ in range(len(records) - 1): e=next(encoder) 128 | 129 | >>> signature = private_key.sign(stream.getvalue(), ec.ECDSA(hashes.SHA256())) 130 | >>> records[-1].signature = DSASignature.load(signature, strict=True).to_p1363() 131 | >>> e=next(encoder) 132 | >>> octets = stream.getvalue() 133 | 134 | >>> records_verified = [] 135 | >>> records_to_verify = [] 136 | >>> known_types = {'urn:nfc:wkt:Sig': ndef.signature.SignatureRecord} 137 | >>> for record in ndef.message_decoder(octets, known_types=known_types): 138 | ... if not record.type == 'urn:nfc:wkt:Sig': 139 | ... records_to_verify.append(record) 140 | ... else: 141 | ... stream_to_verify = io.BytesIO() 142 | ... encoder_to_verify = ndef.message_encoder(records_to_verify + [record], stream_to_verify) 143 | ... for _ in range(len(records_to_verify)): e=next(encoder_to_verify) 144 | ... try: 145 | ... public_key.verify(DSASignature.from_p1363(record.signature).dump(), stream_to_verify.getvalue(), ec.ECDSA(hashes.SHA256())) 146 | ... records_verified.extend(records_to_verify) 147 | ... records_to_verify = [] 148 | ... except InvalidSignature: 149 | ... pass 150 | 151 | >>> records_verified = list(ndef.message_decoder(b''.join(ndef.message_encoder(records_verified)))) 152 | -------------------------------------------------------------------------------- /docs/records/smartposter.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | Smartposter Record 4 | ------------------ 5 | 6 | The `NFC Forum`_ Smart Poster Record Type Definition defines a structure that 7 | associates an Internationalized Resource Identifier (or Uniform Resource 8 | Identifier) with various types of metadata. For a user this is most noteably the 9 | ability to attach descriptive text in different languages as well as image data 10 | for icon rendering. For a smartposter application this is a recommendation for 11 | processing as well as resource type and size hints to guide a strategy for 12 | retrieving the resource. 13 | 14 | .. class:: SmartposterRecord(resource, title=None, action=None, icon=None, \ 15 | resource_size=None, resource_type=None) 16 | 17 | Initialize a `SmartposterRecord` instance. The only required argument is the 18 | Internationalized Resource Identifier *resource*, all other arguments are 19 | optional metadata. 20 | 21 | :param str resource: Internationalized Resource Identifier 22 | :param title: English title `str` or `dict` with language keys and title values 23 | :type title: str or dict 24 | :param action: assigns a value to the :attr:`action` attribute 25 | :type action: str or int 26 | :param icon: PNG data `bytes` or `dict` with {icon-type: icon_data} items 27 | :type icon: bytes or dict 28 | :param int resource_size: assigns a value to the :attr:`resource_size` attribute 29 | :param str resource_type: assigns a value to the :attr:`resource_type` attribute 30 | 31 | .. attribute:: type 32 | 33 | The Smartposter Record type is ``urn:nfc:wkt:Sp``. 34 | 35 | .. attribute:: name 36 | 37 | Value of the NDEF Record ID field, an empty `str` if not set. 38 | 39 | .. attribute:: data 40 | 41 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 42 | current attributes. 43 | 44 | .. attribute:: resource 45 | 46 | Get or set the Smartposter resource identifier. A set value is interpreted 47 | as an internationalized resource identifier (so it can be unicode). When 48 | reading, the resource attribute returns a :class:`UriRecord` which can be 49 | used to set the :attr:`UriRecord.iri` and :attr:`UriRecord.uri` directly. 50 | 51 | .. attribute:: title 52 | 53 | The title string for language code 'en' or the first title string that was 54 | decoded or set. If no title string is available the value is `None`. The 55 | attribute can not be set, use :meth:`set_title`. 56 | 57 | .. attribute:: titles 58 | 59 | A dictionary of all decoded or set titles with language `str` keys and 60 | title `str` values. The attribute can not be set, use :meth:`set_title`. 61 | 62 | .. method:: set_title(title, language='en', encoding='UTF-8') 63 | 64 | Set the title string for a specific language which defaults to 'en'. The 65 | transfer encoding may be set to either 'UTF-8' or 'UTF-16', the default is 66 | 'UTF-8'. 67 | 68 | .. attribute:: action 69 | 70 | Get or set the recommended action for handling the Smartposter resource. A 71 | set value may be 'exec', 'save', 'edit' or an index thereof. A read value 72 | is either one of above strings or `None` if no action value was decoded or 73 | set. 74 | 75 | .. attribute:: icon 76 | 77 | The image data `bytes` for an 'image/png' type smartposter icon or the 78 | first icon decoded or added. If no icon is available the value is 79 | `None`. The attribute can not be set, use :meth:`add_icon`. 80 | 81 | .. attribute:: icons 82 | 83 | A dictionary of icon images with mime-type `str` keys and icon-data 84 | `bytes` values. The attribute can not be set, use :meth:`add_icon`. 85 | 86 | .. method:: add_icon(icon_type, icon_data) 87 | 88 | Add a Smartposter icon as icon_data bytes for the image or video mime-type 89 | string supplied with icon_type. 90 | 91 | .. attribute:: resource_size 92 | 93 | Get or set the `int` size hint for the Smartposter resource. `None` if a 94 | size hint was not decoded or set. 95 | 96 | .. attribute:: resource_type 97 | 98 | Get or set the `str` type hint for the Smartposter resource. `None` if a 99 | type hint was not decoded or set. 100 | 101 | >>> import ndef 102 | >>> record = ndef.SmartposterRecord('https://github.com/nfcpy/ndeflib') 103 | >>> record.set_title('Python package for parsing and generating NDEF', 'en') 104 | >>> record.resource_type = 'text/html' 105 | >>> record.resource_size = 1193970 106 | >>> record.action = 'exec' 107 | >>> len(b''.join(ndef.message_encoder([record]))) 108 | 115 109 | 110 | -------------------------------------------------------------------------------- /docs/records/text.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | Text Record 4 | ----------- 5 | 6 | The NDEF Text Record is a well-known record type defined by the `NFC Forum`_. It 7 | carries a UTF-8 or UTF-16 encoded text string with an associated IANA language 8 | code identifier. 9 | 10 | .. class:: TextRecord(text='', language='en', encoding='UTF-8') 11 | 12 | A :class:`TextRecord` is initialized with the actual text content, an 13 | ISO/IANA language identifier, and the desired transfer encoding UTF-8 or 14 | UTF-16. Default values are empty text, language code 'en', and 'UTF-8' 15 | encoding. 16 | 17 | :param str text: initial value for the `text` attribute, default '' 18 | :param str language: initial value for the `language` attribute, default 'en' 19 | :param str encoding: initial value for the `encoding` attribute, default 'UTF-8' 20 | 21 | .. attribute:: type 22 | 23 | The Text Record type is ``urn:nfc:wkt:T``. 24 | 25 | .. attribute:: name 26 | 27 | Value of the NDEF Record ID field, an empty `str` if not set. 28 | 29 | .. attribute:: data 30 | 31 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 32 | current attributes. 33 | 34 | .. attribute:: text 35 | 36 | The decoded or set text string value. 37 | 38 | .. attribute:: language 39 | 40 | The decoded or set IANA language code identifier. 41 | 42 | .. attribute:: encoding 43 | 44 | The transfer encoding of the text string. Either 'UTF-8' or 'UTF-16'. 45 | 46 | >>> import ndef 47 | >>> record = ndef.TextRecord("Hallo Welt", "de") 48 | >>> octets = b''.join(ndef.message_encoder([record])) 49 | >>> print(list(ndef.message_decoder(octets))[0]) 50 | NDEF Text Record ID '' Text 'Hallo Welt' Language 'de' Encoding 'UTF-8' 51 | 52 | -------------------------------------------------------------------------------- /docs/records/uri.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | URI Record 4 | ---------- 5 | 6 | The NDEF URI Record is a well-known record type defined by the `NFC Forum`_. It 7 | carries a, potentially abbreviated, UTF-8 encoded Internationalized Resource 8 | Identifier (IRI) as defined by :rfc:`3987`. Abbreviation covers certain prefix 9 | patterns that are compactly encoded as a single octet and automatically expanded 10 | when decoding. The `UriRecord` class provides both access attributes for decoded 11 | IRI as well as a converted URI (if a netloc part is present in the IRI). 12 | 13 | .. class:: UriRecord(iri='') 14 | 15 | The `UriRecord` class decodes or encodes an NDEF URI Record. The 16 | `UriRecord.iri` attribute holds the expanded (if a valid abbreviation code 17 | was decoded) internationalized resource identifier (IRI). The `UriRecord.uri` 18 | attribute is a converted version of the IRI. Conversion is applied only for 19 | IRI's that split with a netloc component. A converted URI contains only ASCII 20 | characters with an IDNA encoded netloc component and percent-encoded path, 21 | query and fragment components. 22 | 23 | :param str iri: initial value for the `iri` attribute, default '' 24 | 25 | .. attribute:: type 26 | 27 | The URI Record type is ``urn:nfc:wkt:U``. 28 | 29 | .. attribute:: name 30 | 31 | Value of the NDEF Record ID field, an empty `str` if not set. 32 | 33 | .. attribute:: data 34 | 35 | A `bytes` object containing the NDEF Record PAYLOAD encoded from the 36 | current attributes. 37 | 38 | .. attribute:: iri 39 | 40 | The decoded or set internationalized resource identifier, expanded if an 41 | abbreviation code was used in the record payload. 42 | 43 | .. attribute:: uri 44 | 45 | The uniform resource identifier translated from the `UriRecord.iri` attribute. 46 | 47 | >>> import ndef 48 | >>> record = ndef.UriRecord("http://www.hääyö.com/~user/") 49 | >>> record.iri 50 | 'http://www.hääyö.com/~user/' 51 | >>> record.uri 52 | 'http://www.xn--hy-viaa5g.com/~user/' 53 | >>> record = ndef.UriRecord("http://www.example.com") 54 | >>> b''.join(ndef.message_encoder([record])) 55 | b'\xd1\x01\x0cU\x01example.com' 56 | 57 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # This is an annoteted version because I tend to forget. 2 | # 3 | # After 'git clone': 4 | # 5 | # virtualenv -p python2 venv/python2 && source venv/python2/bin/activate 6 | # python setup.py develop && pip install -U pip -r requirements-dev.txt 7 | # 8 | # virtualenv -p python3 venv/python3 && source venv/python3/bin/activate 9 | # python setup.py develop && pip install -U pip -r requirements-dev.txt 10 | # 11 | # Before 'git push': 12 | # 13 | # pip install --upgrade -r requirements-dev.txt 14 | # tox -c tox.ini # or just tox 15 | # 16 | # Required packages and typical use: 17 | # 18 | pytest # run regression tests "py.test" 19 | pytest-cov # check code coverage "py.test --cov ndef --cov-report html" 20 | flake8 # syntax and style checks "flake8 src/ tests/" 21 | tox # run tests before push "tox -c tox.ini" 22 | sphinx # to create documentation "(cd docs && make html doctest)" 23 | sphinx-rtd-theme # with the final layout "firefox docs/_build/html/index.html" 24 | cryptography # for signature documentation examples 25 | asn1crypto # for signature documentation examples 26 | -------------------------------------------------------------------------------- /requirements-pypi.txt: -------------------------------------------------------------------------------- 1 | # Packages needed for upload to PyPI. Works with the index servers and 2 | # credentials in ~/.pypirc. 3 | # 4 | # python setup.py sdist bdist_wheel 5 | # 6 | # twine upload -r test -s dist/ndeflib-x.y.z* 7 | # 8 | # twine upload -r pypi -s dist/ndeflib-x.y.z* 9 | # 10 | setuptools 11 | wheel 12 | twine 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_file = LICENSE 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import codecs 4 | import os 5 | import re 6 | 7 | from setuptools import setup, find_packages 8 | 9 | ############################################################################### 10 | 11 | NAME = "ndeflib" 12 | PACKAGES = find_packages(where="src") 13 | META_PATH = os.path.join("src", "ndef", "__init__.py") 14 | KEYWORDS = ["ndef", "nfc"] 15 | CLASSIFIERS = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Intended Audience :: Developers", 18 | "Natural Language :: English", 19 | "License :: OSI Approved :: ISC License (ISCL)", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 2", 23 | "Programming Language :: Python :: 2.7", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3.4", 26 | "Programming Language :: Python :: 3.5", 27 | "Programming Language :: Python :: Implementation :: CPython", 28 | "Programming Language :: Python :: Implementation :: PyPy", 29 | "Topic :: Software Development :: Libraries :: Python Modules", 30 | ] 31 | INSTALL_REQUIRES = [] 32 | 33 | ############################################################################### 34 | 35 | HERE = os.path.abspath(os.path.dirname(__file__)) 36 | 37 | 38 | def read(*parts): 39 | """ 40 | Build an absolute path from *parts* and and return the contents of the 41 | resulting file. Assume UTF-8 encoding. 42 | """ 43 | with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: 44 | return f.read() 45 | 46 | 47 | META_FILE = read(META_PATH) 48 | 49 | 50 | def find_meta(meta): 51 | """ 52 | Extract __*meta*__ from META_FILE. 53 | """ 54 | meta_match = re.search( 55 | r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), 56 | META_FILE, re.M 57 | ) 58 | if meta_match: 59 | return meta_match.group(1) 60 | raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) 61 | 62 | 63 | if __name__ == "__main__": 64 | setup( 65 | name=NAME, 66 | description=find_meta("description"), 67 | license=find_meta("license"), 68 | url=find_meta("uri"), 69 | version=find_meta("version"), 70 | author=find_meta("author"), 71 | author_email=find_meta("email"), 72 | maintainer=find_meta("author"), 73 | maintainer_email=find_meta("email"), 74 | keywords=KEYWORDS, 75 | long_description=read("README.rst"), 76 | packages=PACKAGES, 77 | package_dir={"": "src"}, 78 | zip_safe=False, 79 | classifiers=CLASSIFIERS, 80 | install_requires=INSTALL_REQUIRES, 81 | ) 82 | -------------------------------------------------------------------------------- /src/ndef/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """A package for parsing, handling, and generating NDEF messages. 3 | 4 | The NFC Data Exchange Format (NDEF) is a binary message format that 5 | can be used to encapsulate one or more application-defined payloads 6 | into a single message construct for exchange between NFC Devices and 7 | Tags. Each payload is described by a type, a length, and an optional 8 | identifier. The payload type determines the syntactical requirements 9 | for decoding and encoding of the payload octets. The optional payload 10 | identifier allows an application to locate a specific record within 11 | the NDEF Message. 12 | 13 | An NDEF Message is a sequence of one or more NDEF Records, started 14 | with a Message Begin (MB) flag in the first record and terminated with 15 | a Message End (ME) flag in the last record. By convention, the first 16 | record provides the processing context for the whole NDEF Message. 17 | 18 | An NDEF Message can be decoded from bytes, bytearray, or a file-like, 19 | byte-oriented stream with the ndef.message_decoder() generator 20 | function. Note that the NDEF Records are decoded sequentially while 21 | iterating, it is thus possible to successfully decode until some 22 | malformed record later in the message. 23 | 24 | >>> octets = bytearray.fromhex('910301414243005903010158595a30ff') 25 | >>> for record in ndef.message_decoder(octets): print(record) 26 | ... 27 | NDEF Record urn:nfc:wkt:ABC PAYLOAD 1 byte 00 28 | NDEF Record urn:nfc:wkt:XYZ ID 0 PAYLOAD 1 byte ff 29 | 30 | An NDEF Message can be encoded to bytes or into a stream with the 31 | ndef.message_encoder() generator function. When not encoding into a 32 | stream the generator returns the bytes for each encoded record, 33 | joining them produces the complete message bytes. 34 | 35 | >>> record_1 = ndef.Record('urn:nfc:wkt:ABC', None, b'\\x00') 36 | >>> record_2 = ndef.Record('urn:nfc:wkt:XYZ', '0', b'\\xff') 37 | >>> octets = b''.join(ndef.message_encoder([record_1, record_2])) 38 | >>> bytearray(octets).hex() # octets.encode('hex') for Python 2 39 | '910301414243005903010158595a30ff' 40 | 41 | An ndef.Record is initialized with the record type, name, and 42 | data. The data argument is the sequence of octets that will become the 43 | NDEF Record PAYLOAD. The name argument gives the NDEF Record ID with 44 | up to 255 latin characters. The type argument gives the NDEF Record 45 | TNF (Type Name Format) and TYPE field in a combined string 46 | representation. For NFC Forum well-known and external record types 47 | this is the prefix 'urn:nfc:wkt:' and 'urn:nfc:ext:' followed by 48 | TYPE. The media-type and absolute URI type name formats ares 49 | recognized by '/' and ':// 50 | pattern matching. The unknown and unchanged (chunked) type name 51 | formats are selected with the single words 'unknown' and 52 | 'unchanged'. Finally, an empty string (or None) selects the empty 53 | record type name format. 54 | 55 | The ndef.Record type, name, and data attributes provide access to the 56 | init arguments (None arguments are set as to defaults). The type 57 | attribute is generally read-only while the name attribute is 58 | read-writable. The data attribute is read-only but is a bytearray that 59 | can itself be modified. Note that for derived record classes the data 60 | attribute is an non-mutable bytes object. 61 | 62 | The ndef.message_decoder() may return and the ndef.message_encoder() 63 | does also accept ndef.Record derived class instances. Those are either 64 | implemented within the ndef package or may be registered by the 65 | application with ndef.Record.register_type(). 66 | 67 | >>> class MyRecord(ndef.Record): 68 | ... _type = 'urn:nfc:ext:nfcpy.org:MyRecord' 69 | ... def __init__(self, integer): 70 | ... self._value = value 71 | ... def _encode_payload(self): 72 | ... return struct.pack('>H', self._value) 73 | ... _decode_min_payload_length = 2 74 | ... _decode_max_payload_length = 2 75 | ... @classmethod 76 | ... def _decode_payload(cls, octets, errors, known_types): 77 | ... return cls(struct.unpack('>H', octets)[0]) 78 | ... 79 | >>> ndef.Record.register_type(MyRecord) 80 | 81 | The ndef package provides a number of well-known record type classes, 82 | specifically the NFC Forum Text, URI, Smartposter and Connection 83 | Handover Records, as well as the Bluetooth SIG and Wi-Fi Alliance 84 | Carrier Configuration Records for connection handover. They are 85 | documented in the package contents files. 86 | 87 | """ 88 | import sys 89 | if sys.version_info < (2, 7): # pragma: no cover 90 | raise ImportError("The ndef module requires Python 2.7 or newer!") 91 | else: 92 | from . import message 93 | from . import record 94 | from . import uri 95 | from . import text 96 | from . import smartposter 97 | from . import deviceinfo 98 | from . import handover 99 | from . import bluetooth 100 | from . import wifi 101 | from . import signature 102 | 103 | message_decoder = message.message_decoder 104 | message_encoder = message.message_encoder 105 | 106 | DecodeError = record.DecodeError 107 | EncodeError = record.EncodeError 108 | Record = record.Record 109 | UriRecord = uri.UriRecord 110 | TextRecord = text.TextRecord 111 | SmartposterRecord = smartposter.SmartposterRecord 112 | DeviceInformationRecord = deviceinfo.DeviceInformationRecord 113 | HandoverRequestRecord = handover.HandoverRequestRecord 114 | HandoverSelectRecord = handover.HandoverSelectRecord 115 | HandoverMediationRecord = handover.HandoverMediationRecord 116 | HandoverInitiateRecord = handover.HandoverInitiateRecord 117 | HandoverCarrierRecord = handover.HandoverCarrierRecord 118 | WifiSimpleConfigRecord = wifi.WifiSimpleConfigRecord 119 | WifiPeerToPeerRecord = wifi.WifiPeerToPeerRecord 120 | BluetoothEasyPairingRecord = bluetooth.BluetoothEasyPairingRecord 121 | BluetoothLowEnergyRecord = bluetooth.BluetoothLowEnergyRecord 122 | SignatureRecord = signature.SignatureRecord 123 | 124 | # METADATA #################################################################### 125 | 126 | __version__ = "0.3.3" 127 | 128 | __title__ = "ndef" 129 | __description__ = "NFC Data Exchange Format decoder and encoder." 130 | __uri__ = "https://ndeflib.readthedocs.io/" 131 | 132 | __author__ = "Stephen Tiedemann" 133 | __email__ = "stephen.tiedemann@gmail.com" 134 | 135 | __license__ = "ISC" 136 | __copyright__ = "Copyright (c) 2016 Stephen Tiedemann" 137 | 138 | ############################################################################### 139 | -------------------------------------------------------------------------------- /src/ndef/deviceinfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Decoding and encoding of the NDEF Device Information Record. 3 | 4 | The NDEF Device Information Record is a well-known record type defined 5 | by the NFC Forum. It carries a number of Type-Length-Value data 6 | elements that provide information about the device, such as the 7 | manufacturer and device model name. 8 | 9 | """ 10 | from __future__ import absolute_import, division 11 | from .record import Record, GlobalRecord, convert 12 | from collections import namedtuple 13 | import uuid 14 | 15 | 16 | class DeviceInformationRecord(GlobalRecord): 17 | """This class decodes or encodes an NDEF Device Information Record and 18 | provides attributes for the information elements. Individual 19 | attributes are available for the information elements defined in 20 | the NFC Forum specification. Undefined, i.e. reserved-future-use, 21 | elements are provided with the undefined_data_elements list 22 | attribute and may be added with the add_undefined_data_element 23 | method (this should only be used if a future element definition 24 | is not yet implemented). 25 | 26 | >>> record = ndef.DeviceInformationRecord('ABC Company', 'Device XYZ') 27 | 28 | """ 29 | _type = 'urn:nfc:wkt:Di' 30 | 31 | _DataElement = namedtuple('DataElement', 'data_type, data_bytes') 32 | 33 | def __init__(self, vendor_name, model_name, unique_name=None, 34 | uuid_string=None, version_string=None, *undefined_data): 35 | """Initialize the record with required and optional device 36 | information. The vendor_name and model_name arguments are 37 | required, all other arguments are optional information. 38 | 39 | The first five positional arguments set the identically named 40 | record attributes. All further positional arguments must each 41 | be a tuple with data_type and data_bytes arguments for the 42 | add_undefined_data_element method. 43 | 44 | """ 45 | self._unique_name = self._uuid = self._version_string = None 46 | self._unknown_tlvs = [] 47 | self.vendor_name = vendor_name 48 | self.model_name = model_name 49 | if unique_name is not None: 50 | self.unique_name = unique_name 51 | if uuid_string is not None: 52 | self.uuid_string = uuid_string 53 | if version_string is not None: 54 | self.version_string = version_string 55 | for data_type, data_bytes in undefined_data: 56 | self.add_undefined_data_element(data_type, data_bytes) 57 | 58 | @property 59 | def vendor_name(self): 60 | """Get or set the device vendor name string.""" 61 | return self._vendor_name 62 | 63 | @vendor_name.setter 64 | @convert('value_to_unicode') 65 | def vendor_name(self, value): 66 | self._vendor_name = value 67 | 68 | @property 69 | def model_name(self): 70 | """Get or set the device model name string.""" 71 | return self._model_name 72 | 73 | @model_name.setter 74 | @convert('value_to_unicode') 75 | def model_name(self, value): 76 | self._model_name = value 77 | 78 | @property 79 | def unique_name(self): 80 | """Get or set the device unique name string.""" 81 | return self._unique_name 82 | 83 | @unique_name.setter 84 | @convert('value_to_unicode') 85 | def unique_name(self, value): 86 | self._unique_name = value 87 | 88 | @property 89 | def uuid_string(self): 90 | """Get or set the universially unique identifier string.""" 91 | return str(self._uuid) if self._uuid else None 92 | 93 | @uuid_string.setter 94 | @convert('value_to_ascii') 95 | def uuid_string(self, value): 96 | self._uuid = uuid.UUID(value) 97 | 98 | @property 99 | def version_string(self): 100 | """Get or set the device firmware version string.""" 101 | return self._version_string 102 | 103 | @version_string.setter 104 | @convert('value_to_unicode') 105 | def version_string(self, value): 106 | self._version_string = value 107 | 108 | @property 109 | def undefined_data_elements(self): 110 | """A list of undefined data elements as named tuples with data_type 111 | and data_bytes attributes. This is a reference to the internal 112 | list and may thus be updated in-place but it is strongly 113 | recommended to use the add_undefined_data_element method with 114 | data_type and data_bytes validation. It would also not be safe 115 | to rely on such implementation detail. 116 | 117 | """ 118 | return self._unknown_tlvs 119 | 120 | def add_undefined_data_element(self, data_type, data_bytes): 121 | """Add an undefined (reserved future use) device information data 122 | element. The data_type must be an an integer in range(5, 256). The 123 | data_bytes argument provides the up to 255 octets to transmit. 124 | 125 | Undefined data elements should not normally be added. This 126 | method is primarily present to allow transmission of data 127 | elements defined by future revisions of the specification 128 | before this implementaion is potentially updated. 129 | 130 | """ 131 | if not isinstance(data_type, int): 132 | errstr = "data_type argument must be int, not '{}'" 133 | raise self._value_error(errstr, type(data_type).__name__) 134 | if not isinstance(data_bytes, (bytes, bytearray)): 135 | errstr = "data_bytes may be bytes or bytearray, but not '{}'" 136 | raise self._value_error(errstr, type(data_bytes).__name__) 137 | if not 5 <= data_type <= 255: 138 | errstr = "data_type argument must be in range(5, 256), got {}" 139 | raise self._value_error(errstr, data_type) 140 | if len(data_bytes) > 255: 141 | errstr = "data_bytes can not be more than 255 octets, got {}" 142 | raise self._value_error(errstr, len(data_bytes)) 143 | self._unknown_tlvs.append(self._DataElement(data_type, data_bytes)) 144 | 145 | def __format__(self, format_spec): 146 | if format_spec == 'args': 147 | s = ("{r.vendor_name!r}, {r.model_name!r}, {r.unique_name!r}, " 148 | "{r.uuid_string!r}, {r.version_string!r}".format(r=self)) 149 | if self.undefined_data_elements: 150 | for data_type, data_bytes in self.undefined_data_elements: 151 | s += ", ({!r}, {!r})".format(data_type, data_bytes) 152 | return s 153 | 154 | if format_spec == 'data': 155 | s = "Vendor '{r.vendor_name}' Model '{r.model_name}'" 156 | if self.unique_name: 157 | s += " Name '{r.unique_name}'" 158 | if self.uuid_string: 159 | s += " UUID '{r.uuid_string}'" 160 | if self.version_string: 161 | s += " Version '{r.version_string}'" 162 | for item in self.undefined_data_elements: 163 | s += " {}".format(item) 164 | return s.format(r=self) 165 | 166 | return format(str(self), format_spec) 167 | 168 | def _encode_payload(self): 169 | if not (self.vendor_name and self.model_name): 170 | errmsg = "encoding requires that vendor and model name are set" 171 | raise self._encode_error(errmsg) 172 | 173 | def encode(t, v): return self._encode_struct('BB+', t, v) 174 | octets = encode(0, self.vendor_name.encode('utf-8')) 175 | octets += encode(1, self.model_name.encode('utf-8')) 176 | if self.unique_name is not None: 177 | octets += encode(2, self.unique_name.encode('utf-8')) 178 | if self._uuid is not None: 179 | octets += encode(3, self._uuid.bytes) 180 | if self.version_string is not None: 181 | octets += encode(4, self.version_string.encode('utf-8')) 182 | for tlv_type, tlv_value in self.undefined_data_elements: 183 | octets += encode(tlv_type, tlv_value) 184 | 185 | return octets 186 | 187 | _decode_min_payload_length = 2 188 | 189 | @classmethod 190 | def _decode_payload(cls, octets, errors): 191 | record = cls('', '') 192 | offset = 0 193 | while offset < len(octets): 194 | tlv_type, tlv_value = cls._decode_struct('BB+', octets, offset) 195 | offset = offset + 2 + len(tlv_value) 196 | if tlv_type == 0: 197 | record.vendor_name = tlv_value.decode('utf-8') 198 | elif tlv_type == 1: 199 | record.model_name = tlv_value.decode('utf-8') 200 | elif tlv_type == 2: 201 | record.unique_name = tlv_value.decode('utf-8') 202 | elif tlv_type == 3: 203 | record._uuid = uuid.UUID(bytes=tlv_value) 204 | elif tlv_type == 4: 205 | record.version_string = tlv_value.decode('utf-8') 206 | else: 207 | record.add_undefined_data_element(tlv_type, tlv_value) 208 | 209 | if not (record.vendor_name and record.model_name): 210 | errmsg = "decoding requires the manufacturer and model name TLVs" 211 | raise cls._decode_error(errmsg) 212 | 213 | return record 214 | 215 | 216 | Record.register_type(DeviceInformationRecord) 217 | -------------------------------------------------------------------------------- /src/ndef/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Implementation of the message decoder and encoder generator functions. 3 | 4 | 5 | """ 6 | from __future__ import absolute_import, division 7 | 8 | import io 9 | from .record import Record, DecodeError 10 | 11 | 12 | def message_decoder(stream_or_bytes, errors='strict', 13 | known_types=Record._known_types): 14 | """The message_decoder generator function yields ndef.Record class or 15 | subclass instances from an encoded NDEF Message. The NDEF Message 16 | octets can be read either from a file-like, byte-oriented stream 17 | or from bytes or a bytearray. 18 | 19 | >>> import io 20 | >>> from ndef import message_decoder 21 | >>> hexstr = '900000100000500000' 22 | >>> octets = bytearray.fromhex(hexstr) 23 | >>> stream = io.BytesIO(octets) 24 | >>> list(message_decoder(octets)) == list(message_decoder(stream)) 25 | True 26 | >>> for record in message_decoder(octets): 27 | ... print(record) 28 | ... 29 | NDEF Record TYPE '' ID '' PAYLOAD 0 byte 30 | NDEF Record TYPE '' ID '' PAYLOAD 0 byte 31 | NDEF Record TYPE '' ID '' PAYLOAD 0 byte 32 | 33 | A decoding error will result in an ndef.DecodeError exception. By 34 | default, errors are handled 'strict'. Minor errors, such as a 35 | missing message begin or end flag, will pass unnoticed if the 36 | errors argument is set to 'relax'. No exception is raised when 37 | errors is set to 'ignore' but the decoded records may not 38 | represent the complete message. 39 | 40 | The known_types argument, if supplied, must be a mapping of record 41 | type names to record classes. By default, if known_types is None, 42 | all registered record types are recognized. 43 | 44 | """ 45 | if isinstance(stream_or_bytes, (io.RawIOBase, io.BufferedIOBase)): 46 | stream = stream_or_bytes 47 | elif isinstance(stream_or_bytes, (bytes, bytearray)): 48 | stream = io.BytesIO(stream_or_bytes) 49 | else: 50 | errstr = "a stream or bytes type argument is required, not {}" 51 | raise TypeError(errstr.format(type(stream_or_bytes).__name__)) 52 | 53 | try: 54 | record, mb, me, cf = Record._decode(stream, errors, known_types) 55 | except DecodeError: 56 | if errors == 'ignore': 57 | return # just stop decoding 58 | raise 59 | 60 | if record is not None and mb is False and errors == 'strict': 61 | raise DecodeError('MB flag not set in first record') 62 | 63 | if record is not None and known_types is Record._known_types: 64 | known_types = type(record)._known_types 65 | 66 | while record is not None: 67 | yield record 68 | if me is True: 69 | if cf is True and errors == 'strict': 70 | raise DecodeError('CF flag set in last record') 71 | record = None 72 | else: 73 | try: 74 | record, mb, me, cf = Record._decode(stream, errors, 75 | known_types) 76 | except DecodeError: 77 | if errors == 'ignore': 78 | return # just stop decoding 79 | raise 80 | else: 81 | if record is None and errors == 'strict': 82 | raise DecodeError('ME flag not set in last record') 83 | if mb is True and errors == 'strict': 84 | raise DecodeError('MB flag set in middle record') 85 | 86 | 87 | def message_encoder(message=None, stream=None): 88 | """The message_encoder generator function generates the encoded 89 | representation of an NDEF Message. The message argument is the 90 | iterable of ndef.record.Record class or subclass objects that 91 | shall be encoded. If a stream argument is supplied, encoded octets 92 | are written into the file-like, byte-oriented stream and the 93 | number of octets yielded for each record. If the stream argument 94 | is None, octets are yielded as bytes type for each record. 95 | 96 | >>> from ndef.message import message_encoder 97 | >>> from ndef.record import Record 98 | >>> import io 99 | >>> ostream = io.BytesIO() 100 | >>> message = [Record(), Record(), Record()] 101 | >>> list(message_encoder(message, ostream)) 102 | [3, 3, 3] 103 | >>> ostream.getvalue().hex() 104 | '900000100000500000' 105 | >>> b''.join((message_encoder(message))).hex() 106 | '900000100000500000' 107 | 108 | If the message argument is None, the encoder expects client code 109 | to successively send() the records for encoding. As for all 110 | generators, the first value must be None. For the next value, 111 | which must then be a record object, the message encoder still 112 | yields None but for all further records the encoder yields the 113 | octets for the previous record. When the last record was send to 114 | the encoder, it requires a further None value to retrieve the 115 | octets for the final record. 116 | 117 | >>> from ndef.message import message_encoder 118 | >>> from ndef.record import Record 119 | >>> encoder = message_encoder() 120 | >>> results = list() 121 | >>> encoder.send(None) 122 | >>> encoder.send(Record()) 123 | >>> results.append(encoder.send(Record())) 124 | >>> results.append(encoder.send(Record())) 125 | >>> results.append(encoder.send(None)) 126 | >>> b''.join(results).hex() 127 | '900000100000500000' 128 | 129 | """ 130 | encoder = _message_encoder(stream) 131 | if message is None: 132 | record = None 133 | while True: 134 | try: 135 | record = yield (encoder.send(record)) 136 | except StopIteration: 137 | return 138 | else: 139 | itermsg = iter(message) 140 | encoder.send(None) 141 | try: 142 | encoder.send(next(itermsg)) 143 | except StopIteration: 144 | return 145 | for record in itermsg: 146 | yield encoder.send(record) 147 | yield encoder.send(None) 148 | 149 | 150 | def _message_encoder(stream): 151 | mb_flag = True 152 | this_record = yield 153 | next_record = yield 154 | while this_record: 155 | if not isinstance(this_record, Record): 156 | errstr = "an ndef.Record class instance is required, not {}" 157 | raise TypeError(errstr.format(type(this_record).__name__)) 158 | me_flag = next_record is None 159 | cf_flag = not me_flag and next_record.type == 'unchanged' 160 | this_result = this_record._encode(mb_flag, me_flag, cf_flag, stream) 161 | this_record = next_record 162 | next_record = (yield this_result) 163 | mb_flag = False 164 | -------------------------------------------------------------------------------- /src/ndef/signature.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Decoding and encoding of the NDEF Signature Record. 3 | 4 | The NDEF Signature Record is a well-known record type defined by the NFC 5 | Forum. It contains three fields: a version field, a signature field and a 6 | certificate field. 7 | 8 | The version field is static. Currently this implementation only supports v2.0 9 | of the NDEF Signature Record. 10 | 11 | The signature field contains the signature type, the message hash type and 12 | either the signature itself or a URI to the signature. 13 | 14 | The certificate field contains the certificate format type, a certificate chain 15 | store and an option URI to the next certificate in the chain. 16 | 17 | """ 18 | from __future__ import absolute_import, division 19 | from .record import Record, GlobalRecord, convert 20 | from collections import namedtuple 21 | 22 | VersionTuple = namedtuple('Version', 'major, minor') 23 | 24 | 25 | class SignatureRecord(GlobalRecord): 26 | """The SignatureRecord class decodes or encodes an NDEF Signature Record. 27 | 28 | This is default usage: 29 | 30 | >>> import ndef 31 | >>> signature_record = ndef.SignatureRecord( 32 | ... None, 'SHA-256', b'', '', 'X.509', [], '') 33 | 34 | This is a full example creating records, signing them and verifying them: 35 | 36 | >>> import ndef 37 | >>> import io 38 | >>> from cryptography.hazmat.backends import default_backend 39 | >>> from cryptography.hazmat.primitives import hashes 40 | >>> from cryptography.hazmat.primitives.asymmetric import ec 41 | >>> from cryptography.hazmat.primitives.asymmetric import utils 42 | >>> from cryptography.exceptions import InvalidSignature 43 | >>> from asn1crypto.algos import DSASignature 44 | 45 | >>> private_key = ec.generate_private_key( 46 | ... ec.SECP256K1(), default_backend()) 47 | >>> public_key = private_key.public_key() 48 | 49 | >>> r1 = ndef.UriRecord("https://example.com") 50 | >>> r2 = ndef.TextRecord("TEST") 51 | 52 | >>> stream = io.BytesIO() 53 | >>> records = [r1, r2, ndef.SignatureRecord("ECDSA-P256", "SHA-256")] 54 | >>> encoder = ndef.message_encoder(records, stream) 55 | >>> for _ in range(len(records) - 1): next(encoder) 56 | 57 | >>> signature = private_key.sign( 58 | ... stream.getvalue(), ec.ECDSA(hashes.SHA256())) 59 | >>> records[-1].signature = DSASignature.load( 60 | ... signature, strict=True).to_p1363() 61 | >>> next(encoder) 62 | >>> octets = stream.getvalue() 63 | 64 | >>> records_verified = [] 65 | >>> records_to_verify = [] 66 | >>> known_types = {'urn:nfc:wkt:Sig': ndef.signature.SignatureRecord} 67 | >>> for record in ndef.message_decoder(octets, known_types=known_types): 68 | ... if not record.type == 'urn:nfc:wkt:Sig': 69 | ... records_to_verify.append(record) 70 | ... else: 71 | ... stream_to_verify = io.BytesIO() 72 | ... encoder_to_verify = ndef.message_encoder( 73 | ... records_to_verify + [record], stream_to_verify) 74 | ... for _ in range(len(records_to_verify)): next(encoder_to_verify) 75 | ... try: 76 | ... public_key.verify(DSASignature.from_p1363( 77 | ... record.signature).dump(), stream_to_verify.getvalue( 78 | ... ), ec.ECDSA(hashes.SHA256())) 79 | ... records_verified.extend(records_to_verify) 80 | ... records_to_verify = [] 81 | ... except InvalidSignature: 82 | ... pass 83 | 84 | >>> records_verified = list(ndef.message_decoder(b''.join( 85 | ... ndef.message_encoder(records_verified)))) 86 | 87 | """ 88 | _type = 'urn:nfc:wkt:Sig' 89 | _version = 0x20 # this class implements v2.0 of the Signature RTD 90 | _mapping_signature_type = ( 91 | (0x00, None), 92 | (0x01, "RSASSA-PSS-1024"), 93 | (0x02, "RSASSA-PKCS1-v1_5-1024"), 94 | (0x03, "DSA-1024"), 95 | (0x04, "ECDSA-P192"), 96 | (0x05, "RSASSA-PSS-2048"), 97 | (0x06, "RSASSA-PKCS1-v1_5-2048"), 98 | (0x07, "DSA-2048"), 99 | (0x08, "ECDSA-P224"), 100 | (0x09, "ECDSA-K233"), 101 | (0x0a, "ECDSA-B233"), 102 | (0x0b, "ECDSA-P256"), 103 | ) 104 | _mapping_hash_type = ( 105 | (0x02, "SHA-256"), 106 | ) 107 | _mapping_certificate_format = ( 108 | (0x00, "X.509"), 109 | (0x01, "M2M"), 110 | ) 111 | 112 | def __init__(self, signature_type=None, hash_type=None, signature=None, 113 | signature_uri=None, certificate_format=None, 114 | certificate_store=None, certificate_uri=None): 115 | """Initialize the record with signature type, hash type, signature, 116 | signature URI, certificate format, certificate store and/or certificate 117 | URI. All parameters are optional. The default value is an empty 118 | Signature record. 119 | 120 | """ 121 | self.signature_type = signature_type 122 | self.hash_type = hash_type if hash_type is not None else 'SHA-256' 123 | self.signature = signature if signature is not None else b'' 124 | self.signature_uri = signature_uri if signature_uri is not None else '' 125 | if certificate_format is not None: 126 | self.certificate_format = certificate_format 127 | else: 128 | self.certificate_format = 'X.509' 129 | self._certificate_store = [] 130 | if isinstance(certificate_store, list): 131 | for certificate in certificate_store: 132 | self.add_certificate_to_store(certificate) 133 | if certificate_uri is not None: 134 | self.certificate_uri = certificate_uri 135 | else: 136 | self.certificate_uri = '' 137 | 138 | @property 139 | def version(self): 140 | """Signature Record: Version Field (Version)""" 141 | return VersionTuple(self._version >> 4, self._version & 0x0F) 142 | 143 | @property 144 | def signature_type(self): 145 | """Signature Record: Signature Field (Signature Type)""" 146 | return self._get_name_signature_type(self._signature_type) 147 | 148 | @signature_type.setter 149 | def signature_type(self, value): 150 | self._signature_type = self._get_enum_signature_type(value) 151 | 152 | def _get_enum_signature_type(self, value): 153 | for enum, name in self._mapping_signature_type: 154 | if value == name: 155 | return enum 156 | errstr = ("{!r} does not have a known " 157 | "Signature Type mapping").format(value) 158 | raise self._value_error(errstr) 159 | 160 | def _get_name_signature_type(self, value): 161 | for enum, name in self._mapping_signature_type: 162 | if value == enum: 163 | return name 164 | 165 | @property 166 | def hash_type(self): 167 | """Signature Record: Signature Field (Hash Type)""" 168 | return self._get_name_hash_type(self._hash_type) 169 | 170 | @hash_type.setter 171 | def hash_type(self, value): 172 | self._hash_type = self._get_enum_hash_type(value) 173 | 174 | def _get_enum_hash_type(self, value): 175 | for enum, name in self._mapping_hash_type: 176 | if value == name: 177 | return enum 178 | errstr = "{!r} does not have a known Hash Type mapping".format(value) 179 | raise self._value_error(errstr) 180 | 181 | def _get_name_hash_type(self, value): 182 | for enum, name in self._mapping_hash_type: 183 | if value == enum: 184 | return name 185 | 186 | @property 187 | def signature(self): 188 | """Signature Record: Signature Field (Signature)""" 189 | return self._signature 190 | 191 | @signature.setter 192 | def signature(self, value): 193 | if not isinstance(value, (bytes, bytearray)): 194 | errstr = "signature may be bytes or bytearray, but not '{}'" 195 | raise self._value_error(errstr, type(value).__name__) 196 | if len(value) >= 2**16: 197 | errstr = "signature cannot be more than 2^16 octets, got {}" 198 | raise self._value_error(errstr, len(value)) 199 | if value and hasattr(self, '_signature_uri') and self._signature_uri: 200 | errstr = "cannot set both signature and signature_uri" 201 | raise self._value_error(errstr) 202 | self._signature = value 203 | 204 | @property 205 | def signature_uri(self): 206 | """Signature Record: Signature Field (Signature URI)""" 207 | return self._signature_uri 208 | 209 | @signature_uri.setter 210 | @convert('value_to_unicode') 211 | def signature_uri(self, value): 212 | if len(value) >= 2**16: 213 | errstr = "signature_uri cannot be more than 2^16 octets, got {}" 214 | raise self._value_error(errstr, len(value)) 215 | if value and hasattr(self, '_signature') and self._signature: 216 | errstr = "cannot set both signature and signature_uri" 217 | raise self._value_error(errstr) 218 | self._signature_uri = value 219 | 220 | @property 221 | def certificate_format(self): 222 | """Signature Record: Certificate Chain Field (Certificate Format)""" 223 | return self._get_name_certificate_format(self._certificate_format) 224 | 225 | @certificate_format.setter 226 | def certificate_format(self, value): 227 | self._certificate_format = self._get_enum_certificate_format(value) 228 | 229 | def _get_enum_certificate_format(self, value): 230 | for enum, name in self._mapping_certificate_format: 231 | if value == name: 232 | return enum 233 | errstr = ("{!r} does not have a known " 234 | "Certificate Format mapping").format(value) 235 | raise self._value_error(errstr) 236 | 237 | def _get_name_certificate_format(self, value): 238 | for enum, name in self._mapping_certificate_format: 239 | if value == enum: 240 | return name 241 | 242 | @property 243 | def certificate_store(self): 244 | """Signature Record: Certificate Chain Field (Certificate Store)""" 245 | return self._certificate_store 246 | 247 | def add_certificate_to_store(self, value): 248 | if not isinstance(value, (bytes, bytearray)): 249 | errstr = "certificate may be bytes or bytearray, but not '{}'" 250 | raise self._value_error(errstr, type(value).__name__) 251 | if len(value) >= 2**16: 252 | errstr = "certificate cannot be more than 2^16 octets, got {}" 253 | raise self._value_error(errstr, len(value)) 254 | if len(self._certificate_store)+1 >= 2**4: 255 | errstr = ("certificate store cannot hold " 256 | "more than 2^4 certificates, got {}") 257 | raise self._value_error(errstr, len(self._certificate_store)+1) 258 | self._certificate_store.append(value) 259 | 260 | @property 261 | def certificate_uri(self): 262 | """Signature Record: Certificate Chain Field (Certificate URI)""" 263 | return self._certificate_uri 264 | 265 | @certificate_uri.setter 266 | @convert('value_to_unicode') 267 | def certificate_uri(self, value): 268 | self._certificate_uri = value 269 | 270 | def __format__(self, format_spec): 271 | if format_spec == 'args': 272 | s = ("{r.signature_type!r}, {r.hash_type!r}, {r.signature!r}, " 273 | "{r.signature_uri!r}, {r.certificate_format}, " 274 | "{r.certificate_store!r}, {r.certificate_uri!r}") 275 | return s.format(r=self) 276 | 277 | if format_spec == 'data': 278 | s = ["Signature RTD '{r.version}'"] 279 | if self.signature: 280 | s.append("Signature Type '{r.signature_type}'") 281 | s.append("Hash Type '{r.hash_type}'") 282 | if self.signature_uri: 283 | s.append("Signature URI '{r.signature_uri}'") 284 | if self.certificate_store: 285 | s.append("Certificate Format '{r.certificate_format}'") 286 | if self.certificate_uri: 287 | s.append("Certificate URI '{r.certificate_uri}'") 288 | return ' '.join(s).format(r=self) 289 | 290 | return super(SignatureRecord, self).__format__(format_spec) 291 | 292 | def _encode_payload(self): 293 | 294 | # Version Field 295 | VERSION = self._encode_struct('B', self._version) 296 | 297 | # Signature Field 298 | SUP = 0b10000000 if self.signature_uri else 0 299 | SST = self._signature_type & 0b01111111 300 | SHT = self._hash_type 301 | if self.signature_uri: 302 | SIGURI = self.signature_uri.encode('utf-8') 303 | else: 304 | SIGURI = self.signature 305 | SIGNATURE = self._encode_struct('BBH+', SUP | SST, SHT, SIGURI) 306 | 307 | # Certificate Field 308 | CUP = 0b10000000 if len(self.certificate_uri) else 0 309 | CCF = (self._certificate_format << 4) & 0b01110000 310 | CNC = len(self.certificate_store) & 0b00001111 311 | CST = b'' 312 | for certificate in self._certificate_store: 313 | CST += self._encode_struct('H+', certificate) 314 | CERTIFICATE = self._encode_struct('B', CUP | CCF | CNC) 315 | if len(CST): 316 | CERTIFICATE += self._encode_struct(str(len(CST))+'s', CST) 317 | if len(self.certificate_uri): 318 | CERTURI = self._certificate_uri.encode('utf-8') 319 | CERTIFICATE += self._encode_struct('H+', CERTURI) 320 | 321 | return VERSION + SIGNATURE + CERTIFICATE 322 | 323 | _decode_min_payload_length = 7 324 | 325 | @classmethod 326 | def _decode_payload(cls, octets, errors): 327 | # Called from Record._decode with the PAYLOAD of an NDEF Signature 328 | # Record. Returns a new SignatureRecord instance initialized with 329 | # the decoded data fields. Raises a DecodeError if any of the 330 | # decoding steps failed. 331 | 332 | (VERSION, SUP_SST, SHT, SIGURI, 333 | CUP_CCF_CNC, CST_CERTURI) = cls._decode_struct('BBBH+B*', octets) 334 | 335 | # Version Field 336 | if not VERSION == cls._version and errors == 'strict': 337 | errmsg = "decoding of version {} is not supported" 338 | raise cls._decode_error(errmsg.format(VERSION)) 339 | 340 | # Signature Field 341 | signature_uri_present = SUP_SST & 0b10000000 342 | signature_type = cls._get_name_signature_type( 343 | SignatureRecord(), SUP_SST & 0b01111111) 344 | hash_type = cls._get_name_hash_type(SignatureRecord(), SHT) 345 | if signature_uri_present: 346 | signature = None 347 | try: 348 | signature_uri = SIGURI.decode('utf-8') 349 | except UnicodeDecodeError: 350 | raise cls._decode_error("Signature URI field is " 351 | "not valid UTF-8 data") 352 | if any([ord(char) <= 31 for char in signature_uri]): 353 | raise cls._decode_error("Signature URI field contains " 354 | "invalid characters") 355 | else: 356 | signature_uri = None 357 | signature = SIGURI 358 | 359 | # Certificate Field 360 | certificate_uri_present = CUP_CCF_CNC & 0b10000000 361 | certificate_format = cls._get_name_certificate_format( 362 | SignatureRecord(), (CUP_CCF_CNC & 0b01110000) >> 4) 363 | certificate_number_of_certificates = CUP_CCF_CNC & 0b00001111 364 | certificate_store = [] 365 | for certificate_number in range(certificate_number_of_certificates): 366 | certificate, CST_CERTURI = cls._decode_struct('H+*', CST_CERTURI) 367 | certificate_store.append(certificate) 368 | if certificate_uri_present: 369 | CERTURI = cls._decode_struct('H+', CST_CERTURI) 370 | try: 371 | certificate_uri = CERTURI.decode('utf-8') 372 | except UnicodeDecodeError: 373 | raise cls._decode_error("Certificate URI field is " 374 | "not valid UTF-8 data") 375 | if any([ord(char) <= 31 for char in certificate_uri]): 376 | raise cls._decode_error("Certificate URI field contains " 377 | "invalid characters") 378 | else: 379 | certificate_uri = None 380 | 381 | return cls(signature_type, hash_type, signature, signature_uri, 382 | certificate_format, certificate_store, certificate_uri) 383 | 384 | 385 | Record.register_type(SignatureRecord) 386 | -------------------------------------------------------------------------------- /src/ndef/smartposter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Decoding and encoding of the NDEF Smartposter Record. 3 | 4 | The NFC Forum Smart Poster Record Type Definition defines a structure 5 | that associates an Internationalized Resource Identifier (or Uniform 6 | Resource Identifier) with various types of metadata. For a user this 7 | is most noteably the ability to attach descriptive text in different 8 | languages as well as image data for icon rendering. For a smartposter 9 | application this is a recommendation for processing as well as 10 | resource type and size hints to guide a strategy for retrieving the 11 | resource. 12 | 13 | """ 14 | from __future__ import absolute_import, division 15 | from .message import message_decoder, message_encoder 16 | from .record import Record, GlobalRecord, LocalRecord, convert 17 | from .text import TextRecord 18 | from .uri import UriRecord 19 | 20 | 21 | class ActionRecord(LocalRecord): 22 | """This is a local record class used within the payload of a 23 | Smartposter Record. It encodes an action value that translates 24 | into either 'exec', 'save', or 'edit' as the recommended course of 25 | action that strategy for acting on the resource. 26 | 27 | """ 28 | _type = 'urn:nfc:wkt:act' 29 | _action_strings = ('exec', 'save', 'edit') 30 | 31 | def __init__(self, action=None): 32 | """Initialize the record. The action argument, if not None, must be an 33 | acceptable set value for the ActionRecord.action attribute. The 34 | default, if action is None, is 'exec'. 35 | 36 | """ 37 | self.action = action if action else 'exec' 38 | 39 | @property 40 | def action(self): 41 | """Get or set the action value. A set value must be either 'exec', 42 | 'save', 'edit', or an index for that list. 43 | 44 | """ 45 | return self._action_strings[self._action] 46 | 47 | @action.setter 48 | def action(self, value): 49 | if value in self._action_strings: 50 | self._action = self._action_strings.index(value) 51 | elif isinstance(value, int) and 0 <= value < len(self._action_strings): 52 | self._action = value 53 | else: 54 | errstr = "action may be one of {} or index, but not {!r}" 55 | raise self._value_error(errstr.format(self._action_strings, value)) 56 | 57 | def __format__(self, format_spec): 58 | if format_spec == 'args': 59 | return "{!r}".format(self.action) 60 | 61 | if format_spec == 'data': 62 | return "Action '{r.action}'".format(r=self) 63 | 64 | return super(ActionRecord, self).__format__(format_spec) 65 | 66 | def _encode_payload(self): 67 | return self._encode_struct('B', self._action) 68 | 69 | _decode_min_payload_length = 1 70 | _decode_max_payload_length = 1 71 | 72 | @classmethod 73 | def _decode_payload(cls, octets, errors): 74 | ACTION = cls._decode_struct('B', octets) 75 | 76 | if not ACTION < len(cls._action_strings) and errors == 'strict': 77 | errmsg = "decoding of ACTION value {} is not defined" 78 | raise cls._decode_error(errmsg.format(ACTION)) 79 | 80 | action = ACTION if ACTION < len(cls._action_strings) else 0 81 | return ActionRecord(action) 82 | 83 | 84 | class SizeRecord(LocalRecord): 85 | """This is a local record class used within the payload of a 86 | Smartposter Record. It encodes the size of the resource referred 87 | to by the IRI/URI as a 32-bit unsigned integer. 88 | 89 | """ 90 | _type = 'urn:nfc:wkt:s' 91 | 92 | def __init__(self, resource_size=None): 93 | """Initialize the record with a resource_size integer value. The 94 | default is 0. 95 | 96 | """ 97 | self.resource_size = resource_size if resource_size is not None else 0 98 | 99 | @property 100 | def resource_size(self): 101 | """Get or set the resource size value.""" 102 | return self._value 103 | 104 | @resource_size.setter 105 | def resource_size(self, value): 106 | if isinstance(value, int) and 0 <= value <= 0xFFFFFFFF: 107 | self._value = value 108 | else: 109 | errstr = "resource_size expects 32-bit unsigned int, but got {!r}" 110 | raise self._value_error(errstr.format(value)) 111 | 112 | def __format__(self, format_spec): 113 | if format_spec == 'args': 114 | return "{r.resource_size!r}".format(r=self) 115 | 116 | if format_spec == 'data': 117 | return "Resource Size '{r.resource_size} byte'".format(r=self) 118 | 119 | return super(SizeRecord, self).__format__(format_spec) 120 | 121 | def _encode_payload(self): 122 | return self._encode_struct('>L', self.resource_size) 123 | 124 | _decode_min_payload_length = 4 125 | _decode_max_payload_length = 4 126 | 127 | @classmethod 128 | def _decode_payload(cls, octets, errors): 129 | return SizeRecord(cls._decode_struct('>L', octets)) 130 | 131 | 132 | class TypeRecord(LocalRecord): 133 | """This is a local record class used within the payload of a 134 | Smartposter Record. It encodes the type of the resource referred 135 | to by the IRI/URI as a, typically mime-type, string. 136 | 137 | """ 138 | _type = 'urn:nfc:wkt:t' 139 | 140 | def __init__(self, resource_type=None): 141 | """Initialize the record with a resource_type text value. The 142 | default is an empty string. 143 | 144 | """ 145 | self.resource_type = resource_type if resource_type is not None else '' 146 | 147 | @property 148 | def resource_type(self): 149 | """Get or set the resource type string.""" 150 | return self._value 151 | 152 | @resource_type.setter 153 | @convert('value_to_unicode') 154 | def resource_type(self, value): 155 | self._value = value 156 | 157 | def __format__(self, format_spec): 158 | if format_spec == 'args': 159 | return "{r.resource_type!r}".format(r=self) 160 | 161 | if format_spec == 'data': 162 | return "Resource Type '{r.resource_type}'".format(r=self) 163 | 164 | return super(TypeRecord, self).__format__(format_spec) 165 | 166 | def _encode_payload(self): 167 | return self.resource_type.encode('utf-8') 168 | 169 | @classmethod 170 | def _decode_payload(cls, octets, errors): 171 | try: 172 | TYPE = octets.decode('utf-8') 173 | except UnicodeDecodeError: 174 | errstr = "can't decode payload as utf-8" 175 | raise cls._decode_error(errstr.format(errstr)) 176 | return TypeRecord(TYPE) 177 | 178 | 179 | class SmartposterRecord(GlobalRecord): 180 | """The SmartposterRecord provides the attributes and methods to 181 | conviniently access the content of a decoded NDEF Smart Poster 182 | Record or to encode one through the ndef.message_encoder. 183 | 184 | >>> import ndef 185 | >>> record = ndef.SmartposterRecord('https://github.com/nfcpy/ndeflib') 186 | >>> record.set_title("NFC Data Exchange Format decoder", "en") 187 | >>> record.set_title("NFC Datenaustauschformat Dekodierer", "de") 188 | >>> iconfile = open('images/ndeflib.ico', 'rb') 189 | >>> record.add_icon('image/x-icon', iconfile.read()) 190 | >>> record.resource_type = 'text/html' 191 | >>> record.action = 'exec' 192 | 193 | """ 194 | _type = 'urn:nfc:wkt:Sp' 195 | 196 | def __init__(self, resource='', title=None, action=None, icon=None, 197 | resource_size=None, resource_type=None): 198 | """Initialize the record with resource, title, action, icon, 199 | resource_size and resource_type arguments. The resource 200 | argument is an IRI string. The title argument is either an 201 | English title string or a dictionary of {language: title} 202 | items. The action argument is a string or index acceptable as 203 | a ActionRecord.action attribute. The icon argument is either a 204 | sequence of 'image/png' data bytes or a dictionary of 205 | {mime-type: image-data} items. The resource_type is a text 206 | value that is usually a mime-type string. The resource_size is 207 | an integer. 208 | 209 | """ 210 | self.title_records = [] 211 | self.uri_records = [] 212 | self.action_records = [] 213 | self.icon_records = [] 214 | self.size_records = [] 215 | self.type_records = [] 216 | if resource is not None: 217 | self.resource = resource 218 | if title is not None: 219 | if isinstance(title, dict): 220 | for lang, text in title.items(): 221 | self.set_title(text, lang) 222 | else: 223 | self.set_title(title) 224 | if action is not None: 225 | self.action = action 226 | if icon is not None: 227 | png_header = b'\x89PNG\x0d\x0a\x1a\x0a' 228 | if isinstance(icon, dict): 229 | for icon_type, icon_data in icon.items(): 230 | self.add_icon(icon_type, icon_data) 231 | elif isinstance(icon, bytes): 232 | if icon.startswith(png_header): 233 | self.add_icon('image/png', icon) 234 | else: 235 | errstr = "init requires icon bytes with png header, not {}" 236 | raise self._value_error(errstr.format(repr(icon[0:8]))) 237 | else: 238 | errstr = "init icon argument must be bytes or mapping, not {}" 239 | raise self._value_error(errstr.format(type(icon).__name__)) 240 | if resource_size is not None: 241 | self.resource_size = resource_size 242 | if resource_type is not None: 243 | self.resource_type = resource_type 244 | 245 | @property 246 | def resource(self): 247 | """Get or set the Smartposter resource identifier. A set value is 248 | interpreted as an internationalized resource identifier (so it 249 | can be unicode). When reading, the resource attribute returns 250 | the internal ndef.UriRecord instance with the UriRecord.iri 251 | and UriRecord.uri attributes. This can also be used to set a 252 | value as either IRI or URI. 253 | 254 | """ 255 | try: 256 | return self.uri_records[0] 257 | except IndexError: 258 | return None 259 | 260 | @resource.setter 261 | @convert('value_to_unicode') 262 | def resource(self, value): 263 | try: 264 | self.uri_records[0].iri = value 265 | except IndexError: 266 | self.uri_records.append(UriRecord(value)) 267 | 268 | @property 269 | def titles(self): 270 | """Get a dictionary of all titles with {language: text} items.""" 271 | return dict([(t.language, t.text) for t in self.title_records]) 272 | 273 | @property 274 | def title(self): 275 | """Get the title string for the language code 'en'. If that is not 276 | available, the first title string added or decoded is supplied. In 277 | case that no title string is available the value is None. 278 | 279 | """ 280 | try: 281 | return self.titles['en'] 282 | except KeyError: 283 | pass 284 | try: 285 | return self.title_records[0].text 286 | except IndexError: 287 | return None 288 | 289 | def set_title(self, title, language='en', encoding='UTF-8'): 290 | """Set the title string for a specific language, default English. The 291 | transfer encoding may be set to either UTF-8 or UTF-16, it 292 | defaults to UTF-8 if not specified. 293 | 294 | """ 295 | for r in self.title_records: 296 | if r.language == language: 297 | r.text, r.encoding = title, encoding 298 | break 299 | else: 300 | self.title_records.append(TextRecord(title, language, encoding)) 301 | 302 | @property 303 | def action(self): 304 | """Get or set the recommended action for handling the Smartposter 305 | resource. A set value may be 'exec', 'save', 'edit' or a list 306 | index. A read value is either one of the above strings or None 307 | if no action value was set and no action record decoded. 308 | 309 | """ 310 | try: 311 | return self.action_records[0].action 312 | except IndexError: 313 | return None 314 | 315 | @action.setter 316 | def action(self, value): 317 | action_record = ActionRecord(value) 318 | try: 319 | self.action_records[0] = action_record 320 | except IndexError: 321 | self.action_records.append(action_record) 322 | 323 | @property 324 | def icon(self): 325 | """Get the 'image/png' Smartposter icon data. If that is not available 326 | the data of the first icon decoded or added is returned. If no 327 | icon was decoded or added then the attribute reads as None. 328 | 329 | """ 330 | try: 331 | return self.icons['image/png'] 332 | except KeyError: 333 | pass 334 | try: 335 | return self.icon_records[0].data 336 | except IndexError: 337 | return None 338 | 339 | @property 340 | def icons(self): 341 | """Get a dictionary of all icons with {mime-type: icon-data} items.""" 342 | return dict([(r.type, r.data) for r in self.icon_records]) 343 | 344 | def add_icon(self, icon_type, icon_data): 345 | """Add a Smartposter icon as icon_data bytes for the image or video 346 | mime-type string supplied with icon_type. 347 | 348 | """ 349 | if icon_type.startswith('image/') or icon_type.startswith('video/'): 350 | self.icon_records.append(Record(icon_type, None, icon_data)) 351 | else: 352 | errstr = "expects an image or video icon mimetype, not '{}'" 353 | raise self._value_error(errstr.format(icon_type)) 354 | 355 | @property 356 | def resource_size(self): 357 | """Get or set the size hint for the Smartposter resource. This will be 358 | None if the size hint was not previously set and no SizeRecord 359 | decoded. 360 | 361 | """ 362 | try: 363 | return self.size_records[0].resource_size 364 | except IndexError: 365 | return None 366 | 367 | @resource_size.setter 368 | def resource_size(self, value): 369 | size_record = SizeRecord(value) 370 | try: 371 | self.size_records[0] = size_record 372 | except IndexError: 373 | self.size_records.append(size_record) 374 | 375 | @property 376 | def resource_type(self): 377 | """Get or set the type hint for the Smartposter resource. This will be 378 | None if the type hint was not previously set and no TypeRecord 379 | decoded. 380 | 381 | """ 382 | try: 383 | return self.type_records[0].resource_type 384 | except IndexError: 385 | return None 386 | 387 | @resource_type.setter 388 | def resource_type(self, value): 389 | type_record = TypeRecord(value) 390 | try: 391 | self.type_records[0] = type_record 392 | except IndexError: 393 | self.type_records.append(type_record) 394 | 395 | def __format__(self, format_spec): 396 | if format_spec == 'args': 397 | s = ("{r.resource.iri!r}, {r.titles!r}, {r.action!r}, " 398 | "{r.icons!r}, {r.resource_size!r}, {r.resource_type!r})") 399 | return s.format(r=self) 400 | 401 | if format_spec == 'data': 402 | s = ["{r.resource:data}"] 403 | if self.title: 404 | s.append("Title '{r.title}'") 405 | if len(self.icon_records) > 0: 406 | icon_types = [r.type for r in self.icon_records] 407 | s.append(" ".join(["Icon '{}'".format(t) for t in icon_types])) 408 | if self.action: 409 | s.append("{r.action_records[0]:data}") 410 | if self.resource_size: 411 | s.append("{r.size_records[0]:data}") 412 | if self.resource_type: 413 | s.append("{r.type_records[0]:data}") 414 | return ' '.join(s).format(r=self) 415 | 416 | return super(SmartposterRecord, self).__format__(format_spec) 417 | 418 | def _encode_payload(self): 419 | records = ( 420 | self.uri_records + self.title_records + self.action_records + 421 | self.icon_records + self.size_records + self.type_records) 422 | return b''.join(list(message_encoder(records))) 423 | 424 | @classmethod 425 | def _decode_payload(cls, octets, errors): 426 | sp_record = cls(None) 427 | for record in message_decoder(octets, errors, cls._known_types): 428 | if record.type == 'urn:nfc:wkt:T': 429 | sp_record.title_records.append(record) 430 | elif record.type == 'urn:nfc:wkt:U': 431 | sp_record.uri_records.append(record) 432 | elif record.type == 'urn:nfc:wkt:act': 433 | sp_record.action_records.append(record) 434 | elif record.type.startswith('image/'): 435 | sp_record.icon_records.append(record) 436 | elif record.type.startswith('video/'): 437 | sp_record.icon_records.append(record) 438 | elif record.type == 'urn:nfc:wkt:s': 439 | sp_record.size_records.append(record) 440 | elif record.type == 'urn:nfc:wkt:t': 441 | sp_record.type_records.append(record) 442 | if errors == 'strict': 443 | uri_record_count = len(sp_record.uri_records) 444 | if uri_record_count != 1: 445 | errmsg = "payload must contain exactly one URI Record, got {}" 446 | raise cls._decode_error(errmsg.format(uri_record_count)) 447 | return sp_record 448 | 449 | 450 | SmartposterRecord.register_type(UriRecord) 451 | SmartposterRecord.register_type(TextRecord) 452 | SmartposterRecord.register_type(SizeRecord) 453 | SmartposterRecord.register_type(TypeRecord) 454 | SmartposterRecord.register_type(ActionRecord) 455 | Record.register_type(SmartposterRecord) 456 | -------------------------------------------------------------------------------- /src/ndef/text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Decoding and encoding of the NDEF Text Record. 3 | 4 | The NDEF Text Record is a well-known record type defined by the NFC 5 | Forum. It carries a UTF-8 or UTF-16 encoded text string with an 6 | associated IANA language code identifier. The TextRecord class 7 | provides access to the decoded NDEF Text Record content and can be 8 | encoded through the ndef.message_encoder(). 9 | 10 | """ 11 | from __future__ import absolute_import, division 12 | from .record import Record, GlobalRecord, convert 13 | 14 | 15 | class TextRecord(GlobalRecord): 16 | """Representation of an NDEF Text Record as defined by the NFC Forum 17 | Text Record Type Definition specification. 18 | 19 | A TextRecord is initialized with the actual text content, an 20 | ISO/IANA language identifier, and the desired transfer encoding 21 | UTF-8 or UTF-16. Default values are empty text, language code 22 | 'en', and UTF-8 encoding. 23 | 24 | >>> ndef.TextRecord("Hello World") 25 | ndef.text.TextRecord('Hello World', 'en', 'UTF-8') 26 | >>> ndef.TextRecord("Hallo Welt", "de") 27 | ndef.text.TextRecord('Hallo Welt', 'de', 'UTF-8') 28 | 29 | """ 30 | _type = 'urn:nfc:wkt:T' 31 | 32 | def __init__(self, text=None, language=None, encoding=None): 33 | """Initialize an NDEF TextRecord. Default values are the empty text 34 | string, the language code 'en' for English, and UTF-8 encoding. 35 | 36 | """ 37 | self.text = text if text is not None else '' 38 | self.language = language if language is not None else 'en' 39 | self.encoding = encoding if encoding is not None else 'UTF-8' 40 | 41 | @property 42 | def text(self): 43 | """NDEF Text Record content.""" 44 | return self._text 45 | 46 | @text.setter 47 | @convert('value_to_unicode') 48 | def text(self, value): 49 | self._text = value 50 | 51 | @property 52 | def language(self): 53 | """ISO/IANA language code for the text content.""" 54 | return self._lang 55 | 56 | @language.setter 57 | @convert('value_to_ascii') 58 | def language(self, value): 59 | if not (0 < len(value) < 64): 60 | errstr = 'language must be 1..63 characters, got {}' 61 | raise self._value_error(errstr.format(len(value))) 62 | self._lang = value 63 | 64 | @property 65 | def encoding(self): 66 | """Text encoding when transmitted, either 'UTF-8' or 'UTF-16'.""" 67 | return self._utfx 68 | 69 | @encoding.setter 70 | def encoding(self, value): 71 | if value not in ("UTF-8", "UTF-16"): 72 | errstr = "encoding may be 'UTF-8' or 'UTF-16', but not '{}'" 73 | raise self._value_error(errstr.format(value)) 74 | self._utfx = value 75 | 76 | def __format__(self, format_spec): 77 | if format_spec == 'args': 78 | s = "{r.text!r}, {r.language!r}, {r.encoding!r}" 79 | return s.format(r=self) 80 | 81 | if format_spec == 'data': 82 | return ("Text '{r.text}' Language '{r.language}' " 83 | "Encoding '{r.encoding}'".format(r=self)) 84 | 85 | return super(TextRecord, self).__format__(format_spec) 86 | 87 | def _encode_payload(self): 88 | """Called from Record._encode for the byte representation of the NDEF 89 | Text Record PAYLOAD requested through the Record.data attribute. 90 | 91 | """ 92 | UTFX = self.encoding 93 | LANG = self.language.encode('ascii') 94 | TEXT = self.text.encode(UTFX) 95 | FLAG = self._encode_struct('B', len(LANG) | ((UTFX == "UTF-16") << 7)) 96 | return FLAG + LANG + TEXT 97 | 98 | _decode_min_payload_length = 1 99 | 100 | @classmethod 101 | def _decode_payload(cls, octets, errors): 102 | """Called from Record._decode with the PAYLOAD of an NDEF Text 103 | Record. Returns a new TextRecord instance initialized with the 104 | decoded data fields. Raises ndef.DecodeError if any of the 105 | decoding steps failed. All decoding errors are handled 'strict'. 106 | 107 | """ 108 | FLAG = cls._decode_struct('B', octets) 109 | if FLAG & 0x3F == 0: 110 | raise cls._decode_error('language code length can not be zero') 111 | if FLAG & 0x3F >= len(octets): 112 | raise cls._decode_error("language code length exceeds payload") 113 | UTFX = "UTF-16" if FLAG >> 7 else "UTF-8" 114 | LANG = octets[1:1+(FLAG & 0x3F)] 115 | try: 116 | TEXT = octets[1+len(LANG):].decode(UTFX) 117 | except UnicodeDecodeError: 118 | raise cls._decode_error("can't be decoded as {}".format(UTFX)) 119 | return cls(TEXT, LANG, UTFX) 120 | 121 | 122 | Record.register_type(TextRecord) 123 | -------------------------------------------------------------------------------- /src/ndef/uri.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Decoding and encoding of the NDEF URI Record. 3 | 4 | The NDEF URI Record is a well-known record type defined by the NFC 5 | Forum. It carries a, potentially abbreviated, UTF-8 encoded 6 | Internationalized Resource Identifier (IRI) as defined by RFC 7 | 3987. Abbreviation covers certain prefix patterns that are compactly 8 | encoded as a single octet and automatically expanded when 9 | decoding. The UriRecord class provides both access attributes for 10 | decoded IRI as well as a converted URI (if a netloc part is present in 11 | the IRI). 12 | 13 | """ 14 | from __future__ import absolute_import, division 15 | from .record import Record, GlobalRecord, convert, _PY2 16 | 17 | if _PY2: # pragma: no cover 18 | from urlparse import urlsplit, urlunsplit 19 | from urllib import quote as _quote, unquote 20 | else: # pragma: no cover 21 | from urllib.parse import urlsplit, urlunsplit, quote as _quote, unquote 22 | 23 | 24 | def quote(string): 25 | # RFC 3986 includes "~" in the set of reserved characters. 26 | return _quote(string, safe='/~') 27 | 28 | 29 | class UriRecord(GlobalRecord): 30 | """The UriRecord class decodes or encodes an NDEF URI Record. The 31 | UriRecord.iri attribute holds the expanded (if a valid 32 | abbreviation code was decoded) internationalized resource 33 | identifier. The UriRecord.uri attribute is a converted version of 34 | the IRI. Conversion is applied only for IRI's that split with a 35 | netloc component. A converted URI contains only ASCII characters 36 | with an IDNA encoded netloc component and percent-encoded path, 37 | query and fragment components. 38 | 39 | >>> uri_record = ndef.UriRecord("http://www.hääyö.com/~user/") 40 | >>> uri_record.iri 41 | 'http://www.hääyö.com/~user/' 42 | >>> uri_record.uri 43 | 'http://www.xn--hy-viaa5g.com/%7Euser/' 44 | 45 | """ 46 | _type = 'urn:nfc:wkt:U' 47 | _prefix_strings = ( 48 | "", "http://www.", "https://www.", "http://", "https://", "tel:", 49 | "mailto:", "ftp://anonymous:anonymous@", "ftp://ftp.", "ftps://", 50 | "sftp://", "smb://", "nfs://", "ftp://", "dav://", "news:", 51 | "telnet://", "imap:", "rtsp://", "urn:", "pop:", "sip:", "sips:", 52 | "tftp:", "btspp://", "btl2cap://", "btgoep://", "tcpobex://", 53 | "irdaobex://", "file://", "urn:epc:id:", "urn:epc:tag:", 54 | "urn:epc:pat:", "urn:epc:raw:", "urn:epc:", "urn:nfc:") 55 | 56 | def __init__(self, iri=None): 57 | """Initialize the record with an internationalize resource identifier 58 | (IRI) string argument. The default value is an empty string. 59 | 60 | """ 61 | self.iri = iri if iri is not None else '' 62 | 63 | @property 64 | def iri(self): 65 | """The internationalized resource identifier.""" 66 | return self._iri 67 | 68 | @iri.setter 69 | @convert('value_to_unicode') 70 | def iri(self, value): 71 | self._iri = value 72 | 73 | @property 74 | def uri(self): 75 | """The uniform resource identifier.""" 76 | scheme, netloc, path, query, fragment = urlsplit(self._iri) 77 | if netloc: 78 | netloc = netloc.encode('idna').decode() 79 | path, query, fragment = map(quote, [path, query, fragment]) 80 | return urlunsplit((scheme, netloc, path, query, fragment)) 81 | 82 | @uri.setter 83 | @convert('value_to_unicode') 84 | def uri(self, value): 85 | scheme, netloc, path, query, fragment = urlsplit(value) 86 | if netloc: 87 | netloc = netloc.encode().decode('idna') 88 | path, query, fragment = map(unquote, [path, query, fragment]) 89 | self._iri = urlunsplit((scheme, netloc, path, query, fragment)) 90 | 91 | def __format__(self, format_spec): 92 | if format_spec == 'args': 93 | return "{!r}".format(self.iri) 94 | 95 | if format_spec == 'data': 96 | return "Resource '{r.iri}'".format(r=self) 97 | 98 | return super(UriRecord, self).__format__(format_spec) 99 | 100 | def _encode_payload(self): 101 | # Called from Record._encode when the byte representation of 102 | # the NDEF URI Record PAYLOAD is required for Record.data. 103 | for prefix in sorted(self._prefix_strings, reverse=True): 104 | if prefix and self.iri.startswith(prefix): 105 | index = self._prefix_strings.index(prefix) 106 | URI_CODE = self._encode_struct('B', index) 107 | URI_DATA = self.iri[len(prefix):].encode('utf-8') 108 | return URI_CODE + URI_DATA 109 | else: 110 | return self._encode_struct('B*', 0, self.iri.encode('utf-8')) 111 | 112 | _decode_min_payload_length = 1 113 | 114 | @classmethod 115 | def _decode_payload(cls, octets, errors): 116 | # Called from Record._decode with the PAYLOAD of an NDEF URI 117 | # Record. Returns a new UriRecord instance initialized with 118 | # the decoded data fields. Raises a DecodeError if any of the 119 | # decoding steps failed. Undefined abbreviation identifier 120 | # codes map raise DecodeError only for strict error handling, 121 | # otherwise map to code zero (no prefix). 122 | URI_CODE, URI_DATA = cls._decode_struct('B*', octets) 123 | 124 | if not URI_CODE < len(cls._prefix_strings) and errors == 'strict': 125 | errmsg = "decoding of URI identifier {} is not defined" 126 | raise cls._decode_error(errmsg.format(URI_CODE)) 127 | 128 | uri_code = URI_CODE if URI_CODE < len(cls._prefix_strings) else 0 129 | try: 130 | uri_data = URI_DATA.decode('utf-8') 131 | except UnicodeDecodeError: 132 | raise cls._decode_error("URI field is not valid UTF-8 data") 133 | 134 | if any([ord(char) <= 31 for char in uri_data]): 135 | raise cls._decode_error("URI field contains invalid characters") 136 | 137 | return cls(cls._prefix_strings[uri_code] + uri_data) 138 | 139 | 140 | Record.register_type(UriRecord) 141 | -------------------------------------------------------------------------------- /tests/_test_record_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | 3 | import ndef 4 | import pytest 5 | 6 | import sys 7 | import re 8 | 9 | 10 | def generate_tests(metafunc): 11 | if metafunc.cls and issubclass(metafunc.cls, _TestRecordBase): 12 | test_func = metafunc.function.__name__ 13 | if test_func == "test_encode_valid": 14 | test_data = metafunc.cls.test_decode_valid_data 15 | test_data = [(args, payload) for payload, args in test_data] 16 | elif test_func == "test_compare_equal": 17 | test_data = metafunc.cls.test_decode_valid_data 18 | test_data = [(args, args) for payload, args in test_data] 19 | elif test_func == "test_compare_noteq": 20 | test_data = metafunc.cls.test_decode_valid_data 21 | test_data = [args for payload, args in test_data] 22 | test_data = zip(test_data, test_data[1:]) 23 | elif test_func == "test_repr_is_implemented": 24 | test_data = metafunc.cls.test_decode_valid_data 25 | test_data = [(test_data[0][1],)] 26 | elif len(metafunc.fixturenames) > 0: 27 | test_data = eval("metafunc.cls.%s_data" % test_func) 28 | else: 29 | test_data = None 30 | if test_data: 31 | metafunc.parametrize(metafunc.fixturenames, test_data) 32 | 33 | 34 | class _TestRecordBase: 35 | def test_init_args(self, args, attrs): 36 | RECORD = self.RECORD 37 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 38 | ATTRIB = list(map(str.strip, self.ATTRIB.split(','))) 39 | ASSERT = "assert {0}{1}.{2} == {3!r}" 40 | print() 41 | record = RECORD(*args) 42 | for i, attr in enumerate(ATTRIB): 43 | print(ASSERT.format(CLNAME, args, attr, attrs[i])) 44 | assert getattr(record, attr) == attrs[i] 45 | 46 | def test_init_kwargs(self, args, kwargs): 47 | RECORD = self.RECORD 48 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 49 | ASSERT = "assert {0}{1} == {0}({2})" 50 | print('\n' + ASSERT.format(CLNAME, args, kwargs)) 51 | record = RECORD(*args) 52 | assert record == eval("{}({})".format(CLNAME, kwargs)) 53 | 54 | def test_init_fail(self, args, errstr): 55 | RECORD = self.RECORD 56 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 57 | ERRSTR = CLNAME + errstr 58 | ASSERT = "assert {0}{1} ==> {2}" 59 | print('\n' + ASSERT.format(CLNAME, args, errstr)) 60 | with pytest.raises((TypeError, ValueError)) as excinfo: 61 | RECORD(*args) 62 | result = str(excinfo.value) 63 | if sys.version_info < (3,): 64 | # remove b'..' literals but reinsert them for bytearray 65 | ERRSTR = re.sub(r"b'([^']*)'", r"'\1'", ERRSTR) 66 | ERRSTR = re.sub(r"(bytearray)\(('[^']*?')\)", r"\1(b\2)", ERRSTR) 67 | # remove u'..' literals 68 | result = re.sub(r"u'([^']*)'", r"'\1'", result) 69 | assert result == ERRSTR 70 | 71 | def test_decode_valid(self, payload, args): 72 | RECORD = self.RECORD 73 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 74 | OCTETS = bytes(bytearray.fromhex(payload)) 75 | ASSERT = "assert {0}.decode_payload(hex'{1}', 'strict') == {0}{2}" 76 | print('\n' + ASSERT.format(CLNAME, payload, args)) 77 | record = RECORD(*args) 78 | assert RECORD._decode_payload(OCTETS, 'strict') == record 79 | 80 | def test_decode_error(self, payload, errstr): 81 | RECORD = self.RECORD 82 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 83 | OCTETS = bytes(bytearray.fromhex(payload)) 84 | ERRSTR = CLNAME + ' ' + errstr 85 | ASSERT = "assert {0}.decode_payload(hex'{1}', 'strict') ==> {2}" 86 | print('\n' + ASSERT.format(CLNAME, payload, ERRSTR)) 87 | with pytest.raises(ndef.DecodeError) as excinfo: 88 | RECORD._decode_payload(OCTETS, 'strict') 89 | assert ERRSTR in str(excinfo.value) 90 | 91 | def test_decode_relax(self, payload, args): 92 | RECORD = self.RECORD 93 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 94 | OCTETS = bytes(bytearray.fromhex(payload)) 95 | ASSERT = "assert {0}.decode_payload(hex'{1}', 'relax') == {0}{2}" 96 | print('\n' + ASSERT.format(CLNAME, payload, args)) 97 | record = RECORD(*args) 98 | assert RECORD._decode_payload(OCTETS, 'relax') == record 99 | 100 | def test_encode_valid(self, args, payload): 101 | RECORD = self.RECORD 102 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 103 | OCTETS = bytes(bytearray.fromhex(payload)) 104 | ASSERT = "assert {0}({1}).encode_payload() == hex'{2}'" 105 | print('\n' + ASSERT.format(CLNAME, args, payload)) 106 | record = RECORD(*args) 107 | assert record._encode_payload() == OCTETS 108 | 109 | def test_encode_error(self, args, errstr): 110 | RECORD = self.RECORD 111 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 112 | ERRSTR = CLNAME + ' ' + errstr 113 | ASSERT = "assert {0}{1} ==> {2}" 114 | print('\n' + ASSERT.format(CLNAME, args, errstr)) 115 | with pytest.raises(ndef.EncodeError) as excinfo: 116 | record = RECORD(*args) 117 | record = record._encode_payload() 118 | assert str(excinfo.value) == ERRSTR 119 | 120 | def test_compare_equal(self, args_1, args_2): 121 | RECORD = self.RECORD 122 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 123 | ASSERT = "assert {0}{1} == {0}{2}" 124 | print('\n' + ASSERT.format(CLNAME, args_1, args_2)) 125 | record_1 = RECORD(*args_1) 126 | record_2 = RECORD(*args_2) 127 | assert record_1 == record_2 128 | assert record_1.data == record_2.data 129 | 130 | def test_compare_noteq(self, args_1, args_2): 131 | RECORD = self.RECORD 132 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 133 | ASSERT = "assert {0}{1} != {0}{2}" 134 | print('\n' + ASSERT.format(CLNAME, args_1, args_2)) 135 | record_1 = RECORD(*args_1) 136 | record_2 = RECORD(*args_2) 137 | assert record_1 != record_2 138 | assert record_1.data != record_2.data 139 | 140 | def test_format_args(self, args, formatted): 141 | RECORD = self.RECORD 142 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 143 | ASSERT = "assert format({0}{1}, 'args') == \"{2}\"" 144 | print('\n' + ASSERT.format(CLNAME, args, formatted)) 145 | record = RECORD(*args) 146 | result = format(record, 'args') 147 | if sys.version_info < (3,): 148 | # remove b'..' literals but reinsert them for bytearray 149 | formatted = re.sub(r"b'([^']*)'", r"'\1'", formatted) 150 | formatted = re.sub(r"(bytearray)\(('[^']*?')\)", 151 | r"\1(b\2)", formatted) 152 | # remove u'..' literals 153 | result = re.sub(r"u'([^']*)'", r"'\1'", result) 154 | assert result == formatted 155 | 156 | def test_format_str(self, args, formatted): 157 | RECORD = self.RECORD 158 | CLNAME = RECORD.__module__ + '.' + RECORD.__name__ 159 | ASSERT = "assert format({0}{1}, '') == \"{2}\"" 160 | print('\n' + ASSERT.format(CLNAME, args, formatted)) 161 | record = RECORD(*args) 162 | result = format(record, '') 163 | if sys.version_info < (3,): 164 | formatted = re.sub(r"b'([^']*)'", r"'\1'", formatted) 165 | result = re.sub(r"u'([^']*)'", r"'\1'", result) 166 | assert result == formatted 167 | -------------------------------------------------------------------------------- /tests/test_deviceinfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import ndef 6 | import _test_record_base 7 | 8 | 9 | def pytest_generate_tests(metafunc): 10 | _test_record_base.generate_tests(metafunc) 11 | 12 | 13 | class TestDeviceInformationRecord(_test_record_base._TestRecordBase): 14 | RECORD = ndef.deviceinfo.DeviceInformationRecord 15 | ATTRIB = ("vendor_name, model_name, unique_name, uuid_string," 16 | "version_string, undefined_data_elements") 17 | 18 | test_init_args_data = [ 19 | (('Company', 'Device'), 20 | ('Company', 'Device', None, None, None, [])), 21 | (('Company', 'Device', 'Name', 22 | '123e4567-e89b-12d3-a456-426655440000', '10.1.5'), 23 | ('Company', 'Device', 'Name', 24 | '123e4567-e89b-12d3-a456-426655440000', '10.1.5', [])), 25 | ] 26 | test_init_kwargs_data = [ 27 | (('Company', 'Device', 'Name', 28 | '123e4567-e89b-12d3-a456-426655440000', '10.1.5'), 29 | "vendor_name='Company', model_name='Device', unique_name='Name'," 30 | "uuid_string='123e4567-e89b-12d3-a456-426655440000'," 31 | "version_string='10.1.5'"), 32 | ] 33 | test_init_fail_data = [ 34 | ((1, '', None, None, None), 35 | ".vendor_name accepts str or bytes, but not int"), 36 | (('', 1, None, None, None), 37 | ".model_name accepts str or bytes, but not int"), 38 | (('', '', 1, None, None), 39 | ".unique_name accepts str or bytes, but not int"), 40 | (('', '', None, 1, None), 41 | ".uuid_string accepts str or bytes, but not int"), 42 | (('', '', None, None, 1), 43 | ".version_string accepts str or bytes, but not int"), 44 | (('', '', None, None, None, ('', b'')), 45 | " data_type argument must be int, not 'str'"), 46 | (('', '', None, None, None, (255, 1)), 47 | " data_bytes may be bytes or bytearray, but not 'int'"), 48 | (('', '', None, None, None, (1, b'')), 49 | " data_type argument must be in range(5, 256), got 1"), 50 | (('', '', None, None, None, (255, 256*b'1')), 51 | " data_bytes can not be more than 255 octets, got 256"), 52 | ] 53 | test_decode_valid_data = [ 54 | ('0007436f6d70616e790106446576696365', 55 | ("Company", 'Device')), 56 | ('0007436f6d70616e79 0106446576696365 02044e616d65', 57 | ("Company", 'Device', 'Name')), 58 | ('0007436f6d70616e79 0106446576696365 02044e616d65' 59 | '0310123e4567e89b12d3a456426655440000', 60 | ("Company", 'Device', 'Name', 61 | '123e4567-e89b-12d3-a456-426655440000')), 62 | ('0007436f6d70616e79 0106446576696365 02044e616d65 040631302e312e35', 63 | ("Company", 'Device', 'Name', None, '10.1.5')), 64 | ('0007436f6d70616e79 0106446576696365 02044e616d65 ff03313233', 65 | ("Company", 'Device', 'Name', None, None, (255, b'123'))), 66 | ] 67 | test_decode_error_data = [ 68 | ('0100', "decoding requires the manufacturer and model name TLVs"), 69 | ] 70 | test_decode_relax = None 71 | test_encode_error_data = [ 72 | (('', ''), "encoding requires that vendor and model name are set"), 73 | ] 74 | test_format_args_data = [ 75 | (('Company', 'Device'), 76 | "'Company', 'Device', None, None, None"), 77 | (('Company', 'Device', 'Name', '123e4567e89b12d3a456426655440000', 78 | '1.1.1', (255, b'123'), (10, b'')), 79 | "'Company', 'Device', 'Name', '123e4567-e89b-12d3-a456-426655440000'," 80 | " '1.1.1', (255, b'123'), (10, b'')"), 81 | ] 82 | test_format_str_data = [ 83 | ( 84 | ('Company', 'Device'), 85 | "NDEF Device Information Record ID ''" 86 | " Vendor 'Company' Model 'Device'" 87 | ), ( 88 | ('Company', 'Device', 'Name', '123e4567e89b12d3a456426655440000', 89 | '1.1.1', (255, b'123'), (10, b'')), 90 | "NDEF Device Information Record ID ''" 91 | " Vendor 'Company' Model 'Device'" 92 | " Name 'Name' UUID '123e4567-e89b-12d3-a456-426655440000'" 93 | " Version '1.1.1' DataElement(data_type=255, data_bytes=b'123')" 94 | " DataElement(data_type=10, data_bytes=b'')" 95 | ) 96 | ] 97 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import ndef 6 | import pytest 7 | 8 | from ndef import Record 9 | from io import BytesIO 10 | 11 | 12 | test_message_set_1 = [ 13 | ('', []), 14 | ('D00000', [Record()]), 15 | ('900000 500000', [Record(), Record()]), 16 | ('900000 100000 500000', [Record(), Record(), Record()]), 17 | ('900000 100000 500000', 3 * [Record()]), 18 | ('B50000 560000', [Record('unknown'), Record('unchanged')]), 19 | ] 20 | 21 | 22 | @pytest.mark.parametrize("encoded, message", test_message_set_1) 23 | def test_message_decoder_with_bytes_input(encoded, message): 24 | octets = bytes(bytearray.fromhex(encoded)) 25 | assert list(ndef.message_decoder(octets)) == message 26 | 27 | 28 | @pytest.mark.parametrize("encoded, message", test_message_set_1) 29 | def test_message_decoder_with_stream_input(encoded, message): 30 | stream = BytesIO(bytearray.fromhex(encoded)) 31 | assert list(ndef.message_decoder(stream)) == message 32 | 33 | 34 | @pytest.mark.parametrize("encoded, message", test_message_set_1) 35 | def test_message_encoder_with_bytes_output(encoded, message): 36 | octets = bytes(bytearray.fromhex(encoded)) 37 | assert b''.join(list(ndef.message_encoder(message))) == octets 38 | 39 | 40 | @pytest.mark.parametrize("encoded, message", test_message_set_1) 41 | def test_message_encoder_with_stream_output(encoded, message): 42 | stream = BytesIO() 43 | octets = bytes(bytearray.fromhex(encoded)) 44 | assert sum(ndef.message_encoder(message, stream)) == len(octets) 45 | assert stream.getvalue() == octets 46 | 47 | 48 | @pytest.mark.parametrize("encoded, message", test_message_set_1) 49 | def test_message_encoder_with_record_send(encoded, message): 50 | stream = BytesIO() 51 | octets = bytes(bytearray.fromhex(encoded)) 52 | encoder = ndef.message_encoder(stream=stream) 53 | encoder.send(None) 54 | for record in message: 55 | encoder.send(record) 56 | encoder.send(None) 57 | assert stream.getvalue() == octets 58 | 59 | 60 | test_message_set_2 = [ 61 | ('150000 160000 560000', 'MB flag not set in first record'), 62 | ('950000 960000 560000', 'MB flag set in middle record'), 63 | ('950000 160000 160000', 'ME flag not set in last record'), 64 | ('B50000 160000 760000', 'CF flag set in last record'), 65 | ] 66 | 67 | 68 | @pytest.mark.parametrize("encoded, errmsg", test_message_set_2) 69 | def test_fail_decode_invalid_message_strict(encoded, errmsg): 70 | stream = BytesIO(bytearray.fromhex(encoded)) 71 | with pytest.raises(ndef.DecodeError) as excinfo: 72 | list(ndef.message_decoder(stream, errors='strict')) 73 | assert errmsg == str(excinfo.value) 74 | 75 | 76 | @pytest.mark.parametrize("encoded, errmsg", test_message_set_2) 77 | def test_pass_decode_invalid_message_relax(encoded, errmsg): 78 | stream = BytesIO(bytearray.fromhex(encoded)) 79 | message = [Record('unknown'), Record('unchanged'), Record('unchanged')] 80 | assert list(ndef.message_decoder(stream, errors='relax')) == message 81 | 82 | 83 | test_message_set_3 = [ 84 | ('19', 'buffer underflow'), 85 | ('1901', 'buffer underflow'), 86 | ('190101', 'buffer underflow'), 87 | ('19010101', 'buffer underflow'), 88 | ('19010101aa', 'buffer underflow'), 89 | ('19010101aabb', 'buffer underflow'), 90 | ('19000000', 'must be'), 91 | ('18010000', 'must be'), 92 | ('18000001', 'must be'), 93 | ('18000100', 'must be'), 94 | ] 95 | 96 | 97 | @pytest.mark.parametrize("encoded, errmsg", test_message_set_3) 98 | def test_fail_decode_invalid_message_relax(encoded, errmsg): 99 | stream = BytesIO(bytearray.fromhex(encoded)) 100 | with pytest.raises(ndef.DecodeError) as excinfo: 101 | list(ndef.message_decoder(stream, errors='relax')) 102 | assert errmsg in str(excinfo.value) 103 | stream = BytesIO(bytearray.fromhex('900000' + encoded)) 104 | with pytest.raises(ndef.DecodeError) as excinfo: 105 | list(ndef.message_decoder(stream, errors='relax')) 106 | assert errmsg in str(excinfo.value) 107 | 108 | 109 | @pytest.mark.parametrize("encoded, errmsg", test_message_set_3) 110 | def test_pass_decode_invalid_message_ignore(encoded, errmsg): 111 | stream = BytesIO(bytearray.fromhex(encoded)) 112 | assert list(ndef.message_decoder(stream, errors='ignore')) == [] 113 | stream = BytesIO(bytearray.fromhex('900000' + encoded)) 114 | assert list(ndef.message_decoder(stream, errors='ignore')) == [Record()] 115 | 116 | 117 | test_message_set_4 = [ 118 | (1, 'a stream or bytes type argument is required, not int'), 119 | (1.0, 'a stream or bytes type argument is required, not float'), 120 | ] 121 | 122 | 123 | @pytest.mark.parametrize("argument, errmsg", test_message_set_4) 124 | def test_fail_message_decoder_invalid_types(argument, errmsg): 125 | with pytest.raises(TypeError) as excinfo: 126 | list(ndef.message_decoder(argument)) 127 | assert errmsg == str(excinfo.value) 128 | 129 | 130 | test_message_set_5 = [ 131 | ([1], 'an ndef.Record class instance is required, not int'), 132 | ([1.0], 'an ndef.Record class instance is required, not float'), 133 | ] 134 | 135 | 136 | @pytest.mark.parametrize("argument, errmsg", test_message_set_5) 137 | def test_fail_message_encoder_invalid_types(argument, errmsg): 138 | with pytest.raises(TypeError) as excinfo: 139 | list(ndef.message_encoder(argument)) 140 | assert errmsg == str(excinfo.value) 141 | 142 | 143 | def test_message_encoder_stop_iteration(): 144 | encoder = ndef.message_encoder() 145 | encoder.send(None) 146 | encoder.send(None) 147 | with pytest.raises(StopIteration): 148 | encoder.send(None) 149 | -------------------------------------------------------------------------------- /tests/test_record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import ndef 6 | import pytest 7 | 8 | from ndef.record import Record 9 | from io import BytesIO 10 | 11 | valid_record_types = [ 12 | ('urn:nfc:wkt:XYZ', 1, b'XYZ'), 13 | ('urn:nfc:wkt:xyz', 1, b'xyz'), 14 | ('application/octet-stream', 2, b'application/octet-stream'), 15 | ('http://example.com/type.dtd', 3, b'http://example.com/type.dtd'), 16 | ('urn:nfc:ext:example.com:type', 4, b'example.com:type'), 17 | ('unknown', 5, b''), 18 | ('unchanged', 6, b''), 19 | ] 20 | 21 | wrong_record_types = [ 22 | (int(), "record type string may be str or bytes, but not int"), 23 | ('invalid', "can not convert the record type string 'invalid'"), 24 | ('text/', "can not convert the record type string 'text/'"), 25 | ('urn:', "can not convert the record type string 'urn:'"), 26 | ('urn:nfc:', "can not convert the record type string 'urn:nfc:'"), 27 | ('urn:nfc:wkt', "can not convert the record type string 'urn:nfc:wkt'"), 28 | ('http:', "can not convert the record type string 'http:'"), 29 | ('http:/', "can not convert the record type string 'http:/'"), 30 | ('http:/a.b', "can not convert the record type string 'http:/a.b'"), 31 | ('urn:nfc:wkt:'+256*'a', 32 | "an NDEF Record TYPE can not be more than 255 octet"), 33 | ] 34 | 35 | 36 | class TestDecodeType: 37 | @pytest.mark.parametrize("record_type, TNF, TYPE", valid_record_types) 38 | def test_pass(self, record_type, TNF, TYPE): 39 | assert Record._decode_type(TNF, TYPE) == record_type 40 | assert type(Record._decode_type(TNF, TYPE)) == str 41 | 42 | def test_fail(self): 43 | errstr = "ndef.record.Record NDEF Record TNF values must be 0 to 6" 44 | with pytest.raises((TypeError, ValueError)) as excinfo: 45 | Record._decode_type(7, b'') 46 | assert str(excinfo.value) == errstr 47 | 48 | 49 | class TestEncodeType: 50 | @pytest.mark.parametrize("record_type, TNF, TYPE", valid_record_types) 51 | def test_pass(self, record_type, TNF, TYPE): 52 | assert Record._encode_type(record_type) == (TNF, TYPE) 53 | assert type(Record._encode_type(record_type)[1]) == bytes 54 | 55 | @pytest.mark.parametrize("record_type, errstr", wrong_record_types) 56 | def test_fail(self, record_type, errstr): 57 | with pytest.raises((TypeError, ValueError)) as excinfo: 58 | Record._encode_type(record_type) 59 | assert str(excinfo.value) == "ndef.record.Record " + errstr 60 | 61 | 62 | valid_init_args = [ 63 | ((), '', '', b''), 64 | ((None,), '', '', b''), 65 | ((None, None), '', '', b''), 66 | ((None, None, None), '', '', b''), 67 | ((str(), None, None), '', '', b''), 68 | ((bytes(), None, None), '', '', b''), 69 | ((bytearray(), None, None), '', '', b''), 70 | ((None, str(), None), '', '', b''), 71 | ((None, bytes(), None), '', '', b''), 72 | ((None, bytearray(), None), '', '', b''), 73 | ((None, None, str()), '', '', b''), 74 | ((None, None, bytes()), '', '', b''), 75 | ((None, None, bytearray()), '', '', b''), 76 | (('text/plain', None, None), 'text/plain', '', b''), 77 | (('text/plain', 'id', None), 'text/plain', 'id', b''), 78 | (('text/plain', 'id', 'text'), 'text/plain', 'id', b'text'), 79 | (('text/plain', None, 'text'), 'text/plain', '', b'text'), 80 | ((None, 'id', 'text'), '', 'id', b'text'), 81 | ((None, 'id', None), '', 'id', b''), 82 | (('a/'+253*'a', None, None), 'a/'+253*'a', '', b''), 83 | ((None, 255*'a', None), '', 255*'a', b''), 84 | ] 85 | 86 | 87 | wrong_init_args = [ 88 | ((int(),), " record type string may be str or bytes, but not int"), 89 | (('ab',), " can not convert the record type string 'ab'"), 90 | (('', int()), ".name may be str or None, but not int"), 91 | (('', 256*'a'), ".name can not be more than 255 octets NDEF Record ID"), 92 | (('', '', int()), ".data may be sequence or None, but not int"), 93 | ] 94 | 95 | 96 | class TestInitArguments: 97 | @pytest.mark.parametrize("args, _type, _name, _data", valid_init_args) 98 | def test_pass(self, args, _type, _name, _data): 99 | record = Record(*args) 100 | assert record.type == _type 101 | assert record.name == _name 102 | assert record.data == _data 103 | 104 | @pytest.mark.parametrize("args, errstr", wrong_init_args) 105 | def test_fail(self, args, errstr): 106 | with pytest.raises((TypeError, ValueError)) as excinfo: 107 | Record(*args) 108 | assert str(excinfo.value) == "ndef.record.Record" + errstr 109 | 110 | 111 | class TestInitKeywords: 112 | def test_pass(self): 113 | record_1 = Record(type='text/plain', name='name', data='hello') 114 | record_2 = Record(b'text/plain', b'name', b'hello') 115 | assert record_1.type == record_2.type 116 | assert record_1.name == record_2.name 117 | assert record_1.data == record_2.data 118 | 119 | def test_fail(self): 120 | with pytest.raises(TypeError): 121 | Record(undefined_keyword='abc') 122 | 123 | 124 | class TestTypeAttribute: 125 | def test_instance(self): 126 | assert isinstance(Record().type, str) 127 | 128 | def test_update(self): 129 | with pytest.raises(AttributeError): 130 | Record().type = '' 131 | 132 | 133 | class TestNameAttribute: 134 | def test_instance(self): 135 | assert isinstance(Record().name, str) 136 | 137 | def test_update(self): 138 | record = Record() 139 | assert record.name == '' 140 | record.name = 255 * 'a' 141 | assert record.name == 255 * 'a' 142 | with pytest.raises(TypeError): 143 | record.name = 1 144 | with pytest.raises(ValueError): 145 | record.name = 256 * 'a' 146 | 147 | 148 | class TestDataAttribute: 149 | def test_instance(self): 150 | assert isinstance(Record().data, bytearray) 151 | 152 | def test_update(self): 153 | record = Record('unknown', '', 'abc') 154 | assert record.data == b'abc' 155 | record.data.extend(b'def') 156 | assert record.data == b'abcdef' 157 | with pytest.raises(AttributeError): 158 | Record().data = bytearray(b'') 159 | 160 | 161 | class TestStringFormat: 162 | format_args_data = [ 163 | (('', '', ''), "'', '', bytearray(b'')"), 164 | (('unknown', 'id', 'data'), "'unknown', 'id', bytearray(b'data')"), 165 | ] 166 | format_str_data = [ 167 | (('', '', ''), 168 | "TYPE '' ID '' PAYLOAD 0 byte"), 169 | (('text/plain', '', ''), 170 | "TYPE 'text/plain' ID '' PAYLOAD 0 byte"), 171 | (('text/plain', 'id', ''), 172 | "TYPE 'text/plain' ID 'id' PAYLOAD 0 byte"), 173 | (('text/plain', 'id', '\x00\x01'), 174 | "TYPE 'text/plain' ID 'id' PAYLOAD 2 byte '0001'"), 175 | (('text/plain', '', '0123456789'), 176 | "TYPE 'text/plain' ID '' PAYLOAD 10 byte '30313233343536373839'"), 177 | (('text/plain', '', '012345678901'), 178 | "TYPE 'text/plain' ID '' PAYLOAD 12 byte" 179 | " '30313233343536373839' ... 2 more"), 180 | ] 181 | 182 | @pytest.mark.parametrize("args, string", format_args_data) 183 | def test_format_args(self, args, string): 184 | assert "{:args}".format(Record(*args)) == string 185 | 186 | @pytest.mark.parametrize("args, string", format_args_data) 187 | def test_format_repr(self, args, string): 188 | string = "ndef.record.Record({})".format(string) 189 | assert "{!r}".format(Record(*args)) == string 190 | 191 | @pytest.mark.parametrize("args, string", format_str_data) 192 | def test_format_str(self, args, string): 193 | assert "{!s}".format(Record(*args)) == "NDEF Record " + string 194 | assert "{}".format(Record(*args)) == "NDEF Record " + string 195 | 196 | 197 | class TestCompare: 198 | compare_data = [ 199 | ('', '', ''), 200 | ('a/b', '', ''), 201 | ('', 'abc', ''), 202 | ('', '', 'abc'), 203 | ] 204 | 205 | @pytest.mark.parametrize("args", compare_data) 206 | def test_equal(self, args): 207 | assert Record(*args) == Record(*args) 208 | 209 | @pytest.mark.parametrize("args1, args2", 210 | zip(compare_data, compare_data[1:])) 211 | def test_noteq(self, args1, args2): 212 | assert Record(*args1) != Record(*args2) 213 | 214 | 215 | class TestEncode: 216 | valid_encode_data = [ 217 | (('', '', b''), '100000'), 218 | (('urn:nfc:wkt:X', '', b''), '110100 58'), 219 | (('text/plain', '', b''), '120a00 746578742f706c61696e'), 220 | (('http://a.b/c', '', b''), '130c00 687474703a2f2f612e622f63'), 221 | (('urn:nfc:ext:a.com:type', '', b''), '140A00 612e636f6d3a74797065'), 222 | (('unknown', '', b''), '150000'), 223 | (('unchanged', '', b''), '160000'), 224 | (('urn:nfc:wkt:X', 'id', b''), '19010002 586964'), 225 | (('urn:nfc:wkt:X', 'id', b'payload'), '19010702 5869647061796c6f6164'), 226 | (('urn:nfc:wkt:X', 'id', 256*b'p'), '09010000010002 586964'+256*'70'), 227 | ] 228 | 229 | @pytest.mark.parametrize("args, encoded", valid_encode_data) 230 | def test_pass(self, args, encoded): 231 | stream = BytesIO() 232 | record = Record(*args) 233 | octets = bytearray.fromhex(encoded) 234 | assert record._encode(stream=stream) == len(octets) 235 | assert stream.getvalue() == octets 236 | 237 | def test_limit(self): 238 | stream = BytesIO() 239 | record = Record('unknown', '', 0x100000 * b'\0') 240 | octets = bytearray.fromhex('050000100000') + 0x100000 * b'\0' 241 | assert record._encode(stream=stream) == len(octets) 242 | assert stream.getvalue() == octets 243 | record = Record('unknown', '', 0x100001 * b'\0') 244 | errstr = "payload of more than 1048576 octets can not be encoded" 245 | with pytest.raises(ndef.EncodeError) as excinfo: 246 | record._encode(stream=stream) 247 | assert str(excinfo.value) == 'ndef.record.Record ' + errstr 248 | 249 | valid_struct_data = [ 250 | ("B", (1,), "01"), 251 | ("BB", (1, 2), "0102"), 252 | ("BB*", (1, 2, b'123'), "0102313233"), 253 | ("BB+", (1, b'123'), "0103313233"), 254 | ("BB+(B)", (1, (1, 2, 3)), "0103010203"), 255 | ("BB+(B)*", (1, (1, 2, 3), b'123'), "0103010203313233"), 256 | (">H", (1,), "0001"), 257 | (">HH", (1, 2), "00010002"), 258 | (">HH*", (1, 2, b'123'), "00010002313233"), 259 | (">HH+", (1, b'123'), "00010003313233"), 260 | (">HH+(H)", (1, (1, 2, 3)), "00010003000100020003"), 261 | (">HH+(H)*", (1, (1, 2, 3), b'123'), "00010003000100020003313233"), 262 | ] 263 | 264 | @pytest.mark.parametrize("fmt, values, octets", valid_struct_data) 265 | def test_struct(self, fmt, values, octets): 266 | octets = bytearray.fromhex(octets) 267 | assert Record._encode_struct(fmt, *values) == octets 268 | 269 | def test_derived_record(self): 270 | class MyRecord(Record): 271 | _type = 'urn:nfc:wkt:x' 272 | 273 | def __init__(self): 274 | pass 275 | 276 | def _encode_payload(self): 277 | return b'\0' 278 | 279 | stream = BytesIO() 280 | octets = bytearray.fromhex('1101017800') 281 | assert MyRecord()._encode(stream=stream) == len(octets) 282 | assert stream.getvalue() == octets 283 | 284 | 285 | class TestDecode: 286 | valid_decode_data = TestEncode.valid_encode_data + [ 287 | (('', '', b''), '00 00 00 00 00 00'), 288 | (('', '', b''), '00 00 00 00 00 00 00'), 289 | ] 290 | wrong_decode_data = [ 291 | ('07', "TNF field value must be between 0 and 6"), 292 | ('00', "buffer underflow at reading length fields"), 293 | ('0000', "buffer underflow at reading length fields"), 294 | ('000000', "buffer underflow at reading length fields"), 295 | ('00000000', "buffer underflow at reading length fields"), 296 | ('0000000000', "buffer underflow at reading length fields"), 297 | ('10010000', "TYPE_LENGTH must be 0 for TNF value 0"), 298 | ('110000', "TYPE_LENGTH must be > 0 for TNF value 1"), 299 | ('120000', "TYPE_LENGTH must be > 0 for TNF value 2"), 300 | ('130000', "TYPE_LENGTH must be > 0 for TNF value 3"), 301 | ('140000', "TYPE_LENGTH must be > 0 for TNF value 4"), 302 | ('15010000', "TYPE_LENGTH must be 0 for TNF value 5"), 303 | ('16010000', "TYPE_LENGTH must be 0 for TNF value 6"), 304 | ('1800000100', "ID_LENGTH must be 0 for TNF value 0"), 305 | ('10000100', "PAYLOAD_LENGTH must be 0 for TNF value 0"), 306 | ('000000000001', "PAYLOAD_LENGTH must be 0 for TNF value 0"), 307 | ('19010101', "buffer underflow at reading TYPE field"), 308 | ('1901010154', "buffer underflow at reading ID field"), 309 | ('190101015449', "buffer underflow at reading PAYLOAD field"), 310 | ] 311 | valid_flag_data = [ 312 | ('000000000000', 0, 0, 0), 313 | ('800000000000', 1, 0, 0), 314 | ('400000000000', 0, 1, 0), 315 | ('200000000000', 0, 0, 1), 316 | ('c00000000000', 1, 1, 0), 317 | ('a00000000000', 1, 0, 1), 318 | ('e00000000000', 1, 1, 1), 319 | ] 320 | 321 | @pytest.mark.parametrize("args, encoded", valid_decode_data) 322 | def test_pass(self, args, encoded): 323 | stream = BytesIO(bytearray.fromhex(encoded)) 324 | record = Record._decode(stream, 'strict', {})[0] 325 | assert record == Record(*args) 326 | 327 | @pytest.mark.parametrize("encoded, errstr", wrong_decode_data) 328 | def test_fail(self, encoded, errstr): 329 | with pytest.raises(ndef.DecodeError) as excinfo: 330 | stream = BytesIO(bytearray.fromhex(encoded)) 331 | Record._decode(stream, 'strict', {}) 332 | assert errstr in str(excinfo.value) 333 | 334 | @pytest.mark.parametrize("encoded, _mb, _me, _cf", valid_flag_data) 335 | def test_flags(self, encoded, _mb, _me, _cf): 336 | stream = BytesIO(bytearray.fromhex(encoded)) 337 | record, mb, me, cf = Record._decode(stream, 'strict', {}) 338 | assert mb == _mb 339 | assert me == _me 340 | assert cf == _cf 341 | 342 | def test_limit(self): 343 | octets = bytearray.fromhex('') 344 | record = Record._decode(BytesIO(octets), 'strict', {})[0] 345 | assert record is None 346 | octets = bytearray.fromhex('050000100000') + 0x100000 * b'\0' 347 | record = Record._decode(BytesIO(octets), 'strict', {})[0] 348 | assert len(record.data) == 0x100000 349 | errstr = "payload of more than 1048576 octets can not be decoded" 350 | octets = bytearray.fromhex('050000100001') + 0x100001 * b'\0' 351 | with pytest.raises(ndef.DecodeError) as excinfo: 352 | Record._decode(BytesIO(octets), 'strict', {}) 353 | assert str(excinfo.value) == 'ndef.record.Record ' + errstr 354 | 355 | def test_decode_payload_is_not_implemented(self): 356 | errstr = "must implement the _decode_payload() method" 357 | with pytest.raises(NotImplementedError) as excinfo: 358 | Record._decode_payload(b'', 'strict') 359 | assert str(excinfo.value) == 'ndef.record.Record ' + errstr 360 | 361 | def test_decode_known_type(self): 362 | class MyRecord(Record): 363 | _type = 'urn:nfc:wkt:x' 364 | _decode_min_payload_length = 1 365 | _decode_max_payload_length = 1 366 | 367 | @classmethod 368 | def _decode_payload(cls, octets, errors): 369 | return MyRecord() 370 | 371 | known_types = {MyRecord._type: MyRecord} 372 | stream = BytesIO(bytearray.fromhex('1101017800')) 373 | record = Record._decode(stream, 'strict', known_types)[0] 374 | assert type(record) == MyRecord 375 | 376 | errstr = 'payload length can not be less than 1' 377 | stream = BytesIO(bytearray.fromhex('11010078')) 378 | with pytest.raises(ndef.DecodeError) as excinfo: 379 | Record._decode(stream, 'strict', known_types) 380 | assert str(excinfo.value) == 'test_record.MyRecord ' + errstr 381 | 382 | errstr = 'payload length can not be more than 1' 383 | stream = BytesIO(bytearray.fromhex('110102780000')) 384 | with pytest.raises(ndef.DecodeError) as excinfo: 385 | Record._decode(stream, 'strict', known_types) 386 | assert str(excinfo.value) == 'test_record.MyRecord ' + errstr 387 | 388 | valid_struct_data = [ 389 | ("B", "01", 0, 1), 390 | ("BB", "0102", 0, (1, 2)), 391 | ("BB*", "0102313233", 0, (1, 2, b'123')), 392 | ("BB+", "0102313233", 0, (1, b'12')), 393 | ("BB+", "000102313233", 1, (1, b'12')), 394 | ("BB+*", "0102313233", 0, (1, b'12', b'3')), 395 | ("BB+(B)", "01020102", 0, (1, (1, 2))), 396 | ("BB+(2s)", "010231323334", 0, (1, (b'12', b'34'))), 397 | ("BB+(B)*", "010201023132", 0, (1, (1, 2), b'12')), 398 | (">H", "0001", 0, 1), 399 | (">HH+", "00010002313233", 0, (1, b'12')), 400 | (">HH+(H)", "0001000200010002", 0, (1, (1, 2))), 401 | ("BB+BB+", "010231320203313233", 0, (1, b'12', 2, b'123')), 402 | ] 403 | 404 | @pytest.mark.parametrize("fmt, octets, offset, values", valid_struct_data) 405 | def test_struct(self, fmt, octets, offset, values): 406 | octets = bytearray.fromhex(octets) 407 | assert Record._decode_struct(fmt, octets, offset) == values 408 | 409 | 410 | class TestValueToAscii: 411 | pass_values = [ 412 | 'abc', u'abc', b'abc', bytearray(b'abc') 413 | ] 414 | fail_values = [ 415 | (int(), "accepts str or bytes, but not int"), 416 | ('\x80', "conversion requires ascii text, but got '\\x80'"), 417 | ] 418 | 419 | @pytest.mark.parametrize("value", pass_values) 420 | def test_pass(self, value): 421 | assert Record._value_to_ascii(value, 'value') == 'abc' 422 | 423 | @pytest.mark.parametrize("value, errstr", fail_values) 424 | def test_fail(self, value, errstr): 425 | with pytest.raises((TypeError, ValueError)) as excinfo: 426 | Record._value_to_ascii(value, 'value') 427 | assert str(excinfo.value) == "ndef.record.Record value " + errstr 428 | 429 | 430 | class TestValueToLatin: 431 | pass_values = [ 432 | '\xe4bc', u'\xe4bc', b'\xe4bc', bytearray(b'\xe4bc') 433 | ] 434 | fail_values = [ 435 | (int(), "accepts str or bytes, but not int"), 436 | (u'\u0394', "conversion requires latin text, but got {u}'\u0394'"), 437 | ] 438 | 439 | @pytest.mark.parametrize("value", pass_values) 440 | def test_pass(self, value): 441 | assert Record._value_to_latin(value, 'value') == '\xe4bc' 442 | 443 | @pytest.mark.parametrize("value, errstr", fail_values) 444 | def test_fail(self, value, errstr): 445 | errstr = errstr.format(u=('', 'u')[ndef.record._PY2]) 446 | with pytest.raises((TypeError, ValueError)) as excinfo: 447 | Record._value_to_latin(value, 'value') 448 | assert str(excinfo.value) == "ndef.record.Record value " + errstr 449 | 450 | 451 | class TestValueToUnicode: 452 | pass_values = [ 453 | 'abc', u'abc', b'abc', bytearray(b'abc') 454 | ] 455 | fail_values = [ 456 | (int(), "accepts str or bytes, but not int"), 457 | (b'\x80', "conversion requires ascii text, but got {b}'\\x80'"), 458 | ] 459 | 460 | @pytest.mark.parametrize("value", pass_values) 461 | def test_pass(self, value): 462 | assert Record._value_to_unicode(value, 'value') == u'abc' 463 | 464 | @pytest.mark.parametrize("value, errstr", fail_values) 465 | def test_fail(self, value, errstr): 466 | errstr = errstr.format(b=('b', '')[ndef.record._PY2]) 467 | with pytest.raises((TypeError, ValueError)) as excinfo: 468 | Record._value_to_unicode(value, 'value') 469 | assert str(excinfo.value) == "ndef.record.Record value " + errstr 470 | -------------------------------------------------------------------------------- /tests/test_signature.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | import ndef 7 | import _test_record_base 8 | 9 | 10 | def pytest_generate_tests(metafunc): 11 | _test_record_base.generate_tests(metafunc) 12 | 13 | 14 | class TestSignatureRecord(_test_record_base._TestRecordBase): 15 | cls = 'ndef.signature.SignatureRecord' 16 | RECORD = ndef.signature.SignatureRecord 17 | ATTRIB = ("signature_type, hash_type, signature, signature_uri," 18 | "certificate_format, certificate_store, certificate_uri") 19 | # signature_type=None, hash_type="SHA-256", signature=None, signature_uri=None, 20 | # certificate_format="X.509", certificate_store=[], certificate_uri=None 21 | test_init_args_data = [ 22 | ((), 23 | (None, 'SHA-256', b'', '', 'X.509', [], '')), 24 | ] 25 | test_init_kwargs_data = [ 26 | ((None, None, None, None, None, None, None), 27 | ("signature_type=None, hash_type=None, signature=None, " 28 | "signature_uri=None, certificate_format=None, " 29 | "certificate_store=None, certificate_uri=None")), 30 | ] 31 | test_init_fail_data = [ 32 | ((None, None, None, 1, None, None, None), 33 | ".signature_uri accepts str or bytes, but not int"), 34 | ((None, None, None, None, None, None, 1), 35 | ".certificate_uri accepts str or bytes, but not int"), 36 | ((None, None, 1, None, None, None, None), 37 | ".signature may be bytes or bytearray, but not 'int'"), 38 | ((None, None, (2**16)*b'1', None, None, None, None), 39 | ".signature cannot be more than 2^16 octets, got 65536"), 40 | ((None, None, b'1', '1', None, None, None), 41 | " cannot set both signature and signature_uri"), 42 | ((None, None, None, (2**16)*'1', None, None, None), 43 | ".signature_uri cannot be more than 2^16 octets, got 65536"), 44 | ((None, None, None, None, None, [1], None), 45 | " certificate may be bytes or bytearray, but not 'int'"), 46 | ((None, None, None, None, None, [(2**16)*b'1'], None), 47 | " certificate cannot be more than 2^16 octets, got 65536"), 48 | ((None, None, None, None, None, [b'1' for x in range(2**4)], None), 49 | " certificate store cannot hold more than 2^4 certificates, got 16"), 50 | (('1', None, None, None, None, None, None), 51 | " '1' does not have a known Signature Type mapping"), 52 | ((None, '1', None, None, None, None, None), 53 | " '1' does not have a known Hash Type mapping"), 54 | ((None, None, None, None, '1', None, None), 55 | " '1' does not have a known Certificate Format mapping"), 56 | ] 57 | test_decode_valid_data = [ 58 | ('200002000000', 59 | (None, 'SHA-256', b'', '', 'X.509', [], '')), 60 | (('200b0200473045022100a410c28fd9437fd24f6656f121e62bcc5f65e36257f5faa' 61 | 'df68e3e83d40d481a0220335b1dff8d6fe722fcf7018be9684d2de5670b256fdfc0' 62 | '2aa25bdae16f624b8000'), 63 | ('ECDSA-P256', 'SHA-256', 64 | (b'0E\x02!\x00\xa4\x10\xc2\x8f\xd9C\x7f\xd2OfV\xf1!\xe6+\xcc_e\xe3bW' 65 | b'\xf5\xfa\xad\xf6\x8e>\x83\xd4\rH\x1a\x02 3[\x1d\xff\x8do\xe7"\xfc' 66 | b'\xf7\x01\x8b\xe9hM-\xe5g\x0b%o\xdf\xc0*\xa2[\xda\xe1obK\x80'), 67 | '', 'X.509', [], '')), 68 | (('208002000d7369676e61747572655f75726980000f63657274696669636174655f7' 69 | '57269'), 70 | (None, 'SHA-256', b'', 'signature_uri', 'X.509', [], 71 | 'certificate_uri')), 72 | ('200002000002000131000132', 73 | (None, None, None, None, None, [b'1', b'2'], None)), 74 | ('200b02000010', 75 | ('ECDSA-P256', 'SHA-256', None, None, 'M2M', None, None)), 76 | ] 77 | test_decode_error_data = [ 78 | ('100002000000', "decoding of version 16 is not supported"), 79 | ('208002000180000000', ("Signature URI field is " 80 | "not valid UTF-8 data")), 81 | ('208002000100000000', ("Signature URI field contains " 82 | "invalid characters")), 83 | ('200002000080000180', ("Certificate URI field is " 84 | "not valid UTF-8 data")), 85 | ('200002000080000100', ("Certificate URI field contains " 86 | "invalid characters")), 87 | ] 88 | test_decode_relax = None 89 | test_encode_error = None 90 | test_format_args_data = [ 91 | ((), 92 | "None, 'SHA-256', b'', '', X.509, [], ''"), 93 | (('ECDSA-P256', 'SHA-256', 94 | (b'0E\x02!\x00\xa4\x10\xc2\x8f\xd9C\x7f\xd2OfV\xf1!\xe6+\xcc_e\xe3bW' 95 | b'\xf5\xfa\xad\xf6\x8e>\x83\xd4\rH\x1a\x02 3[\x1d\xff\x8do\xe7"\xfc' 96 | b'\xf7\x01\x8b\xe9hM-\xe5g\x0b%o\xdf\xc0*\xa2[\xda\xe1obK\x80'), 97 | '', 'X.509', [], ''), 98 | ('\'ECDSA-P256\', \'SHA-256\', b\'0E\\x02!\\x00\\xa4\\x10\\xc2\\x8f\\' 99 | 'xd9C\\x7f\\xd2OfV\\xf1!\\xe6+\\xcc_e\\xe3bW\\xf5\\xfa\\xad\\xf6\\x8' 100 | 'e>\\x83\\xd4\\rH\\x1a\\x02 3[\\x1d\\xff\\x8do\\xe7"\\xfc\\xf7\\x01' 101 | '\\x8b\\xe9hM-\\xe5g\\x0b%o\\xdf\\xc0*\\xa2[\\xda\\xe1obK\\x80\', ' 102 | '\'\', X.509, [], \'\'')), 103 | ] 104 | test_format_str_data = [ 105 | ((), 106 | ("NDEF Signature Record ID '' Signature RTD " 107 | "'Version(major=2, minor=0)'")), 108 | (('ECDSA-P256', 'SHA-256', 109 | (b'0E\x02!\x00\xa4\x10\xc2\x8f\xd9C\x7f\xd2OfV\xf1!\xe6+\xcc_e\xe3bW' 110 | b'\xf5\xfa\xad\xf6\x8e>\x83\xd4\rH\x1a\x02 3[\x1d\xff\x8do\xe7"\xfc' 111 | b'\xf7\x01\x8b\xe9hM-\xe5g\x0b%o\xdf\xc0*\xa2[\xda\xe1obK\x80'), 112 | '', 'X.509', [], ''), 113 | ('NDEF Signature Record ID \'\' Signature RTD ' 114 | '\'Version(major=2, minor=0)\' Signature Type ' 115 | '\'ECDSA-P256\' Hash Type \'SHA-256\'')), 116 | (('ECDSA-P256', 'SHA-256', b'', 'signature_uri', 'X.509', [b'1', b'2'], 117 | 'certificate_uri'), 118 | ('NDEF Signature Record ID \'\' Signature RTD ' 119 | '\'Version(major=2, minor=0)\' Signature URI ' 120 | '\'signature_uri\' Certificate Format \'X.509\' ' 121 | 'Certificate URI \'certificate_uri\'')), 122 | ] 123 | 124 | def test_signature_signature_uri(self): 125 | obj = ndef.signature.SignatureRecord( 126 | None, None, None, '1', None, None, None) 127 | with pytest.raises(ValueError) as excinfo: 128 | obj.signature = b'1' 129 | errstr = "cannot set both signature and signature_uri" 130 | assert str(excinfo.value) == self.cls + ' ' + errstr 131 | 132 | def test_signature_enum_names(self): 133 | obj = ndef.signature.SignatureRecord( 134 | 'ECDSA-P256', 'SHA-256', None, None, 'M2M', None, None) 135 | obj._get_name_signature_type('ECDSA-P256') 136 | obj._get_name_hash_type('SHA-256') 137 | obj._get_name_certificate_format('M2M') 138 | -------------------------------------------------------------------------------- /tests/test_smartposter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import ndef 6 | import pytest 7 | import _test_record_base 8 | 9 | 10 | def pytest_generate_tests(metafunc): 11 | _test_record_base.generate_tests(metafunc) 12 | 13 | 14 | class TestActionRecord(_test_record_base._TestRecordBase): 15 | RECORD = ndef.smartposter.ActionRecord 16 | ATTRIB = "action" 17 | 18 | test_init_args_data = [ 19 | ((), ('exec',)), 20 | ((None,), ('exec',)), 21 | (('exec',), ('exec',)), 22 | (('save',), ('save',)), 23 | (('edit',), ('edit',)), 24 | ((0,), ('exec',)), 25 | ((1,), ('save',)), 26 | ((2,), ('edit',)), 27 | ] 28 | test_init_kwargs_data = [ 29 | (('save',), "action='save'"), 30 | ] 31 | test_init_fail_data = [ 32 | ((3,), 33 | ".action may be one of ('exec', 'save', 'edit') or index, but not 3"), 34 | ] 35 | test_decode_valid_data = [ 36 | ('00', ('exec',)), 37 | ('01', ('save',)), 38 | ('02', ('edit',)), 39 | ] 40 | test_decode_error_data = [ 41 | ('03', "decoding of ACTION value 3 is not defined"), 42 | ('ff', "decoding of ACTION value 255 is not defined"), 43 | ] 44 | test_decode_relax_data = [ 45 | ('03', ('exec',)), 46 | ('ff', ('exec',)), 47 | ] 48 | test_encode_error = None 49 | test_format_args_data = [ 50 | ((), "'exec'"), 51 | (('save',), "'save'"), 52 | ] 53 | test_format_str_data = [ 54 | ((), "NDEF Smartposter Action Record ID '' Action 'exec'"), 55 | (('save',), "NDEF Smartposter Action Record ID '' Action 'save'"), 56 | ] 57 | 58 | 59 | class TestSizeRecord(_test_record_base._TestRecordBase): 60 | RECORD = ndef.smartposter.SizeRecord 61 | ATTRIB = "resource_size" 62 | 63 | test_init_args_data = [ 64 | ((), (0,)), 65 | ((1234,), (1234,)), 66 | ] 67 | test_init_kwargs_data = [ 68 | ((1234,), "resource_size=1234"), 69 | ] 70 | test_init_fail_data = [ 71 | (('ab',), ".resource_size expects 32-bit unsigned int, but got 'ab'"), 72 | ((-1,), ".resource_size expects 32-bit unsigned int, but got -1"), 73 | ] 74 | test_decode_valid_data = [ 75 | ('00000000', (0,)), 76 | ('12345678', (0x12345678,)), 77 | ('ffffffff', (0xffffffff,)), 78 | ] 79 | test_decode_error = None 80 | test_decode_relax = None 81 | test_encode_error = None 82 | test_format_args_data = [ 83 | ((), "0"), 84 | ((1234,), "1234"), 85 | ] 86 | test_format_str_data = [ 87 | ((), 88 | "NDEF Smartposter Size Record ID '' Resource Size '0 byte'"), 89 | ((1234,), 90 | "NDEF Smartposter Size Record ID '' Resource Size '1234 byte'"), 91 | ] 92 | 93 | 94 | class TestTypeRecord(_test_record_base._TestRecordBase): 95 | RECORD = ndef.smartposter.TypeRecord 96 | ATTRIB = "resource_type" 97 | 98 | test_init_args_data = [ 99 | ((), ('',)), 100 | (('text/html',), ('text/html',)), 101 | ] 102 | test_init_kwargs_data = [ 103 | (('text/html',), "resource_type='text/html'"), 104 | ] 105 | test_init_fail_data = [ 106 | ((int(),), ".resource_type accepts str or bytes, but not int"), 107 | ] 108 | test_decode_valid_data = [ 109 | ('', ('',)), 110 | ('746578742f68746d6c', ('text/html',)), 111 | ] 112 | test_decode_error_data = [ 113 | ('ff', "can't decode payload as utf-8"), 114 | ] 115 | test_decode_relax = None 116 | test_encode_error = None 117 | test_format_args_data = [ 118 | ((), "''"), 119 | (('text/html',), "'text/html'"), 120 | ] 121 | test_format_str_data = [ 122 | ((), 123 | "NDEF Smartposter Type Record ID '' Resource Type ''"), 124 | (('text/html',), 125 | "NDEF Smartposter Type Record ID '' Resource Type 'text/html'"), 126 | ] 127 | 128 | 129 | class TestSmartposterRecord(_test_record_base._TestRecordBase): 130 | RECORD = ndef.smartposter.SmartposterRecord 131 | ATTRIB = ("resource, title, titles, action, icon, icons," + 132 | "resource_size, resource_type") 133 | 134 | test_init_args_data = [ 135 | ((), 136 | (ndef.UriRecord(''), None, {}, None, None, {}, None, None)), 137 | (('tel:123',), 138 | (ndef.UriRecord('tel:123'), None, {}, None, None, {}, None, None)), 139 | ((None, 'phone',), 140 | (None, 'phone', {'en': 'phone'}, None, None, {}, None, None)), 141 | ((None, None, 'exec'), 142 | (None, None, {}, 'exec', None, {}, None, None)), 143 | ((None, None, None, None, 1000), 144 | (None, None, {}, None, None, {}, 1000, None)), 145 | ((None, None, None, None, None, 'a/b'), 146 | (None, None, {}, None, None, {}, None, 'a/b')), 147 | ((None, None, None, {'image/gif': 'data'}), 148 | (None, None, {}, None, b'data', {'image/gif': b'data'}, None, None)), 149 | ] 150 | test_init_kwargs_data = [ 151 | (('tel:123', 'phone', 'exec', {'image/gif': b'icon'}, 10, 'text/html'), 152 | ("resource='tel:123', title='phone', action='exec', " + 153 | "icon={'image/gif': b'icon'}, resource_size=10, " + 154 | "resource_type='text/html'")), 155 | ] 156 | test_init_fail_data = [ 157 | ((None, None, None, b'123'), 158 | (" init requires icon bytes with png header, not b'123'")), 159 | ((None, None, None, 1), 160 | " init icon argument must be bytes or mapping, not int"), 161 | ((None, None, None, {'text/plain': b'123'}), 162 | " expects an image or video icon mimetype, not 'text/plain'"), 163 | ] 164 | test_decode_valid_data = [ 165 | ('d101015500', ('',)), 166 | ('d1010a55036e666370792e6f7267', ('http://nfcpy.org',)), 167 | ('9101015500 5101085402656e6e66637079', ('', 'nfcpy')), 168 | ('9101015500 510108540264656e66637079', ('', {'de': 'nfcpy'})), 169 | ('9101015500 51030161637400', ('', None, 'exec')), 170 | ('9101015500 5101047300000001', ('', None, None, None, 1)), 171 | ('9101015500 51010374612f62', ('', None, None, None, None, 'a/b')), 172 | ('9101015500 520908 696d6167652f706e67 89504e470d0a1a0a', 173 | ('', None, None, b'\x89PNG\x0d\x0a\x1a\x0a')), 174 | ('9101015500 520901766964656f2f6d703400', 175 | ('', None, None, {'video/mp4': b'\0'})), 176 | ('9101015500 520901696d6167652f706e6701', 177 | ('', None, None, {'image/png': b'\1'})), 178 | ('9101015500 520901696d6167652f67696601', 179 | ('', None, None, {'image/gif': b'\1'})), 180 | ] 181 | test_decode_error_data = [ 182 | ('', "payload must contain exactly one URI Record, got 0"), 183 | ('9101015500 5101015500', 184 | "payload must contain exactly one URI Record, got 2"), 185 | ] 186 | test_decode_relax_data = [ 187 | ('9101015500 510301616374ff', ('', None, 'exec')), 188 | ] 189 | test_encode_error = None 190 | test_format_args_data = [ 191 | (('http://nfcpy.org', 'nfcpy project', 'save', 192 | {'image/png': b'image data'}, 10000, 'text/html'), 193 | ("'http://nfcpy.org', {'en': 'nfcpy project'}, 'save', " + 194 | "{'image/png': bytearray(b'image data')}, 10000, 'text/html')")), 195 | ] 196 | test_format_str_data = [ 197 | (('http://nfcpy.org',), 198 | "NDEF Smartposter Record ID '' Resource 'http://nfcpy.org'"), 199 | (('http://nfcpy.org', "nfcpy project"), 200 | "NDEF Smartposter Record ID '' Resource 'http://nfcpy.org' " 201 | "Title 'nfcpy project'"), 202 | (('http://nfcpy.org', 'nfcpy project', 'exec'), 203 | "NDEF Smartposter Record ID '' Resource 'http://nfcpy.org' " 204 | "Title 'nfcpy project' Action 'exec'"), 205 | (('http://nfcpy.org', 'nfcpy', 'exec', {'image/png': b''}), 206 | "NDEF Smartposter Record ID '' Resource 'http://nfcpy.org' " 207 | "Title 'nfcpy' Icon 'image/png' Action 'exec'"), 208 | (('http://nfcpy.org', 'nfcpy', None, None, 999, 'text/html'), 209 | "NDEF Smartposter Record ID '' Resource 'http://nfcpy.org' " 210 | "Title 'nfcpy' Resource Size '999 byte' Resource Type 'text/html'"), 211 | ] 212 | 213 | def test_embedded_record_lists(self): 214 | record = ndef.smartposter.SmartposterRecord(None) 215 | assert record.uri_records == [] 216 | assert record.title_records == [] 217 | assert record.action_records == [] 218 | assert record.size_records == [] 219 | assert record.type_records == [] 220 | assert record.icon_records == [] 221 | 222 | 223 | def test_set_title(): 224 | record = ndef.SmartposterRecord() 225 | assert record.titles == {} 226 | record.set_title("English Text",) 227 | record.set_title("German Text", "de") 228 | assert record.titles == {'en': 'English Text', 'de': 'German Text'} 229 | record.set_title("Deutscher Text", "de") 230 | assert record.titles == {'en': 'English Text', 'de': 'Deutscher Text'} 231 | 232 | 233 | smartposter_messages = [ 234 | ('d102055370 d101015500', 235 | [ndef.SmartposterRecord('')]), 236 | ('d1020e5370 d1010a55036e666370792e6f7267', 237 | [ndef.SmartposterRecord('http://nfcpy.org')]), 238 | ('d1021a5370 91010a55036e666370792e6f7267 5101085402656e6e66637079', 239 | [ndef.SmartposterRecord('http://nfcpy.org', 'nfcpy')]), 240 | ('d102215370 91010a55036e666370792e6f7267 1101085402656e6e66637079' 241 | '51030161637400', 242 | [ndef.SmartposterRecord('http://nfcpy.org', 'nfcpy', 'exec')]), 243 | ] 244 | 245 | 246 | @pytest.mark.parametrize("encoded, message", smartposter_messages + [ 247 | ('d102085370 9101015500 500000', 248 | [ndef.SmartposterRecord('')]), 249 | ]) 250 | def test_message_decode(encoded, message): 251 | octets = bytes(bytearray.fromhex(encoded)) 252 | print(list(ndef.message_decoder(octets))) 253 | assert list(ndef.message_decoder(octets)) == message 254 | 255 | 256 | @pytest.mark.parametrize("encoded, message", smartposter_messages) 257 | def test_message_encode(encoded, message): 258 | octets = bytes(bytearray.fromhex(encoded)) 259 | print(list(ndef.message_encoder(message))) 260 | assert b''.join(list(ndef.message_encoder(message))) == octets 261 | 262 | 263 | @pytest.mark.parametrize("encoded, errstr", [ 264 | ('d1020c5370 9101015500 51030061637400', 265 | "ActionRecord payload length can not be less than 1"), 266 | ('d1020c5370 9101015500 510301616374ff', 267 | "ActionRecord decoding of ACTION value 255 is not defined"), 268 | ('d1020c5370 9101015500 51010373000000', 269 | "SizeRecord payload length can not be less than 4"), 270 | ('d1020e5370 9101015500 510105730000000000', 271 | "SizeRecord payload length can not be more than 4"), 272 | ('d1020f5370 9101015500 51010674ff6578742f70', 273 | "TypeRecord can't decode payload as utf-8"), 274 | ]) 275 | def test_message_decode_fail(encoded, errstr): 276 | octets = bytes(bytearray.fromhex(encoded)) 277 | with pytest.raises(ndef.DecodeError) as excinfo: 278 | print(list(ndef.message_decoder(octets))) 279 | assert str(excinfo.value) == "ndef.smartposter." + errstr 280 | -------------------------------------------------------------------------------- /tests/test_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import ndef 6 | import pytest 7 | import _test_record_base 8 | 9 | 10 | def pytest_generate_tests(metafunc): 11 | _test_record_base.generate_tests(metafunc) 12 | 13 | 14 | class TestTextRecord(_test_record_base._TestRecordBase): 15 | RECORD = ndef.text.TextRecord 16 | ATTRIB = "text, language, encoding" 17 | 18 | test_init_args_data = [ 19 | ((), ('', 'en', 'UTF-8')), 20 | ((None, None, None), ('', 'en', 'UTF-8')), 21 | (('Hello',), ('Hello', 'en', 'UTF-8')), 22 | (('Hello', 'en',), ('Hello', 'en', 'UTF-8')), 23 | (('Hello', 'en', 'UTF-8'), ('Hello', 'en', 'UTF-8')), 24 | (('Hello', 'de', 'UTF-16'), ('Hello', 'de', 'UTF-16')), 25 | (('Hello', 63*'a'), ('Hello', 63*'a', 'UTF-8')), 26 | ((u'Hallo', u'de',), ('Hallo', 'de', 'UTF-8')), 27 | ((b'Hallo', b'de',), ('Hallo', 'de', 'UTF-8')), 28 | ] 29 | test_init_kwargs_data = [ 30 | (('T', 'de', 'UTF-16'), "text='T', language='de', encoding='UTF-16'"), 31 | ] 32 | test_init_fail_data = [ 33 | ((1,), ".text accepts str or bytes, but not int"), 34 | (('', ''), ".language must be 1..63 characters, got 0"), 35 | (('', 64*'a'), ".language must be 1..63 characters, got 64"), 36 | (('', 'a', 'X'), ".encoding may be 'UTF-8' or 'UTF-16', but not 'X'"), 37 | (('', 0,), ".language accepts str or bytes, but not int"), 38 | (('', 'a', 0), ".encoding may be 'UTF-8' or 'UTF-16', but not '0'"), 39 | ] 40 | test_decode_valid_data = [ 41 | ('02656e', ("", "en", "UTF-8")), 42 | ('026465', ("", "de", "UTF-8")), 43 | ('02656e48656c6c6f', ("Hello", "en", "UTF-8")), 44 | ('82656efffe480065006c006c006f00', ("Hello", "en", "UTF-16")), 45 | ('02656cce94', (u"\u0394", "el", "UTF-8")), 46 | ('82656cfffe9403', (u"\u0394", "el", "UTF-16")), 47 | ] 48 | test_decode_error_data = [ 49 | ("82656e54", "can't be decoded as UTF-16"), 50 | ("02656efffe5400", "can't be decoded as UTF-8"), 51 | ("00", "language code length can not be zero"), 52 | ("01", "language code length exceeds payload"), 53 | ] 54 | test_decode_relax = None 55 | test_encode_error = None 56 | test_format_args_data = [ 57 | ((), "'', 'en', 'UTF-8'"), 58 | (('a',), "'a', 'en', 'UTF-8'"), 59 | (('a', 'de'), "'a', 'de', 'UTF-8'"), 60 | ] 61 | test_format_str_data = [ 62 | ((), 63 | "NDEF Text Record ID '' Text '' Language 'en' Encoding 'UTF-8'"), 64 | (('T'), 65 | "NDEF Text Record ID '' Text 'T' Language 'en' Encoding 'UTF-8'"), 66 | (('T', 'de'), 67 | "NDEF Text Record ID '' Text 'T' Language 'de' Encoding 'UTF-8'"), 68 | ] 69 | 70 | 71 | text_messages = [ 72 | ('D101075402656e54455854', 73 | [ndef.TextRecord('TEXT', 'en', 'UTF-8')]), 74 | ('9101075402656e54585431 5101075402656e54585432', 75 | [ndef.TextRecord('TXT1', 'en', 'UTF-8'), 76 | ndef.TextRecord('TXT2', 'en', 'UTF-8')]), 77 | ] 78 | 79 | 80 | @pytest.mark.parametrize("encoded, message", text_messages) 81 | def test_message_decode(encoded, message): 82 | octets = bytes(bytearray.fromhex(encoded)) 83 | print(list(ndef.message_decoder(octets))) 84 | assert list(ndef.message_decoder(octets)) == message 85 | 86 | 87 | @pytest.mark.parametrize("encoded, message", text_messages) 88 | def test_message_encode(encoded, message): 89 | octets = bytes(bytearray.fromhex(encoded)) 90 | print(list(ndef.message_encoder(message))) 91 | assert b''.join(list(ndef.message_encoder(message))) == octets 92 | -------------------------------------------------------------------------------- /tests/test_uri.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import ndef 6 | import pytest 7 | import _test_record_base 8 | 9 | 10 | def pytest_generate_tests(metafunc): 11 | _test_record_base.generate_tests(metafunc) 12 | 13 | 14 | class TestUriRecord(_test_record_base._TestRecordBase): 15 | RECORD = ndef.uri.UriRecord 16 | ATTRIB = "iri, uri" 17 | 18 | test_init_args_data = [ 19 | ((), ('', '')), 20 | ((None,), ('', '')), 21 | (('http://nfcpy.org',), ('http://nfcpy.org', 'http://nfcpy.org')), 22 | ((u'http://nfcpy.org',), ('http://nfcpy.org', 'http://nfcpy.org')), 23 | ((b'http://nfcpy.org',), ('http://nfcpy.org', 'http://nfcpy.org')), 24 | (('nfcpy',), ('nfcpy', 'nfcpy')), 25 | (("http://www.nfcpy",), ("http://www.nfcpy", "http://www.nfcpy")), 26 | (("https://www.nfcpy",), ("https://www.nfcpy", "https://www.nfcpy")), 27 | (("http://nfcpy",), ("http://nfcpy", "http://nfcpy")), 28 | (("https://nfcpy",), ("https://nfcpy", "https://nfcpy")), 29 | (("tel:01234",), ("tel:01234", "tel:01234")), 30 | (("mailto:nfcpy",), ("mailto:nfcpy", "mailto:nfcpy")), 31 | (("ftp://anonymous:anonymous@nfcpy",), 32 | ("ftp://anonymous:anonymous@nfcpy", 33 | "ftp://anonymous:anonymous@nfcpy")), 34 | (("ftp://ftp.nfcpy",), ("ftp://ftp.nfcpy", "ftp://ftp.nfcpy")), 35 | (("ftps://nfcpy",), ("ftps://nfcpy", "ftps://nfcpy")), 36 | (("sftp://nfcpy",), ("sftp://nfcpy", "sftp://nfcpy")), 37 | (("smb://nfcpy",), ("smb://nfcpy", "smb://nfcpy")), 38 | (("nfs://nfcpy",), ("nfs://nfcpy", "nfs://nfcpy")), 39 | (("ftp://nfcpy",), ("ftp://nfcpy", "ftp://nfcpy")), 40 | (("dav://nfcpy",), ("dav://nfcpy", "dav://nfcpy")), 41 | (("news:nfcpy",), ("news:nfcpy", "news:nfcpy")), 42 | (("telnet://nfcpy",), ("telnet://nfcpy", "telnet://nfcpy")), 43 | (("imap://nfcpy",), ("imap://nfcpy", "imap://nfcpy")), 44 | (("rtsp://nfcpy",), ("rtsp://nfcpy", "rtsp://nfcpy")), 45 | (("urn:nfcpy",), ("urn:nfcpy", "urn:nfcpy")), 46 | (("pop:nfcpy",), ("pop:nfcpy", "pop:nfcpy")), 47 | (("sip:123@server.net",), 48 | ("sip:123@server.net", "sip:123@server.net")), 49 | (("sips:nfcpy",), ("sips:nfcpy", "sips:nfcpy")), 50 | (("tftp:nfcpy",), ("tftp:nfcpy", "tftp:nfcpy")), 51 | (("btspp://nfcpy",), ("btspp://nfcpy", "btspp://nfcpy")), 52 | (("btl2cap://nfcpy",), ("btl2cap://nfcpy", "btl2cap://nfcpy")), 53 | (("btgoep://nfcpy",), ("btgoep://nfcpy", "btgoep://nfcpy")), 54 | (("tcpobex://nfcpy",), ("tcpobex://nfcpy", "tcpobex://nfcpy")), 55 | (("irdaobex://nfcpy",), ("irdaobex://nfcpy", "irdaobex://nfcpy")), 56 | (("file://nfcpy",), ("file://nfcpy", "file://nfcpy")), 57 | (("urn:epc:id:12345",), ("urn:epc:id:12345", "urn:epc:id:12345")), 58 | (("urn:epc:tag:12345",), ("urn:epc:tag:12345", "urn:epc:tag:12345")), 59 | (("urn:epc:pat:12345",), ("urn:epc:pat:12345", "urn:epc:pat:12345")), 60 | (("urn:epc:raw:12345",), ("urn:epc:raw:12345", "urn:epc:raw:12345")), 61 | (("urn:epc:12345",), ("urn:epc:12345", "urn:epc:12345")), 62 | (("urn:nfc:12345",), ("urn:nfc:12345", "urn:nfc:12345")), 63 | ((u"http://www.hääyö.com/~user/index.html",), 64 | (u"http://www.hääyö.com/~user/index.html", 65 | u"http://www.xn--hy-viaa5g.com/~user/index.html")), 66 | ] 67 | test_init_kwargs_data = [ 68 | (('URI',), "iri='URI'"), 69 | ] 70 | test_init_fail_data = [ 71 | ((1,), ".iri accepts str or bytes, but not int"), 72 | ] 73 | test_decode_valid_data = [ 74 | ('006e66637079', ("nfcpy",)), 75 | ('016e66637079', ("http://www.nfcpy",)), 76 | ('026e66637079', ("https://www.nfcpy",)), 77 | ('036e66637079', ("http://nfcpy",)), 78 | ('046e66637079', ("https://nfcpy",)), 79 | ('053031323334', ("tel:01234",)), 80 | ("066e66637079", ("mailto:nfcpy",)), 81 | ("076e66637079", ("ftp://anonymous:anonymous@nfcpy",)), 82 | ("086e66637079", ("ftp://ftp.nfcpy",)), 83 | ("096e66637079", ("ftps://nfcpy",)), 84 | ("0a6e66637079", ("sftp://nfcpy",)), 85 | ("0b6e66637079", ("smb://nfcpy",)), 86 | ("0c6e66637079", ("nfs://nfcpy",)), 87 | ("0d6e66637079", ("ftp://nfcpy",)), 88 | ("0e6e66637079", ("dav://nfcpy",)), 89 | ("0f6e66637079", ("news:nfcpy",)), 90 | ("106e66637079", ("telnet://nfcpy",)), 91 | ("116e66637079", ("imap:nfcpy",)), 92 | ("126e66637079", ("rtsp://nfcpy",)), 93 | ("136e66637079", ("urn:nfcpy",)), 94 | ("146e66637079", ("pop:nfcpy",)), 95 | ("156e66637079", ("sip:nfcpy",)), 96 | ("166e66637079", ("sips:nfcpy",)), 97 | ("176e66637079", ("tftp:nfcpy",)), 98 | ("186e66637079", ("btspp://nfcpy",)), 99 | ("196e66637079", ("btl2cap://nfcpy",)), 100 | ("1a6e66637079", ("btgoep://nfcpy",)), 101 | ("1b6e66637079", ("tcpobex://nfcpy",)), 102 | ("1c6e66637079", ("irdaobex://nfcpy",)), 103 | ("1d6e66637079", ("file://nfcpy",)), 104 | ("1e3132333435", ("urn:epc:id:12345",)), 105 | ("1f3132333435", ("urn:epc:tag:12345",)), 106 | ("203132333435", ("urn:epc:pat:12345",)), 107 | ("213132333435", ("urn:epc:raw:12345",)), 108 | ("223132333435", ("urn:epc:12345",)), 109 | ("233132333435", ("urn:nfc:12345",)), 110 | ] 111 | test_decode_error_data = [ 112 | ('246e66637079', "decoding of URI identifier 36 is not defined"), 113 | ('ff6e66637079', "decoding of URI identifier 255 is not defined"), 114 | ('0380', "URI field is not valid UTF-8 data"), 115 | ('0300', "URI field contains invalid characters"), 116 | ] 117 | test_decode_relax_data = [ 118 | ('246e66637079', ("nfcpy",)), 119 | ('ff6e66637079', ("nfcpy",)), 120 | ] 121 | test_encode_error = None 122 | test_format_args_data = [ 123 | ((), "''"), 124 | (('http://example.com',), "'http://example.com'"), 125 | ] 126 | test_format_str_data = [ 127 | ((), "NDEF Uri Record ID '' Resource ''"), 128 | (('tel:1234',), "NDEF Uri Record ID '' Resource 'tel:1234'"), 129 | ] 130 | 131 | 132 | def test_uri_to_iri_conversion(): 133 | record = ndef.UriRecord() 134 | # no netloc -> no conversion 135 | record.uri = u"tel:1234" 136 | assert record.iri == u"tel:1234" 137 | # with netloc -> conversion 138 | record.uri = u"http://www.xn--hy-viaa5g.com/%7Euser/index.html" 139 | assert record.iri == u"http://www.hääyö.com/~user/index.html" 140 | 141 | 142 | uri_messages = [ 143 | ('D1010a55036e666370792e6f7267', 144 | [ndef.UriRecord('http://nfcpy.org')]), 145 | ('91010a55036e666370792e6f7267 51010a55046e666370792e6f7267', 146 | [ndef.UriRecord('http://nfcpy.org'), 147 | ndef.UriRecord('https://nfcpy.org')]), 148 | ('D101115500736d74703a2f2f6e666370792e6f7267', 149 | [ndef.UriRecord('smtp://nfcpy.org')]), 150 | ] 151 | 152 | 153 | @pytest.mark.parametrize("encoded, message", uri_messages) 154 | def test_message_decode(encoded, message): 155 | octets = bytes(bytearray.fromhex(encoded)) 156 | print(list(ndef.message_decoder(octets))) 157 | assert list(ndef.message_decoder(octets)) == message 158 | 159 | 160 | @pytest.mark.parametrize("encoded, message", uri_messages) 161 | def test_message_encode(encoded, message): 162 | octets = bytes(bytearray.fromhex(encoded)) 163 | print(list(ndef.message_encoder(message))) 164 | assert b''.join(list(ndef.message_encoder(message))) == octets 165 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = coverage-clean,py2,py3,flake8,manifest,docs,readme,coverage-report 3 | 4 | 5 | [pytest] 6 | minversion = 2.8 7 | addopts = -ra 8 | testpaths = tests 9 | 10 | 11 | [testenv] 12 | deps = -rrequirements-dev.txt 13 | commands = coverage run --parallel -m pytest {posargs} 14 | passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY 15 | 16 | 17 | [testenv:flake8] 18 | deps = flake8 19 | commands = flake8 src tests setup.py 20 | 21 | 22 | [testenv:docs] 23 | setenv = 24 | PYTHONHASHSEED = 313 25 | deps = 26 | sphinx 27 | cryptography 28 | asn1crypto 29 | commands = 30 | sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html 31 | sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html 32 | python -m doctest README.rst 33 | 34 | 35 | [testenv:manifest] 36 | deps = check-manifest 37 | skip_install = true 38 | commands = check-manifest 39 | 40 | 41 | [testenv:readme] 42 | deps = readme_renderer 43 | skip_install = true 44 | commands = python setup.py check -r -s 45 | 46 | 47 | [testenv:coverage-clean] 48 | deps = coverage 49 | skip_install = true 50 | commands = coverage erase 51 | 52 | 53 | [testenv:coverage-report] 54 | deps = coverage 55 | skip_install = true 56 | commands = 57 | coverage combine 58 | coverage report 59 | 60 | --------------------------------------------------------------------------------