├── .coveragerc ├── .gitignore ├── .readthedocs.yaml ├── .travis.yml ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── commands │ ├── identifier.rst │ ├── load.rst │ ├── payload.rst │ ├── print.rst │ ├── save.rst │ ├── smartposter.rst │ ├── text.rst │ ├── typename.rst │ └── uri.rst ├── conf.py ├── contributing.rst ├── images │ ├── ndeftool.ico │ └── ndeftool.png ├── index.rst ├── license.rst ├── ndeftool.rst ├── requirements.in └── requirements.txt ├── pytest.ini ├── requirements-dev.txt ├── requirements-pypi.txt ├── setup.cfg ├── setup.py ├── src └── ndeftool │ ├── __init__.py │ ├── cli.py │ └── commands │ ├── IDentifier.py │ ├── Load.py │ ├── PayLoad.py │ ├── Print.py │ ├── SMartPoster.py │ ├── Save.py │ ├── TeXT.py │ ├── TypeName.py │ ├── URI.py │ └── __init__.py ├── tests ├── test_identifier.py ├── test_load.py ├── test_main.py ├── test_payload.py ├── test_print.py ├── test_save.py ├── test_smartposter.py ├── test_text.py ├── test_typename.py └── test_uri.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = ndeftool 4 | 5 | [paths] 6 | source = 7 | src/ndeftool 8 | .tox/*/lib/python*/site-packages/ndeftool 9 | .tox/pypy/site-packages/ndeftool 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 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Thank you for considering contributing to **ndeftool**. 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 versions of **ndeftool** 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/ndeftool.git 44 | cd ndeftool 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 ndeftool --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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017 by 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 *.ini LICENSE .coveragerc docs/Makefile 2 | recursive-include src *.py 3 | recursive-include tests *.py 4 | recursive-include docs *.ico 5 | recursive-include docs *.png 6 | recursive-include docs *.py 7 | recursive-include docs *.rst 8 | recursive-include docs *.txt 9 | prune docs/_build 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================== 2 | Create, modify and print NDEF Records 3 | ===================================== 4 | 5 | .. image:: https://img.shields.io/pypi/v/ndeftool 6 | :target: https://pypi.python.org/pypi/ndeftool 7 | :alt: Python Package 8 | 9 | .. image:: https://img.shields.io/readthedocs/ndeftool 10 | :target: http://ndeftool.readthedocs.io 11 | :alt: Documentation 12 | 13 | .. image:: https://img.shields.io/codecov/c/github/nfcpy/ndeftool 14 | :target: https://codecov.io/gh/nfcpy/ndeftool 15 | :alt: Code Coverage 16 | 17 | The ``ndeftool`` is a command line utility to create or inspect NFC 18 | Data Exchange Format (NDEF) records and messages, released under the 19 | `ISC `_ license. 20 | 21 | .. code-block:: shell 22 | 23 | $ ndeftool text "Hello World" id "r1" uri "http://nfcpy.org" save -k "message.ndef" print 24 | Saving 2 records to message.ndef. 25 | NDEF Text Record ID 'r1' Text 'Hello World' Language 'en' Encoding 'UTF-8' 26 | NDEF Uri Record ID '' Resource 'http://nfcpy.org' 27 | 28 | The ``ndeftool`` documentation can be found on `Read the Docs 29 | `_ and the code on `GitHub 30 | `_. 31 | -------------------------------------------------------------------------------- /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/commands/identifier.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _identifier: 4 | 5 | identifier 6 | ========== 7 | 8 | Change the identifier of the last record. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool identifier [OPTIONS] NAME 16 | ndeftool id [OPTIONS] NAME 17 | 18 | Description 19 | ----------- 20 | 21 | The **identifier** command either changes the current last record's name (NDEF 22 | Record ID) or, if the current message does not have any records, creates a 23 | record with unknown record type and the given record name. 24 | 25 | Options 26 | ------- 27 | 28 | .. option:: --help 29 | 30 | Show this message and exit. 31 | 32 | Examples 33 | -------- 34 | 35 | Create a record with `unknown` type and set the identifier. 36 | 37 | .. command-output:: ndeftool identifier 'record identifier' print 38 | 39 | Create two text records with specific identifiers. 40 | 41 | .. command-output:: ndeftool text 'first' id 'r1' text 'second' id 'r2' print 42 | 43 | -------------------------------------------------------------------------------- /docs/commands/load.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _load: 4 | 5 | load 6 | ==== 7 | 8 | Load records or payloads from disk. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool load [OPTIONS] PATH 16 | ndeftool l [OPTIONS] PATH 17 | 18 | Description 19 | ----------- 20 | 21 | The **load** command reads records or payloads from disk files or 22 | standard input. The files for reading are determined by the pattern 23 | specified by PATH, which in the simplest form is an existing file 24 | name. Other forms may include `*`, `?` and character ranges expressed 25 | by `[]`. A single `-` may be used to read from standard input. Note 26 | that patterns containg wildcards may need to be escaped to avoid shell 27 | expansion. 28 | 29 | The default mode of operation is to load files containing NDEF 30 | records. In `--pack` mode the files are loaded into the payload of 31 | NDEF records with record type (NDEF Record TNF and TYPE) set to the 32 | mimetype discovered from the payload and record name (NDEF Record ID) 33 | set to the filename. 34 | 35 | Options 36 | ------- 37 | 38 | .. option:: -p, --pack 39 | 40 | Pack files as payload into mimetype records. 41 | 42 | .. option:: --help 43 | 44 | Show this message and exit. 45 | 46 | Examples 47 | -------- 48 | 49 | Pack text from standard input and pack as record. 50 | 51 | .. command-output:: echo -n "Hello World" | ndeftool load --pack - print 52 | :shell: 53 | 54 | Read text from file and pack as record. 55 | 56 | .. command-output:: echo -n "Hello World" > /tmp/hello && ndeftool load --pack /tmp/hello print 57 | :shell: 58 | 59 | Read with path containing wildcard characters. 60 | 61 | .. command-output:: ndeftool load --pack 'i?d*.rst' print 62 | 63 | Read and pack multiple files. 64 | 65 | .. command-output:: ndeftool load --pack '../se?up.*' print 66 | -------------------------------------------------------------------------------- /docs/commands/payload.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _payload: 4 | 5 | payload 6 | ======= 7 | 8 | Change the payload of the last record. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool payload [OPTIONS] DATA 16 | ndeftool pl [OPTIONS] DATA 17 | 18 | Description 19 | ----------- 20 | 21 | The **payload** command either changes the current last record's data (NDEF 22 | Record PAYLOAD) or, if the current message does not have any records, creates a 23 | record with the given record data. The changed record is verified to 24 | successfully encode and decode unless disabled with `-x`. 25 | 26 | The data string may contain hexadecimal bytes using `\xNN` notation where 27 | each N is a nibble from [0-F]. 28 | 29 | Options 30 | ------- 31 | 32 | .. option:: -x, --no-check 33 | 34 | Do not check decoding after type name change. 35 | 36 | .. option:: --help 37 | 38 | Show this message and exit. 39 | 40 | Examples 41 | -------- 42 | 43 | Create a plain text payload record. 44 | 45 | .. command-output:: ndeftool payload 'Hello World' typename 'text/plain' print 46 | 47 | Create an NFC Forum Text record with language code and content. 48 | 49 | .. command-output:: ndeftool payload '\x02enHello World' typename 'urn:nfc:wkt:T' print -l 50 | 51 | Create a record with a payload that does not match the record type. The first 52 | command creates an NFC Forum Text Record with language code identifier and text 53 | content. The second command then replaces the payload with just the text and 54 | would make decoding fail. 55 | 56 | .. command-output:: ndeftool text 'Hello World' payload -x 'Hello World' print -l 57 | -------------------------------------------------------------------------------- /docs/commands/print.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. print: 4 | 5 | print 6 | ===== 7 | 8 | Print records as human readable. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool print [OPTIONS] 16 | ndeftool p [OPTIONS] 17 | 18 | Description 19 | ----------- 20 | 21 | The **print** command outputs a formatted representation of all current NDEF 22 | Records. By default this is the one line str() representation for each 23 | record. The `--long` format produces multiple indented lines per record in an 24 | attempt to provide a more readable output. Printing consumes all records so that 25 | no more data is send to stdout or given to the next command. This can be changed 26 | with the `--keep` flag. 27 | 28 | When given as the first command **print** attempts to decode an NDEF message 29 | from standard input and process the generated list of records. 30 | 31 | Options 32 | ------- 33 | 34 | .. option:: -l, --long 35 | 36 | Output in a long print format. 37 | 38 | .. option:: -k, --keep 39 | 40 | Keep records for next command. 41 | 42 | .. option:: --help 43 | 44 | Show this message and exit. 45 | 46 | Examples 47 | -------- 48 | 49 | Print records in short format. 50 | 51 | .. command-output:: ndeftool text "Hello World" print 52 | 53 | Print records in long format. 54 | 55 | .. command-output:: ndeftool text "Hello World" print --long 56 | 57 | Print records in both short and long format. 58 | 59 | .. command-output:: ndeftool text "Hello World" print --keep print --long 60 | -------------------------------------------------------------------------------- /docs/commands/save.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _save: 4 | 5 | save 6 | ==== 7 | 8 | Save records or payloads to disk. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool save [OPTIONS] PATH 16 | ndeftool s [OPTIONS] PATH 17 | 18 | Description 19 | ----------- 20 | 21 | The **save** command writes the current records to disk. The records to write 22 | can be restricted to the subset selected with `--skip`, `--count`, `--head` and 23 | `--tail` applied in that order. The default mode is to save all selected records 24 | as one NDEF message into a single file given by PATH. In `--burst` mode each 25 | record is written as one NDEF message into a separate file under the directory 26 | given by PATH. The file names are three digit numbers created from the record 27 | index. In `--unpack` mode the payload of each record is written to a separate 28 | file under directory PATH with the file name set to the record name (NDEF Record 29 | ID). Records without name are not written unless `--unpack` and `--burst` are 30 | both set. 31 | 32 | The **save** command does not replace existing files or directories unless this is 33 | requested with `--force`. 34 | 35 | The **save** command consumes records from the internal message pipe. This can 36 | be prevented with `--keep`, all records are then forwarded to the next command 37 | or written to standard output. When **save** is the first command it creates the 38 | pipe by reading from standard input. 39 | 40 | Options 41 | ------- 42 | 43 | .. option:: --skip N 44 | 45 | Skip the first N records. 46 | 47 | .. option:: --count N 48 | 49 | Skip the first N records. 50 | 51 | .. option:: --head N 52 | 53 | Save the first N records. 54 | 55 | .. option:: --tail N 56 | 57 | Save the last N records. 58 | 59 | .. option:: -b, --burst 60 | 61 | Save single record files in directory. 62 | 63 | .. option:: -u, --unpack 64 | 65 | Unpack records to files in directory. 66 | 67 | .. option:: -f, --force 68 | 69 | Replace existing file or directory. 70 | 71 | .. option:: -k, --keep 72 | 73 | Forward records to next command. 74 | 75 | .. option:: --help 76 | 77 | Show this message and exit. 78 | 79 | Examples 80 | -------- 81 | 82 | Create an NFC Forum Text Record and save it to to a file in the /tmp directory, 83 | overwriting the file if it exists. 84 | 85 | .. command-output:: ndeftool text "Hello World" save --force /tmp/hello.ndef 86 | 87 | Same as above but the with three NDEF Text Records. 88 | 89 | .. command-output:: ndeftool text One text Two text Three save --force /tmp/text.ndef 90 | 91 | Out of three records the second is saved using the `--skip` and `--count` 92 | options and the others are forwarded to print. 93 | 94 | .. command-output:: ndeftool txt aa txt bb txt cc save -f --skip 1 --count 1 /tmp/text.ndef print 95 | 96 | Out of three records the second is saved using the `--head` and `--tail` options 97 | and the others are forwarded to print. 98 | 99 | .. command-output:: ndeftool txt aa txt bb txt cc save -f --head 2 --tail 1 /tmp/text.ndef print 100 | 101 | Save each record to a separate file with auto-numbered file name plus `.ndef` 102 | extension. 103 | 104 | .. command-output:: ndeftool txt aa txt bb txt cc save -f --burst /tmp/text/ 105 | 106 | Unpack record payloads to separate files using the record identifier as the file 107 | name. 108 | 109 | .. command-output:: ndeftool txt aa id 1.txt txt bb id 2.txt txt cc id 3.txt save -f --unpack /tmp/text/ 110 | -------------------------------------------------------------------------------- /docs/commands/smartposter.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _smartposter: 4 | 5 | smartposter 6 | =========== 7 | 8 | Create an NFC Forum Smart Poster Record. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool smartposter [OPTIONS] RESOURCE 16 | ndeftool smp [OPTIONS] RESOURCE 17 | 18 | Description 19 | ----------- 20 | 21 | The **smartposter** command creates an NFC Forum Smart Poster Record for the 22 | resource identifier. A smart poster record combines the uniform resource 23 | identifier with additional data such as titles and icons for representation and 24 | processing instructions for the reader application. 25 | 26 | A smart poster record should have title text for the desired languages, added 27 | with repetitive `-t` options. An English title text may also be added with 28 | `-T`. The recommended action set with `-a` tells the reader application to 29 | either run the default action for the URI, save it for later or open for 30 | editing. 31 | 32 | A smart poster may also provide a collection of icons for graphical 33 | representation. An icon file is added with the `-i` option that may be given 34 | more than once. The icon type is determined from the file content and must be an 35 | image or video mime type. 36 | 37 | Options 38 | ------- 39 | 40 | .. option:: -T TEXT 41 | 42 | Smartposter title for language code 'en'. 43 | 44 | .. option:: -t LANG TEXT 45 | 46 | Smartposter title for a given language code. 47 | 48 | .. option:: -a [exec|save|edit] 49 | 50 | Recommended action for handling the resource. 51 | 52 | .. option:: -i FILENAME 53 | 54 | Icon file for a graphical representation. 55 | 56 | .. option:: --help 57 | 58 | Show this message and exit. 59 | 60 | Examples 61 | -------- 62 | 63 | An NFC Forum Smart Poster Record with just a link, nothing more useful than a URI Record. 64 | 65 | .. command-output:: ndeftool smartposter http://nfcpy.org print 66 | 67 | Same as above but with an English title. 68 | 69 | .. command-output:: ndeftool smartposter -T 'nfcpy project' http://nfcpy.org print 70 | 71 | Titles for other languages must be given with a language code. 72 | 73 | .. command-output:: ndeftool smartposter -t de 'Google Deutschland' https://www.google.de print 74 | 75 | An emergency call number should be called immediately. 76 | 77 | .. command-output:: ndeftool smartposter -T 'EMERGENCY CALL 911' -a exec 'tel:911' print -l 78 | 79 | Add an icon file to a smart poster. 80 | 81 | .. command-output:: ndeftool smp -i images/ndeftool.png https://github.com/nfcpy/ndeftool print -l 82 | -------------------------------------------------------------------------------- /docs/commands/text.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _text: 4 | 5 | text 6 | ==== 7 | 8 | Create an NFC Forum Text Record. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool text [OPTIONS] TEXT 16 | ndeftool txt [OPTIONS] TEXT 17 | 18 | Description 19 | ----------- 20 | 21 | The **text** command creates an NFC Forum Text Record with the given input 22 | text. The text language defaults to English (language code `en`) and can be set 23 | with `--language` followed by the IANA language code. The text content is 24 | encoded as UTF-8 or UTF-16 depending on `--encoding`. The default encoding is 25 | UTF-8. 26 | 27 | Options 28 | ------- 29 | 30 | .. option:: -l, --language TEXT 31 | 32 | Set the IANA language code. 33 | 34 | .. option:: --encoding [UTF-8|UTF-16] 35 | 36 | Set the encoding (default UTF-8). 37 | 38 | .. option:: --help 39 | 40 | Show this message and exit. 41 | 42 | Examples 43 | -------- 44 | 45 | Create an NFC Forum Text Record with the default language `en` and encoding `UTF-8`. 46 | 47 | .. command-output:: ndeftool text 'created with the nfcpy ndeftool' print 48 | 49 | Create one text record with English text and one record with German text. 50 | 51 | .. command-output:: ndeftool text --language en 'English' text --language de 'Deutsch' print 52 | 53 | Create a text record with UTF-16 encoding. 54 | 55 | .. command-output:: ndeftool text --encoding UTF-16 'text encoded in UTF-16' | xxd -g 1 56 | :shell: 57 | -------------------------------------------------------------------------------- /docs/commands/typename.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _typename: 4 | 5 | typename 6 | ======== 7 | 8 | Change the type name of the last record. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool typename [OPTIONS] TYPE 16 | ndeftool tn [OPTIONS] TYPE 17 | 18 | Description 19 | ----------- 20 | 21 | The **typename** command either changes the current last record's type (NDEF 22 | Record TNF and TYPE) or, if the current message does not have any records, 23 | creates a record with the given record type. The changed record is verified to 24 | successfully encode and decode unless disabled with `-x`. 25 | 26 | Options 27 | ------- 28 | 29 | .. option:: -x, --no-check 30 | 31 | Do not check decoding after type name change. 32 | 33 | .. option:: --help 34 | 35 | Show this message and exit. 36 | 37 | Examples 38 | -------- 39 | 40 | Create a record with `text/plain` mime type and no payload. 41 | 42 | .. command-output:: ndeftool typename 'text/plain' print 43 | 44 | Create a plain text record and add some payload. 45 | 46 | .. command-output:: ndeftool typename 'text/plain' payload 'Hello World' print 47 | 48 | Create a record with a payload that does not match the record type. 49 | 50 | .. command-output:: ndeftool payload 'Hello World' typename -x 'urn:nfc:wkt:T' print -l 51 | -------------------------------------------------------------------------------- /docs/commands/uri.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | .. _uri: 4 | 5 | uri 6 | === 7 | 8 | Create an NFC Forum URI Record. 9 | 10 | Synopsis 11 | -------- 12 | 13 | .. code:: 14 | 15 | ndeftool uri [OPTIONS] RESOURCE 16 | 17 | Description 18 | ----------- 19 | 20 | The **uri** command creates an NFC Forum URI Record with the given resource 21 | identifier. Note that this is actually an Internationalized Resource Identifier 22 | (IRI). 23 | 24 | Options 25 | ------- 26 | 27 | .. option:: --help 28 | 29 | Show this message and exit. 30 | 31 | Examples 32 | -------- 33 | 34 | Create a URI record that links to `http://nfcpy.org`. 35 | 36 | .. command-output:: ndeftool uri 'http://nfcpy.org' print 37 | -------------------------------------------------------------------------------- /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 sys, os, re 17 | 18 | try: 19 | import sphinx_rtd_theme 20 | except ImportError: 21 | sphinx_rtd_theme = None 22 | 23 | 24 | def read(*parts): 25 | """ 26 | Build an absolute path from *parts* and and return the contents of the 27 | resulting file. Assume UTF-8 encoding. 28 | """ 29 | here = os.path.abspath(os.path.dirname(__file__)) 30 | with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: 31 | return f.read() 32 | 33 | 34 | def find_version(*file_paths): 35 | """ 36 | Build a path from *file_paths* and search for a ``__version__`` 37 | string inside. 38 | """ 39 | version_file = read(*file_paths) 40 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 41 | version_file, re.M) 42 | if version_match: 43 | return version_match.group(1) 44 | raise RuntimeError("Unable to find version string.") 45 | 46 | 47 | # If extensions (or modules to document with autodoc) are in another directory, 48 | # add these directories to sys.path here. If the directory is relative to the 49 | # documentation root, use os.path.abspath to make it absolute, like shown here. 50 | #sys.path.insert(0, os.path.abspath('.')) 51 | 52 | # -- General configuration ------------------------------------------------ 53 | 54 | # If your documentation needs a minimal Sphinx version, state it here. 55 | needs_sphinx = '1.0' 56 | 57 | # Add any Sphinx extension module names here, as strings. They can be 58 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 59 | # ones. 60 | extensions = [ 61 | 'sphinxcontrib.programoutput', 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_setup = { 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'ndeftool' 90 | year = datetime.date.today().year 91 | copyright = u'2017{0}, Stephen Tiedemann'.format( 92 | u'-{0}'.format(year) if year > 2017 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/ndeftool/__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 = 'en' 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/ndeftool.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/ndeftool.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 = 'ndeftooldoc' 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', 'ndeftool.tex', u'ndeftool 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', 'ndeftool', u'ndeftool 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', 'ndeftool', u'ndeftool Documentation', u'Stephen Tiedemann', 311 | 'ndeftool', 'Create or inspect NFC Data Exchange Format 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/images/ndeftool.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcpy/ndeftool/3a526e42f57d107b99b81289fe854734d3957267/docs/images/ndeftool.ico -------------------------------------------------------------------------------- /docs/images/ndeftool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcpy/ndeftool/3a526e42f57d107b99b81289fe854734d3957267/docs/images/ndeftool.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | =================================== 4 | NDEF Creation and Modification Tool 5 | =================================== 6 | 7 | .. _ISCL: http://choosealicense.com/licenses/isc/ 8 | .. _GitHub: https://github.com/nfcpy/ndeftool 9 | .. _PyPI: https://pypi.python.org/pypi/ndeftool 10 | 11 | The **ndeftool** is a command line utility for creating, modifying and printing 12 | NFC Data Exchange Format (NDEF) Records. It is licensed under the `ISCL`_, 13 | hosted on `GitHub`_ and installable from `PyPI`_. 14 | 15 | .. code:: 16 | 17 | $ pip install ndeftool 18 | $ ndeftool --help 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | 23 | ndeftool 24 | contributing 25 | license 26 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | ======= 4 | License 5 | ======= 6 | 7 | The **ndeftool** 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/ndeftool.rst: -------------------------------------------------------------------------------- 1 | .. -*- mode: rst; fill-column: 80 -*- 2 | 3 | ======== 4 | NDEFTOOL 5 | ======== 6 | 7 | Create, modify or print NFC Data Exchange Format Records. 8 | 9 | Synopsis 10 | -------- 11 | 12 | .. code:: 13 | 14 | ndeftool [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... 15 | 16 | Description 17 | ----------- 18 | 19 | The **ndeftool** provides a number of commands to create, modify or print NDEF 20 | records. Commands can be chained together to successively extend or modify the 21 | list of records that are generated. Except for **save** and **print**, all 22 | records forwarded by the last command are send to standard output as binary NDEF 23 | message data. The **save** and **print** commands implicitly consume all records 24 | unless the `--keep` option is set. They also, unlike all other commands, do not 25 | start an empty record list but read from standard input when given as the first 26 | command (the same behavior can be achieved for the **load** command by setting 27 | the file path to `-`). 28 | 29 | By default, NDEF records read from disk or standard input must be correctly 30 | formatted NDEF message data (for example, the first and the last record must 31 | have the message begin and end flag set). The `--relax` makes the decoder accept 32 | correctable data errors and the `--ignore` option will additionally skip records 33 | with uncorrectable errors. 34 | 35 | 36 | Options 37 | ------- 38 | 39 | .. option:: --version 40 | 41 | Show the version and exit. 42 | 43 | .. option:: --relax 44 | 45 | Ignore some errors when decoding. 46 | 47 | .. option:: --ignore 48 | 49 | Ignore all errors when decoding. 50 | 51 | .. option:: --silent 52 | 53 | Suppress all progress information. 54 | 55 | .. option:: --debug 56 | 57 | Output debug progress information. 58 | 59 | .. option:: --help 60 | 61 | Show this message and exit. 62 | 63 | 64 | Commands 65 | -------- 66 | 67 | .. toctree:: 68 | :maxdepth: 1 69 | 70 | commands/load 71 | commands/save 72 | commands/print 73 | 74 | commands/identifier 75 | commands/typename 76 | commands/payload 77 | 78 | commands/text 79 | commands/uri 80 | commands/smartposter 81 | 82 | 83 | Examples 84 | -------- 85 | 86 | Any NDEF Record can be constructed with the :ref:`payload`, :ref:`typename` and 87 | :ref:`identifier` commands. 88 | 89 | .. command-output:: ndeftool payload '\02ensample text' typename 'urn:nfc:wkt:T' id 'r1' print 90 | 91 | The same record can be created with the :ref:`text` command. Here the output 92 | goes to stdout and is then printed with a separate ndeftool process call. 93 | 94 | .. command-output:: ndeftool text 'sample text' id 'r1' | ndeftool print 95 | :shell: 96 | 97 | The :ref:`save` command writes the records to disk or for path name 98 | `-`. The following example creates an NDEF message with three NFC Forum Text 99 | Records, the first and last record with the message begin and message end flags 100 | set. 101 | 102 | .. command-output:: ndeftool text ONE text TWO text THREE save - | xxd -g 1 103 | :shell: 104 | 105 | The :ref:`save` command can be used to write intermediate results, here 106 | immediately after a text record has been created. Note that by writing to 107 | the result is a sequence of three individual NDEF messages of one 108 | record each. This would not be a proper NDEF message file. 109 | 110 | .. command-output:: ndeftool text ONE save - text TWO save - text THREE save - | xxd -g 1 111 | :shell: 112 | 113 | The :ref:`load` command reads records from disk or for path name `-`. 114 | 115 | .. command-output:: ndeftool text ONE text TWO text THREE | ndeftool load - print 116 | :shell: 117 | 118 | An empty NDEF Record can be created with an empty type name string. The first 119 | octet ``11010000b`` sets the Message Begin (MB) and Message End (ME) flags in 120 | the two most signifant bits. The Type Name Format (TNF) value 0 in the least 121 | significant three bits indicates that there is no type or payload associated 122 | with this record and thus the `TYPE LENGTH` and `PAYLOAD LENGTH` fields must be 123 | zero. 124 | 125 | .. command-output:: ndeftool typename '' | xxd -g 1 126 | :shell: 127 | 128 | The default decoding of an NDEF message requires correct data format. Data with 129 | minor format errors can be decoded with the `--relax` option. The following 130 | example creates two empty records with invalid MB and ME flags that do only 131 | decode with `--relax`. 132 | 133 | .. command-output:: python3 -c "import sys; sys.stdout.buffer.write(b'\x10\0\0\x10\0\0')" | ndeftool --relax print 134 | :shell: 135 | 136 | NDEF message data with uncorrectable errors can be skipped with the `--ignore` 137 | option. The payload length 1 in the second record is an invalid value for an 138 | empty record. 139 | 140 | .. command-output:: python3 -c "import sys; sys.stdout.buffer.write(b'\x10\0\0\x10\1\0')" | ndeftool --ignore print 141 | :shell: 142 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | # 2 | # Required packages for build on readthedocs.org 3 | # 4 | sphinx==7.2.5 5 | sphinx_rtd_theme==1.3.0 6 | readthedocs-sphinx-search==0.3.1 7 | sphinxcontrib-programoutput==0.17 8 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile docs/requirements.in 6 | # 7 | alabaster==0.7.13 8 | # via sphinx 9 | babel==2.12.1 10 | # via sphinx 11 | certifi==2023.7.22 12 | # via requests 13 | charset-normalizer==3.2.0 14 | # via requests 15 | docutils==0.18.1 16 | # via 17 | # sphinx 18 | # sphinx-rtd-theme 19 | idna==3.4 20 | # via requests 21 | imagesize==1.4.1 22 | # via sphinx 23 | jinja2==3.1.2 24 | # via sphinx 25 | markupsafe==2.1.3 26 | # via jinja2 27 | packaging==23.1 28 | # via sphinx 29 | pygments==2.16.1 30 | # via sphinx 31 | readthedocs-sphinx-search==0.3.1 32 | # via -r docs/requirements.in 33 | requests==2.31.0 34 | # via sphinx 35 | snowballstemmer==2.2.0 36 | # via sphinx 37 | sphinx==7.2.5 38 | # via 39 | # -r docs/requirements.in 40 | # sphinx-rtd-theme 41 | # sphinxcontrib-applehelp 42 | # sphinxcontrib-devhelp 43 | # sphinxcontrib-htmlhelp 44 | # sphinxcontrib-jquery 45 | # sphinxcontrib-programoutput 46 | # sphinxcontrib-qthelp 47 | # sphinxcontrib-serializinghtml 48 | sphinx-rtd-theme==1.3.0 49 | # via -r docs/requirements.in 50 | sphinxcontrib-applehelp==1.0.7 51 | # via sphinx 52 | sphinxcontrib-devhelp==1.0.5 53 | # via sphinx 54 | sphinxcontrib-htmlhelp==2.0.4 55 | # via sphinx 56 | sphinxcontrib-jquery==4.1 57 | # via sphinx-rtd-theme 58 | sphinxcontrib-jsmath==1.0.1 59 | # via sphinx 60 | sphinxcontrib-programoutput==0.17 61 | # via -r docs/requirements.in 62 | sphinxcontrib-qthelp==1.0.6 63 | # via sphinx 64 | sphinxcontrib-serializinghtml==1.1.9 65 | # via sphinx 66 | urllib3==2.0.4 67 | # via requests 68 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | minversion = 6.0 3 | addopts = -ra 4 | testpaths = tests 5 | -------------------------------------------------------------------------------- /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 ndeftool --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 | sphinxcontrib-programoutput 25 | -------------------------------------------------------------------------------- /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 dist/ndeftool-x.y.z* 7 | # 8 | # twine upload -r pypi dist/ndeftool-x.y.z* 9 | # 10 | setuptools 11 | wheel 12 | twine 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_files = 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 = "ndeftool" 12 | PACKAGES = find_packages(where="src") 13 | META_PATH = os.path.join("src", "ndeftool", "__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 = ["ndeflib", "click", "python-magic"] 32 | CONSOLE_SCRIPTS = ['ndeftool=ndeftool.cli:main'] 33 | 34 | ############################################################################### 35 | 36 | HERE = os.path.abspath(os.path.dirname(__file__)) 37 | 38 | 39 | def read(*parts): 40 | """ 41 | Build an absolute path from *parts* and and return the contents of the 42 | resulting file. Assume UTF-8 encoding. 43 | """ 44 | with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: 45 | return f.read() 46 | 47 | 48 | META_FILE = read(META_PATH) 49 | 50 | 51 | def find_meta(meta): 52 | """ 53 | Extract __*meta*__ from META_FILE. 54 | """ 55 | meta_match = re.search( 56 | r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), 57 | META_FILE, re.M 58 | ) 59 | if meta_match: 60 | return meta_match.group(1) 61 | raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) 62 | 63 | 64 | if __name__ == "__main__": 65 | setup( 66 | name=NAME, 67 | description=find_meta("description"), 68 | license=find_meta("license"), 69 | url=find_meta("uri"), 70 | version=find_meta("version"), 71 | author=find_meta("author"), 72 | author_email=find_meta("email"), 73 | maintainer=find_meta("author"), 74 | maintainer_email=find_meta("email"), 75 | keywords=KEYWORDS, 76 | long_description=read("README.rst"), 77 | packages=PACKAGES, 78 | package_dir={"": "src"}, 79 | zip_safe=False, 80 | classifiers=CLASSIFIERS, 81 | install_requires=INSTALL_REQUIRES, 82 | entry_points={'console_scripts': CONSOLE_SCRIPTS}, 83 | ) 84 | -------------------------------------------------------------------------------- /src/ndeftool/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # METADATA #################################################################### 4 | 5 | __version__ = "0.1.3" 6 | 7 | __title__ = "ndeftool" 8 | __description__ = "Create or inspect NFC Data Exchange Format messages." 9 | __uri__ = "https://github.com/nfcpy/ndeftool/" 10 | 11 | __author__ = "Stephen Tiedemann" 12 | __email__ = "stephen.tiedemann@gmail.com" 13 | 14 | __license__ = "ISC" 15 | __copyright__ = "Copyright (c) 2017 Stephen Tiedemann" 16 | 17 | ############################################################################### 18 | -------------------------------------------------------------------------------- /src/ndeftool/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from functools import wraps 4 | import os.path 5 | import click 6 | import ndef 7 | 8 | version_message = '%%(prog)s %%(version)s (ndeflib %s)' % ndef.__version__ 9 | command_plugins = os.path.join(os.path.dirname(__file__), 'commands') 10 | 11 | 12 | def echo(*args, **kwargs): 13 | click.echo(*args, **kwargs) 14 | 15 | 16 | def info(*args): 17 | if click.get_current_context().meta['output-logmsg'] != 'silent': 18 | click.secho(*args, err=True, fg='green') 19 | 20 | 21 | def dmsg(*args): 22 | if click.get_current_context().meta['output-logmsg'] == 'debug': 23 | click.secho(*args, err=True, fg='yellow') 24 | 25 | 26 | def warn(*args): 27 | click.secho(*args, err=True, fg='red') 28 | 29 | 30 | class CommandGroup(click.Group): 31 | def list_commands(self, ctx): 32 | command_list = [] 33 | 34 | # All commands are separate Python files within the 35 | # command_plugins folder. 36 | for filename in sorted(os.listdir(command_plugins)): 37 | basename, extension = os.path.splitext(filename) 38 | if extension == '.py' and basename != '__init__': 39 | command_list.append(basename) 40 | 41 | return command_list 42 | 43 | def get_command(self, ctx, name): 44 | name = name.lower() # all commands treated case insensitive 45 | 46 | # From list_commands() we get the command file names without 47 | # extension. We use the upper case letters to construct the 48 | # abbreviated name and import the module if the requested 49 | # command name matches either the lower case full or short 50 | # name. 51 | for cmd_name in self.list_commands(ctx): 52 | cmd_abbr = ''.join(x for x in cmd_name if 'A' <= x <= 'Z') 53 | if name in (cmd_name.lower(), cmd_abbr.lower()): 54 | module = 'ndeftool.commands.' + cmd_name 55 | return __import__(module, None, None, ['cmd']).cmd 56 | 57 | def format_commands(self, ctx, formatter): 58 | rows = [] 59 | 60 | # From list_commands() we get the command file names without 61 | # extension. We use the upper case letters to construct the 62 | # abbreviated name and store lower case versions of short and 63 | # long command name. 64 | for cmd_name in self.list_commands(ctx): 65 | cmd_abbr = ''.join(x for x in cmd_name if 'A' <= x <= 'Z') 66 | cmd_help = self.get_command(ctx, cmd_name).short_help or '' 67 | rows.append((cmd_abbr.lower(), cmd_name.lower(), cmd_help)) 68 | 69 | # We want the command list to be sorted by abbreviated command 70 | # name with the shortest names first. 71 | rows = sorted(rows, key=lambda x: '%02d %s' % (len(x[0]), x[0])) 72 | rows = [('%s, %s' % (a, n) if a != n else a, h) for a, n, h in rows] 73 | 74 | with formatter.section('Commands'): 75 | formatter.write_dl(rows) 76 | 77 | 78 | @click.command(cls=CommandGroup, chain=True) 79 | @click.version_option(message=version_message) 80 | @click.option('--relax', 'errors', flag_value='relax', 81 | help='Ignore some errors when decoding.') 82 | @click.option('--ignore', 'errors', flag_value='ignore', 83 | help='Ignore all errors when decoding.') 84 | @click.option('--silent', 'logmsg', flag_value='silent', 85 | help='Suppress all progress information.') 86 | @click.option('--debug', 'logmsg', flag_value='debug', 87 | help='Output debug progress information.') 88 | @click.pass_context 89 | def main(ctx, **kwargs): 90 | """Create or inspect NFC Data Exchange Format messages. 91 | 92 | The ndeftool provides a number of commands to create or inspect 93 | NDEF messages. All commands can be chained to an internal 94 | processing pipeline and the whole fit into a command shell 95 | pipeline. 96 | 97 | \b 98 | ndeftool load FILE1 load FILE2 save FILE3 99 | ndeftool load - load FILE2 < FILE1 > FILE3 100 | cat FILE1 | ndeftool load - load FILE2 | hexdump -Cv 101 | 102 | The ndeftool processing pipeline builds an NDEF message from left 103 | to right, each command adds some NDEF record(s) to the message 104 | until it is either send to standard output or consumed by an 105 | ndeftool command (unless the --keep option is given to a command 106 | that would otherwise consume the message). 107 | 108 | \b 109 | ndeftool text 'one' text 'two' print --keep > two_text_records.ndef 110 | ndeftool text 'one' text 'two' save --keep two_text_records.ndef print 111 | 112 | A new pipeline is started after ndeftool command that consumed the 113 | current message. This can be used to generate or inspect multiple 114 | messages. 115 | 116 | \b 117 | ndeftool text 'one' save text_1.ndef text 'two' save text_2.ndef 118 | ndeftool load text_1.ndef print load text_2.ndef print 119 | 120 | Each command has it's own help page: 'ndeftool --help' 121 | """ 122 | ctx.meta['decode-errors'] = kwargs['errors'] or 'strict' 123 | ctx.meta['output-logmsg'] = kwargs['logmsg'] or 'normal' 124 | 125 | 126 | @main.result_callback() 127 | def process_commands(processors, **kwargs): 128 | message = None 129 | for processor in processors: 130 | message = processor(message) 131 | dmsg('records = ' + str(message)) 132 | echo(b''.join(ndef.message_encoder(message)), nl=False) 133 | 134 | 135 | def command_processor(func): 136 | @wraps(func) 137 | def wrapper(*args, **kwargs): 138 | def processor(message): 139 | return func(message, *args, **kwargs) 140 | return processor 141 | return wrapper 142 | -------------------------------------------------------------------------------- /src/ndeftool/commands/IDentifier.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | import ndef 5 | 6 | from ndeftool.cli import command_processor, dmsg 7 | 8 | 9 | @click.command(short_help="Change the identifier of the last record.") 10 | @click.argument('name') 11 | @command_processor 12 | def cmd(message, **kwargs): 13 | """The *identifier* command either changes the current last record's 14 | name (NDEF Record ID) or, if the current message does not have any 15 | records, creates a record with unknown record type and the given 16 | record name. 17 | 18 | \b 19 | Examples: 20 | ndeftool identifier 'record identifier' print 21 | ndeftool text 'first' id 'r1' text 'second' id 'r2' print 22 | 23 | """ 24 | dmsg(__name__ + ' ' + str(kwargs)) 25 | 26 | if not message: 27 | message = [ndef.Record('unknown')] 28 | 29 | try: 30 | message[-1].name = kwargs['name'].encode('latin', 'replace') 31 | except ValueError as error: 32 | raise click.ClickException(str(error)) 33 | 34 | return message 35 | -------------------------------------------------------------------------------- /src/ndeftool/commands/Load.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | import magic 5 | import glob 6 | import ndef 7 | 8 | from ndeftool.cli import command_processor, info, warn, dmsg 9 | 10 | 11 | @click.command(short_help="Load records or payloads from disk.") 12 | @click.argument('path', type=click.Path()) 13 | @click.option('-p', '--pack', is_flag=True, 14 | help="Pack files as payload into mimetype records.") 15 | @command_processor 16 | @click.pass_context 17 | def cmd(ctx, message, **kwargs): 18 | """The *load* command reads records or payloads from disk files or 19 | standard input. The files to read are searched with the pattern 20 | specified by PATH which in the simplest form is an existing file 21 | name. Other forms may include '*', '?' and character ranges 22 | expressed by '[]'. A single '-' may be used to read from standard 23 | input. Note that patterns containg wildcards may need to be 24 | escaped to avoid shell expansion. 25 | 26 | The default mode of operation is to load files containing NDEF 27 | records. In '--pack' mode the files are loaded into the payload of 28 | NDEF records with record type (NDEF Record TNF and TYPE) set to 29 | the mimetype discovered from the payload and record name (NDEF 30 | Record ID) set to the filename. 31 | 32 | \b 33 | Examples: 34 | ndeftool load message.ndef print 35 | ndeftool load '*.ndef' print 36 | cat message.ndef | ndeftool load - print 37 | ndeftool load --pack /etc/hostname print 38 | ndeftool load --pack '/etc/cron.daily/*' print 39 | 40 | """ 41 | dmsg(__name__ + ' ' + str(kwargs)) 42 | 43 | if message is None: 44 | message = [] 45 | 46 | if kwargs['path'] == '-': 47 | filenames = kwargs['path'] 48 | else: 49 | filenames = sorted(glob.iglob(kwargs['path'])) 50 | 51 | if len(filenames) == 0: 52 | info("No files selected by path '%s'." % kwargs['path']) 53 | 54 | for filename in filenames: 55 | try: 56 | f = click.open_file(filename, 'rb') 57 | except (OSError, IOError) as error: 58 | warn(str(error)) 59 | else: 60 | if kwargs['pack']: 61 | message.append(pack_file(f)) 62 | else: 63 | message.extend(load_file(f, ctx.meta['decode-errors'])) 64 | 65 | return message 66 | 67 | 68 | def pack_file(f): 69 | record_data = f.read() 70 | record_type = magic.from_buffer(record_data, mime=1) 71 | record_name = getattr(f, 'name', '')[0:255] 72 | return ndef.Record(record_type, record_name, record_data) 73 | 74 | 75 | def load_file(f, decode_errors): 76 | fn = getattr(f, 'name', '') 77 | try: 78 | records = list(ndef.message_decoder(f.read(), decode_errors)) 79 | info("loaded %d record(s) from %s" % (len(records), fn)) 80 | return records 81 | except ndef.DecodeError as error: 82 | dmsg(str(error)) 83 | errmsg = "%s does not contain a valid NDEF message." % fn 84 | raise click.ClickException(errmsg) 85 | -------------------------------------------------------------------------------- /src/ndeftool/commands/PayLoad.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | import ndef 5 | 6 | from ndeftool.cli import command_processor, dmsg 7 | 8 | 9 | @click.command(short_help='Change the payload of the last record.') 10 | @click.argument('data', type=click.UNPROCESSED) 11 | @click.option('-x', '--no-check', is_flag=True, 12 | help='Do not check decoding after data change.') 13 | @command_processor 14 | @click.pass_context 15 | def cmd(ctx, message, **kwargs): 16 | """The *payload* command either changes the current last record's 17 | data (NDEF Record PAYLOAD) or, if the current message does 18 | not have any records, creates a record with the given record 19 | data. The changed record is verified to successfully encode and 20 | decode unless disabled with -x. 21 | 22 | The data string may contain hexadecimal bytes using '\\xNN' 23 | notation where each N is a nibble from [0-F]. 24 | 25 | \b 26 | Examples: 27 | ndeftool payload 'Hello World' typename 'text/plain' print 28 | ndeftool payload '\\x02enHello World' typename 'urn:nfc:wkt:T' print -l 29 | 30 | """ 31 | dmsg(__name__ + ' ' + str(kwargs)) 32 | dmsg(repr(kwargs['data'])) 33 | 34 | if not message: 35 | message = [ndef.Record('unknown')] 36 | 37 | record_type = message[-1].type 38 | record_name = message[-1].name 39 | record_data = eval(repr(kwargs['data'].encode()).replace('\\\\', '\\')) 40 | 41 | record = ndef.Record(record_type, record_name, record_data) 42 | 43 | if not kwargs['no_check']: 44 | octets = b''.join(ndef.message_encoder([record])) 45 | errors = ctx.meta['decode-errors'] 46 | try: 47 | record = next(ndef.message_decoder(octets, errors)) 48 | except ndef.DecodeError as error: 49 | raise click.ClickException(str(error)) 50 | 51 | message[-1] = record 52 | return message 53 | -------------------------------------------------------------------------------- /src/ndeftool/commands/Print.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | import ndef 5 | 6 | from ndeftool.cli import command_processor, dmsg, info, echo 7 | 8 | LONG_FORMAT = " {:10} {}" 9 | 10 | 11 | @click.command(short_help="Print records as human readable.") 12 | @click.option('-l', '--long', is_flag=True, help="use a long print format") 13 | @click.option('-k', '--keep', is_flag=True, help="keep message in pipeline") 14 | @command_processor 15 | @click.pass_context 16 | def cmd(ctx, message, **kwargs): 17 | """The *print* command outputs a formatted representation of all 18 | current NDEF Records. By default this is the one line str() 19 | representation for each record. The '--long' format produces 20 | multiple indented lines per record in an attempt to provide a more 21 | readable output. Printing consumes all records so that no more 22 | data is send to stdout or given to the next command. This can be 23 | changed with the '--keep' flag. 24 | 25 | When given as the first command *print* attempts to decode an NDEF 26 | message from standard input and then process the generated list of 27 | records. 28 | 29 | \b 30 | Examples: 31 | ndeftool text 'made with ndeftool' print 32 | ndeftool text 'say one' print text 'say two' print 33 | ndeftool text one print --keep text two print 34 | ndeftool text 'print from stdin' | ndeftool print 35 | ndeftool text 'before' | ndeftool print text 'after' print 36 | 37 | """ 38 | dmsg(__name__ + ' ' + str(kwargs)) 39 | 40 | if message is None: 41 | info("Reading data from standard input") 42 | octets = click.get_binary_stream('stdin').read() 43 | errors = ctx.meta['decode-errors'] 44 | try: 45 | message = list(ndef.message_decoder(octets, errors)) 46 | except ndef.DecodeError as error: 47 | raise click.ClickException(str(error)) 48 | 49 | for index, record in enumerate(message): 50 | if not kwargs['long']: 51 | echo(str(record)) 52 | continue 53 | 54 | if isinstance(record, ndef.TextRecord): 55 | echo("NFC Forum Text Record [record #{}]".format(index+1)) 56 | echo(LONG_FORMAT.format("content", record.text)) 57 | echo(LONG_FORMAT.format("language", record.language)) 58 | echo(LONG_FORMAT.format("encoding", record.encoding)) 59 | elif isinstance(record, ndef.UriRecord): 60 | echo("NFC Forum URI Record [record #{}]".format(index+1)) 61 | echo(LONG_FORMAT.format("resource", record.iri)) 62 | elif isinstance(record, ndef.SmartposterRecord): 63 | echo("NFC Forum Smart Poster Record [record #{}]".format(index+1)) 64 | echo(LONG_FORMAT.format("resource", record.resource.iri)) 65 | if record.action: 66 | echo(LONG_FORMAT.format("action", record.action)) 67 | for lang, text in record.titles.items(): 68 | echo(LONG_FORMAT.format("title_" + lang, text)) 69 | for icon_type, icon_data in record.icons.items(): 70 | echo(LONG_FORMAT.format(icon_type, "%d byte" % len(icon_data))) 71 | else: 72 | echo("Record [record #{}]".format(index+1)) 73 | echo(LONG_FORMAT.format("type", record.type)) 74 | echo(LONG_FORMAT.format("name", record.name)) 75 | echo(LONG_FORMAT.format("data", bytes(record.data))) 76 | 77 | return message if kwargs['keep'] else [] 78 | -------------------------------------------------------------------------------- /src/ndeftool/commands/SMartPoster.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import magic 4 | import click 5 | import ndef 6 | 7 | from ndeftool.cli import command_processor, dmsg 8 | 9 | 10 | @click.command(short_help='Create an NFC Forum Smart Poster record.') 11 | @click.argument('resource') 12 | @click.option('-T', 'title', 13 | help='Smartposter title for language code \'en\'.') 14 | @click.option('-t', 'titles', nargs=2, multiple=True, metavar='LANG TEXT', 15 | help='Smartposter title for a given language code.') 16 | @click.option('-a', 'action', type=click.Choice(['exec', 'save', 'edit']), 17 | help='Recommended action for handling the resource.') 18 | @click.option('-i', 'icons', multiple=True, type=click.File('rb', lazy=True), 19 | help='Icon file for a graphical representation.') 20 | @command_processor 21 | def cmd(message, **kwargs): 22 | """The *smartposter* command creates an NFC Forum Smart Poster Record 23 | for the resource identifier. A smart poster record combines the 24 | uniform resource identifier with additional data such as titles 25 | and icons for representation and processing instructions for the 26 | reader application. 27 | 28 | A smart poster should have title text entries for the desired 29 | languages, added with repetitive '-t' options. An English title 30 | text may also be added with '-T'. The recommended action set with 31 | '-a' tells the reader application to either run the default action 32 | for the URI, save it for later or open for editing. 33 | 34 | A smart poster may also provide a collection of icons for 35 | graphical representation. An icon file is added with the '-i' 36 | option which may be used more than once. The icon type is 37 | determined from the file content and must be an 'image' or 'video' 38 | mime type. 39 | 40 | \b 41 | Examples: 42 | ndeftool smartposter http://nfcpy.org print 43 | ndeftool smartposter -T 'nfcpy project' http://nfcpy.org print 44 | ndeftool smartposter -t en 'nfcpy project' http://nfcpy.org print 45 | ndeftool smartposter -T 'EMERGENCY CALL 911' -a exec tel:911 46 | ndeftool smartposter -i nfcpy-logo-32x32.ico http://nfcpy.org 47 | 48 | """ 49 | dmsg(__name__ + ' ' + str(kwargs)) 50 | 51 | if message is None: 52 | message = [] 53 | 54 | record = ndef.SmartposterRecord(kwargs['resource']) 55 | for lang, text in kwargs['titles']: 56 | record.set_title(text, lang) 57 | if kwargs['title']: 58 | record.set_title(kwargs['title'], 'en') 59 | if kwargs['action']: 60 | record.action = kwargs['action'] 61 | for icon_file in kwargs['icons']: 62 | icon_data = icon_file.read() 63 | icon_type = magic.from_buffer(icon_data, mime=True) 64 | if icon_type.startswith('image/') or icon_type.startswith('video/'): 65 | record.add_icon(icon_type, icon_data) 66 | else: 67 | errmsg = "file %s is not a proper icon file" % icon_file.name 68 | raise click.ClickException(errmsg) 69 | 70 | message.append(record) 71 | return message 72 | -------------------------------------------------------------------------------- /src/ndeftool/commands/Save.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path 4 | import shutil 5 | import click 6 | import ndef 7 | 8 | from ndeftool.cli import command_processor, dmsg, info, warn 9 | 10 | 11 | @click.command(short_help="Save records or payloads to disk.") 12 | @click.argument('path', type=click.Path(writable=True)) 13 | @click.option('--skip', type=click.IntRange(min=0), default=0, 14 | metavar='N', help="Skip the first N records.") 15 | @click.option('--count', type=click.IntRange(min=1), default=None, 16 | metavar='N', help="Skip the first N records.") 17 | @click.option('--head', type=click.IntRange(min=1), default=None, 18 | metavar='N', help="Save the first N records.") 19 | @click.option('--tail', type=click.IntRange(min=1), default=None, 20 | metavar='N', help="Save the last N records.") 21 | @click.option('-b', '--burst', is_flag=True, 22 | help="Save single record files in directory.") 23 | @click.option('-u', '--unpack', is_flag=True, 24 | help="Unpack records to files in directory.") 25 | @click.option('-f', '--force', is_flag=True, 26 | help="Replace existing file or directory.") 27 | @click.option('-k', '--keep', is_flag=True, 28 | help="Forward records to next command.") 29 | @command_processor 30 | @click.pass_context 31 | def cmd(ctx, message, **kwargs): 32 | """The *save* command writes the current records to disk. The records 33 | to write can be restricted to the subset selected with '--skip', 34 | '--count', '--head' and '--tail' applied in that order. The 35 | default mode is to save all selected records as one NDEF message 36 | into a single file given by PATH. In '--burst' mode each record is 37 | written as one NDEF message into a separate file under the 38 | directory given by PATH. The file names are three digit numbers 39 | created from the record index. In '--unpack' mode the payload of 40 | each record is written to a separate file under directory PATH 41 | with the file name set to the record name (NDEF Record ID). 42 | Records without name are not written unless '--unpack' and 43 | '--burst' are both set. 44 | 45 | The *save* command does not replace existing files or directories 46 | unless this is requested with '--force'. 47 | 48 | The *save* command consumes records from the internal message 49 | pipe. This can be prevented with '--keep', all records are then 50 | forwarded to the next command or written to standard output. When 51 | *save* is the first command it creates the pipe by reading from 52 | standard input. 53 | 54 | \b 55 | Examples: 56 | ndeftool text 'Hello World' save text.ndef 57 | ndeftool text 'Hello World' | ndeftool save text.ndef 58 | ndeftool text 'One' save one.ndef text 'Two' save two.ndef 59 | 60 | """ 61 | dmsg(__name__ + ' ' + str(kwargs)) 62 | 63 | path = kwargs['path'] 64 | 65 | if os.path.exists(path) and not kwargs['force']: 66 | errmsg = "path '%s' exists. Use '--force' to replace." 67 | raise click.ClickException(errmsg % path) 68 | 69 | if message is None: 70 | info("Reading data from standard input") 71 | octets = click.get_binary_stream('stdin').read() 72 | errors = ctx.meta['decode-errors'] 73 | try: 74 | message = list(ndef.message_decoder(octets, errors)) 75 | except ndef.DecodeError as error: 76 | raise click.ClickException(str(error)) 77 | 78 | first = min(kwargs['skip'], len(message)) 79 | count = min(kwargs['count'] or len(message), len(message)) 80 | head = min(kwargs['head'] or count, count) 81 | tail = min(kwargs['tail'] or count, count) 82 | dmsg("first=%d count=%d head=%d tail=%d" % (first, count, head, tail)) 83 | count = min(head, tail, len(message) - first) 84 | first = first + head - min(head, tail) 85 | dmsg("first=%d count=%d head=%d tail=%d" % (first, count, head, tail)) 86 | 87 | if kwargs['burst'] or kwargs['unpack']: 88 | path = os.path.normpath(path) 89 | try: 90 | if os.path.isdir(path): 91 | shutil.rmtree(path) 92 | os.mkdir(path) 93 | except (OSError, IOError) as error: 94 | raise click.ClickException(str(error)) 95 | 96 | for index, record in enumerate(message[first:first+count]): 97 | name = None 98 | if kwargs['unpack'] and record.name: 99 | name = record.name 100 | if kwargs['burst'] and not name: 101 | name = '%03d.ndef' % index 102 | if name: 103 | with click.open_file('%s/%s' % (path, name), 'wb') as f: 104 | info("Saving 1 record to {}.".format(f.name)) 105 | if kwargs['unpack']: 106 | f.write(record.data) 107 | else: 108 | f.write(b''.join(ndef.message_encoder([record]))) 109 | else: 110 | warn("Skipping 1 record without name") 111 | else: 112 | with click.open_file(path, 'wb') as f: 113 | filename = f.name if f.name != '-' else '' 114 | 115 | info("Saving {num} record{s} to {path}.".format( 116 | num=count, path=filename, s=('', 's')[count > 1])) 117 | 118 | f.write(b''.join(ndef.message_encoder(message[first:first+count]))) 119 | 120 | if not kwargs['keep']: 121 | del message[first:first+count] 122 | 123 | return message 124 | -------------------------------------------------------------------------------- /src/ndeftool/commands/TeXT.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | import ndef 5 | 6 | from ndeftool.cli import command_processor, dmsg 7 | 8 | 9 | @click.command(short_help="Create an NFC Forum Text Record.") 10 | @click.argument('text') 11 | @click.option('-l', '--language', default='en', 12 | help="Set the IANA language code.") 13 | @click.option('--encoding', default='UTF-8', 14 | type=click.Choice(['UTF-8', 'UTF-16']), 15 | help="Set the encoding (default UTF-8).") 16 | @command_processor 17 | def cmd(message, **kwargs): 18 | """The *text* command creates an NFC Forum Text Record with the given 19 | input text. The text language defaults to 'en' and can be set 20 | with --language followed by the IANA language code. 21 | 22 | \b 23 | Examples: 24 | ndeftool text '' | xxd -g 1 25 | ndeftool text 'Created with the nfcpy ndeftool.' print 26 | ndeftool text 'first record' text 'second record' print 27 | ndeftool text -l en 'English' text -l de 'Deutsch' print 28 | 29 | """ 30 | dmsg(__name__ + ' ' + str(kwargs)) 31 | 32 | if message is None: 33 | message = [] 34 | 35 | content = kwargs['text'] 36 | language = kwargs['language'] 37 | encoding = kwargs['encoding'] 38 | 39 | record = ndef.TextRecord(content, language, encoding) 40 | 41 | message.append(record) 42 | return message 43 | -------------------------------------------------------------------------------- /src/ndeftool/commands/TypeName.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | import ndef 5 | 6 | from ndeftool.cli import command_processor, dmsg 7 | 8 | 9 | @click.command(short_help='Change the type name of the last record.') 10 | @click.argument('type') 11 | @click.option('-x', '--no-check', is_flag=True, 12 | help='Do not check decoding after type name change.') 13 | @command_processor 14 | @click.pass_context 15 | def cmd(ctx, message, **kwargs): 16 | """The *typename* command either changes the current last record's 17 | type (NDEF Record TNF and TYPE) or, if the current message does 18 | not have any records, creates a record with the given record 19 | type. The changed record is verified to successfully encode and 20 | decode unless disabled with -x. 21 | 22 | \b 23 | Examples: 24 | ndeftool typename 'text/plain' print 25 | ndeftool typename 'text/plain' payload 'Hello World' print 26 | 27 | """ 28 | dmsg(__name__ + ' ' + str(kwargs)) 29 | 30 | if not message: 31 | message = [ndef.Record()] 32 | 33 | record_type = kwargs['type'].encode('latin', 'replace') 34 | record_name = message[-1].name 35 | record_data = message[-1].data 36 | 37 | try: 38 | record = ndef.Record(record_type, record_name, record_data) 39 | except ValueError as error: 40 | raise click.ClickException(str(error)) 41 | 42 | if not kwargs['no_check']: 43 | octets = b''.join(ndef.message_encoder([record])) 44 | errors = ctx.meta['decode-errors'] 45 | try: 46 | record = next(ndef.message_decoder(octets, errors)) 47 | except ndef.DecodeError as error: 48 | raise click.ClickException(str(error)) 49 | 50 | message[-1] = record 51 | return message 52 | -------------------------------------------------------------------------------- /src/ndeftool/commands/URI.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | import ndef 5 | 6 | from ndeftool.cli import command_processor, dmsg 7 | 8 | 9 | @click.command(short_help="Create an NFC Forum URI Record.") 10 | @click.argument('resource') 11 | @command_processor 12 | def cmd(message, **kwargs): 13 | """The *uri* command creates an NFC Forum URI Record wit the given 14 | resource identifier. Note that this is actually an 15 | Internationalized Resource Identifier (IRI). 16 | 17 | \b 18 | Examples: 19 | ndeftool uri 'http://nfcpy.org' print 20 | 21 | """ 22 | dmsg(__name__ + ' ' + str(kwargs)) 23 | 24 | if message is None: 25 | message = [] 26 | 27 | record = ndef.UriRecord(kwargs['resource']) 28 | 29 | message.append(record) 30 | return message 31 | -------------------------------------------------------------------------------- /src/ndeftool/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfcpy/ndeftool/3a526e42f57d107b99b81289fe854734d3957267/src/ndeftool/commands/__init__.py -------------------------------------------------------------------------------- /tests/test_identifier.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | from ndeftool.cli import main 7 | 8 | 9 | @pytest.fixture 10 | def runner(): 11 | import click.testing 12 | return click.testing.CliRunner() 13 | 14 | 15 | @pytest.fixture 16 | def isolated_runner(runner): 17 | with runner.isolated_filesystem(): 18 | yield runner 19 | 20 | 21 | def test_help_option_prints_usage(runner): 22 | result = runner.invoke(main, ['identifier', '--help']) 23 | assert result.exit_code == 0 24 | assert result.output.startswith('Usage: main identifier [OPTIONS] NAME') 25 | 26 | 27 | def test_abbreviated_command_name(runner): 28 | result = runner.invoke(main, ['id', '--help']) 29 | assert result.exit_code == 0 30 | assert result.output.startswith('Usage: main id [OPTIONS] NAME') 31 | 32 | 33 | def test_debug_option_prints_kwargs(runner): 34 | params = '--debug identifier name'.split() 35 | result = runner.invoke(main, params) 36 | assert result.exit_code == 0 37 | assert result.output.startswith("ndeftool.commands.IDentifier {") 38 | 39 | 40 | def test_identifier_as_first_command(runner): 41 | params = 'identifier name'.split() 42 | result = runner.invoke(main, params) 43 | assert result.exit_code == 0 44 | assert result.stdout_bytes == b'\xdd\x00\x00\x04name' 45 | 46 | 47 | def test_identifier_as_second_command(runner): 48 | params = 'typename text/plain identifier name'.split() 49 | result = runner.invoke(main, params) 50 | assert result.exit_code == 0 51 | assert result.stdout_bytes == b'\xda\n\x00\x04text/plainname' 52 | 53 | 54 | def test_very_long_name_raises_error(runner): 55 | result = runner.invoke(main, ['id', 256*'0']) 56 | assert result.exit_code == 1 57 | assert 'Error:' in result.output 58 | -------------------------------------------------------------------------------- /tests/test_load.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import os 6 | import pytest 7 | from ndeftool.cli import main 8 | 9 | 10 | @pytest.fixture 11 | def runner(): 12 | import click.testing 13 | return click.testing.CliRunner() 14 | 15 | 16 | @pytest.fixture 17 | def isolated_runner(runner): 18 | with runner.isolated_filesystem(): 19 | yield runner 20 | 21 | 22 | def test_help_option_prints_usage(runner): 23 | result = runner.invoke(main, ['load', '--help']) 24 | assert result.exit_code == 0 25 | assert result.output.startswith('Usage: main load [OPTIONS] PATH') 26 | 27 | 28 | def test_abbreviated_command_name(runner): 29 | result = runner.invoke(main, ['l', '--help']) 30 | assert result.exit_code == 0 31 | assert result.output.startswith('Usage: main l [OPTIONS] PATH') 32 | 33 | 34 | def test_debug_option_prints_kwargs(runner): 35 | params = '--debug load -'.split() 36 | result = runner.invoke(main, params, input='') 37 | assert result.exit_code == 0 38 | assert result.output.startswith("ndeftool.commands.Load {") 39 | 40 | 41 | def test_pack_from_standard_input(runner): 42 | result = runner.invoke(main, ['load', '--pack', '-'], input=b"Hello World") 43 | assert result.exit_code == 0 44 | assert result.stdout_bytes == b'\xda\n\x0b\x07text/plainHello World' 45 | 46 | 47 | def test_pack_from_full_file_path(isolated_runner): 48 | open('hello.txt', 'w').write('Hello World') 49 | result = isolated_runner.invoke(main, ['load', '--pack', 'hello.txt']) 50 | assert result.exit_code == 0 51 | assert result.stdout_bytes == b'\xda\n\x0b\ttext/plainhello.txtHello World' 52 | 53 | 54 | def test_pack_from_glob_file_path(isolated_runner): 55 | open('hello.txt', 'w').write('Hello World') 56 | result = isolated_runner.invoke(main, ['load', '--pack', '*.txt']) 57 | assert result.exit_code == 0 58 | assert result.stdout_bytes == b'\xda\n\x0b\ttext/plainhello.txtHello World' 59 | 60 | 61 | def test_pack_glob_multiple_files(isolated_runner): 62 | open('hello1.txt', 'w').write('Hello World') 63 | open('hello2.txt', 'w').write('World Hello') 64 | result = isolated_runner.invoke(main, ['load', '--pack', 'hello?.txt']) 65 | assert result.exit_code == 0 66 | assert result.stdout_bytes == \ 67 | b'\x9a\n\x0b\ntext/plainhello1.txtHello World' \ 68 | b'Z\n\x0b\ntext/plainhello2.txtWorld Hello' 69 | 70 | 71 | def test_pack_pipe_multiple_files(isolated_runner): 72 | open('hello1.txt', 'w').write('Hello World') 73 | open('hello2.txt', 'w').write('World Hello') 74 | command = 'load --pack hello1.txt load --pack hello2.txt' 75 | result = isolated_runner.invoke(main, command.split()) 76 | assert result.exit_code == 0 77 | assert result.stdout_bytes == \ 78 | b'\x9a\n\x0b\ntext/plainhello1.txtHello World' \ 79 | b'Z\n\x0b\ntext/plainhello2.txtWorld Hello' 80 | 81 | 82 | def test_load_from_standard_input(runner): 83 | octets = b'\xda\n\x0b\x07text/plainHello World' 84 | params = '--silent load -' 85 | result = runner.invoke(main, params.split(), input=octets) 86 | assert result.exit_code == 0 87 | assert result.stdout_bytes == octets 88 | 89 | 90 | def test_load_from_full_file_path(isolated_runner): 91 | octets = b'\xda\n\x0b\x09text/plainhello.txtHello World' 92 | open('hello.txt.ndef', 'wb').write(octets) 93 | params = '--silent load hello.txt.ndef' 94 | result = isolated_runner.invoke(main, params.split()) 95 | assert result.exit_code == 0 96 | assert result.stdout_bytes == octets 97 | 98 | 99 | def test_load_from_glob_file_path(isolated_runner): 100 | octets = b'\xda\n\x0b\ttext/plainhello.txtHello World' 101 | open('hello.txt.ndef', 'wb').write(octets) 102 | params = '--silent load *.ndef' 103 | result = isolated_runner.invoke(main, params.split()) 104 | assert result.exit_code == 0 105 | assert result.stdout_bytes == octets 106 | 107 | 108 | def test_pack_load_multiple_files(isolated_runner): 109 | octets1 = b'\xda\n\x0b\ntext/plainhello1.txtHello World' 110 | octets2 = b'\xda\n\x0b\ntext/plainhello2.txtWorld Hello' 111 | open('hello1.txt.ndef', 'wb').write(octets1) 112 | open('hello2.txt.ndef', 'wb').write(octets2) 113 | params = '--silent load hello?.txt.ndef' 114 | result = isolated_runner.invoke(main, params.split()) 115 | assert result.exit_code == 0 116 | assert result.stdout_bytes == b'\x9a' + octets1[1:] + b'\x5a' + octets2[1:] 117 | 118 | 119 | def test_load_pipe_multiple_files(isolated_runner): 120 | octets1 = b'\xda\n\x0b\ntext/plainhello1.txtHello World' 121 | octets2 = b'\xda\n\x0b\ntext/plainhello2.txtWorld Hello' 122 | open('hello1.txt.ndef', 'wb').write(octets1) 123 | open('hello2.txt.ndef', 'wb').write(octets2) 124 | params = '--silent load hello1.txt.ndef load hello2.txt.ndef' 125 | result = isolated_runner.invoke(main, params.split()) 126 | assert result.exit_code == 0 127 | assert result.stdout_bytes == b'\x9a' + octets1[1:] + b'\x5a' + octets2[1:] 128 | 129 | 130 | def test_load_strict_then_relax(isolated_runner): 131 | octets = b'\x1a\n\x0b\ttext/plainhello.txtHello World' 132 | open('hello.txt.ndef', 'wb').write(octets) 133 | params = '--silent load hello.txt.ndef' 134 | result = isolated_runner.invoke(main, params.split()) 135 | assert result.exit_code == 1 136 | assert result.output.startswith( 137 | 'Error: hello.txt.ndef does not contain a valid NDEF message.') 138 | params = '--relax ' + params 139 | result = isolated_runner.invoke(main, params.split()) 140 | assert result.exit_code == 0 141 | assert result.stdout_bytes == b'\xda' + octets[1:] 142 | 143 | 144 | def test_load_relax_then_ignore(isolated_runner): 145 | octets1 = b'\xda\n\xff\ntext/plainhello1.txtHello World' 146 | octets2 = b'\x1a\n\x0b\ntext/plainhello2.txtWorld Hello' 147 | open('hello1.txt.ndef', 'wb').write(octets1) 148 | open('hello2.txt.ndef', 'wb').write(octets2) 149 | params = '--relax --silent load hello1.txt.ndef load hello2.txt.ndef' 150 | result = isolated_runner.invoke(main, params.split()) 151 | assert result.exit_code == 1 152 | assert result.output.startswith( 153 | 'Error: hello1.txt.ndef does not contain a valid NDEF message.') 154 | params = '--ignore --silent load hello1.txt.ndef load hello2.txt.ndef' 155 | result = isolated_runner.invoke(main, params.split()) 156 | assert result.exit_code == 0 157 | assert result.stdout_bytes == b'\xda' + octets2[1:] 158 | 159 | 160 | def test_no_files_selected_by_path(isolated_runner): 161 | params = 'load hello.txt.ndef load *.txt.ndef' 162 | result = isolated_runner.invoke(main, params.split()) 163 | assert result.exit_code == 0 164 | assert result.output.splitlines() == [ 165 | "No files selected by path 'hello.txt.ndef'.", 166 | "No files selected by path '*.txt.ndef'.", 167 | ] 168 | 169 | 170 | def test_permission_denied_error(isolated_runner): 171 | open('hello.txt', 'w').write("Hello World") 172 | os.chmod('hello.txt', 0) 173 | params = 'load --pack *.txt' 174 | result = isolated_runner.invoke(main, params.split()) 175 | assert result.exit_code == 0 176 | assert result.output == "[Errno 13] Permission denied: 'hello.txt'\n" 177 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | from ndeftool.cli import main 7 | 8 | 9 | @pytest.fixture 10 | def runner(): 11 | import click.testing 12 | return click.testing.CliRunner() 13 | 14 | 15 | def test_no_command_prints_usage(runner): 16 | result = runner.invoke(main) 17 | assert result.exit_code == 0 18 | assert result.output.startswith( 19 | 'Usage: main [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...') 20 | 21 | 22 | def test_help_option_prints_usage(runner): 23 | result = runner.invoke(main, ['--help', 'unknown-command']) 24 | assert result.exit_code == 0 25 | assert result.output.startswith( 26 | 'Usage: main [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...') 27 | 28 | 29 | def test_unknown_command_prints_error(runner): 30 | result = runner.invoke(main, ['unknown-command']) 31 | assert result.exit_code == 2 32 | assert result.output.startswith( 33 | 'Usage: main [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...') 34 | assert "Error:" in result.output 35 | -------------------------------------------------------------------------------- /tests/test_payload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | from ndeftool.cli import main 7 | 8 | 9 | @pytest.fixture 10 | def runner(): 11 | import click.testing 12 | return click.testing.CliRunner() 13 | 14 | 15 | @pytest.fixture 16 | def isolated_runner(runner): 17 | with runner.isolated_filesystem(): 18 | yield runner 19 | 20 | 21 | def test_help_option_prints_usage(runner): 22 | result = runner.invoke(main, ['payload', '--help']) 23 | assert result.exit_code == 0 24 | assert result.output.startswith('Usage: main payload [OPTIONS] DATA') 25 | 26 | 27 | def test_abbreviated_command_name(runner): 28 | result = runner.invoke(main, ['pl', '--help']) 29 | assert result.exit_code == 0 30 | assert result.output.startswith('Usage: main pl [OPTIONS] DATA') 31 | 32 | 33 | def test_debug_option_prints_kwargs(isolated_runner): 34 | params = '--debug payload DATA'.split() 35 | result = isolated_runner.invoke(main, params, input='') 36 | assert result.exit_code == 0 37 | assert result.output.startswith("ndeftool.commands.PayLoad {") 38 | 39 | 40 | def test_payload_as_first_command(runner): 41 | params = 'payload helloworld'.split() 42 | result = runner.invoke(main, params) 43 | assert result.exit_code == 0 44 | assert result.stdout_bytes == b'\xd5\x00\nhelloworld' 45 | 46 | 47 | def test_payload_as_second_command(runner): 48 | params = 'typename text/plain payload helloworld'.split() 49 | result = runner.invoke(main, params) 50 | assert result.exit_code == 0 51 | assert result.stdout_bytes == b'\xd2\n\ntext/plainhelloworld' 52 | 53 | 54 | def test_payload_makes_decode_error(runner): 55 | params = '--silent load - payload helloworld'.split() 56 | result = runner.invoke(main, params, input=b'\xd1\x01\x03T\x02en') 57 | assert result.exit_code == 1 58 | assert 'Error:' in result.output 59 | 60 | 61 | def test_payload_error_not_checked(runner): 62 | params = '--silent load - payload --no-check helloworld'.split() 63 | result = runner.invoke(main, params, input=b'\xd1\x01\x03T\x02en') 64 | assert result.exit_code == 0 65 | assert result.stdout_bytes == b'\xd1\x01\nThelloworld' 66 | -------------------------------------------------------------------------------- /tests/test_print.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import sys 6 | import pytest 7 | from ndeftool.cli import main 8 | 9 | 10 | @pytest.fixture 11 | def runner(): 12 | import click.testing 13 | return click.testing.CliRunner() 14 | 15 | 16 | @pytest.fixture 17 | def isolated_runner(runner): 18 | with runner.isolated_filesystem(): 19 | yield runner 20 | 21 | 22 | def test_help_option_prints_usage(runner): 23 | result = runner.invoke(main, ['print', '--help']) 24 | assert result.exit_code == 0 25 | assert result.output.startswith('Usage: main print [OPTIONS]') 26 | 27 | 28 | def test_abbreviated_command_name(runner): 29 | result = runner.invoke(main, ['p', '--help']) 30 | assert result.exit_code == 0 31 | assert result.output.startswith('Usage: main p [OPTIONS]') 32 | 33 | 34 | def test_debug_option_prints_kwargs(isolated_runner): 35 | params = '--debug print'.split() 36 | result = isolated_runner.invoke(main, params, input='') 37 | assert result.exit_code == 0 38 | assert result.output.startswith("ndeftool.commands.Print {") 39 | 40 | 41 | def test_read_from_standard_input(runner): 42 | octets = b'\xda\n\x0b\x02text/plainR1Hello World' 43 | params = 'print'.split() 44 | result = runner.invoke(main, params, input=octets) 45 | assert result.exit_code == 0 46 | assert "NDEF Record TYPE 'text/plain' ID 'R1' PAYLOAD 11" in result.output 47 | 48 | 49 | def test_read_from_stdin_with_errors(runner): 50 | octets = b'\x1a\n\x0b\x02text/plainR1Hello World' 51 | params = 'print'.split() 52 | result = runner.invoke(main, params, input=octets) 53 | assert result.exit_code == 1 54 | assert "Error:" in result.output 55 | params = '--relax print'.split() 56 | result = runner.invoke(main, params, input=octets) 57 | assert result.exit_code == 0 58 | assert "NDEF Record TYPE 'text/plain' ID 'R1' PAYLOAD 11" in result.output 59 | 60 | 61 | def test_read_from_message_pipe(runner): 62 | octets = b'\xda\n\x0b\x02text/plainR1Hello World' 63 | params = '--silent print --keep print'.split() 64 | result = runner.invoke(main, params, input=octets) 65 | assert result.exit_code == 0 66 | assert len(result.output.splitlines()) == 2 67 | assert result.output.splitlines()[0].startswith("NDEF Record") 68 | assert result.output.splitlines()[1].startswith("NDEF Record") 69 | 70 | 71 | def test_long_print_generic_record(runner): 72 | octets = b'\xda\x09\x0a\x02some/mimeR1HelloWorld' 73 | params = '--silent print --long'.split() 74 | result = runner.invoke(main, params, input=octets) 75 | assert result.exit_code == 0 76 | output = result.output.splitlines() 77 | assert len(output) == 4 78 | assert output[0] == "Record [record #1]" 79 | assert output[1].split() == ["type", "some/mime"] 80 | assert output[2].split() == ["name", "R1"] 81 | if sys.version_info.major > 2: 82 | assert output[3].split() == ["data", "b'HelloWorld'"] 83 | else: 84 | assert output[3].split() == ["data", "HelloWorld"] 85 | 86 | 87 | def test_long_print_text_record(runner): 88 | octets = b'\xd1\x01\x06T\x02enone' 89 | params = '--silent print --long'.split() 90 | result = runner.invoke(main, params, input=octets) 91 | assert result.exit_code == 0 92 | output = result.output.splitlines() 93 | assert len(output) == 4 94 | assert output[0] == "NFC Forum Text Record [record #1]" 95 | assert output[1].split() == ["content", "one"] 96 | assert output[2].split() == ["language", "en"] 97 | assert output[3].split() == ["encoding", "UTF-8"] 98 | 99 | 100 | def test_long_print_uri_record(runner): 101 | octets = b'\xd1\x01\nU\x03nfcpy.org' 102 | params = '--silent print --long'.split() 103 | result = runner.invoke(main, params, input=octets) 104 | assert result.exit_code == 0 105 | output = result.output.splitlines() 106 | assert len(output) == 2 107 | assert output[0] == "NFC Forum URI Record [record #1]" 108 | assert output[1].split() == ["resource", "http://nfcpy.org"] 109 | 110 | 111 | def test_long_print_smartposter_1(runner): 112 | octets = b'\xd1\x02\x0eSp\xd1\x01\nU\x03nfcpy.org' 113 | params = '--silent print --long'.split() 114 | result = runner.invoke(main, params, input=octets) 115 | assert result.exit_code == 0 116 | output = result.output.splitlines() 117 | assert len(output) == 2 118 | assert output[0] == "NFC Forum Smart Poster Record [record #1]" 119 | assert output[1].split() == ["resource", "http://nfcpy.org"] 120 | 121 | 122 | def test_long_print_smartposter_2(runner): 123 | octets = b'\xd1\x02\x1aSp\x91\x01\nU\x03nfcpy.orgQ\x01\x08T\x02enTitle' 124 | params = '--silent print --long'.split() 125 | result = runner.invoke(main, params, input=octets) 126 | assert result.exit_code == 0 127 | output = result.output.splitlines() 128 | assert len(output) == 3 129 | assert output[0] == "NFC Forum Smart Poster Record [record #1]" 130 | assert output[1].split() == ["resource", "http://nfcpy.org"] 131 | assert output[2].split() == ["title_en", "Title"] 132 | 133 | 134 | def test_long_print_smartposter_3(runner): 135 | octets = b'\xd1\x02\x15Sp\x91\x01\nU\x03nfcpy.orgQ\x03\x01act\x00' 136 | params = '--silent print --long'.split() 137 | result = runner.invoke(main, params, input=octets) 138 | assert result.exit_code == 0 139 | output = result.output.splitlines() 140 | print(result.output) 141 | assert len(output) == 3 142 | assert output[0] == "NFC Forum Smart Poster Record [record #1]" 143 | assert output[1].split() == ["resource", "http://nfcpy.org"] 144 | assert output[2].split() == ["action", "exec"] 145 | 146 | 147 | def test_long_print_smartposter_4(runner): 148 | octets = ( 149 | b'\xd1\x02_Sp\x91\x01\nU\x03nfcpy.orgR\tEimage/png\x89PNG\r\n\x1a\n' 150 | b'\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00' 151 | b'\x00\x90wS\xde\x00\x00\x00\x0cIDAT\x08\xd7c\xf8\xff\xff?\x00\x05' 152 | b'\xfe\x02\xfe\xdc\xccY\xe7\x00\x00\x00\x00IEND\xaeB`\x82' 153 | ) 154 | params = '--silent print --long'.split() 155 | result = runner.invoke(main, params, input=octets) 156 | assert result.exit_code == 0 157 | output = result.output.splitlines() 158 | print(result.output) 159 | assert len(output) == 3 160 | assert output[0] == "NFC Forum Smart Poster Record [record #1]" 161 | assert output[1].split() == ["resource", "http://nfcpy.org"] 162 | assert output[2].split() == ["image/png", "69", "byte"] 163 | -------------------------------------------------------------------------------- /tests/test_save.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import os 6 | import pytest 7 | from ndeftool.cli import main 8 | 9 | 10 | @pytest.fixture 11 | def runner(): 12 | import click.testing 13 | return click.testing.CliRunner() 14 | 15 | 16 | @pytest.fixture 17 | def isolated_runner(runner): 18 | with runner.isolated_filesystem(): 19 | yield runner 20 | 21 | 22 | plain_text_records = [ 23 | b'\x9a\n\x0b\x02text/plainR1Hello World', 24 | b'\x1a\n\x0b\x02text/plainR2Hello World', 25 | b'\x1a\n\x0b\x02text/plainR3Hello World', 26 | b'\x1a\n\x0b\x02text/plainR4Hello World', 27 | b'Z\n\x0b\x02text/plainR5Hello World'] 28 | 29 | 30 | def test_help_option_prints_usage(runner): 31 | result = runner.invoke(main, ['save', '--help']) 32 | assert result.exit_code == 0 33 | assert result.output.startswith('Usage: main save [OPTIONS] PATH') 34 | 35 | 36 | def test_abbreviated_command_name(runner): 37 | result = runner.invoke(main, ['s', '--help']) 38 | assert result.exit_code == 0 39 | assert result.output.startswith('Usage: main s [OPTIONS] PATH') 40 | 41 | 42 | def test_debug_option_prints_kwargs(isolated_runner): 43 | params = '--debug save text.ndef'.split() 44 | result = isolated_runner.invoke(main, params, input='') 45 | assert result.exit_code == 0 46 | assert result.output.startswith("ndeftool.commands.Save {") 47 | 48 | 49 | def test_read_from_standard_input(isolated_runner): 50 | octets = b'\xda\n\x0b\x02text/plainR1Hello World' 51 | params = 'save hello.txt.ndef'.split() 52 | result = isolated_runner.invoke(main, params, input=octets) 53 | assert result.exit_code == 0 54 | assert open('hello.txt.ndef', 'rb').read() == octets 55 | 56 | 57 | def test_read_from_stdin_with_errors(isolated_runner): 58 | octets = b'\x9a\n\x0b\x02text/plainR1Hello World' 59 | params = 'save hello.txt.ndef'.split() 60 | result = isolated_runner.invoke(main, params, input=octets) 61 | assert result.exit_code == 1 62 | assert "Error:" in result.output 63 | params = '--relax save hello.txt.ndef'.split() 64 | result = isolated_runner.invoke(main, params, input=octets) 65 | assert result.exit_code == 0 66 | assert open('hello.txt.ndef', 'rb').read() == b'\xda' + octets[1:] 67 | 68 | 69 | def test_read_from_message_pipe(isolated_runner): 70 | octets = b'\xda\n\x0b\x02text/plainR1Hello World' 71 | params = 'save --keep hello1.ndef save hello2.ndef'.split() 72 | result = isolated_runner.invoke(main, params, input=octets) 73 | assert result.exit_code == 0 74 | assert open('hello1.ndef', 'rb').read() == octets 75 | assert open('hello2.ndef', 'rb').read() == octets 76 | 77 | 78 | def test_save_all_records_to_one_file(isolated_runner): 79 | octets = b''.join(plain_text_records) 80 | params = 'save hello.txt.ndef'.split() 81 | result = isolated_runner.invoke(main, params, input=octets) 82 | assert result.exit_code == 0 83 | assert open('hello.txt.ndef', 'rb').read() == octets 84 | 85 | 86 | def test_save_first_record_with_head(isolated_runner): 87 | octets = b''.join(plain_text_records) 88 | params = 'save --head 1 hello.txt.ndef'.split() 89 | result = isolated_runner.invoke(main, params, input=octets) 90 | assert result.exit_code == 0 91 | assert open('hello.txt.ndef', 'rb').read() == \ 92 | b'\xda' + plain_text_records[0][1:] 93 | 94 | 95 | def test_save_first_record_with_count(isolated_runner): 96 | octets = b''.join(plain_text_records) 97 | params = 'save --count 1 hello.txt.ndef'.split() 98 | result = isolated_runner.invoke(main, params, input=octets) 99 | assert result.exit_code == 0 100 | assert open('hello.txt.ndef', 'rb').read() == \ 101 | b'\xda' + plain_text_records[0][1:] 102 | 103 | 104 | def test_save_last_record_with_tail(isolated_runner): 105 | octets = b''.join(plain_text_records) 106 | params = 'save --tail 1 hello.txt.ndef'.split() 107 | result = isolated_runner.invoke(main, params, input=octets) 108 | assert result.exit_code == 0 109 | assert open('hello.txt.ndef', 'rb').read() == \ 110 | b'\xda' + plain_text_records[-1][1:] 111 | 112 | 113 | def test_save_last_record_with_skip(isolated_runner): 114 | toskip = len(plain_text_records) - 1 115 | octets = b''.join(plain_text_records) 116 | params = 'save --skip {} hello.txt.ndef'.format(toskip).split() 117 | result = isolated_runner.invoke(main, params, input=octets) 118 | assert result.exit_code == 0 119 | assert open('hello.txt.ndef', 'rb').read() == \ 120 | b'\xda' + plain_text_records[-1][1:] 121 | 122 | 123 | def test_save_skip_count_head_tail(isolated_runner): 124 | assert len(plain_text_records) == 5 125 | octets = b''.join(plain_text_records) 126 | params = 'save --skip 1 --count 3 --head 2 --tail 1 hello.txt.ndef'.split() 127 | result = isolated_runner.invoke(main, params, input=octets) 128 | assert result.exit_code == 0 129 | assert open('hello.txt.ndef', 'rb').read() == \ 130 | b'\xda' + plain_text_records[2][1:] 131 | 132 | 133 | def test_save_do_not_overwrite_file(isolated_runner): 134 | open('hello.ndef', 'wb') 135 | octets = b''.join(plain_text_records) 136 | params = 'save hello.ndef'.split() 137 | result = isolated_runner.invoke(main, params, input=octets) 138 | assert result.exit_code == 1 139 | assert "Error: path 'hello.ndef' exists." in result.output 140 | 141 | 142 | def test_save_force_overwrite_file(isolated_runner): 143 | open('hello.ndef', 'wb') 144 | octets = b''.join(plain_text_records) 145 | params = 'save --force hello.ndef'.split() 146 | result = isolated_runner.invoke(main, params, input=octets) 147 | assert result.exit_code == 0 148 | assert open('hello.ndef', 'rb').read() == octets 149 | 150 | 151 | def test_burst_records_to_files(isolated_runner): 152 | octets = b''.join(plain_text_records) 153 | params = 'save --burst hello'.split() 154 | result = isolated_runner.invoke(main, params, input=octets) 155 | assert result.exit_code == 0 156 | for index in range(len(plain_text_records)): 157 | assert open('hello/%03d.ndef' % index, 'rb').read() == \ 158 | b'\xda' + plain_text_records[index][1:] 159 | 160 | 161 | def test_unpack_records_to_files(isolated_runner): 162 | octets = b''.join(plain_text_records) 163 | params = 'save --unpack hello'.split() 164 | result = isolated_runner.invoke(main, params, input=octets) 165 | assert result.exit_code == 0 166 | for index in range(len(plain_text_records)): 167 | assert open('hello/R%d' % (index+1)).read() == "Hello World" 168 | 169 | 170 | def test_unpack_force_remove_dir(isolated_runner): 171 | os.mkdir('hello') 172 | for index in range(len(plain_text_records)): 173 | open('hello/R%d' % (index+1), 'w') 174 | octets = b''.join(plain_text_records) 175 | params = 'save --force --unpack hello'.split() 176 | result = isolated_runner.invoke(main, params, input=octets) 177 | assert result.exit_code == 0 178 | for index in range(len(plain_text_records)): 179 | assert open('hello/R%d' % (index+1)).read() == "Hello World" 180 | 181 | 182 | def test_unpack_record_without_name(isolated_runner): 183 | octets = b'\xda\n\x0b\x00text/plainHello World' 184 | params = 'save --unpack hello'.split() 185 | result = isolated_runner.invoke(main, params, input=octets) 186 | assert result.exit_code == 0 187 | assert "Skipping 1 record without name" in result.output 188 | -------------------------------------------------------------------------------- /tests/test_smartposter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | from ndeftool.cli import main 7 | 8 | 9 | @pytest.fixture 10 | def runner(): 11 | import click.testing 12 | return click.testing.CliRunner() 13 | 14 | 15 | @pytest.fixture 16 | def isolated_runner(runner): 17 | with runner.isolated_filesystem(): 18 | yield runner 19 | 20 | 21 | def test_help_option_prints_usage(runner): 22 | result = runner.invoke(main, ['smartposter', '--help']) 23 | assert result.exit_code == 0 24 | assert result.output.startswith( 25 | 'Usage: main smartposter [OPTIONS] RESOURCE') 26 | 27 | 28 | def test_abbreviated_command_name(runner): 29 | result = runner.invoke(main, ['smp', '--help']) 30 | assert result.exit_code == 0 31 | assert result.output.startswith('Usage: main smp [OPTIONS] RESOURCE') 32 | 33 | 34 | def test_debug_option_prints_kwargs(runner): 35 | params = '--debug smartposter http://nfcpy.org'.split() 36 | result = runner.invoke(main, params) 37 | assert result.exit_code == 0 38 | assert result.output.startswith("ndeftool.commands.SMartPoster {") 39 | 40 | 41 | def test_smartposter_with_no_options(runner): 42 | params = 'smartposter http://nfcpy.org'.split() 43 | result = runner.invoke(main, params) 44 | assert result.exit_code == 0 45 | assert result.stdout_bytes == b'\xd1\x02\x0eSp\xd1\x01\nU\x03nfcpy.org' 46 | 47 | 48 | def test_two_smartposter_commands(runner): 49 | params = 'smartposter http://nfcpy.org smartposter tel:12'.split() 50 | result = runner.invoke(main, params) 51 | assert result.exit_code == 0 52 | assert result.stdout_bytes == \ 53 | b'\x91\x02\x0eSp\xd1\x01\nU\x03nfcpy.orgQ\x02\x07Sp\xd1\x01\x03U\x0512' 54 | 55 | 56 | def test_smartposter_with_english_title(runner): 57 | params = 'smartposter -T Title http://nfcpy.org'.split() 58 | result = runner.invoke(main, params) 59 | assert result.exit_code == 0 60 | assert result.stdout_bytes == \ 61 | b'\xd1\x02\x1aSp\x91\x01\nU\x03nfcpy.orgQ\x01\x08T\x02enTitle' 62 | 63 | 64 | def test_smartposter_with_german_title(runner): 65 | params = 'smartposter -t de Titel http://nfcpy.org'.split() 66 | result = runner.invoke(main, params) 67 | assert result.exit_code == 0 68 | assert result.stdout_bytes == \ 69 | b'\xd1\x02\x1aSp\x91\x01\nU\x03nfcpy.orgQ\x01\x08T\x02deTitel' 70 | 71 | 72 | def test_smartposter_with_two_titles(runner): 73 | params = 'smartposter -T Title -t de Titel http://nfcpy.org'.split() 74 | result = runner.invoke(main, params) 75 | assert result.exit_code == 0 76 | assert result.stdout_bytes == \ 77 | b'\xd1\x02&Sp\x91\x01\nU\x03nfcpy.org' \ 78 | b'\x11\x01\x08T\x02deTitelQ\x01\x08T\x02enTitle' 79 | 80 | 81 | def test_smartposter_with_action_exec(runner): 82 | params = 'smartposter -a exec http://nfcpy.org'.split() 83 | result = runner.invoke(main, params) 84 | assert result.exit_code == 0 85 | assert result.stdout_bytes == \ 86 | b'\xd1\x02\x15Sp\x91\x01\nU\x03nfcpy.orgQ\x03\x01act\x00' 87 | 88 | 89 | def test_smartposter_with_png_icon(isolated_runner): 90 | icon_1x1_png_data = bytearray.fromhex( 91 | '89504e470d0a1a0a0000000d494844520000000100000001080200000090' 92 | '7753de0000000c4944415408d763f8ffff3f0005fe02fedccc59e7000000' 93 | '0049454e44ae426082') 94 | open('1x1.png', 'wb').write(icon_1x1_png_data) 95 | params = 'smartposter -i 1x1.png http://nfcpy.org'.split() 96 | result = isolated_runner.invoke(main, params) 97 | assert result.exit_code == 0 98 | assert result.stdout_bytes == ( 99 | b'\xd1\x02_Sp\x91\x01\nU\x03nfcpy.orgR\tEimage/png\x89PNG\r\n\x1a\n' 100 | b'\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00' 101 | b'\x00\x90wS\xde\x00\x00\x00\x0cIDAT\x08\xd7c\xf8\xff\xff?\x00\x05' 102 | b'\xfe\x02\xfe\xdc\xccY\xe7\x00\x00\x00\x00IEND\xaeB`\x82') 103 | 104 | 105 | def test_smartposter_with_invalid_icon(isolated_runner): 106 | open('1x1.png', 'w').write('this is not a png file') 107 | params = 'smartposter -i 1x1.png http://nfcpy.org'.split() 108 | result = isolated_runner.invoke(main, params) 109 | assert result.exit_code == 1 110 | assert "Error:" in result.output 111 | -------------------------------------------------------------------------------- /tests/test_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | from ndeftool.cli import main 7 | 8 | 9 | @pytest.fixture 10 | def runner(): 11 | import click.testing 12 | return click.testing.CliRunner() 13 | 14 | 15 | @pytest.fixture 16 | def isolated_runner(runner): 17 | with runner.isolated_filesystem(): 18 | yield runner 19 | 20 | 21 | def test_help_option_prints_usage(runner): 22 | result = runner.invoke(main, ['text', '--help']) 23 | assert result.exit_code == 0 24 | assert result.output.startswith('Usage: main text [OPTIONS] TEXT') 25 | 26 | 27 | def test_abbreviated_command_name(runner): 28 | result = runner.invoke(main, ['txt', '--help']) 29 | assert result.exit_code == 0 30 | assert result.output.startswith('Usage: main txt [OPTIONS] TEXT') 31 | 32 | 33 | def test_debug_option_prints_kwargs(runner): 34 | params = '--debug text sometext'.split() 35 | result = runner.invoke(main, params) 36 | assert result.exit_code == 0 37 | assert result.output.startswith("ndeftool.commands.TeXT {") 38 | 39 | 40 | def test_one_text_record_created(runner): 41 | params = 'text one'.split() 42 | result = runner.invoke(main, params) 43 | assert result.exit_code == 0 44 | assert result.stdout_bytes == b'\xd1\x01\x06T\x02enone' 45 | 46 | 47 | def test_two_text_records_created(runner): 48 | params = 'text one text two'.split() 49 | result = runner.invoke(main, params) 50 | assert result.exit_code == 0 51 | assert result.stdout_bytes == b'\x91\x01\x06T\x02enoneQ\x01\x06T\x02entwo' 52 | 53 | 54 | def test_default_text_language(runner): 55 | params = 'text English'.split() 56 | result = runner.invoke(main, params) 57 | assert result.exit_code == 0 58 | assert result.stdout_bytes == b'\xd1\x01\nT\x02enEnglish' 59 | 60 | 61 | def test_specific_text_language(runner): 62 | params = 'text --language de German'.split() 63 | result = runner.invoke(main, params) 64 | assert result.exit_code == 0 65 | assert result.stdout_bytes == b'\xd1\x01\tT\x02deGerman' 66 | -------------------------------------------------------------------------------- /tests/test_typename.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | from ndeftool.cli import main 7 | 8 | 9 | @pytest.fixture 10 | def runner(): 11 | import click.testing 12 | return click.testing.CliRunner() 13 | 14 | 15 | @pytest.fixture 16 | def isolated_runner(runner): 17 | with runner.isolated_filesystem(): 18 | yield runner 19 | 20 | 21 | def test_help_option_prints_usage(runner): 22 | result = runner.invoke(main, ['typename', '--help']) 23 | assert result.exit_code == 0 24 | assert result.output.startswith('Usage: main typename [OPTIONS] TYPE') 25 | 26 | 27 | def test_abbreviated_command_name(runner): 28 | result = runner.invoke(main, ['tn', '--help']) 29 | assert result.exit_code == 0 30 | assert result.output.startswith('Usage: main tn [OPTIONS] TYPE') 31 | 32 | 33 | def test_debug_option_prints_kwargs(isolated_runner): 34 | params = '--debug typename text/plain'.split() 35 | result = isolated_runner.invoke(main, params, input='') 36 | assert result.exit_code == 0 37 | assert result.output.startswith("ndeftool.commands.TypeName {") 38 | 39 | 40 | def test_typename_as_first_command(runner): 41 | params = 'typename text/plain'.split() 42 | result = runner.invoke(main, params) 43 | assert result.exit_code == 0 44 | assert result.stdout_bytes == b'\xd2\n\x00text/plain' 45 | 46 | 47 | def test_typename_as_second_command(runner): 48 | params = 'identifier name typename text/plain'.split() 49 | result = runner.invoke(main, params) 50 | assert result.exit_code == 0 51 | assert result.stdout_bytes == b'\xda\n\x00\x04text/plainname' 52 | 53 | 54 | def test_invalid_type_raises_error(runner): 55 | params = '--silent typename invalid-type'.split() 56 | result = runner.invoke(main, params) 57 | assert result.exit_code == 1 58 | assert 'Error:' in result.output 59 | 60 | 61 | def test_payload_type_error_raises_error(runner): 62 | params = '--silent payload helloworld typename urn:nfc:wkt:T'.split() 63 | result = runner.invoke(main, params) 64 | assert result.exit_code == 1 65 | assert 'Error:' in result.output 66 | 67 | 68 | def test_payload_type_error_not_checked(runner): 69 | params = 'payload helloworld typename --no-check urn:nfc:wkt:T'.split() 70 | result = runner.invoke(main, params) 71 | assert result.exit_code == 0 72 | assert result.stdout_bytes == b'\xd1\x01\nThelloworld' 73 | -------------------------------------------------------------------------------- /tests/test_uri.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, division 4 | 5 | import pytest 6 | from ndeftool.cli import main 7 | 8 | 9 | @pytest.fixture 10 | def runner(): 11 | import click.testing 12 | return click.testing.CliRunner() 13 | 14 | 15 | @pytest.fixture 16 | def isolated_runner(runner): 17 | with runner.isolated_filesystem(): 18 | yield runner 19 | 20 | 21 | def test_help_option_prints_usage(runner): 22 | result = runner.invoke(main, ['uri', '--help']) 23 | assert result.exit_code == 0 24 | assert result.output.startswith('Usage: main uri [OPTIONS] RESOURCE') 25 | 26 | 27 | def test_debug_option_prints_kwargs(runner): 28 | params = '--debug uri http://nfcpy.org'.split() 29 | result = runner.invoke(main, params) 30 | assert result.exit_code == 0 31 | assert result.output.startswith("ndeftool.commands.URI {") 32 | 33 | 34 | def test_one_uri_record_created(runner): 35 | params = 'uri http://nfcpy.org'.split() 36 | result = runner.invoke(main, params) 37 | assert result.exit_code == 0 38 | assert result.stdout_bytes == b'\xd1\x01\nU\x03nfcpy.org' 39 | 40 | 41 | def test_two_uri_records_created(runner): 42 | params = 'uri http://nfcpy.org uri tel:123'.split() 43 | result = runner.invoke(main, params) 44 | assert result.exit_code == 0 45 | assert result.stdout_bytes == b'\x91\x01\nU\x03nfcpy.orgQ\x01\x04U\x05123' 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = coverage-clean,py2,py3,flake8,manifest,docs,readme,coverage-report 3 | 4 | 5 | [testenv] 6 | deps = -rrequirements-dev.txt 7 | commands = coverage run --parallel -m pytest {posargs} 8 | passenv = http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY 9 | 10 | 11 | [testenv:flake8] 12 | deps = flake8 13 | commands = flake8 src tests setup.py 14 | 15 | 16 | [testenv:docs] 17 | setenv = 18 | PYTHONHASHSEED = 313 19 | deps = 20 | -r docs/requirements.txt 21 | commands = 22 | sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html 23 | sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html 24 | python -m doctest README.rst 25 | 26 | 27 | [testenv:manifest] 28 | deps = check-manifest 29 | skip_install = true 30 | commands = check-manifest 31 | 32 | 33 | [testenv:readme] 34 | deps = readme_renderer 35 | skip_install = true 36 | commands = python setup.py check -r -s 37 | 38 | 39 | [testenv:coverage-clean] 40 | deps = coverage 41 | skip_install = true 42 | commands = coverage erase 43 | 44 | 45 | [testenv:coverage-report] 46 | deps = coverage 47 | skip_install = true 48 | commands = 49 | coverage combine 50 | coverage report 51 | 52 | --------------------------------------------------------------------------------