├── .hgeol ├── .hgignore ├── .hgtags ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── doc ├── Makefile ├── make.bat ├── pyplots │ └── cobsr_overhead.py └── source │ ├── cobs.cobs.rst │ ├── cobs.cobsr.rst │ ├── cobsr-intro.rst │ ├── conf.py │ ├── index.rst │ └── intro.rst ├── pyproject.toml ├── setup.py ├── src ├── cobs │ ├── __init__.py │ ├── _version │ │ └── __init__.py │ ├── cobs │ │ ├── __init__.py │ │ ├── _cobs_py.py │ │ └── test.py │ └── cobsr │ │ ├── __init__.py │ │ ├── _cobsr_py.py │ │ └── test.py └── ext │ ├── _cobs_ext.c │ └── _cobsr_ext.c └── test ├── plot_cobsr_overhead.py ├── test_cobs.py └── test_cobsr.py /.hgeol: -------------------------------------------------------------------------------- 1 | [patterns] 2 | **.c = native 3 | **.h = native 4 | **.py = native 5 | **.txt = native 6 | **.rst = native 7 | 8 | **.bat = CRLF 9 | 10 | Makefile = LF 11 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | 2 | ^\..*$ 3 | ^dist/.* 4 | ^build/.* 5 | ^MANIFEST$ 6 | \.pyc 7 | ^doc/.*\.log$ 8 | ^doc/build/.* 9 | 10 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 30649feeb16e1f56baef688270c21a9b878fc5db version_0.5 2 | a2ba558e5bf99d687b38f214facf20d867f4aef1 version_0.5.1 3 | 3dc8fdda0725cf2c152ebb322b1b4d7f1183ee88 version_0.6 4 | 4801a6ed6a0101611626ffc9fa372ea8e670f920 version_0.6.1 5 | cd8ee3f9f303b1bf6ddad5b7a46e89419bd17538 version_0.7.0 6 | 87ad976263e05db6a7a9073e0d2bd46b83489e3e version_0.8.0 7 | 1580cc6ab2e6f01906f66bedbd3fda0d22722412 version_1.0.0 8 | bcf0df29d169b6f7100ea46a5dc6b3319e309edc version_1.1.1 9 | a608fb0307efca24813ce08a91b8a85a6673b6f8 version_1.1.2 10 | 70c93bfa20332a97ca1cfbff0c45546dafaf0fdb version_1.1.3 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------- 2 | Copyright (c) 2010 Craig McQueen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ---------------------------------------------------------------------------- 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | recursive-include src * 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | Consistent Overhead Byte Stuffing (COBS) 3 | ======================================== 4 | 5 | :Author: Craig McQueen 6 | :Contact: http://craig.mcqueen.id.au/ 7 | :Copyright: 2010 Craig McQueen 8 | 9 | 10 | Python functions for encoding and decoding COBS. 11 | 12 | ----- 13 | Intro 14 | ----- 15 | 16 | The ``cobs`` package is provided, which contains modules containing functions 17 | for encoding and decoding according to COBS methods. 18 | 19 | 20 | What Is COBS? 21 | ````````````` 22 | 23 | COBS is a method of encoding a packet of bytes into a form that contains no 24 | bytes with value zero (0x00). The input packet of bytes can contain bytes 25 | in the full range of 0x00 to 0xFF. The COBS encoded packet is guaranteed to 26 | generate packets with bytes only in the range 0x01 to 0xFF. Thus, in a 27 | communication protocol, packet boundaries can be reliably delimited with 0x00 28 | bytes. 29 | 30 | The COBS encoding does have to increase the packet size to achieve this 31 | encoding. However, compared to other byte-stuffing methods, the packet size 32 | increase is reasonable and predictable. COBS always adds 1 byte to the 33 | message length. Additionally, for longer packets of length *n*, it *may* add 34 | n/254 (rounded down) additional bytes to the encoded packet size. 35 | 36 | For example, compare to the PPP protocol, which uses 0x7E bytes to delimit 37 | PPP packets. The PPP protocol uses an "escape" style of byte stuffing, 38 | replacing all occurences of 0x7E bytes in the packet with 0x7D 0x5E. But that 39 | byte-stuffing method can potentially double the size of the packet in the 40 | worst case. COBS uses a different method for byte-stuffing, which has a much 41 | more reasonable worst-case overhead. 42 | 43 | For more details about COBS, see the references [#ieeeton]_ [#ppp]_. 44 | 45 | I have included a variant on COBS, `COBS/R`_, which slightly modifies COBS to 46 | often avoid the +1 byte overhead of COBS. So in many cases, especially for 47 | smaller packets, the size of a COBS/R encoded packet is the same size as the 48 | original packet. See below for more details about `COBS/R`_. 49 | 50 | 51 | References 52 | `````````` 53 | 54 | .. [#ieeeton] | `Consistent Overhead Byte Stuffing`__ 55 | | Stuart Cheshire and Mary Baker 56 | | IEEE/ACM Transations on Networking, Vol. 7, No. 2, April 1999 57 | 58 | .. __: 59 | .. _Consistent Overhead Byte Stuffing (for IEEE): 60 | http://www.stuartcheshire.org/papers/COBSforToN.pdf 61 | 62 | .. [#ppp] | `PPP Consistent Overhead Byte Stuffing (COBS)`_ 63 | | PPP Working Group Internet Draft 64 | | James Carlson, IronBridge Networks 65 | | Stuart Cheshire and Mary Baker, Stanford University 66 | | November 1997 67 | 68 | .. _PPP Consistent Overhead Byte Stuffing (COBS): 69 | http://tools.ietf.org/html/draft-ietf-pppext-cobs-00 70 | 71 | 72 | ---------------- 73 | Modules Provided 74 | ---------------- 75 | 76 | ================== ================== =============================================================== 77 | Module Short Name Long Name 78 | ================== ================== =============================================================== 79 | ``cobs.cobs`` COBS Consistent Overhead Byte Stuffing (basic method) [#ieeeton]_ 80 | ``cobs.cobsr`` `COBS/R`_ `Consistent Overhead Byte Stuffing--Reduced`_ 81 | ================== ================== =============================================================== 82 | 83 | "`Consistent Overhead Byte Stuffing--Reduced`_" (`COBS/R`_) is my own invention, 84 | a modification of basic COBS encoding, and is described in more detail below. 85 | 86 | The following are not implemented: 87 | 88 | ================== ====================================================================== 89 | Short Name Long Name 90 | ================== ====================================================================== 91 | COBS/ZPE Consistent Overhead Byte Stuffing--Zero Pair Elimination [#ieeeton]_ 92 | COBS/ZRE Consistent Overhead Byte Stuffing--Zero Run Elimination [#ppp]_ 93 | ================== ====================================================================== 94 | 95 | A pure Python implementation and a C extension implementation are provided. If 96 | the C extension is not available for some reason, the pure Python version will 97 | be used. 98 | 99 | 100 | ----- 101 | Usage 102 | ----- 103 | 104 | The modules provide an ``encode`` and a ``decode`` function. 105 | 106 | The input should be a byte string, not a Unicode string. Basic usage:: 107 | 108 | >>> from cobs import cobs 109 | >>> encoded = cobs.encode(b'Hello world\x00This is a test') 110 | >>> encoded 111 | b'\x0cHello world\x0fThis is a test' 112 | >>> cobs.decode(encoded) 113 | b'Hello world\x00This is a test' 114 | 115 | `COBS/R`_ usage is almost identical:: 116 | 117 | >>> from cobs import cobsr 118 | >>> encoded = cobsr.encode(b'Hello world\x00This is a test') 119 | >>> encoded 120 | b'\x0cHello worldtThis is a tes' 121 | >>> cobsr.decode(encoded) 122 | b'Hello world\x00This is a test' 123 | 124 | Any type that implements the buffer protocol, providing a single block of 125 | bytes, is also acceptable as input:: 126 | 127 | >>> from cobs import cobs 128 | >>> encoded = cobs.encode(bytearray(b'Hello world\x00This is a test')) 129 | >>> encoded 130 | b'\x0cHello world\x0fThis is a test' 131 | >>> cobs.decode(encoded) 132 | b'Hello world\x00This is a test' 133 | 134 | Note that the ``encode`` function does not add any framing ``0x00`` byte at 135 | the end (or beginning) of the encoded data. Similarly, the ``decode`` function 136 | does not strip or split on any framing ``0x00`` bytes, but treats any ``0x00`` 137 | bytes as a data input error. The details of data framing is 138 | application-specific, so it is the user's application's responsibility to 139 | implement the framing and deframing that is suitable for the needs of the 140 | application. 141 | 142 | 143 | ------------------------- 144 | Supported Python Versions 145 | ------------------------- 146 | 147 | Python >= 3.10 are supported, and have both a C extension and a pure Python 148 | implementation. 149 | 150 | Python versions < 3.10 might work, but have not been tested. 151 | 152 | 153 | ------------ 154 | Installation 155 | ------------ 156 | 157 | The cobs package is installed using ``distutils``. If you have the tools 158 | installed to build a Python extension module, run the following command:: 159 | 160 | python setup.py install 161 | 162 | If you cannot build the C extension, you may install just the pure Python 163 | implementation, using the following command:: 164 | 165 | python setup.py build_py install --skip-build 166 | 167 | 168 | ------------ 169 | Unit Testing 170 | ------------ 171 | 172 | Basic unit testing is in the ``test`` sub-module, e.g. ``cobs.cobs.test``. To run it:: 173 | 174 | python -m cobs.cobs.test 175 | python -m cobs.cobsr.test 176 | 177 | 178 | ------------- 179 | Documentation 180 | ------------- 181 | 182 | Documentation is written with Sphinx. Source files are provided in the ``doc`` 183 | directory. It can be built using Sphinx 0.6.5. It uses the ``pngmath`` Sphinx 184 | extension, which requires Latex and ``dvipng`` to be installed. 185 | 186 | The documentation is available online at: http://packages.python.org/cobs/ 187 | 188 | 189 | ------- 190 | License 191 | ------- 192 | 193 | The code is released under the MIT license. See LICENSE.txt for details. 194 | 195 | 196 | .. _COBS/R: 197 | .. _Consistent Overhead Byte Stuffing--Reduced: 198 | 199 | --------------------------------------------------- 200 | Consistent Overhead Byte Stuffing--Reduced (COBS/R) 201 | --------------------------------------------------- 202 | 203 | A modification of COBS, which I'm calling "Consistent Overhead Byte 204 | Stuffing--Reduced" (COBS/R), is provided in the ``cobs.cobsr`` module. Its 205 | purpose is to save one byte from the encoded form in some cases. Plain COBS 206 | encoding always has a +1 byte encoding overhead. See the references for 207 | details [#ieeeton]_. COBS/R can often avoid the +1 byte, which can be a useful 208 | savings if it is mostly small messages that are being encoded. 209 | 210 | In plain COBS, the last length code byte in the message has some inherent 211 | redundancy: if it is greater than the number of remaining bytes, this is 212 | detected as an error. 213 | 214 | In COBS/R, instead we opportunistically replace the final length code byte with 215 | the final data byte, whenever the value of the final data byte is greater than 216 | or equal to what the final length value would normally be. This variation can be 217 | unambiguously decoded: the decoder notices that the length code is greater than 218 | the number of remaining bytes. 219 | 220 | Examples 221 | ```````` 222 | 223 | The byte values in the examples are in hex. 224 | 225 | First example: 226 | 227 | Input: 228 | 229 | ====== ====== ====== ====== ====== ====== 230 | 2F A2 00 92 73 02 231 | ====== ====== ====== ====== ====== ====== 232 | 233 | This example is encoded the same in COBS and COBS/R. Encoded (length code bytes 234 | are bold): 235 | 236 | ====== ====== ====== ====== ====== ====== ====== 237 | **03** 2F A2 **04** 92 73 02 238 | ====== ====== ====== ====== ====== ====== ====== 239 | 240 | Second example: 241 | 242 | The second example is almost the same, except the final data byte value is 243 | greater than what the length byte would be. 244 | 245 | Input: 246 | 247 | ====== ====== ====== ====== ====== ====== 248 | 2F A2 00 92 73 26 249 | ====== ====== ====== ====== ====== ====== 250 | 251 | Encoded in plain COBS (length code bytes are bold): 252 | 253 | ====== ====== ====== ====== ====== ====== ====== 254 | **03** 2F A2 **04** 92 73 26 255 | ====== ====== ====== ====== ====== ====== ====== 256 | 257 | Encoded in COBS/R: 258 | 259 | ====== ====== ====== ====== ====== ====== 260 | **03** 2F A2 **26** 92 73 261 | ====== ====== ====== ====== ====== ====== 262 | 263 | Because the last data byte (**26**) is greater than the usual length code 264 | (**04**), the last data byte can be inserted in place of the length code, and 265 | removed from the end of the sequence. This avoids the usual +1 byte overhead of 266 | the COBS encoding. 267 | 268 | The decoder detects this variation on the encoding simply by detecting that the 269 | length code is greater than the number of remaining bytes. That situation would 270 | be a decoding error in regular COBS, but in COBS/R it is used to save one byte 271 | in the encoded message. 272 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 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) source 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex 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 " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/COBS.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/COBS.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set BUILDDIR=build 7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 8 | if NOT "%PAPER%" == "" ( 9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 10 | ) 11 | 12 | if "%1" == "" goto help 13 | 14 | if "%1" == "help" ( 15 | :help 16 | echo.Please use `make ^` where ^ is one of 17 | echo. html to make standalone HTML files 18 | echo. dirhtml to make HTML files named index.html in directories 19 | echo. pickle to make pickle files 20 | echo. json to make JSON files 21 | echo. htmlhelp to make HTML files and a HTML help project 22 | echo. qthelp to make HTML files and a qthelp project 23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 24 | echo. changes to make an overview over all changed/added/deprecated items 25 | echo. linkcheck to check all external links for integrity 26 | echo. doctest to run all doctests embedded in the documentation if enabled 27 | goto end 28 | ) 29 | 30 | if "%1" == "clean" ( 31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 32 | del /q /s %BUILDDIR%\* 33 | goto end 34 | ) 35 | 36 | if "%1" == "html" ( 37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 38 | echo. 39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 40 | goto end 41 | ) 42 | 43 | if "%1" == "dirhtml" ( 44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 47 | goto end 48 | ) 49 | 50 | if "%1" == "pickle" ( 51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 52 | echo. 53 | echo.Build finished; now you can process the pickle files. 54 | goto end 55 | ) 56 | 57 | if "%1" == "json" ( 58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 59 | echo. 60 | echo.Build finished; now you can process the JSON files. 61 | goto end 62 | ) 63 | 64 | if "%1" == "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 | goto end 70 | ) 71 | 72 | if "%1" == "qthelp" ( 73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 74 | echo. 75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 76 | .qhcp project file in %BUILDDIR%/qthelp, like this: 77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\COBS.qhcp 78 | echo.To view the help file: 79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\COBS.ghc 80 | goto end 81 | ) 82 | 83 | if "%1" == "latex" ( 84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 85 | echo. 86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 87 | goto end 88 | ) 89 | 90 | if "%1" == "changes" ( 91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 92 | echo. 93 | echo.The overview file is in %BUILDDIR%/changes. 94 | goto end 95 | ) 96 | 97 | if "%1" == "linkcheck" ( 98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 99 | echo. 100 | echo.Link check complete; look for any errors in the above output ^ 101 | or in %BUILDDIR%/linkcheck/output.txt. 102 | goto end 103 | ) 104 | 105 | if "%1" == "doctest" ( 106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 107 | echo. 108 | echo.Testing of doctests in the sources finished, look at the ^ 109 | results in %BUILDDIR%/doctest/output.txt. 110 | goto end 111 | ) 112 | 113 | :end 114 | -------------------------------------------------------------------------------- /doc/pyplots/cobsr_overhead.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from matplotlib import pyplot as plt 4 | import numpy as np 5 | from cobs import cobs 6 | from cobs import cobsr 7 | 8 | # TODO: review value 9 | NUM_TESTS = 100000 10 | 11 | def cobsr_overhead_calc(num_bytes): 12 | return 257./256 - (255./256)**num_bytes 13 | 14 | def cobsr_overhead_measure(num_bytes): 15 | overhead = 0 16 | for _i in range(NUM_TESTS): 17 | output = cobsr.encode(np.random.bytes(num_bytes)) 18 | overhead += (len(output) - num_bytes) 19 | return overhead / float(NUM_TESTS) 20 | 21 | def cobs_overhead_measure(num_bytes): 22 | overhead = 0 23 | for _i in range(NUM_TESTS): 24 | output = cobs.encode(np.random.bytes(num_bytes)) 25 | overhead += (len(output) - num_bytes) 26 | return overhead / float(NUM_TESTS) 27 | 28 | fig = plt.figure() 29 | ax1 = fig.add_subplot(111) 30 | 31 | # x-range for plot 32 | num_bytes_list = np.arange(1, 30) 33 | 34 | # Calculate values and plot 35 | 36 | # Measured values for COBS 37 | if 0: 38 | cobs_measured_overhead = [ cobs_overhead_measure(num_bytes) for num_bytes in num_bytes_list ] 39 | ax1.plot(num_bytes_list, cobs_measured_overhead, 'g.') 40 | 41 | # Measured values for COBS/R 42 | cobsr_measured_overhead = [ cobsr_overhead_measure(num_bytes) for num_bytes in num_bytes_list ] 43 | ax1.plot(num_bytes_list, cobsr_measured_overhead, 'r.') 44 | 45 | # Calculated values for COBS/R 46 | cobsr_calc_overhead = [ cobsr_overhead_calc(num_bytes) for num_bytes in num_bytes_list ] 47 | ax1.plot(num_bytes_list, cobsr_calc_overhead, 'b.') 48 | 49 | ax1.set_xlabel('message length (bytes)') 50 | ax1.set_xlim(min(num_bytes_list), max(num_bytes_list)) 51 | 52 | # Make the y-axis label and tick labels match the line color. 53 | ax1.set_ylabel('encoding overhead (bytes)') 54 | if 0: 55 | ax1.set_ylabel('encoding overhead (bytes)', color='b') 56 | for tl in ax1.get_yticklabels(): 57 | tl.set_color('b') 58 | 59 | plt.show() 60 | -------------------------------------------------------------------------------- /doc/source/cobs.cobs.rst: -------------------------------------------------------------------------------- 1 | 2 | :mod:`cobs.cobs`—COBS Encoding and Decoding 3 | ============================================ 4 | 5 | .. module:: cobs.cobs 6 | :synopsis: Consistent Overhead Byte Stuffing (COBS) 7 | .. moduleauthor:: Craig McQueen 8 | .. sectionauthor:: Craig McQueen 9 | 10 | This module provides functions for encoding and decoding byte strings using 11 | the COBS encoding method. 12 | 13 | In Python 2.x, the input type to the functions must be a byte string. Unicode 14 | strings may *appear* to work at first, but only if they can be automatically 15 | encoded to bytes using the ``ascii`` encoding. So Unicode strings should not be 16 | used. 17 | 18 | In Python 3.x, byte strings are acceptable input. Types that implement the 19 | buffer protocol, providing a simple buffer of bytes, are also acceptable. Thus 20 | types such as ``bytearray`` and ``array('B',...)`` are accepted input. The 21 | output type is always a byte string. 22 | 23 | 24 | :func:`encode` -- COBS encode 25 | ----------------------------- 26 | 27 | The function encodes a byte string according to the COBS encoding method. 28 | 29 | .. function:: encode(data) 30 | 31 | :param data: Data to encode. 32 | :type data: byte string 33 | 34 | :return: COBS encoded data. 35 | :rtype: byte string 36 | 37 | The COBS encoded data is guaranteed not to contain zero ``b'\x00'`` bytes. 38 | 39 | The encoded data length will always be at least one byte longer than the 40 | input length. Additionally, it *may* increase by one extra byte for every 41 | 254 bytes of input data. 42 | 43 | 44 | :func:`decode` -- COBS decode 45 | ----------------------------- 46 | 47 | The function decodes a byte string according to the COBS method. 48 | 49 | .. function:: decode(data) 50 | 51 | :param data: COBS encoded data to decode. 52 | :type data: byte string 53 | 54 | :return: Decoded data. 55 | :rtype: byte string 56 | 57 | If a zero ``b'\x00'`` byte is found in the encoded input data, a 58 | ``cobs.cobs.DecodeError`` exception will be raised. 59 | 60 | If the final length code byte in the encoded input data is not followed by 61 | the expected number of data bytes, this is an invalid COBS encoded data 62 | input, and ``cobs.cobs.DecodeError`` is raised. 63 | 64 | 65 | ``__version__`` -- package version information 66 | ---------------------------------------------- 67 | 68 | .. data:: __version__ 69 | 70 | The variable contains the package version number as a string. 71 | 72 | 73 | .. _cobs-examples: 74 | 75 | Examples 76 | ^^^^^^^^ 77 | 78 | Basic usage example, in Python 2.x using byte string inputs:: 79 | 80 | >>> from cobs import cobs 81 | 82 | >>> encoded = cobs.encode(b'Hello world\x00This is a test') 83 | >>> encoded 84 | '\x0cHello world\x0fThis is a test' 85 | 86 | >>> cobs.decode(encoded) 87 | 'Hello world\x00This is a test' 88 | 89 | 90 | For Python 3.x, input cannot be Unicode strings. Byte strings are acceptable 91 | input. Also, any type that implements the buffer protocol, providing a single 92 | block of bytes, is also acceptable as input:: 93 | 94 | >>> from cobs import cobs 95 | >>> encoded = cobs.encode(bytearray(b'Hello world\x00This is a test')) 96 | >>> encoded 97 | b'\x0cHello world\x0fThis is a test' 98 | >>> cobs.decode(encoded) 99 | b'Hello world\x00This is a test' 100 | 101 | -------------------------------------------------------------------------------- /doc/source/cobs.cobsr.rst: -------------------------------------------------------------------------------- 1 | 2 | :mod:`cobs.cobsr`—COBS/R Encoding and Decoding 3 | ============================================== 4 | 5 | .. module:: cobs.cobsr 6 | :synopsis: Consistent Overhead Byte Stuffing—Reduced (COBS/R) 7 | .. moduleauthor:: Craig McQueen 8 | .. sectionauthor:: Craig McQueen 9 | 10 | This module provides functions for encoding and decoding byte strings using 11 | the :ref:`COBS/R ` encoding method. 12 | 13 | In Python 2.x, the input type to the functions must be a byte string. Unicode 14 | strings may *appear* to work at first, but only if they can be automatically 15 | encoded to bytes using the ``ascii`` encoding. So Unicode strings should not be 16 | used. 17 | 18 | In Python 3.x, byte strings are acceptable input. Types that implement the 19 | buffer protocol, providing a simple buffer of bytes, are also acceptable. Thus 20 | types such as ``bytearray`` and ``array('B',...)`` are accepted input. The 21 | output type is always a byte string. 22 | 23 | 24 | :func:`encode` -- COBS/R encode 25 | ------------------------------- 26 | 27 | The function encodes a byte string according to the COBS/R encoding method. 28 | 29 | .. function:: encode(data) 30 | 31 | :param data: Data to encode. 32 | :type data: byte string 33 | 34 | :return: COBS/R encoded data. 35 | :rtype: byte string 36 | 37 | The COBS/R encoded data is guaranteed not to contain zero ``b'\x00'`` 38 | bytes. 39 | 40 | The encoded data length *may* be one byte longer than the input length. 41 | Additionally, it *may* increase by one extra byte for every 254 bytes of 42 | input data. 43 | 44 | 45 | :func:`decode` -- COBS/R decode 46 | ------------------------------- 47 | 48 | The function decodes a byte string according to the COBS/R method. 49 | 50 | .. function:: decode(data) 51 | 52 | :param data: COBS/R encoded data to decode. 53 | :type data: byte string 54 | 55 | :return: Decoded data. 56 | :rtype: byte string 57 | 58 | If a zero ``b'\x00'`` byte is found in the input data, a 59 | ``cobs.cobsr.DecodeError`` exception will be raised. 60 | 61 | 62 | ``__version__`` -- package version information 63 | ---------------------------------------------- 64 | 65 | .. data:: __version__ 66 | 67 | The variable contains the package version number as a string. 68 | 69 | 70 | .. _cobsr-examples: 71 | 72 | Examples 73 | ^^^^^^^^ 74 | 75 | Basic usage example, in Python 2.x using byte string inputs:: 76 | 77 | 78 | >>> from cobs import cobsr 79 | 80 | >>> encoded = cobsr.encode(b'Hello world\x00This is a test') 81 | >>> encoded 82 | '\x0cHello worldtThis is a tes' 83 | 84 | >>> cobsr.decode(encoded) 85 | 'Hello world\x00This is a test' 86 | 87 | 88 | For Python 3.x, input cannot be Unicode strings. Byte strings are acceptable 89 | input. Also, any type that implements the buffer protocol, providing a single 90 | block of bytes, is also acceptable as input:: 91 | 92 | >>> from cobs import cobsr 93 | >>> encoded = cobsr.encode(bytearray(b'Hello world\x00This is a test')) 94 | >>> encoded 95 | b'\x0cHello worldtThis is a tes' 96 | >>> cobsr.decode(encoded) 97 | b'Hello world\x00This is a test' 98 | 99 | -------------------------------------------------------------------------------- /doc/source/cobsr-intro.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _COBS/R: 3 | .. _Consistent Overhead Byte Stuffing—Reduced: 4 | 5 | =================================================== 6 | Consistent Overhead Byte Stuffing—Reduced (COBS/R) 7 | =================================================== 8 | 9 | :Author: Craig McQueen 10 | :Contact: http://craig.mcqueen.id.au/ 11 | :Copyright: 2010 Craig McQueen 12 | 13 | This describes a modification of COBS, which I'm calling "Consistent Overhead 14 | Byte Stuffing—Reduced" (COBS/R). Its purpose is to save one byte from the 15 | encoded form in some cases. Plain COBS encoding always has a +1 byte encoding 16 | overhead [C1]_. This is possibly undesirable, particularly in a system that 17 | encodes mostly small messages, where the +1 byte overhead would impose a 18 | noticeable increase in the bandwidth requirements. 19 | 20 | "Base Adaptation Byte Stuffing" (BABS) is one proposal to avoid the +1 byte 21 | overhead imposed by COBS, however BABS is computationally expensive [C2]_. 22 | 23 | COBS/R is a small modification of COBS that can often avoid the +1 byte 24 | overhead of COBS, yet is computationally very simple. In terms of message 25 | encoding overhead in bytes, it is not expected to achieve performance as close 26 | to the theoretical optimum as BABS, yet it is an improvement over plain COBS 27 | encoding without any significant increase in computational complexity. 28 | 29 | COBS/R has the following features: 30 | 31 | * Worst-case encoding overhead is the same as COBS 32 | * Best-case encoding overhead is zero bytes, an improvement over COBS 33 | * Computation complexity is approximately the same as COBS, much simpler than BABS 34 | * Same theoretical encoding delay as COBS (up to 254 bytes of temporary buffering) 35 | * Same theoretical decoding delay as COBS (1 byte of temporary buffering) 36 | 37 | 38 | ---------------------------- 39 | COBS/R Encoding Modification 40 | ---------------------------- 41 | 42 | In plain COBS, the last length code byte in the message has some inherent 43 | redundancy: if it is greater than the number of remaining bytes, this is 44 | detected as an error. 45 | 46 | In COBS/R, instead we opportunistically replace the final length code byte with 47 | the final data byte, whenever the value of the final data byte is greater than 48 | or equal to what the final length value would normally be. This variation can 49 | be unambiguously decoded: the decoder notices that the length code is greater 50 | than the number of remaining bytes. 51 | 52 | 53 | Examples 54 | ^^^^^^^^ 55 | 56 | The byte values in the examples are in hex. 57 | 58 | First example: 59 | """""""""""""" 60 | 61 | Input: 62 | 63 | ====== ====== ====== ====== ====== ====== 64 | 2F A2 00 92 73 02 65 | ====== ====== ====== ====== ====== ====== 66 | 67 | This example is encoded the same in COBS and COBS/R. Encoded (length code bytes 68 | are bold): 69 | 70 | ====== ====== ====== ====== ====== ====== ====== 71 | **03** 2F A2 **04** 92 73 02 72 | ====== ====== ====== ====== ====== ====== ====== 73 | 74 | Second example: 75 | """"""""""""""" 76 | 77 | The second example is almost the same, except the final data byte value is 78 | greater than what the length byte would be. 79 | 80 | Input: 81 | 82 | ====== ====== ====== ====== ====== ====== 83 | 2F A2 00 92 73 26 84 | ====== ====== ====== ====== ====== ====== 85 | 86 | Encoded in plain COBS (length code bytes are bold): 87 | 88 | ====== ====== ====== ====== ====== ====== ====== 89 | **03** 2F A2 **04** 92 73 26 90 | ====== ====== ====== ====== ====== ====== ====== 91 | 92 | Encoded in COBS/R: 93 | 94 | ====== ====== ====== ====== ====== ====== 95 | **03** 2F A2 **26** 92 73 96 | ====== ====== ====== ====== ====== ====== 97 | 98 | Because the last data byte (**26**) is greater than the usual length code 99 | (**04**), the last data byte can be inserted in place of the length code, and 100 | removed from the end of the sequence. This avoids the usual +1 byte overhead of 101 | the COBS encoding. 102 | 103 | The decoder detects this variation on the encoding simply by detecting that the 104 | length code is greater than the number of remaining bytes. That situation would 105 | be a decoding error in regular COBS, but in COBS/R it is used to save one byte 106 | in the encoded message. 107 | 108 | 109 | ------------------------ 110 | COBS/R Encoding Overhead 111 | ------------------------ 112 | 113 | Given an input data packet of size *n*, COBS/R encoding may or may not add a +1 114 | byte overhead, depending on the contents of the input data. The probability of 115 | the +1 byte overhead can be calculated, and hence, the average overhead can be 116 | calculated. 117 | 118 | The calculations are more complicated for the case of message sizes greater 119 | than 254 bytes. COBS/R is of particular interest for smaller message sizes, so 120 | the calculations will focus on the simpler case of message sizes smaller than 121 | 255 bytes. 122 | 123 | 124 | General Case 125 | ^^^^^^^^^^^^ 126 | 127 | Example for *n*\ =4: 128 | 129 | ============== ============== ============== ============== ====================== ====================== 130 | :math:`x_0` :math:`x_1` :math:`x_2` :math:`x_3` Probability of Pattern Probability of +1 byte 131 | ============== ============== ============== ============== ====================== ====================== 132 | any any any :math:`=0` |fp0| :math:`1` 133 | any any :math:`=0` :math:`\ne 0` |fp1| :math:`P(x_3 \le 1 \vert x_3\ne 0)` 134 | any :math:`=0` :math:`\ne 0` :math:`\ne 0` |fp2| :math:`P(x_3 \le 2 \vert x_3\ne 0)` 135 | :math:`=0` :math:`\ne 0` :math:`\ne 0` :math:`\ne 0` |fp3| :math:`P(x_3 \le 3 \vert x_3\ne 0)` 136 | :math:`\ne 0` :math:`\ne 0` :math:`\ne 0` :math:`\ne 0` |fp4| :math:`P(x_3 \le 4 \vert x_3\ne 0)` 137 | ============== ============== ============== ============== ====================== ====================== 138 | 139 | .. |fp0| replace:: :math:`P(x_3=0)` 140 | .. |fp1| replace:: :math:`P(x_2=0) \times P(x_3\ne 0)` 141 | .. |fp2| replace:: :math:`P(x_1=0) \times P(x_2\ne 0) \times P(x_3\ne 0)` 142 | .. |fp3| replace:: :math:`P(x_0=0) \times P(x_1\ne 0) \times P(x_2\ne 0) \times P(x_3\ne 0)` 143 | .. |fp4| replace:: :math:`P(x_0\ne 0) \times P(x_1\ne 0) \times P(x_2\ne 0) \times P(x_3\ne 0)` 144 | 145 | Multiply the last two columns, and sum for all rows. For a message of length 146 | *n* where :math:`1 \le n \le 254`, the general equation for the 147 | probability of the +1 byte is: 148 | 149 | .. math:: P(x_{n-1} \le n \vert x_{n-1}\ne 0) \prod_{k=0}^{n-1} P(x_k\ne 0) + \sum_{i=0}^{n-2} \left[ P\bigl(x_{n-1} \le (n-1-i) \vert x_{n-1}\ne 0 \bigr) P(x_i=0) \prod_{k=i+1}^{n-1} P(x_k\ne 0) \right] + P(x_{n-1}=0) 150 | 151 | 152 | Even Byte Distribution Case 153 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 154 | 155 | We can simplify this for the simpler case of messages with byte value 156 | probabilities that are evenly distributed. In this case: 157 | 158 | .. math:: P(x_{n-1} \le n \vert x_{n-1}\ne 0) = \frac{n}{255} 159 | 160 | .. math:: P(x_i\ne 0) = \frac{255}{256} 161 | 162 | .. math:: P(x_i=0) = \frac{1}{256} 163 | 164 | Simplified example for *n*\ =4: 165 | 166 | ============== ============== ============== ============== ====================== ========================== 167 | :math:`x_0` :math:`x_1` :math:`x_2` :math:`x_3` Probability of Pattern Probability of +1 byte 168 | ============== ============== ============== ============== ====================== ========================== 169 | any any any :math:`=0` |f2p0| :math:`1` 170 | any any :math:`=0` :math:`\ne 0` |f2p1| :math:`\frac{1}{255}` 171 | any :math:`=0` :math:`\ne 0` :math:`\ne 0` |f2p2| :math:`\frac{2}{255}` 172 | :math:`=0` :math:`\ne 0` :math:`\ne 0` :math:`\ne 0` |f2p3| :math:`\frac{3}{255}` 173 | :math:`\ne 0` :math:`\ne 0` :math:`\ne 0` :math:`\ne 0` |f2p4| :math:`\frac{4}{255}` 174 | ============== ============== ============== ============== ====================== ========================== 175 | 176 | .. |f2p0| replace:: :math:`\frac{1}{256}` 177 | .. |f2p1| replace:: :math:`\frac{1}{256}\left(\frac{255}{256}\right)^1` 178 | .. |f2p2| replace:: :math:`\frac{1}{256}\left(\frac{255}{256}\right)^2` 179 | .. |f2p3| replace:: :math:`\frac{1}{256}\left(\frac{255}{256}\right)^3` 180 | .. |f2p4| replace:: :math:`\left(\frac{255}{256}\right)^4` 181 | 182 | The simplified equation for a message of length *n* where 183 | :math:`1 \le n \le 254` is: 184 | 185 | .. math:: \frac{n}{255} \left(\frac{255}{256}\right)^n + \frac{1}{255 \times 256} \sum_{i=1}^{n-1} \left[ i \left(\frac{255}{256}\right)^i \right] + \frac{1}{256} 186 | 187 | Which simplifies to: 188 | 189 | .. math:: \frac{257}{256}-\left(\frac{255}{256}\right)^n 190 | 191 | 192 | Table 1 of [C2]_ shows the overhead of BABS compared to COBS and PPP. 193 | We will duplicate this table up to N=128, comparing the figures for COBS/R 194 | (instead of PPP): 195 | 196 | ==== ================ ================ ================ ========================== 197 | N mean(OH) BABS mean(OH) COBS mean(OH) COBS/R max(OH) BABS, COBS, COBS/R 198 | ==== ================ ================ ================ ========================== 199 | 1 0.39062 100 0.78125 100 200 | 2 0.39062 50 0.58517 50 201 | 4 0.38948 25 0.48600 25 202 | 8 0.38665 12.5 0.43415 12.5 203 | 16 0.38078 6.25 0.40380 6.25 204 | 32 0.36927 3.125 0.38008 3.125 205 | 64 0.34756 1.5625 0.35232 1.5625 206 | 128 0.30906 0.78125 0.31091 0.78125 207 | ==== ================ ================ ================ ========================== 208 | 209 | .. math:: OVERHEAD: OH = \frac {M-N}{N} \times 100 \% 210 | 211 | 212 | Further Observations for General Case 213 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 214 | 215 | Going back to the general case, we can make several observations about what 216 | sort of byte distributions more favourably avoid the +1 byte in the COBS/R 217 | encoding. 218 | 219 | * For all bytes except the final one, a higher probability of a zero 220 | byte value is more favourable. 221 | * For the final byte of the message, a probability distribution that 222 | favours high byte values is more favourable. 223 | 224 | If the byte distribution of a communication protocol is known in advance, it 225 | may be possible and worthwhile to pre-process the data bytes before COBS/R 226 | encoding, to reduce the average size of the COBS/R encoded data. For example, 227 | possible byte manipulations may be: 228 | 229 | * For all bytes except the final one, if a particular byte value is 230 | statistically common, XOR every byte of the message (except the last 231 | byte) with that byte value. 232 | * For the final byte of the message, add an offset to the final byte 233 | value, or negate the final byte value, to shift the distribution to 234 | favour high byte values. 235 | 236 | Of course after decoding, the data would have to be post-processed to reverse 237 | the effects of any encoding pre-processing step. 238 | 239 | 240 | ---------- 241 | References 242 | ---------- 243 | 244 | .. [C1] | `Consistent Overhead Byte Stuffing `_ 245 | | Stuart Cheshire and Mary Baker 246 | | IEEE/ACM Transations on Networking, Vol. 7, No. 2, April 1999 247 | 248 | .. [C2] | `Bandwidth-efficient byte stuffing `_ 249 | | Jaime S. Cardoso 250 | | Universidade do Porto / INESC Porto 251 | | IEEE ICC 2007 252 | 253 | .. [C3] | `C Implementation of COBS and COBS/R `_ 254 | | Craig McQueen 255 | 256 | .. [C4] | :ref:`Python Implementation of COBS and COBS/R ` 257 | | Craig McQueen 258 | 259 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # COBS documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Apr 25 10:50:26 2010. 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 sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.append(os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # Add any Sphinx extension module names here, as strings. They can be extensions 24 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 25 | extensions = [ 26 | 'sphinx.ext.pngmath', 27 | # 'sphinx.ext.jsmath', 28 | # 'matplotlib.sphinxext.mathmpl', 29 | # 'matplotlib.sphinxext.only_directives', 30 | # 'matplotlib.sphinxext.plot_directive', 31 | ] 32 | jsmath_path = 'jsMath/easy/load.js' 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'COBS' 48 | copyright = u'2010, Craig McQueen' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = '1.1.0' 56 | # The full version, including alpha/beta/rc tags. 57 | release = '1.1.0' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of documents that shouldn't be included in the build. 70 | #unused_docs = [] 71 | 72 | # List of directories, relative to source directory, that shouldn't be searched 73 | # for source files. 74 | exclude_trees = [] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. Major themes that come with 100 | # Sphinx are currently 'default' and 'sphinxdoc'. 101 | html_theme = 'default' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | #html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 133 | # using the given strftime format. 134 | #html_last_updated_fmt = '%b %d, %Y' 135 | 136 | # If true, SmartyPants will be used to convert quotes and dashes to 137 | # typographically correct entities. 138 | #html_use_smartypants = True 139 | 140 | # Custom sidebar templates, maps document names to template names. 141 | #html_sidebars = {} 142 | 143 | # Additional templates that should be rendered to pages, maps page names to 144 | # template names. 145 | #html_additional_pages = {} 146 | 147 | # If false, no module index is generated. 148 | #html_use_modindex = True 149 | 150 | # If false, no index is generated. 151 | #html_use_index = True 152 | 153 | # If true, the index is split into individual pages for each letter. 154 | #html_split_index = False 155 | 156 | # If true, links to the reST sources are added to the pages. 157 | #html_show_sourcelink = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = '' 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'COBSdoc' 169 | 170 | 171 | # -- Options for LaTeX output -------------------------------------------------- 172 | 173 | # The paper size ('letter' or 'a4'). 174 | #latex_paper_size = 'letter' 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #latex_font_size = '10pt' 178 | 179 | # Grouping the document tree into LaTeX files. List of tuples 180 | # (source start file, target name, title, author, documentclass [howto/manual]). 181 | latex_documents = [ 182 | ('index', 'COBS.tex', u'COBS Python Module Documentation', 183 | u'Craig McQueen', 'manual'), 184 | ] 185 | 186 | # The name of an image file (relative to this directory) to place at the top of 187 | # the title page. 188 | #latex_logo = None 189 | 190 | # For "manual" documents, if this is true, then toplevel headings are parts, 191 | # not chapters. 192 | #latex_use_parts = False 193 | 194 | # Additional stuff for the LaTeX preamble. 195 | #latex_preamble = '' 196 | 197 | # Documents to append as an appendix to all manuals. 198 | #latex_appendices = [] 199 | 200 | # If false, no module index is generated. 201 | #latex_use_modindex = True 202 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. COBS documentation master file, created by 2 | sphinx-quickstart on Sun Apr 25 10:50:26 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. _index: 7 | 8 | Consistent Overhead Byte Stuffing (COBS) 9 | ======================================== 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | intro.rst 17 | cobs.cobs.rst 18 | cobs.cobsr.rst 19 | cobsr-intro.rst 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | -------------------------------------------------------------------------------- /doc/source/intro.rst: -------------------------------------------------------------------------------- 1 | 2 | ======================================== 3 | Consistent Overhead Byte Stuffing (COBS) 4 | ======================================== 5 | 6 | :Author: Craig McQueen 7 | :Contact: http://craig.mcqueen.id.au/ 8 | :Copyright: 2010 Craig McQueen 9 | 10 | 11 | Python functions for encoding and decoding COBS. 12 | 13 | ----- 14 | Intro 15 | ----- 16 | 17 | The :mod:`cobs` package is provided, which contains modules containing functions 18 | for encoding and decoding according to COBS methods. 19 | 20 | 21 | What Is COBS? 22 | ````````````` 23 | 24 | COBS is a method of encoding a packet of bytes into a form that contains no 25 | bytes with value zero (0x00). The input packet of bytes can contain bytes 26 | in the full range of 0x00 to 0xFF. The COBS encoded packet is guaranteed to 27 | generate packets with bytes only in the range 0x01 to 0xFF. Thus, in a 28 | communication protocol, packet boundaries can be reliably delimited with 0x00 29 | bytes. 30 | 31 | The COBS encoding does have to increase the packet size to achieve this 32 | encoding. However, compared to other byte-stuffing methods, the packet size 33 | increase is reasonable and predictable. COBS always adds 1 byte to the 34 | message length. Additionally, for longer packets of length *n*, it *may* add 35 | :math:`\left\lfloor\frac{n}{254}\right\rfloor` 36 | additional bytes to the encoded packet size. 37 | 38 | For example, compare to the PPP protocol, which uses 0x7E bytes to delimit 39 | PPP packets. The PPP protocol uses an "escape" style of byte stuffing, 40 | replacing all occurences of 0x7E bytes in the packet with 0x7D 0x5E. But that 41 | byte-stuffing method can potentially double the size of the packet in the 42 | worst case. COBS uses a different method for byte-stuffing, which has a much 43 | more reasonable worst-case overhead. 44 | 45 | For more details about COBS, see the references [COBS]_ [COBSPPP]_. 46 | 47 | I have included a variant on COBS, :ref:`COBS/R `, which slightly 48 | modifies COBS to often avoid the +1 byte overhead of COBS. So in many cases, 49 | especially for smaller packets, the size of a COBS/R encoded packet is the 50 | same size as the original packet. For more details about COBS/R see the 51 | :ref:`documentation for Consistent Overhead Byte Stuffing—Reduced `. 52 | 53 | 54 | References 55 | `````````` 56 | 57 | .. [COBS] | `Consistent Overhead Byte Stuffing `_ 58 | | Stuart Cheshire and Mary Baker 59 | | IEEE/ACM Transations on Networking, Vol. 7, No. 2, April 1999 60 | 61 | .. [COBSPPP] | `PPP Consistent Overhead Byte Stuffing (COBS) `_ 62 | | PPP Working Group Internet Draft 63 | | James Carlson, IronBridge Networks 64 | | Stuart Cheshire and Mary Baker, Stanford University 65 | | November 1997 66 | 67 | .. [Cimpl] | `C Implementation of COBS and COBS/R `_ 68 | | Craig McQueen 69 | 70 | 71 | ---------------- 72 | Modules Provided 73 | ---------------- 74 | 75 | ================== ====================== =============================================================== 76 | Module Short Name Long Name 77 | ================== ====================== =============================================================== 78 | :mod:`cobs.cobs` COBS Consistent Overhead Byte Stuffing (basic method) [COBS]_ 79 | :mod:`cobs.cobsr` :ref:`COBS/R ` :ref:`Consistent Overhead Byte Stuffing—Reduced ` 80 | ================== ====================== =============================================================== 81 | 82 | "Consistent Overhead Byte Stuffing—Reduced" (COBS/R) is my own invention, a 83 | modification of basic COBS encoding, and is described in more detail in the 84 | :ref:`documentation for Consistent Overhead Byte Stuffing—Reduced `. 85 | 86 | The following are not implemented: 87 | 88 | ================== ====================================================================== 89 | Short Name Long Name 90 | ================== ====================================================================== 91 | COBS/ZPE Consistent Overhead Byte Stuffing—Zero Pair Elimination [COBS]_ 92 | COBS/ZRE Consistent Overhead Byte Stuffing—Zero Run Elimination [COBSPPP]_ 93 | ================== ====================================================================== 94 | 95 | A pure Python implementation and a C extension implementation are provided. If 96 | the C extension is not available for some reason, the pure Python version will 97 | be used. 98 | 99 | 100 | ----- 101 | Usage 102 | ----- 103 | 104 | The modules provide an :func:`encode` and a :func:`decode` function. 105 | 106 | For usage, see the examples provided in the modules: 107 | 108 | * :ref:`COBS Examples ` in :mod:`cobs.cobs` 109 | * :ref:`COBS/R Examples ` in :mod:`cobs.cobsr` 110 | 111 | 112 | ------------------------- 113 | Supported Python Versions 114 | ------------------------- 115 | 116 | Python >= 2.4 and 3.x are supported, and have both a C extension and a pure 117 | Python implementation. 118 | 119 | Python versions < 2.4 might work, but have not been tested. Python 3.0 has 120 | also not been tested. 121 | 122 | 123 | -------- 124 | Download 125 | -------- 126 | 127 | Source code and binaries can be downloaded from the `Python package index `_. 128 | 129 | The Git source code repository for development is on `GitHub `_. 130 | 131 | 132 | ------------ 133 | Installation 134 | ------------ 135 | 136 | The cobs package is installed using :mod:`distutils`. If you have the tools 137 | installed to build a Python extension module, run the following command:: 138 | 139 | python setup.py install 140 | 141 | If you cannot build the C extension, you may install just the pure Python 142 | implementation, using the following command:: 143 | 144 | python setup.py build_py install --skip-build 145 | 146 | 147 | ------------ 148 | Unit Testing 149 | ------------ 150 | 151 | Unit testing is in the :mod:`test` sub-module, e.g. :mod:`cobs.cobs.test`. 152 | To run it on Python >=2.5:: 153 | 154 | python -m cobs.cobs.test 155 | python -m cobs.cobsr.test 156 | 157 | Alternatively, in the :file:`test` directory run:: 158 | 159 | python test_cobs.py 160 | python test_cobsr.py 161 | 162 | 163 | ------- 164 | License 165 | ------- 166 | 167 | The code is released under the MIT license. 168 | 169 | Copyright (c) 2010 Craig McQueen 170 | 171 | Permission is hereby granted, free of charge, to any person obtaining a copy 172 | of this software and associated documentation files (the "Software"), to deal 173 | in the Software without restriction, including without limitation the rights 174 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 175 | copies of the Software, and to permit persons to whom the Software is 176 | furnished to do so, subject to the following conditions: 177 | 178 | The above copyright notice and this permission notice shall be included in 179 | all copies or substantial portions of the Software. 180 | 181 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 182 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 183 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 184 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 185 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 186 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 187 | SOFTWARE. 188 | 189 | 190 | --------------------- 191 | Other Implementations 192 | --------------------- 193 | 194 | The author has also developed pure C implementation [Cimpl]_ of both COBS and 195 | COBS/R, developed in close conjunction with this Python module. The C 196 | implementation is very similar to this Python module's C extension 197 | implementation. 198 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "cobs" 7 | version = "1.2.1" 8 | authors = [ 9 | { name = "Craig McQueen", email = "python@craig.mcqueen.id.au" }, 10 | ] 11 | description = "Consistent Overhead Byte Stuffing (COBS)" 12 | keywords = [ "byte stuffing" ] 13 | readme = "README.rst" 14 | requires-python = ">=3.6" 15 | license = { file = "LICENSE.txt" } 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Python :: 3", 21 | "Topic :: Communications", 22 | ] 23 | 24 | [project.urls] 25 | "Homepage" = "https://github.com/cmcqueen/cobs-python" 26 | "Bug Tracker" = "https://github.com/cmcqueen/cobs-python/issues" 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | from setuptools import setup, Extension 6 | 7 | if sys.version_info[0] == 2: 8 | raise Exception('Python 2.x is no longer supported') 9 | 10 | setup_dict = dict( 11 | packages=[ 'cobs', 'cobs.cobs', 'cobs.cobsr', 'cobs._version', ], 12 | package_dir={ 13 | 'cobs' : 'src/cobs', 14 | }, 15 | ext_modules=[ 16 | Extension('cobs.cobs._cobs_ext', [ 'src/ext/_cobs_ext.c', ]), 17 | Extension('cobs.cobsr._cobsr_ext', [ 'src/ext/_cobsr_ext.c', ]), 18 | ], 19 | ) 20 | 21 | try: 22 | setup(**setup_dict) 23 | except KeyboardInterrupt: 24 | raise 25 | except: 26 | del setup_dict['ext_modules'] 27 | setup(**setup_dict) 28 | -------------------------------------------------------------------------------- /src/cobs/__init__.py: -------------------------------------------------------------------------------- 1 | """Consistent Overhead Byte Stuffing (COBS) 2 | 3 | Encoding and decoding functions for COBS and variants. 4 | 5 | The following sub-modules are provided: 6 | 7 | * ``cobs.cobs`` which implements plain COBS. 8 | * ``cobs.cobsr`` which implements COBS/Reduced. 9 | """ 10 | 11 | __all__ = [ 'cobs', 'cobsr', ] 12 | 13 | #from . import cobs 14 | #from . import cobsr 15 | 16 | from ._version import * 17 | 18 | -------------------------------------------------------------------------------- /src/cobs/_version/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | _version 3 | 4 | Version information for cobs. 5 | """ 6 | 7 | __all__ = [ 8 | 'VERSION_MAJOR', 9 | 'VERSION_MINOR', 10 | 'VERSION_PATCH', 11 | 'VERSION_STRING', 12 | '__version__', 13 | ] 14 | 15 | VERSION_MAJOR = 1 16 | VERSION_MINOR = 2 17 | VERSION_PATCH = 1 18 | 19 | VERSION_STRING = '%s.%s.%s' % (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) 20 | 21 | __version__ = VERSION_STRING 22 | 23 | -------------------------------------------------------------------------------- /src/cobs/cobs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consistent Overhead Byte Stuffing (COBS) encoding and decoding. 3 | 4 | Functions are provided for encoding and decoding according to 5 | the basic COBS method. 6 | 7 | The COBS variant "Zero Pair Elimination" (ZPE) is not 8 | implemented. 9 | 10 | A pure Python implementation and a C extension implementation 11 | are provided. If the C extension is not available for some reason, 12 | the pure Python version will be used. 13 | 14 | References: 15 | http://www.stuartcheshire.org/papers/COBSforToN.pdf 16 | http://tools.ietf.org/html/draft-ietf-pppext-cobs-00 17 | """ 18 | 19 | try: 20 | from ._cobs_ext import * 21 | _using_extension = True 22 | except ImportError: 23 | from ._cobs_py import * 24 | _using_extension = False 25 | 26 | DecodeError.__module__ = 'cobs.cobs' 27 | 28 | from .._version import * 29 | 30 | 31 | def encoding_overhead(source_len): 32 | """Calculates the maximum overhead when encoding a message with the given length. 33 | The overhead is a maximum of [n/254] bytes (one in 254 bytes) rounded up.""" 34 | if source_len == 0: 35 | return 1 36 | return (source_len + 253) // 254 37 | 38 | 39 | def max_encoded_length(source_len): 40 | """Calculates how maximum possible size of an encoded message given the length of the 41 | source message.""" 42 | return source_len + encoding_overhead(source_len) 43 | -------------------------------------------------------------------------------- /src/cobs/cobs/_cobs_py.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consistent Overhead Byte Stuffing (COBS) 3 | 4 | This version is for Python 3.x. 5 | """ 6 | 7 | 8 | class DecodeError(Exception): 9 | pass 10 | 11 | 12 | def _get_buffer_view(in_bytes): 13 | mv = memoryview(in_bytes) 14 | if mv.ndim > 1 or mv.itemsize > 1: 15 | raise BufferError('object must be a single-dimension buffer of bytes.') 16 | try: 17 | mv = mv.cast('c') 18 | except AttributeError: 19 | pass 20 | return mv 21 | 22 | def encode(in_bytes): 23 | """Encode a string using Consistent Overhead Byte Stuffing (COBS). 24 | 25 | Input is any byte string. Output is also a byte string. 26 | 27 | Encoding guarantees no zero bytes in the output. The output 28 | string will be expanded slightly, by a predictable amount. 29 | 30 | An empty string is encoded to '\\x01'""" 31 | if isinstance(in_bytes, str): 32 | raise TypeError('Unicode-objects must be encoded as bytes first') 33 | in_bytes_mv = _get_buffer_view(in_bytes) 34 | final_zero = True 35 | out_bytes = bytearray() 36 | idx = 0 37 | search_start_idx = 0 38 | for in_char in in_bytes_mv: 39 | if in_char == b'\x00': 40 | final_zero = True 41 | out_bytes.append(idx - search_start_idx + 1) 42 | out_bytes += in_bytes_mv[search_start_idx:idx] 43 | search_start_idx = idx + 1 44 | else: 45 | if idx - search_start_idx == 0xFD: 46 | final_zero = False 47 | out_bytes.append(0xFF) 48 | out_bytes += in_bytes_mv[search_start_idx:idx+1] 49 | search_start_idx = idx + 1 50 | idx += 1 51 | if idx != search_start_idx or final_zero: 52 | out_bytes.append(idx - search_start_idx + 1) 53 | out_bytes += in_bytes_mv[search_start_idx:idx] 54 | return bytes(out_bytes) 55 | 56 | 57 | def decode(in_bytes): 58 | """Decode a string using Consistent Overhead Byte Stuffing (COBS). 59 | 60 | Input should be a byte string that has been COBS encoded. Output 61 | is also a byte string. 62 | 63 | A cobs.DecodeError exception will be raised if the encoded data 64 | is invalid.""" 65 | if isinstance(in_bytes, str): 66 | raise TypeError('Unicode-objects are not supported; byte buffer objects only') 67 | in_bytes_mv = _get_buffer_view(in_bytes) 68 | out_bytes = bytearray() 69 | idx = 0 70 | 71 | if len(in_bytes_mv) > 0: 72 | while True: 73 | length = ord(in_bytes_mv[idx]) 74 | if length == 0: 75 | raise DecodeError("zero byte found in input") 76 | idx += 1 77 | end = idx + length - 1 78 | copy_mv = in_bytes_mv[idx:end] 79 | if b'\x00' in copy_mv: 80 | raise DecodeError("zero byte found in input") 81 | out_bytes += copy_mv 82 | idx = end 83 | if idx > len(in_bytes_mv): 84 | raise DecodeError("not enough input bytes for length code") 85 | if idx < len(in_bytes_mv): 86 | if length < 0xFF: 87 | out_bytes.append(0) 88 | else: 89 | break 90 | return bytes(out_bytes) 91 | -------------------------------------------------------------------------------- /src/cobs/cobs/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consistent Overhead Byte Stuffing (COBS) 3 | 4 | Unit Tests 5 | 6 | This version is for Python 3.x. 7 | """ 8 | 9 | from array import array 10 | import random 11 | import unittest 12 | 13 | from .. import cobs as cobs 14 | #from ..cobs import _cobs_py as cobs 15 | 16 | def infinite_non_zero_generator(): 17 | while True: 18 | for i in range(1,50): 19 | for j in range(1,256, i): 20 | yield j 21 | 22 | def non_zero_generator(length): 23 | non_zeros = infinite_non_zero_generator() 24 | for i in range(length): 25 | yield next(non_zeros) 26 | 27 | def non_zero_bytes(length): 28 | return b''.join(bytes([i]) for i in non_zero_generator(length)) 29 | 30 | 31 | class PredefinedEncodingsTests(unittest.TestCase): 32 | predefined_encodings = [ 33 | [ b"", b"\x01" ], 34 | [ b"1", b"\x021" ], 35 | [ b"12345", b"\x0612345" ], 36 | [ b"12345\x006789", b"\x0612345\x056789" ], 37 | [ b"\x0012345\x006789", b"\x01\x0612345\x056789" ], 38 | [ b"12345\x006789\x00", b"\x0612345\x056789\x01" ], 39 | [ b"\x00", b"\x01\x01" ], 40 | [ b"\x00\x00", b"\x01\x01\x01" ], 41 | [ b"\x00\x00\x00", b"\x01\x01\x01\x01" ], 42 | [ bytes(bytearray(range(1, 254))), bytes(b"\xfe" + bytearray(range(1, 254))) ], 43 | [ bytes(bytearray(range(1, 255))), bytes(b"\xff" + bytearray(range(1, 255))) ], 44 | [ bytes(bytearray(range(1, 256))), bytes(b"\xff" + bytearray(range(1, 255)) + b"\x02\xff") ], 45 | [ bytes(bytearray(range(0, 256))), bytes(b"\x01\xff" + bytearray(range(1, 255)) + b"\x02\xff") ], 46 | ] 47 | 48 | def test_predefined_encodings(self): 49 | for (test_string, expected_encoded_string) in self.predefined_encodings: 50 | encoded = cobs.encode(test_string) 51 | self.assertEqual(encoded, expected_encoded_string) 52 | 53 | def test_decode_predefined_encodings(self): 54 | for (test_string, expected_encoded_string) in self.predefined_encodings: 55 | decoded = cobs.decode(expected_encoded_string) 56 | self.assertEqual(test_string, decoded) 57 | 58 | 59 | class PredefinedDecodeErrorTests(unittest.TestCase): 60 | decode_error_test_strings = [ 61 | b"\x00", 62 | b"\x05123", 63 | b"\x051234\x00", 64 | b"\x0512\x004", 65 | ] 66 | 67 | def test_predefined_decode_error(self): 68 | for test_encoded in self.decode_error_test_strings: 69 | with self.assertRaises(cobs.DecodeError): 70 | cobs.decode(test_encoded) 71 | 72 | 73 | class ZerosTest(unittest.TestCase): 74 | def test_zeros(self): 75 | for length in range(520): 76 | test_string = b'\x00' * length 77 | encoded = cobs.encode(test_string) 78 | expected_encoded = b'\x01' * (length + 1) 79 | self.assertEqual(encoded, expected_encoded, "encoding zeros failed for length %d" % length) 80 | decoded = cobs.decode(encoded) 81 | self.assertEqual(decoded, test_string, "decoding zeros failed for length %d" % length) 82 | 83 | 84 | class NonZerosTest(unittest.TestCase): 85 | def simple_encode_non_zeros_only(self, in_bytes): 86 | out_list = [] 87 | for i in range(0, len(in_bytes), 254): 88 | data_block = in_bytes[i: i+254] 89 | out_list.append(bytes([ len(data_block) + 1 ])) 90 | out_list.append(data_block) 91 | return b''.join(out_list) 92 | 93 | def test_non_zeros(self): 94 | for length in range(1, 1000): 95 | test_string = non_zero_bytes(length) 96 | encoded = cobs.encode(test_string) 97 | expected_encoded = self.simple_encode_non_zeros_only(test_string) 98 | self.assertEqual(encoded, expected_encoded, 99 | "encoded != expected_encoded for length %d\nencoded: %s\nexpected_encoded: %s" % 100 | (length, repr(encoded), repr(expected_encoded))) 101 | 102 | def test_non_zeros_and_trailing_zero(self): 103 | for length in range(1, 1000): 104 | non_zeros_string = non_zero_bytes(length) 105 | test_string = non_zeros_string + b'\x00' 106 | encoded = cobs.encode(test_string) 107 | if (len(non_zeros_string) % 254) == 0: 108 | expected_encoded = self.simple_encode_non_zeros_only(non_zeros_string) + b'\x01\x01' 109 | else: 110 | expected_encoded = self.simple_encode_non_zeros_only(non_zeros_string) + b'\x01' 111 | self.assertEqual(encoded, expected_encoded, 112 | "encoded != expected_encoded for length %d\nencoded: %s\nexpected_encoded: %s" % 113 | (length, repr(encoded), repr(expected_encoded))) 114 | 115 | 116 | class RandomDataTest(unittest.TestCase): 117 | NUM_TESTS = 5000 118 | MAX_LENGTH = 2000 119 | 120 | def test_random(self): 121 | try: 122 | for _test_num in range(self.NUM_TESTS): 123 | length = random.randint(0, self.MAX_LENGTH) 124 | test_string = bytes(random.randint(0,255) for x in range(length)) 125 | encoded = cobs.encode(test_string) 126 | self.assertTrue(b'\x00' not in encoded, 127 | "encoding contains zero byte(s):\noriginal: %s\nencoded: %s" % (repr(test_string), repr(encoded))) 128 | self.assertTrue(len(encoded) <= len(test_string) + 1 + (len(test_string) // 254), 129 | "encoding too big:\noriginal: %s\nencoded: %s" % (repr(test_string), repr(encoded))) 130 | decoded = cobs.decode(encoded) 131 | self.assertEqual(decoded, test_string, 132 | "encoding and decoding random data failed:\noriginal: %s\ndecoded: %s" % (repr(test_string), repr(decoded))) 133 | except KeyboardInterrupt: 134 | pass 135 | 136 | 137 | class InputTypesTest(unittest.TestCase): 138 | predefined_encodings = [ 139 | [ b"", b"\x01" ], 140 | [ b"1", b"\x021" ], 141 | [ b"12345", b"\x0612345" ], 142 | [ b"12345\x006789", b"\x0612345\x056789" ], 143 | [ b"\x0012345\x006789", b"\x01\x0612345\x056789" ], 144 | [ b"12345\x006789\x00", b"\x0612345\x056789\x01" ], 145 | ] 146 | 147 | def test_unicode_string(self): 148 | """Test that Unicode strings are not encoded or decoded. 149 | They should raise a TypeError.""" 150 | for (test_string, expected_encoded_string) in self.predefined_encodings: 151 | unicode_test_string = test_string.decode('latin') 152 | with self.assertRaises(TypeError): 153 | cobs.encode(unicode_test_string) 154 | unicode_encoded_string = expected_encoded_string.decode('latin') 155 | with self.assertRaises(TypeError): 156 | cobs.decode(unicode_encoded_string) 157 | 158 | def test_bytearray(self): 159 | """Test that bytearray objects can be encoded or decoded.""" 160 | for (test_string, expected_encoded_string) in self.predefined_encodings: 161 | bytearray_test_string = bytearray(test_string) 162 | encoded = cobs.encode(bytearray_test_string) 163 | self.assertEqual(encoded, expected_encoded_string) 164 | bytearray_encoded_string = bytearray(expected_encoded_string) 165 | decoded = cobs.decode(bytearray_encoded_string) 166 | self.assertEqual(decoded, test_string) 167 | 168 | def test_array_of_bytes(self): 169 | """Test that array of bytes objects (array('B', ...)) can be encoded or decoded.""" 170 | for (test_string, expected_encoded_string) in self.predefined_encodings: 171 | array_test_string = array('B', test_string) 172 | encoded = cobs.encode(array_test_string) 173 | self.assertEqual(encoded, expected_encoded_string) 174 | array_encoded_string = array('B', expected_encoded_string) 175 | decoded = cobs.decode(array_encoded_string) 176 | self.assertEqual(decoded, test_string) 177 | 178 | def test_array_of_half_words(self): 179 | """Test that array of half-word objects (array('H', ...)) are not encoded or decoded. 180 | They should raise a BufferError.""" 181 | # Array typecodes for types with size greater than 1. 182 | # Don't include 'u' for Unicode because it expects a Unicode initialiser. 183 | typecodes = [ 'H', 'h', 'i', 'I', 'l', 'L', 'f', 'd' ] 184 | for typecode in typecodes: 185 | array_test_string = array(typecode, [ 49, 50, 51, 52, 53 ]) 186 | with self.assertRaises(BufferError): 187 | cobs.encode(array_test_string) 188 | array_encoded_string = array(typecode, [6, 49, 50, 51, 52, 53 ]) 189 | with self.assertRaises(BufferError): 190 | cobs.decode(array_encoded_string) 191 | 192 | 193 | class UtilTests(unittest.TestCase): 194 | 195 | def test_encoded_len_calc(self): 196 | self.assertEqual(cobs.encoding_overhead(5), 1) 197 | self.assertEqual(cobs.max_encoded_length(5), 6) 198 | 199 | def test_encoded_len_calc_empty_packet(self): 200 | self.assertEqual(cobs.encoding_overhead(0), 1) 201 | self.assertEqual(cobs.max_encoded_length(0), 1) 202 | 203 | def test_encoded_len_calc_still_one_byte_overhead(self): 204 | self.assertEqual(cobs.encoding_overhead(254), 1) 205 | self.assertEqual(cobs.max_encoded_length(254), 255) 206 | 207 | def test_encoded_len_calc_two_byte_overhead(self): 208 | self.assertEqual(cobs.encoding_overhead(255), 2) 209 | self.assertEqual(cobs.max_encoded_length(255), 257) 210 | 211 | 212 | def runtests(): 213 | unittest.main() 214 | 215 | 216 | if __name__ == '__main__': 217 | runtests() 218 | -------------------------------------------------------------------------------- /src/cobs/cobsr/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consistent Overhead Byte Stuffing/Reduced (COBS/R) encoding and decoding. 3 | 4 | Functions are provided for encoding and decoding according to 5 | the COBS/R method. 6 | 7 | A pure Python implementation and a C extension implementation 8 | are provided. If the C extension is not available for some reason, 9 | the pure Python version will be used. 10 | """ 11 | 12 | try: 13 | from ._cobsr_ext import * 14 | _using_extension = True 15 | except ImportError: 16 | from ._cobsr_py import * 17 | _using_extension = False 18 | 19 | DecodeError.__module__ = 'cobs.cobsr' 20 | 21 | from .._version import * 22 | 23 | 24 | def encoding_overhead(source_len): 25 | """Calculates the maximum overhead when encoding a message with the given length. 26 | The overhead is a maximum of [n/254] bytes (one in 254 bytes) rounded up.""" 27 | if source_len == 0: 28 | return 1 29 | return (source_len + 253) // 254 30 | 31 | 32 | def max_encoded_length(source_len): 33 | """Calculates how maximum possible size of an encoded message given the length of the 34 | source message.""" 35 | return source_len + encoding_overhead(source_len) 36 | -------------------------------------------------------------------------------- /src/cobs/cobsr/_cobsr_py.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consistent Overhead Byte Stuffing/Reduced (COBS/R) 3 | 4 | This version is for Python 3.x. 5 | """ 6 | 7 | 8 | class DecodeError(Exception): 9 | pass 10 | 11 | 12 | def _get_buffer_view(in_bytes): 13 | mv = memoryview(in_bytes) 14 | if mv.ndim > 1 or mv.itemsize > 1: 15 | raise BufferError('object must be a single-dimension buffer of bytes.') 16 | try: 17 | mv = mv.cast('c') 18 | except AttributeError: 19 | pass 20 | return mv 21 | 22 | def encode(in_bytes): 23 | """Encode a string using Consistent Overhead Byte Stuffing/Reduced (COBS/R). 24 | 25 | Input is any byte string. Output is also a byte string. 26 | 27 | Encoding guarantees no zero bytes in the output. The output 28 | string may be expanded slightly, by a predictable amount. 29 | 30 | An empty string is encoded to '\\x01'""" 31 | if isinstance(in_bytes, str): 32 | raise TypeError('Unicode-objects must be encoded as bytes first') 33 | in_bytes_mv = _get_buffer_view(in_bytes) 34 | out_bytes = bytearray() 35 | idx = 0 36 | search_start_idx = 0 37 | for in_char in in_bytes_mv: 38 | if idx - search_start_idx == 0xFE: 39 | out_bytes.append(0xFF) 40 | out_bytes += in_bytes_mv[search_start_idx:idx] 41 | search_start_idx = idx 42 | if in_char == b'\x00': 43 | out_bytes.append(idx - search_start_idx + 1) 44 | out_bytes += in_bytes_mv[search_start_idx:idx] 45 | search_start_idx = idx + 1 46 | idx += 1 47 | try: 48 | final_byte_value = ord(in_bytes_mv[-1]) 49 | except IndexError: 50 | final_byte_value = 0 51 | length_value = idx - search_start_idx + 1 52 | if final_byte_value < length_value: 53 | # Encoding same as plain COBS 54 | out_bytes.append(length_value) 55 | out_bytes += in_bytes_mv[search_start_idx:idx] 56 | else: 57 | # Special COBS/R encoding: length code is final byte, 58 | # and final byte is removed from data sequence. 59 | out_bytes.append(final_byte_value) 60 | out_bytes += in_bytes_mv[search_start_idx:idx - 1] 61 | return bytes(out_bytes) 62 | 63 | 64 | def decode(in_bytes): 65 | """Decode a string using Consistent Overhead Byte Stuffing/Reduced (COBS/R). 66 | 67 | Input should be a byte string that has been COBS/R encoded. Output 68 | is also a byte string. 69 | 70 | A cobsr.DecodeError exception will be raised if the encoded data 71 | is invalid. That is, if the encoded data contains zeros.""" 72 | if isinstance(in_bytes, str): 73 | raise TypeError('Unicode-objects are not supported; byte buffer objects only') 74 | in_bytes_mv = _get_buffer_view(in_bytes) 75 | out_bytes = bytearray() 76 | idx = 0 77 | 78 | if len(in_bytes_mv) > 0: 79 | while True: 80 | length = ord(in_bytes_mv[idx]) 81 | if length == 0: 82 | raise DecodeError("zero byte found in input") 83 | idx += 1 84 | end = idx + length - 1 85 | copy_mv = in_bytes_mv[idx:end] 86 | if b'\x00' in copy_mv: 87 | raise DecodeError("zero byte found in input") 88 | out_bytes += copy_mv 89 | idx = end 90 | if idx > len(in_bytes_mv): 91 | out_bytes.append(length) 92 | break 93 | elif idx < len(in_bytes_mv): 94 | if length < 0xFF: 95 | out_bytes.append(0) 96 | else: 97 | break 98 | return bytes(out_bytes) 99 | -------------------------------------------------------------------------------- /src/cobs/cobsr/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consistent Overhead Byte Stuffing/Reduced (COBS/R) 3 | 4 | Unit Tests 5 | 6 | This version is for Python 3.x. 7 | """ 8 | 9 | from array import array 10 | import random 11 | import unittest 12 | 13 | from .. import cobsr as cobsr 14 | #from ..cobsr import _cobsr_py as cobsr 15 | 16 | 17 | def infinite_non_zero_generator(): 18 | while True: 19 | for i in range(1,50): 20 | for j in range(1,256, i): 21 | yield j 22 | 23 | def non_zero_generator(length): 24 | non_zeros = infinite_non_zero_generator() 25 | for i in range(length): 26 | yield next(non_zeros) 27 | 28 | def non_zero_bytes(length): 29 | return b''.join(bytes([i]) for i in non_zero_generator(length)) 30 | 31 | 32 | class PredefinedEncodingsTests(unittest.TestCase): 33 | predefined_encodings = [ 34 | [ b"", b"\x01" ], 35 | [ b"\x01", b"\x02\x01" ], 36 | [ b"\x02", b"\x02" ], 37 | [ b"\x03", b"\x03" ], 38 | [ b"\x7E", b"\x7E" ], 39 | [ b"\x7F", b"\x7F" ], 40 | [ b"\x80", b"\x80" ], 41 | [ b"\xD5", b"\xD5" ], 42 | [ b"\xFE", b"\xFE" ], 43 | [ b"\xFF", b"\xFF" ], 44 | [ b"a\x02", b"\x03a\x02" ], 45 | [ b"a\x03", b"\x03a" ], 46 | [ b"a\xFF", b"\xFFa" ], 47 | [ b"\x05\x04\x03\x02\x01", b"\x06\x05\x04\x03\x02\x01" ], 48 | [ b"12345", b"51234" ], 49 | [ b"12345\x00\x04\x03\x02\x01", b"\x0612345\x05\x04\x03\x02\x01" ], 50 | [ b"12345\x006789", b"\x06123459678" ], 51 | [ b"\x0012345\x006789", b"\x01\x06123459678" ], 52 | [ b"12345\x006789\x00", b"\x0612345\x056789\x01" ], 53 | [ b"\x00", b"\x01\x01" ], 54 | [ b"\x00\x00", b"\x01\x01\x01" ], 55 | [ b"\x00\x00\x00", b"\x01\x01\x01\x01" ], 56 | [ bytes(bytearray(range(1, 254))), bytes(b"\xfe" + bytearray(range(1, 254))) ], 57 | [ bytes(bytearray(range(1, 255))), bytes(b"\xff" + bytearray(range(1, 255))) ], 58 | [ bytes(bytearray(range(1, 256))), bytes(b"\xff" + bytearray(range(1, 255)) + b"\xff") ], 59 | [ bytes(bytearray(range(0, 256))), bytes(b"\x01\xff" + bytearray(range(1, 255)) + b"\xff") ], 60 | [ bytes(bytearray(range(2, 256))), bytes(b"\xff" + bytearray(range(2, 255))) ], 61 | ] 62 | 63 | def test_predefined_encodings(self): 64 | for (test_string, expected_encoded_string) in self.predefined_encodings: 65 | encoded = cobsr.encode(test_string) 66 | self.assertEqual(encoded, expected_encoded_string) 67 | 68 | def test_decode_predefined_encodings(self): 69 | for (test_string, expected_encoded_string) in self.predefined_encodings: 70 | decoded = cobsr.decode(expected_encoded_string) 71 | self.assertEqual(test_string, decoded) 72 | 73 | 74 | class PredefinedDecodeErrorTests(unittest.TestCase): 75 | decode_error_test_strings = [ 76 | b"\x00", 77 | b"\x051234\x00", 78 | b"\x0512\x004", 79 | ] 80 | 81 | def test_predefined_decode_error(self): 82 | for test_encoded in self.decode_error_test_strings: 83 | with self.assertRaises(cobsr.DecodeError): 84 | cobsr.decode(test_encoded) 85 | 86 | 87 | class ZerosTest(unittest.TestCase): 88 | def test_zeros(self): 89 | for length in range(520): 90 | test_string = b'\x00' * length 91 | encoded = cobsr.encode(test_string) 92 | expected_encoded = b'\x01' * (length + 1) 93 | self.assertEqual(encoded, expected_encoded, "encoding zeros failed for length %d" % length) 94 | decoded = cobsr.decode(encoded) 95 | self.assertEqual(decoded, test_string, "decoding zeros failed for length %d" % length) 96 | 97 | 98 | class NonZerosTest(unittest.TestCase): 99 | def simple_encode_non_zeros_only(self, in_bytes): 100 | out_list = [] 101 | for i in range(0, len(in_bytes), 254): 102 | data_block = in_bytes[i: i+254] 103 | out_list.append(bytes([ len(data_block) + 1 ])) 104 | out_list.append(data_block) 105 | return b''.join(out_list) 106 | 107 | def cobsr_encode_final_non_zeros_only(self, in_bytes): 108 | out_list = [] 109 | for i in range(0, len(in_bytes), 254): 110 | data_block = in_bytes[i: i+254] 111 | out_list.append(bytes([ len(data_block) + 1 ])) 112 | out_list.append(data_block) 113 | last_block = out_list[-1] 114 | last_char_value = last_block[-1] 115 | if last_char_value >= len(last_block) + 1: 116 | del(out_list[-2:]) 117 | out_list.append(bytes([ last_char_value ])) 118 | out_list.append(last_block[:-1]) 119 | return b''.join(out_list) 120 | 121 | def test_non_zeros(self): 122 | for length in range(1, 1000): 123 | test_string = non_zero_bytes(length) 124 | encoded = cobsr.encode(test_string) 125 | expected_encoded = self.cobsr_encode_final_non_zeros_only(test_string) 126 | self.assertEqual(encoded, expected_encoded, 127 | "encoded != expected_encoded for length %d\nencoded: %s\nexpected_encoded: %s" % 128 | (length, repr(encoded), repr(expected_encoded))) 129 | 130 | def test_non_zeros_and_trailing_zero(self): 131 | for length in range(1, 1000): 132 | non_zeros_string = non_zero_bytes(length) 133 | test_string = non_zeros_string + b'\x00' 134 | encoded = cobsr.encode(test_string) 135 | if (len(non_zeros_string) % 254) == 0: 136 | expected_encoded = self.simple_encode_non_zeros_only(non_zeros_string) + b'\x01\x01' 137 | else: 138 | expected_encoded = self.simple_encode_non_zeros_only(non_zeros_string) + b'\x01' 139 | self.assertEqual(encoded, expected_encoded, 140 | "encoded != expected_encoded for length %d\nencoded: %s\nexpected_encoded: %s" % 141 | (length, repr(encoded), repr(expected_encoded))) 142 | 143 | 144 | class RandomDataTest(unittest.TestCase): 145 | NUM_TESTS = 5000 146 | MAX_LENGTH = 2000 147 | 148 | def test_random(self): 149 | try: 150 | for _test_num in range(self.NUM_TESTS): 151 | length = random.randint(0, self.MAX_LENGTH) 152 | test_string = bytes(random.randint(0,255) for x in range(length)) 153 | encoded = cobsr.encode(test_string) 154 | self.assertTrue(b'\x00' not in encoded, 155 | "encoding contains zero byte(s):\noriginal: %s\nencoded: %s" % (repr(test_string), repr(encoded))) 156 | self.assertTrue(len(encoded) <= len(test_string) + 1 + (len(test_string) // 254), 157 | "encoding too big:\noriginal: %s\nencoded: %s" % (repr(test_string), repr(encoded))) 158 | decoded = cobsr.decode(encoded) 159 | self.assertEqual(decoded, test_string, 160 | "encoding and decoding random data failed:\noriginal: %s\ndecoded: %s" % (repr(test_string), repr(decoded))) 161 | except KeyboardInterrupt: 162 | pass 163 | 164 | 165 | class InputTypesTest(unittest.TestCase): 166 | predefined_encodings = [ 167 | [ b"", b"\x01" ], 168 | [ b"\x01", b"\x02\x01" ], 169 | [ b"\x02", b"\x02" ], 170 | [ b"\x7F", b"\x7F" ], 171 | [ b"\x80", b"\x80" ], 172 | [ b"\xD5", b"\xD5" ], 173 | [ b"1", b"1" ], 174 | [ b"\x05\x04\x03\x02\x01", b"\x06\x05\x04\x03\x02\x01" ], 175 | [ b"12345", b"51234" ], 176 | [ b"12345\x00\x04\x03\x02\x01", b"\x0612345\x05\x04\x03\x02\x01" ], 177 | [ b"12345\x006789", b"\x06123459678" ], 178 | ] 179 | 180 | def test_unicode_string(self): 181 | """Test that Unicode strings are not encoded or decoded. 182 | They should raise a TypeError.""" 183 | for (test_string, expected_encoded_string) in self.predefined_encodings: 184 | unicode_test_string = test_string.decode('latin') 185 | with self.assertRaises(TypeError): 186 | cobsr.encode(unicode_test_string) 187 | unicode_encoded_string = expected_encoded_string.decode('latin') 188 | with self.assertRaises(TypeError): 189 | cobsr.decode(unicode_encoded_string) 190 | 191 | def test_bytearray(self): 192 | """Test that bytearray objects can be encoded or decoded.""" 193 | for (test_string, expected_encoded_string) in self.predefined_encodings: 194 | bytearray_test_string = bytearray(test_string) 195 | encoded = cobsr.encode(bytearray_test_string) 196 | self.assertEqual(encoded, expected_encoded_string) 197 | bytearray_encoded_string = bytearray(expected_encoded_string) 198 | decoded = cobsr.decode(bytearray_encoded_string) 199 | self.assertEqual(decoded, test_string) 200 | 201 | def test_array_of_bytes(self): 202 | """Test that array of bytes objects (array('B', ...)) can be encoded or decoded.""" 203 | for (test_string, expected_encoded_string) in self.predefined_encodings: 204 | array_test_string = array('B', test_string) 205 | encoded = cobsr.encode(array_test_string) 206 | self.assertEqual(encoded, expected_encoded_string) 207 | array_encoded_string = array('B', expected_encoded_string) 208 | decoded = cobsr.decode(array_encoded_string) 209 | self.assertEqual(decoded, test_string) 210 | 211 | def test_array_of_half_words(self): 212 | """Test that array of half-word objects (array('H', ...)) are not encoded or decoded. 213 | They should raise a BufferError.""" 214 | # Array typecodes for types with size greater than 1. 215 | # Don't include 'u' for Unicode because it expects a Unicode initialiser. 216 | typecodes = [ 'H', 'h', 'i', 'I', 'l', 'L', 'f', 'd' ] 217 | for typecode in typecodes: 218 | array_test_string = array(typecode, [ 49, 50, 51, 52, 53 ]) 219 | with self.assertRaises(BufferError): 220 | cobsr.encode(array_test_string) 221 | array_encoded_string = array(typecode, [6, 49, 50, 51, 52, 53 ]) 222 | with self.assertRaises(BufferError): 223 | cobsr.decode(array_encoded_string) 224 | 225 | 226 | class UtilTests(unittest.TestCase): 227 | 228 | def test_encoded_len_calc(self): 229 | self.assertEqual(cobsr.encoding_overhead(5), 1) 230 | self.assertEqual(cobsr.max_encoded_length(5), 6) 231 | 232 | def test_encoded_len_calc_empty_packet(self): 233 | self.assertEqual(cobsr.encoding_overhead(0), 1) 234 | self.assertEqual(cobsr.max_encoded_length(0), 1) 235 | 236 | def test_encoded_len_calc_still_one_byte_overhead(self): 237 | self.assertEqual(cobsr.encoding_overhead(254), 1) 238 | self.assertEqual(cobsr.max_encoded_length(254), 255) 239 | 240 | def test_encoded_len_calc_two_byte_overhead(self): 241 | self.assertEqual(cobsr.encoding_overhead(255), 2) 242 | self.assertEqual(cobsr.max_encoded_length(255), 257) 243 | 244 | 245 | def runtests(): 246 | unittest.main() 247 | 248 | 249 | if __name__ == '__main__': 250 | runtests() 251 | -------------------------------------------------------------------------------- /src/ext/_cobs_ext.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Consistent Overhead Byte Stuffing (COBS) 3 | * 4 | * Python C extension for COBS encoding and decoding functions. 5 | * 6 | * Copyright (c) 2010 Craig McQueen 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to 10 | * deal in the Software without restriction, including without limitation the 11 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | * sell copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | * IN THE SOFTWARE. 25 | */ 26 | 27 | 28 | /***************************************************************************** 29 | * Includes 30 | ****************************************************************************/ 31 | 32 | // Force Py_ssize_t to be used for s# conversions. 33 | #define PY_SSIZE_T_CLEAN 34 | #include 35 | 36 | 37 | /***************************************************************************** 38 | * Defines 39 | ****************************************************************************/ 40 | 41 | #ifndef FALSE 42 | #define FALSE (0) 43 | #endif 44 | 45 | #ifndef TRUE 46 | #define TRUE (!FALSE) 47 | #endif 48 | 49 | 50 | #define GETSTATE(M) ((struct module_state *) PyModule_GetState(M)) 51 | 52 | 53 | /* 54 | * Given a PyObject* obj, fill in the Py_buffer* viewp with the result 55 | * of PyObject_GetBuffer. Sets and exception and issues a return NULL 56 | * on any errors. 57 | */ 58 | #define GET_BUFFER_VIEW_OR_ERROUT(obj, viewp) do { \ 59 | if (!PyObject_CheckBuffer((obj))) { \ 60 | PyErr_SetString(PyExc_TypeError, \ 61 | "object supporting the buffer API is required"); \ 62 | return NULL; \ 63 | } \ 64 | if (PyObject_GetBuffer((obj), (viewp), PyBUF_FORMAT) == -1) { \ 65 | return NULL; \ 66 | } \ 67 | if (((viewp)->ndim > 1) || ((viewp)->itemsize > 1)) { \ 68 | PyErr_SetString(PyExc_BufferError, \ 69 | "object must be a single-dimension buffer of bytes"); \ 70 | PyBuffer_Release((viewp)); \ 71 | return NULL; \ 72 | } \ 73 | } while(0); 74 | 75 | 76 | #define COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + ((SRC_LEN)/254u) + 1) 77 | #define COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) <= 1) ? 1 : ((SRC_LEN) - 1)) 78 | 79 | 80 | /***************************************************************************** 81 | * Types 82 | ****************************************************************************/ 83 | 84 | struct module_state 85 | { 86 | /* cobs.DecodeError exception class. */ 87 | PyObject * CobsDecodeError; 88 | }; 89 | 90 | 91 | /***************************************************************************** 92 | * Functions 93 | ****************************************************************************/ 94 | 95 | static int cobs_traverse(PyObject *m, visitproc visit, void *arg) 96 | { 97 | Py_VISIT(GETSTATE(m)->CobsDecodeError); 98 | return 0; 99 | } 100 | 101 | 102 | static int cobs_clear(PyObject *m) 103 | { 104 | Py_CLEAR(GETSTATE(m)->CobsDecodeError); 105 | return 0; 106 | } 107 | 108 | 109 | /* 110 | * cobs.encode 111 | */ 112 | PyDoc_STRVAR(cobs_encode__doc__, 113 | "Encode a string using Consistent Overhead Byte Stuffing (COBS).\n" 114 | "\n" 115 | "Input is any byte string. Output is also a byte string.\n" 116 | "\n" 117 | "Encoding guarantees no zero bytes in the output. The output\n" 118 | "string will be expanded slightly, by a predictable amount.\n" 119 | "\n" 120 | "An empty string is encoded to '\\x01'." 121 | ); 122 | 123 | /* 124 | * This Python C extension function uses arguments method METH_O, 125 | * meaning the arg parameter contains the single parameter 126 | * to the function. 127 | */ 128 | static PyObject* 129 | cobs_encode(PyObject* module, PyObject* arg) 130 | { 131 | Py_buffer src_py_buffer; 132 | const char * src_ptr; 133 | Py_ssize_t src_len; 134 | const char * src_end_ptr; 135 | char * dst_buf_ptr; 136 | char * dst_code_write_ptr; 137 | char * dst_write_ptr; 138 | char src_byte; 139 | unsigned char search_len; 140 | PyObject * dst_py_obj_ptr; 141 | 142 | 143 | if (PyUnicode_Check((arg))) 144 | { 145 | PyErr_SetString(PyExc_TypeError, 146 | "Unicode-objects must be encoded as bytes first"); 147 | return NULL; 148 | } 149 | GET_BUFFER_VIEW_OR_ERROUT(arg, &src_py_buffer); 150 | src_ptr = src_py_buffer.buf; 151 | src_len = src_py_buffer.len; 152 | 153 | src_end_ptr = src_ptr + src_len; 154 | 155 | /* Make an output string */ 156 | dst_py_obj_ptr = PyBytes_FromStringAndSize(NULL, COBS_ENCODE_DST_BUF_LEN_MAX(src_len)); 157 | if (dst_py_obj_ptr == NULL) 158 | { 159 | return NULL; 160 | } 161 | dst_buf_ptr = PyBytes_AsString(dst_py_obj_ptr); 162 | 163 | /* Encode */ 164 | dst_code_write_ptr = dst_buf_ptr; 165 | dst_write_ptr = dst_code_write_ptr + 1; 166 | search_len = 1; 167 | 168 | /* Iterate over the source bytes */ 169 | if (src_len != 0) 170 | { 171 | for (;;) 172 | { 173 | src_byte = *src_ptr++; 174 | if (src_byte == 0) 175 | { 176 | /* We found a zero byte */ 177 | *dst_code_write_ptr = (char) search_len; 178 | dst_code_write_ptr = dst_write_ptr++; 179 | search_len = 1; 180 | if (src_ptr >= src_end_ptr) 181 | { 182 | break; 183 | } 184 | } 185 | else 186 | { 187 | /* Copy the non-zero byte to the destination buffer */ 188 | *dst_write_ptr++ = src_byte; 189 | search_len++; 190 | if (src_ptr >= src_end_ptr) 191 | { 192 | break; 193 | } 194 | if (search_len == 0xFF) 195 | { 196 | /* We have a long string of non-zero bytes */ 197 | *dst_code_write_ptr = (char) search_len; 198 | dst_code_write_ptr = dst_write_ptr++; 199 | search_len = 1; 200 | } 201 | } 202 | } 203 | } 204 | 205 | /* We're done with the input buffer now, so we have to release the PyBuffer. */ 206 | PyBuffer_Release(&src_py_buffer); 207 | 208 | /* We've reached the end of the source data. 209 | * Finalise the remaining output. In particular, write the code (length) byte. 210 | * Update the pointer to calculate the final output length. 211 | */ 212 | *dst_code_write_ptr = (char) search_len; 213 | 214 | /* Calculate the output length, from the value of dst_code_write_ptr */ 215 | _PyBytes_Resize(&dst_py_obj_ptr, dst_write_ptr - dst_buf_ptr); 216 | 217 | return dst_py_obj_ptr; 218 | } 219 | 220 | 221 | /* 222 | * cobs.decode 223 | */ 224 | PyDoc_STRVAR(cobs_decode__doc__, 225 | "Decode a string using Consistent Overhead Byte Stuffing (COBS).\n" 226 | "\n" 227 | "Input should be a byte string that has been COBS encoded. Output\n" 228 | "is also a byte string.\n" 229 | "\n" 230 | "A cobs.DecodeError exception will be raised if the encoded data\n" 231 | "is invalid." 232 | ); 233 | 234 | /* 235 | * This Python C extension function uses arguments method METH_O, 236 | * meaning the arg parameter contains the single parameter 237 | * to the function. 238 | */ 239 | static PyObject* 240 | cobs_decode(PyObject* module, PyObject* arg) 241 | { 242 | Py_buffer src_py_buffer; 243 | const char * src_ptr; 244 | Py_ssize_t src_len; 245 | const char * src_end_ptr; 246 | char * dst_buf_ptr; 247 | char * dst_write_ptr; 248 | Py_ssize_t remaining_bytes; 249 | unsigned char len_code; 250 | unsigned char src_byte; 251 | unsigned char i; 252 | PyObject * dst_py_obj_ptr; 253 | 254 | 255 | if (PyUnicode_Check((arg))) 256 | { 257 | PyErr_SetString(PyExc_TypeError, 258 | "Unicode-objects are not supported; byte buffer objects only"); 259 | return NULL; 260 | } 261 | GET_BUFFER_VIEW_OR_ERROUT(arg, &src_py_buffer); 262 | src_ptr = src_py_buffer.buf; 263 | src_len = src_py_buffer.len; 264 | 265 | src_end_ptr = src_ptr + src_len; 266 | 267 | /* Make an output string */ 268 | dst_py_obj_ptr = PyBytes_FromStringAndSize(NULL, COBS_DECODE_DST_BUF_LEN_MAX(src_len)); 269 | if (dst_py_obj_ptr == NULL) 270 | { 271 | PyBuffer_Release(&src_py_buffer); 272 | return NULL; 273 | } 274 | dst_buf_ptr = PyBytes_AsString(dst_py_obj_ptr); 275 | 276 | /* Decode */ 277 | dst_write_ptr = dst_buf_ptr; 278 | 279 | if (src_len != 0) 280 | { 281 | for (;;) 282 | { 283 | len_code = (unsigned char) *src_ptr++; 284 | if (len_code == 0) 285 | { 286 | PyBuffer_Release(&src_py_buffer); 287 | Py_DECREF(dst_py_obj_ptr); 288 | PyErr_SetString(GETSTATE(module)->CobsDecodeError, "zero byte found in input"); 289 | return NULL; 290 | } 291 | len_code--; 292 | 293 | remaining_bytes = src_end_ptr - src_ptr; 294 | if (len_code > remaining_bytes) 295 | { 296 | PyBuffer_Release(&src_py_buffer); 297 | Py_DECREF(dst_py_obj_ptr); 298 | PyErr_SetString(GETSTATE(module)->CobsDecodeError, "not enough input bytes for length code"); 299 | return NULL; 300 | } 301 | 302 | for (i = len_code; i != 0; i--) 303 | { 304 | src_byte = *src_ptr++; 305 | if (src_byte == 0) 306 | { 307 | PyBuffer_Release(&src_py_buffer); 308 | Py_DECREF(dst_py_obj_ptr); 309 | PyErr_SetString(GETSTATE(module)->CobsDecodeError, "zero byte found in input"); 310 | return NULL; 311 | } 312 | *dst_write_ptr++ = src_byte; 313 | } 314 | 315 | if (src_ptr >= src_end_ptr) 316 | { 317 | break; 318 | } 319 | 320 | /* Add a zero to the end */ 321 | if (len_code != 0xFE) 322 | { 323 | *dst_write_ptr++ = 0; 324 | } 325 | } 326 | } 327 | 328 | /* We're done with the input buffer now, so we have to release the PyBuffer. */ 329 | PyBuffer_Release(&src_py_buffer); 330 | 331 | /* Calculate the output length, from the value of dst_code_write_ptr */ 332 | _PyBytes_Resize(&dst_py_obj_ptr, dst_write_ptr - dst_buf_ptr); 333 | 334 | return dst_py_obj_ptr; 335 | } 336 | 337 | 338 | /***************************************************************************** 339 | * Module definitions 340 | ****************************************************************************/ 341 | 342 | PyDoc_STRVAR(module__doc__, 343 | "Consistent Overhead Byte Stuffing (COBS)" 344 | ); 345 | 346 | static PyMethodDef methodTable[] = 347 | { 348 | { "encode", cobs_encode, METH_O, cobs_encode__doc__ }, 349 | { "decode", cobs_decode, METH_O, cobs_decode__doc__ }, 350 | { NULL, NULL, 0, NULL } 351 | }; 352 | 353 | 354 | static struct PyModuleDef moduleDef = 355 | { 356 | PyModuleDef_HEAD_INIT, 357 | "_cobs_ext", // name of module 358 | module__doc__, // module documentation 359 | sizeof(struct module_state), // size of per-interpreter state of the module 360 | methodTable, 361 | NULL, 362 | cobs_traverse, 363 | cobs_clear, 364 | NULL 365 | }; 366 | 367 | 368 | /***************************************************************************** 369 | * Module initialisation 370 | ****************************************************************************/ 371 | 372 | PyMODINIT_FUNC 373 | PyInit__cobs_ext(void) 374 | { 375 | PyObject * module; 376 | struct module_state * st; 377 | 378 | 379 | /* Initialise cobs module C extension cobs._cobsext */ 380 | module = PyModule_Create(&moduleDef); 381 | if (module == NULL) 382 | { 383 | return NULL; 384 | } 385 | 386 | st = GETSTATE(module); 387 | 388 | /* Initialise cobs.DecodeError exception class. */ 389 | st->CobsDecodeError = PyErr_NewException("_cobs_ext.DecodeError", NULL, NULL); 390 | if (st->CobsDecodeError == NULL) 391 | { 392 | Py_DECREF(module); 393 | return NULL; 394 | } 395 | Py_INCREF(st->CobsDecodeError); 396 | PyModule_AddObject(module, "DecodeError", st->CobsDecodeError); 397 | 398 | return module; 399 | } 400 | 401 | -------------------------------------------------------------------------------- /src/ext/_cobsr_ext.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Consistent Overhead Byte Stuffing/Reduced (COBS/R) 3 | * 4 | * Python C extension for COBS/R encoding and decoding functions. 5 | * 6 | * Copyright (c) 2010 Craig McQueen 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to 10 | * deal in the Software without restriction, including without limitation the 11 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | * sell copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | * IN THE SOFTWARE. 25 | */ 26 | 27 | 28 | /***************************************************************************** 29 | * Includes 30 | ****************************************************************************/ 31 | 32 | // Force Py_ssize_t to be used for s# conversions. 33 | #define PY_SSIZE_T_CLEAN 34 | #include 35 | 36 | 37 | /***************************************************************************** 38 | * Defines 39 | ****************************************************************************/ 40 | 41 | #ifndef FALSE 42 | #define FALSE (0) 43 | #endif 44 | 45 | #ifndef TRUE 46 | #define TRUE (!FALSE) 47 | #endif 48 | 49 | 50 | #define GETSTATE(M) ((struct module_state *) PyModule_GetState(M)) 51 | 52 | 53 | /* 54 | * Given a PyObject* obj, fill in the Py_buffer* viewp with the result 55 | * of PyObject_GetBuffer. Sets and exception and issues a return NULL 56 | * on any errors. 57 | */ 58 | #define GET_BUFFER_VIEW_OR_ERROUT(obj, viewp) do { \ 59 | if (!PyObject_CheckBuffer((obj))) { \ 60 | PyErr_SetString(PyExc_TypeError, \ 61 | "object supporting the buffer API is required"); \ 62 | return NULL; \ 63 | } \ 64 | if (PyObject_GetBuffer((obj), (viewp), PyBUF_FORMAT) == -1) { \ 65 | return NULL; \ 66 | } \ 67 | if (((viewp)->ndim > 1) || ((viewp)->itemsize > 1)) { \ 68 | PyErr_SetString(PyExc_BufferError, \ 69 | "object must be a single-dimension buffer of bytes"); \ 70 | PyBuffer_Release((viewp)); \ 71 | return NULL; \ 72 | } \ 73 | } while(0); 74 | 75 | 76 | #define COBSR_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + ((SRC_LEN)/254u) + 1) 77 | #define COBSR_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) <= 1) ? 1 : (SRC_LEN)) 78 | 79 | 80 | /***************************************************************************** 81 | * Types 82 | ****************************************************************************/ 83 | 84 | struct module_state 85 | { 86 | /* cobsr.DecodeError exception class. */ 87 | PyObject * CobsrDecodeError; 88 | }; 89 | 90 | 91 | /***************************************************************************** 92 | * Functions 93 | ****************************************************************************/ 94 | 95 | static int cobsr_traverse(PyObject *m, visitproc visit, void *arg) 96 | { 97 | Py_VISIT(GETSTATE(m)->CobsrDecodeError); 98 | return 0; 99 | } 100 | 101 | 102 | static int cobsr_clear(PyObject *m) 103 | { 104 | Py_CLEAR(GETSTATE(m)->CobsrDecodeError); 105 | return 0; 106 | } 107 | 108 | 109 | /* 110 | * cobsr.encode 111 | */ 112 | PyDoc_STRVAR(cobsr_encode__doc__, 113 | "Encode a string using Consistent Overhead Byte Stuffing/Reduced (COBS/R).\n" 114 | "\n" 115 | "Input is any byte string. Output is also a byte string.\n" 116 | "\n" 117 | "Encoding guarantees no zero bytes in the output. The output\n" 118 | "string may be expanded slightly, by a predictable amount.\n" 119 | "\n" 120 | "An empty string is encoded to '\\x01'." 121 | ); 122 | 123 | /* 124 | * This Python C extension function uses arguments method METH_O, 125 | * meaning the arg parameter contains the single parameter 126 | * to the function. 127 | */ 128 | static PyObject* 129 | cobsr_encode(PyObject* module, PyObject* arg) 130 | { 131 | Py_buffer src_py_buffer; 132 | const char * src_ptr; 133 | Py_ssize_t src_len; 134 | const char * src_end_ptr; 135 | char * dst_buf_ptr; 136 | char * dst_code_write_ptr; 137 | char * dst_write_ptr; 138 | unsigned char src_byte; 139 | unsigned char search_len; 140 | PyObject * dst_py_obj_ptr; 141 | 142 | 143 | if (PyUnicode_Check((arg))) 144 | { 145 | PyErr_SetString(PyExc_TypeError, 146 | "Unicode-objects must be encoded as bytes first"); 147 | return NULL; 148 | } 149 | GET_BUFFER_VIEW_OR_ERROUT(arg, &src_py_buffer); 150 | src_ptr = src_py_buffer.buf; 151 | src_len = src_py_buffer.len; 152 | 153 | src_end_ptr = src_ptr + src_len; 154 | 155 | /* Make an output string */ 156 | dst_py_obj_ptr = PyBytes_FromStringAndSize(NULL, COBSR_ENCODE_DST_BUF_LEN_MAX(src_len)); 157 | if (dst_py_obj_ptr == NULL) 158 | { 159 | return NULL; 160 | } 161 | dst_buf_ptr = PyBytes_AsString(dst_py_obj_ptr); 162 | 163 | /* Encode */ 164 | dst_code_write_ptr = dst_buf_ptr; 165 | dst_write_ptr = dst_code_write_ptr + 1; 166 | search_len = 1; 167 | src_byte = 0; 168 | 169 | /* Iterate over the source bytes */ 170 | if (src_len != 0) 171 | { 172 | for (;;) 173 | { 174 | src_byte = *src_ptr++; 175 | if (src_byte == 0) 176 | { 177 | /* We found a zero byte */ 178 | *dst_code_write_ptr = (char) search_len; 179 | dst_code_write_ptr = dst_write_ptr++; 180 | search_len = 1; 181 | if (src_ptr >= src_end_ptr) 182 | { 183 | break; 184 | } 185 | } 186 | else 187 | { 188 | /* Copy the non-zero byte to the destination buffer */ 189 | *dst_write_ptr++ = src_byte; 190 | search_len++; 191 | if (src_ptr >= src_end_ptr) 192 | { 193 | break; 194 | } 195 | if (search_len == 0xFF) 196 | { 197 | /* We have a long string of non-zero bytes */ 198 | *dst_code_write_ptr = (char) search_len; 199 | dst_code_write_ptr = dst_write_ptr++; 200 | search_len = 1; 201 | } 202 | } 203 | } 204 | } 205 | 206 | /* We're done with the input buffer now, so we have to release the PyBuffer. */ 207 | PyBuffer_Release(&src_py_buffer); 208 | 209 | /* We've reached the end of the source data. 210 | * Finalise the remaining output. In particular, write the code (length) byte. 211 | * 212 | * For COBS/R, the final code (length) byte is special: if the final data byte is 213 | * greater than or equal to what would normally be the final code (length) byte, 214 | * then replace the final code byte with the final data byte, and remove the final 215 | * data byte from the end of the sequence. This saves one byte in the output. 216 | * 217 | * Update the pointer to calculate the final output length. 218 | */ 219 | if (src_byte < search_len) 220 | { 221 | /* Encoding same as plain COBS */ 222 | *dst_code_write_ptr = (char) search_len; 223 | } 224 | else 225 | { 226 | /* Special COBS/R encoding: length code is final byte, 227 | * and final byte is removed from data sequence. */ 228 | *dst_code_write_ptr = (char) src_byte; 229 | dst_write_ptr--; 230 | } 231 | 232 | /* Calculate the output length, from the value of dst_code_write_ptr */ 233 | _PyBytes_Resize(&dst_py_obj_ptr, dst_write_ptr - dst_buf_ptr); 234 | 235 | return dst_py_obj_ptr; 236 | } 237 | 238 | 239 | /* 240 | * cobsr.decode 241 | */ 242 | PyDoc_STRVAR(cobsr_decode__doc__, 243 | "Decode a string using Consistent Overhead Byte Stuffing/Reduced (COBS/R).\n" 244 | "\n" 245 | "Input should be a byte string that has been COBS/R encoded. Output\n" 246 | "is also a byte string.\n" 247 | "\n" 248 | "A cobsr.DecodeError exception will be raised if the encoded data\n" 249 | "is invalid. That is, if the encoded data contains zeros." 250 | ); 251 | 252 | /* 253 | * This Python C extension function uses arguments method METH_O, 254 | * meaning the arg parameter contains the single parameter 255 | * to the function. 256 | */ 257 | static PyObject* 258 | cobsr_decode(PyObject* module, PyObject* arg) 259 | { 260 | Py_buffer src_py_buffer; 261 | const char * src_ptr; 262 | Py_ssize_t src_len; 263 | const char * src_end_ptr; 264 | char * dst_buf_ptr; 265 | char * dst_write_ptr; 266 | Py_ssize_t remaining_bytes; 267 | unsigned char len_code; 268 | unsigned char src_byte; 269 | unsigned char i; 270 | PyObject * dst_py_obj_ptr; 271 | 272 | 273 | if (PyUnicode_Check((arg))) 274 | { 275 | PyErr_SetString(PyExc_TypeError, 276 | "Unicode-objects are not supported; byte buffer objects only"); 277 | return NULL; 278 | } 279 | GET_BUFFER_VIEW_OR_ERROUT(arg, &src_py_buffer); 280 | src_ptr = src_py_buffer.buf; 281 | src_len = src_py_buffer.len; 282 | 283 | src_end_ptr = src_ptr + src_len; 284 | 285 | /* Make an output string */ 286 | dst_py_obj_ptr = PyBytes_FromStringAndSize(NULL, COBSR_DECODE_DST_BUF_LEN_MAX(src_len)); 287 | if (dst_py_obj_ptr == NULL) 288 | { 289 | PyBuffer_Release(&src_py_buffer); 290 | return NULL; 291 | } 292 | dst_buf_ptr = PyBytes_AsString(dst_py_obj_ptr); 293 | 294 | /* Decode */ 295 | dst_write_ptr = dst_buf_ptr; 296 | 297 | if (src_len != 0) 298 | { 299 | for (;;) 300 | { 301 | len_code = (unsigned char) *src_ptr++; 302 | if (len_code == 0) 303 | { 304 | PyBuffer_Release(&src_py_buffer); 305 | Py_DECREF(dst_py_obj_ptr); 306 | PyErr_SetString(GETSTATE(module)->CobsrDecodeError, "zero byte found in input"); 307 | return NULL; 308 | } 309 | 310 | remaining_bytes = src_end_ptr - src_ptr; 311 | 312 | if ((len_code - 1) < remaining_bytes) 313 | { 314 | for (i = len_code - 1; i != 0; i--) 315 | { 316 | src_byte = *src_ptr++; 317 | if (src_byte == 0) 318 | { 319 | PyBuffer_Release(&src_py_buffer); 320 | Py_DECREF(dst_py_obj_ptr); 321 | PyErr_SetString(GETSTATE(module)->CobsrDecodeError, "zero byte found in input"); 322 | return NULL; 323 | } 324 | *dst_write_ptr++ = src_byte; 325 | } 326 | 327 | /* Add a zero to the end */ 328 | if (len_code != 0xFF) 329 | { 330 | *dst_write_ptr++ = 0; 331 | } 332 | } 333 | else 334 | { 335 | /* We've reached the last length code, so write the remaining 336 | * bytes and then exit the loop. */ 337 | 338 | for (i = remaining_bytes; i != 0; i--) 339 | { 340 | src_byte = *src_ptr++; 341 | if (src_byte == 0) 342 | { 343 | PyBuffer_Release(&src_py_buffer); 344 | Py_DECREF(dst_py_obj_ptr); 345 | PyErr_SetString(GETSTATE(module)->CobsrDecodeError, "zero byte found in input"); 346 | return NULL; 347 | } 348 | *dst_write_ptr++ = src_byte; 349 | } 350 | 351 | /* Write final data byte, if applicable for COBS/R encoding. */ 352 | if (len_code - 1 > remaining_bytes) 353 | { 354 | *dst_write_ptr++ = len_code; 355 | } 356 | 357 | /* Exit the loop */ 358 | break; 359 | } 360 | } 361 | } 362 | 363 | /* We're done with the input buffer now, so we have to release the PyBuffer. */ 364 | PyBuffer_Release(&src_py_buffer); 365 | 366 | /* Calculate the output length, from the value of dst_code_write_ptr */ 367 | _PyBytes_Resize(&dst_py_obj_ptr, dst_write_ptr - dst_buf_ptr); 368 | 369 | return dst_py_obj_ptr; 370 | } 371 | 372 | 373 | /***************************************************************************** 374 | * Module definitions 375 | ****************************************************************************/ 376 | 377 | PyDoc_STRVAR(module__doc__, 378 | "Consistent Overhead Byte Stuffing/Reduced (COBS/R)" 379 | ); 380 | 381 | static PyMethodDef methodTable[] = 382 | { 383 | { "encode", cobsr_encode, METH_O, cobsr_encode__doc__ }, 384 | { "decode", cobsr_decode, METH_O, cobsr_decode__doc__ }, 385 | { NULL, NULL, 0, NULL } 386 | }; 387 | 388 | 389 | static struct PyModuleDef moduleDef = 390 | { 391 | PyModuleDef_HEAD_INIT, 392 | "_cobsr_ext", // name of module 393 | module__doc__, // module documentation 394 | sizeof(struct module_state), // size of per-interpreter state of the module 395 | methodTable, 396 | NULL, 397 | cobsr_traverse, 398 | cobsr_clear, 399 | NULL 400 | }; 401 | 402 | 403 | /***************************************************************************** 404 | * Module initialisation 405 | ****************************************************************************/ 406 | 407 | PyMODINIT_FUNC 408 | PyInit__cobsr_ext(void) 409 | { 410 | PyObject * module; 411 | struct module_state * st; 412 | 413 | 414 | /* Initialise cobsr module C extension cobsr._cobsr_ext */ 415 | module = PyModule_Create(&moduleDef); 416 | if (module == NULL) 417 | { 418 | return NULL; 419 | } 420 | 421 | st = GETSTATE(module); 422 | 423 | /* Initialise cobsr.DecodeError exception class. */ 424 | st->CobsrDecodeError = PyErr_NewException("_cobsr_ext.DecodeError", NULL, NULL); 425 | if (st->CobsrDecodeError == NULL) 426 | { 427 | Py_DECREF(module); 428 | return NULL; 429 | } 430 | Py_INCREF(st->CobsrDecodeError); 431 | PyModule_AddObject(module, "DecodeError", st->CobsrDecodeError); 432 | 433 | return module; 434 | } 435 | 436 | -------------------------------------------------------------------------------- /test/plot_cobsr_overhead.py: -------------------------------------------------------------------------------- 1 | 2 | from matplotlib import pyplot as plt 3 | import numpy as np 4 | from cobs import cobs 5 | from cobs import cobsr 6 | 7 | 8 | def cobsr_overhead_calc(num_bytes): 9 | return 257./256 - (255./256)**num_bytes 10 | 11 | def cobsr_overhead_measure(num_bytes): 12 | # TODO: review value 13 | NUM_TESTS = 10000 14 | overhead = 0 15 | for _i in xrange(NUM_TESTS): 16 | output = cobsr.encode(np.random.bytes(num_bytes)) 17 | overhead += (len(output) - num_bytes) 18 | return overhead / float(NUM_TESTS) 19 | 20 | def cobs_overhead_measure(num_bytes): 21 | # TODO: review value 22 | NUM_TESTS = 10000 23 | overhead = 0 24 | for _i in xrange(NUM_TESTS): 25 | output = cobs.encode(np.random.bytes(num_bytes)) 26 | overhead += (len(output) - num_bytes) 27 | return overhead / float(NUM_TESTS) 28 | 29 | fig = plt.figure() 30 | ax1 = fig.add_subplot(111) 31 | 32 | # x-range for plot 33 | num_bytes_list = np.arange(1, 30) 34 | 35 | # Calculate values and plot 36 | 37 | # Measured values for COBS 38 | #cobs_measured_overhead = [ cobs_overhead_measure(num_bytes) for num_bytes in num_bytes_list ] 39 | #ax1.plot(num_bytes_list, cobs_measured_overhead, 'g.') 40 | 41 | # Measured values for COBS/R 42 | cobsr_measured_overhead = [ cobsr_overhead_measure(num_bytes) for num_bytes in num_bytes_list ] 43 | ax1.plot(num_bytes_list, cobsr_measured_overhead, 'r.') 44 | 45 | # Calculated values for COBS/R 46 | cobsr_calc_overhead = [ cobsr_overhead_calc(num_bytes) for num_bytes in num_bytes_list ] 47 | ax1.plot(num_bytes_list, cobsr_calc_overhead, 'b.') 48 | 49 | ax1.set_xlabel('message length (bytes)') 50 | ax1.set_xlim(min(num_bytes_list), max(num_bytes_list)) 51 | 52 | # Make the y-axis label and tick labels match the line color. 53 | ax1.set_ylabel('encoding overhead (bytes)') 54 | if 0: 55 | ax1.set_ylabel('encoding overhead (bytes)', color='b') 56 | for tl in ax1.get_yticklabels(): 57 | tl.set_color('b') 58 | 59 | plt.show() 60 | -------------------------------------------------------------------------------- /test/test_cobs.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | import cobs.cobs.test 5 | 6 | unittest.main(module=cobs.cobs.test) 7 | -------------------------------------------------------------------------------- /test/test_cobsr.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | import cobs.cobsr.test 5 | 6 | unittest.main(module=cobs.cobsr.test) 7 | --------------------------------------------------------------------------------