diff --git a/.github/workflows/push-dev.yml b/.github/workflows/push-dev.yml new file mode 100644 index 0000000..dea848d --- /dev/null +++ b/.github/workflows/push-dev.yml @@ -0,0 +1,12 @@ +name: Push (dev), Pull Request +on: + push: + branches: ["**"] + pull_request: +jobs: + lint-python: + name: Run Python lint + uses: kuba2k2/kuba2k2/.github/workflows/lint-python.yml@master + test-python: + name: Run Python tests + uses: kuba2k2/kuba2k2/.github/workflows/test-python.yml@master diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 071540d..357363c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,10 +6,14 @@ jobs: lint-python: name: Run Python lint uses: kuba2k2/kuba2k2/.github/workflows/lint-python.yml@master + test-python: + name: Run Python tests + uses: kuba2k2/kuba2k2/.github/workflows/test-python.yml@master publish-pypi: name: Publish PyPI package needs: - lint-python + - test-python uses: kuba2k2/kuba2k2/.github/workflows/publish-pypi.yml@master secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/datastruct/adapters/time.py b/datastruct/adapters/time.py index 752c3ff..11ee8a8 100644 --- a/datastruct/adapters/time.py +++ b/datastruct/adapters/time.py @@ -13,7 +13,7 @@ def filetime_field(*, default=...): " bytes: return adapter(ByteStr())(field(length, default=default)) -def varbytes( - length: Value[int], - *, - default: bytes = ... -): + +def varbytes(length: Value[int], *, default: bytes = ...): return field( - lambda ctx: ( - len(ctx.P.self) if ctx.G.packing else evaluate(ctx, length) - ), + lambda ctx: (len(ctx.P.self) if ctx.G.packing else evaluate(ctx, length)), default=default, ) + def text( length: Value[int], *, diff --git a/poetry.lock b/poetry.lock index fa1eb6a..4cb9ab6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,227 +1,531 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "autoflake" +version = "2.3.1" +description = "Removes unused imports and unused variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840"}, + {file = "autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e"}, +] + +[package.dependencies] +pyflakes = ">=3.0.0" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[[package]] +name = "bitstruct" +version = "8.19.0" +description = "This module performs conversions between Python values and C bit field structs represented as Python byte strings." +optional = false +python-versions = ">=3.7" +files = [ + {file = "bitstruct-8.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d1f3eb18ddc33ba73f5cbb55c885584bcec51c421ac3551b79edc0ffeaecc3d"}, + {file = "bitstruct-8.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35e0b267d12438e6a7b28850a15d4cffe767db6fc443a406d0ead97fa1d7d5b"}, + {file = "bitstruct-8.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5732aff5c8eb3a572f7b20d09fc4c213215f9e60c0e66f2910b31eb65b457744"}, + {file = "bitstruct-8.19.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc8f1871b42b705eb34b8722c3ec358fbf1b97fd37a62693564ee72648afb100"}, + {file = "bitstruct-8.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:01bdfc3adbe15b05ba27ab6dce7959caa29a000f066201944b29c64bb8888f03"}, + {file = "bitstruct-8.19.0-cp310-cp310-win32.whl", hash = "sha256:961845a29333119b70dd9aab54bc714bf9ba5efefc55cb4c747c35c1390b8842"}, + {file = "bitstruct-8.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fbe12d464db909f58d5e2a2485b3047a488fa1373e8f74b22d6759ee6b2437a"}, + {file = "bitstruct-8.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1300cd635814e40b1f4105aa4f404cb5d1b8cc54e06e267ba1616725f9c2beea"}, + {file = "bitstruct-8.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2fb23b5973ce1e9f349c4dc90873eeff9800fe917ffd345f39b9b964f6d119"}, + {file = "bitstruct-8.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e0c18d557474d8452c4f8b59320fd4d9efcf52eae2144bdf317d25c64dcf85"}, + {file = "bitstruct-8.19.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bba06607f956cc39ceee19fd11b542e8e66a43180d48fa36c4609443893c273e"}, + {file = "bitstruct-8.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f2fa607d111077145e6374d49be6098f33e7cee0967b42cfc117df53eee13332"}, + {file = "bitstruct-8.19.0-cp311-cp311-win32.whl", hash = "sha256:abdb7bdb5b04c2f1bbda0eae828c627252243ddc042aea6b72af8fcc63696598"}, + {file = "bitstruct-8.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:464f102999402a2624ee3106dbfa1f3745810036814a33e6bc706b7d312c480f"}, + {file = "bitstruct-8.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:55768b1f5e33594178f0b3e1596b89d831b006713a60caa09de61fd385bf22b1"}, + {file = "bitstruct-8.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c026a7cf8d954ef53cf4d0ae5ee3dd1ac66e24e9a474c5afe55467ab7d609f2e"}, + {file = "bitstruct-8.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7488fd4e2fde3d8111971e2040cd5b008be918381afc80387d3fdf047c801293"}, + {file = "bitstruct-8.19.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:45b66e20633f1e083e37fa396c81761e0fc688ffa06ff5559e990e37234f9e18"}, + {file = "bitstruct-8.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9c1542d5ae888ebc31614775938bfd13454f0d897dc2515363a4607efadc990b"}, + {file = "bitstruct-8.19.0-cp312-cp312-win32.whl", hash = "sha256:7ea57e4e793b595cd3e037920852f2c676b4f5f1734c41985db3f48783928e2c"}, + {file = "bitstruct-8.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c4d9b75248adee84e7e6c95bf95966f152b78363cb20a81920da2aeadc4375f"}, + {file = "bitstruct-8.19.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7b4745b099d3d85307495e25ff0f265deeea675621dcecb25ba059ee68ce88d5"}, + {file = "bitstruct-8.19.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645da560acd20dd73a1ef220e3ddc08e108866e30a708ef2f6193e0a3725113e"}, + {file = "bitstruct-8.19.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01402fbc3dba2286b3ac9b74d5936dd984736f928aacd371458a4b0cf95f0755"}, + {file = "bitstruct-8.19.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2c5eda42d55db67072c6cf7cc79b1df1074269004bad119b79e4ad38cfa61877"}, + {file = "bitstruct-8.19.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2ea093522b12ce714a3a95851a8c3dd97f620126bbe983eb261b3bf18ac945e7"}, + {file = "bitstruct-8.19.0-cp37-cp37m-win32.whl", hash = "sha256:da00da004830800323554e7a83f1f32a1f49345f5379476de4b5f6ae227ee962"}, + {file = "bitstruct-8.19.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0ca55fba25d6c631e17933f20cf87f553d7bceec7659e3de9ef48dc85ced2bf"}, + {file = "bitstruct-8.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3f6e3aeb598215062c505a06135fbdfa3bb4eeb249b55f87e865a86b3fd9e99"}, + {file = "bitstruct-8.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df74c72feba80014b05ab6f1e1a0bb90be9f9e7eb60a9bab1e00728f7f46d79d"}, + {file = "bitstruct-8.19.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:976c39ad771c6773d6fbd14d71e62242d5b3bca7b72428fd183e1f1085d5e858"}, + {file = "bitstruct-8.19.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d2c176ff6727206805760f45c2151468aed843256aa239c14f4730b9e1d84fc7"}, + {file = "bitstruct-8.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7774e2a51e254ef1ba98a1ee38573c819d4ee7e396d5121c5ecae17df927501"}, + {file = "bitstruct-8.19.0-cp38-cp38-win32.whl", hash = "sha256:b86d192d658eaf35f10efb2e1940ec755cc28e081f46de294a2e91a74ea298aa"}, + {file = "bitstruct-8.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:5e7f78aedec2881017026eb7f7ab79514aef09a24afd8acf5fa8c73b1cd0e9f4"}, + {file = "bitstruct-8.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bb49acc2ccc6efd3c9613cae8f7e1316c92f832bff860a6fcb78a4275974e90"}, + {file = "bitstruct-8.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bed7b2761c18a515298145a4f67b6c71ce302453fe7d87ec6b7d2e77fd3c22b"}, + {file = "bitstruct-8.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d0cafd2e2974c4bbe349fb67951d43d221ea304218c2ee65f9fe4c62acabc2f"}, + {file = "bitstruct-8.19.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d9ba0299f624e7c8ea1eec926fc77741f82ffc5b3c3ba4f89303d33d5605f4d8"}, + {file = "bitstruct-8.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfa0326057c9b02c4e65e74e45b9914a7f8c59590a8e718e20a899a02b41f2e6"}, + {file = "bitstruct-8.19.0-cp39-cp39-win32.whl", hash = "sha256:14c3ebdec92c486142327d934cb451d96b411543ec6f72aeb2b4b4334e9408bf"}, + {file = "bitstruct-8.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:7836852d5c15444e87a2029f922b48717e6e199d2332d55e8738e92d8590987e"}, + {file = "bitstruct-8.19.0.tar.gz", hash = "sha256:d75ba9dded85c17e885a209a00eb8e248ee40762149f2f2a79360ca857467dac"}, +] + [[package]] name = "black" -version = "22.12.0" +version = "24.8.0" description = "The uncompromising code formatter." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] -name = "importlib-metadata" -version = "6.0.0" -description = "Read metadata from Python packages" -category = "dev" +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] [[package]] name = "isort" -version = "5.11.4" +version = "5.13.2" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] [package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] +colors = ["colorama (>=0.4.6)"] [[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +name = "macaddress" +version = "2.0.2" +description = "Like ``ipaddress``, but for hardware identifiers such as MAC addresses." optional = false python-versions = "*" +files = [ + {file = "macaddress-2.0.2-py3-none-any.whl", hash = "sha256:6f4a0430f9b5af6d98a582b8d527ba2cd3f0825fce5503a9ce5c73acb772c30f"}, + {file = "macaddress-2.0.2.tar.gz", hash = "sha256:1400ccdc28d747102d57ae61e5b78d8985872930810ceb8860cd49abd1e1fa37"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] [[package]] name = "pathspec" -version = "0.10.3" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] [[package]] name = "platformdirs" -version = "2.6.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycryptodome" +version = "3.21.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, + {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] [package.dependencies] -typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" +name = "tomli" +version = "2.0.2" +description = "A lil' TOML parser" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, +] [[package]] name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] [[package]] -name = "zipp" -version = "3.11.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "7d7e09eb0090309848438532354df91ebb9cd58f45b7d2b3aa3eb29085f24bd1" - -[metadata.files] -black = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -importlib-metadata = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, -] -isort = [ - {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, - {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -pathspec = [ - {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, - {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, -] -platformdirs = [ - {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, - {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -zipp = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, -] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "e9db26b8a12074561c233b7fc17b1b7b653e87d1a839c29a11f5917932025dcc" diff --git a/pyproject.toml b/pyproject.toml index a857f1d..d6c1010 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,19 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8" [tool.poetry.group.dev.dependencies] -black = "^22.12.0" -isort = "^5.11.4" +black = "^24.1.0" +isort = "^5.12.0" +autoflake = "^2.1.1" + +[tool.poetry.group.test.dependencies] +pytest = "^8.3.3" +macaddress = "^2.0.2" +pycryptodome = "^3.21.0" +requests = "^2.32.3" +bitstruct = "^8.19.0" [build-system] requires = ["poetry-core"] diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..6704781 --- /dev/null +++ b/tests/base.py @@ -0,0 +1,142 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-11. + +import re +from dataclasses import dataclass +from pprint import pformat +from typing import Type + +import pytest + +from datastruct import DataStruct + + +@dataclass +class TestData: + __test__ = False + + cls: Type[DataStruct] | None = None + data: bytes | None = None + obj_full: DataStruct | None = None + obj_simple: DataStruct | None = None + context: dict = None + + full_after_packing: bool = True + unpack_then_pack: bool = True + pack_then_unpack: bool = True + + def __post_init__(self) -> None: + if self.context is None: + self.context = {} + + +class TestBase: + test: TestData + + @pytest.fixture(scope="function", autouse=True) + def setup_and_teardown(self, test: TestData) -> None: + self.test = test + + def get_cls(self) -> Type[DataStruct]: + return self.test.cls or type(self.test.obj_full) or type(self.test.obj_simple) + + def obj_to_str(self, obj: DataStruct) -> str: + pp = pformat(obj) + # fix enum representation + pp = re.sub(r"<([^.]+\.[^:]+?):.+?>", "\\1", pp) + pp = re.sub(r"([A-Za-z][A-Za-z0-9]+?)\.0", "\\1(0)", pp) + return pp + + def bytes_to_hex_repr(self, data: bytes) -> str: + out = "" + for i in range(0, len(data), 16): + line = data[i : i + 16] + out += 'b"\\x' + line.hex(" ").replace(" ", "\\x") + '"\n' + return out + + def bytes_to_hex_str(self, data: bytes) -> str: + out = "" + for i in range(0, len(data), 16): + line = data[i : i + 16] + out += line.hex(" ") + "\n" + return out + + def test_unpack_from_bytes(self) -> None: + if self.test.data is None: + pytest.skip() + unpacked = self.get_cls().unpack(self.test.data, **self.test.context) + if self.test.obj_full is None: + print("Unpacked (from bytes):") + print(self.obj_to_str(unpacked)) + return + if unpacked != self.test.obj_full: + print() + print(unpacked) + print(self.test.obj_full) + assert unpacked == self.test.obj_full + + def test_pack_full_to_bytes(self) -> None: + if self.test.obj_full is None: + pytest.skip() + packed = self.test.obj_full.pack(**self.test.context) + if self.test.data is None: + print("Packed (full):") + print(self.bytes_to_hex_repr(packed)) + return + if packed != self.test.data: + print() + print(packed.hex(" ")) + print(self.test.data.hex(" ")) + assert packed == self.test.data + + def test_pack_simple_to_bytes(self) -> None: + if self.test.obj_simple is None: + pytest.skip() + packed = self.test.obj_simple.pack(**self.test.context) + if self.test.obj_full is None: + print("Unpacked (from simple):") + print(self.obj_to_str(self.test.obj_simple)) + if self.test.data is None: + print("Packed (simple):") + print(self.bytes_to_hex_repr(packed)) + return + if packed != self.test.data: + print() + print(packed.hex(" ")) + print(self.test.data.hex(" ")) + assert packed == self.test.data + + def test_full_after_packing(self) -> None: + if ( + not self.test.full_after_packing + or self.test.obj_full is None + or self.test.obj_simple is None + ): + pytest.skip() + self.test.obj_simple.pack(**self.test.context) + if self.test.obj_full != self.test.obj_simple: + print() + print(self.test.obj_full) + print(self.test.obj_simple) + assert self.test.obj_full == self.test.obj_simple + + def test_unpack_then_pack(self) -> None: + if not self.test.unpack_then_pack or self.test.data is None: + pytest.skip() + unpacked = self.get_cls().unpack(self.test.data, **self.test.context) + packed = unpacked.pack(**self.test.context) + if packed != self.test.data: + print() + print(packed.hex(" ")) + print(self.test.data.hex(" ")) + assert packed == self.test.data + + def test_pack_then_unpack(self) -> None: + if not self.test.pack_then_unpack or self.test.obj_full is None: + pytest.skip() + packed = self.test.obj_full.pack(**self.test.context) + unpacked = self.get_cls().unpack(packed, **self.test.context) + if unpacked != self.test.obj_full: + print() + print(unpacked) + print(self.test.obj_full) + assert unpacked == self.test.obj_full diff --git a/tests/test_ambz2.py b/tests/test_ambz2.py new file mode 100644 index 0000000..582fa7d --- /dev/null +++ b/tests/test_ambz2.py @@ -0,0 +1,113 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-11. + +import pytest +from base import TestBase, TestData +from test_ambz2_structs import * +from util import read_data_file + +config = get_image_config() + + +def build_firmware(): + # defaults from libretiny/boards/bw15 + board_flash = { + "part_table": (0x000000, 0x1000, 0x000000 + 0x1000), + "system": (0x001000, 0x1000, 0x001000 + 0x1000), + "calibration": (0x002000, 0x1000, 0x002000 + 0x1000), + "boot": (0x004000, 0x8000, 0x004000 + 0x8000), + "ota1": (0x00C000, 0xF8000, 0x00C000 + 0xF8000), + "ota2": (0x104000, 0xF8000, 0x104000 + 0xF8000), + "kvs": (0x1FC000, 0x4000, 0x1FC000 + 0x400), + } + + ptab_offset, _, ptab_end = board_flash["part_table"] + boot_offset, _, boot_end = board_flash["boot"] + ota1_offset, _, ota1_end = board_flash["ota1"] + + # build the partition table + ptable = PartitionTable(user_data=b"\xFF" * 256) + for region, type in config.ptable.items(): + offset, length, _ = board_flash[region] + hash_key = config.keys.hash_keys[region] + ptable.partitions.append( + PartitionRecord(offset, length, type, hash_key=hash_key), + ) + ptable = Image( + keyblock=build_keyblock(config, "part_table"), + header=ImageHeader( + type=ImageType.PARTAB, + ), + data=ptable, + ) + + # build boot image + region = "boot" + boot = Image( + keyblock=build_keyblock(config, region), + header=ImageHeader( + type=ImageType.BOOT, + user_keys=[config.keys.user_keys[region], FF_32], + ), + data=build_section(config.boot), + ) + + # build firmware (sub)images + firmware = [] + region = "ota1" + for idx, image in enumerate(config.fw): + obj = Image( + keyblock=build_keyblock(config, region), + header=ImageHeader( + type=image.type, + # use FF to allow recalculating by OTA code + serial=0xFFFFFFFF if idx == 0 else 0, + user_keys=( + [FF_32, config.keys.user_keys[region]] + if idx == 0 + else [FF_32, FF_32] + ), + ), + data=Firmware( + sections=[build_section(section) for section in image.sections], + ), + ) + # remove empty sections + obj.data.sections = [s for s in obj.data.sections if s.data] + firmware.append(obj) + if image.type != ImageType.XIP: + continue + # update SCE keys for XIP images + for section in obj.data.sections: + section.header.sce_key = config.keys.xip_sce_key + section.header.sce_iv = config.keys.xip_sce_iv + + # build main flash image + return Flash( + ptable=ptable, + boot=boot, + firmware=firmware, + ) + + +TEST_DATA = [ + pytest.param( + TestData( + cls=Flash, + data=read_data_file(TEST_DATA_URLS["image_flash_is.bin"]), + obj_full=None, + obj_simple=build_firmware(), + context=dict( + hash_key=config.keys.hash_keys["part_table"], + ), + ), + id="dummy", + ), +] + + +@pytest.mark.parametrize("test", TEST_DATA) +class TestAmbZ2(TestBase): + pass + + +del TestBase diff --git a/tests/test_ambz2_structs.py b/tests/test_ambz2_structs.py new file mode 100644 index 0000000..bdd2581 --- /dev/null +++ b/tests/test_ambz2_structs.py @@ -0,0 +1,686 @@ +# Copyright (c) Kuba Szczodrzyński 2023-1-19. +# https://github.com/libretiny-eu/ltchiptool/tree/master/ltchiptool/soc/ambz2/util/models + +from dataclasses import dataclass +from enum import Enum, IntEnum, IntFlag +from hashlib import sha256 +from hmac import HMAC +from typing import Any, Callable, Dict, Iterable, List, Type, TypeVar + +from util import read_data_file + +from datastruct import Adapter, Context, DataStruct, datastruct, sizeof +from datastruct.fields import ( + action, + adapter, + align, + alignto, + bitfield, + built, + checksum_end, + checksum_field, + checksum_start, + cond, + field, + packing, + padding, + repeat, + subfield, + switch, +) +from datastruct.utils.misc import pad_up + +GITHUB_URL = "https://github.com/kuba2k2/datastruct/raw/refs/heads/test-data/tests" +TEST_DATA_URLS = { + "image_flash_is.bin": f"{GITHUB_URL}/test_ambz2.image_flash_is.bin", + "raw.boot.sram.bin": f"{GITHUB_URL}/test_ambz2.raw.boot.sram.bin", + "raw.fwhs.sram.bin": f"{GITHUB_URL}/test_ambz2.raw.fwhs.sram.bin", + "raw.fwhs.xip_c.bin": f"{GITHUB_URL}/test_ambz2.raw.fwhs.xip_c.bin", + "raw.fwhs.xip_p.bin": f"{GITHUB_URL}/test_ambz2.raw.fwhs.xip_p.bin", +} + +FLASH_CALIBRATION = b"\x99\x99\x96\x96\x3F\xCC\x66\xFC\xC0\x33\xCC\x03\xE5\xDC\x31\x62" + +FF_48 = b"\xFF" * 48 +FF_32 = b"\xFF" * 32 +FF_16 = b"\xFF" * 16 + +T = TypeVar("T") + + +def str2enum(cls: Type[Enum], key: str): + if not key: + return None + try: + return next(e for e in cls if e.name.lower() == key.lower()) + except StopIteration: + return None + + +class FlashSpeed(IntEnum): + F_100MHZ = 0xFFFF + F_83MHZ = 0x7FFF + F_71MHZ = 0x3FFF + F_62MHZ = 0x1FFF + F_55MHZ = 0x0FFF + F_50MHZ = 0x07FF + F_45MHZ = 0x03FF + + +class FlashMode(IntEnum): + QIO = 0xFFFF # Quad IO + QO = 0x7FFF # Quad Output + DIO = 0x3FFF # Dual IO + DO = 0x1FFF # Dual Output + SINGLE = 0x0FFF # One IO + + +@dataclass +class SystemData(DataStruct): + @dataclass + class ForceOldOTA: + is_disabled: bool + port: int + pin: int + + @dataclass + class RSIPMask: + length: int + offset: int + is_disabled: bool + + # OTA section + ota2_address: int = field("I", default=0xFFFFFFFF) + ota2_switch: int = field("I", default=0xFFFFFFFF) + force_old_ota: ForceOldOTA = bitfield("b1P1u1u5", ForceOldOTA, 0xFF) + # RDP section (AmebaZ only) + _1: ... = alignto(0x10) + rdp_address: int = field("I", default=0xFFFFFFFF) + rdp_length: int = field("I", default=0xFFFFFFFF) + # Flash section + _2: ... = alignto(0x20) + flash_mode: FlashMode = field("H", default=FlashMode.QIO) + flash_speed: FlashSpeed = field("H", default=FlashSpeed.F_100MHZ) # AmebaZ only + flash_id: int = field("H", default=0xFFFF) + flash_size_mb: int = adapter( + encode=lambda v, ctx: 0xFFFF if v == 2 else (v << 10) - 1, + decode=lambda v, ctx: 2 if v == 0xFFFF else (v + 1) >> 10, + )(field("H", default=2)) + flash_status: int = field("H", default=0x0000) + # Log UART section + _3: ... = alignto(0x30) + baudrate: int = adapter( + encode=lambda v, ctx: 0xFFFFFFFF if v == 115200 else v, + decode=lambda v, ctx: 115200 if v == 0xFFFFFFFF else v, + )(field("I", default=115200)) + # Calibration data (AmebaZ2 only) + _4: ... = alignto(0x40) + spic_calibration: bytes = field("16s", default=FF_16) + # RSIP section (AmebaZ only) + _5: ... = alignto(0x50) + rsip_mask1: RSIPMask = bitfield("u7P2u22u1", RSIPMask, 0xFFFFFFFF) + rsip_mask2: RSIPMask = bitfield("u7P2u22u1", RSIPMask, 0xFFFFFFFF) + # Calibration data (AmebaZ2 only) + _6: ... = alignto(0xFE0) + bt_ftl_gc_status: int = field("I", default=0xFFFFFFFF) + _7: ... = alignto(0xFF0) + bt_calibration: bytes = field("16s", default=FF_16) + + +class ImageType(IntEnum): + PARTAB = 0 + BOOT = 1 + FWHS_S = 2 + FWHS_NS = 3 + FWLS = 4 + ISP = 5 + VOE = 6 + WLN = 7 + XIP = 8 + CPFW = 9 + WOWLN = 10 + CINIT = 11 + + +class PartitionType(IntEnum): + PARTAB = 0 + BOOT = 1 + SYS = 2 + CAL = 3 + USER = 4 + FW1 = 5 + FW2 = 6 + VAR = 7 + MP = 8 + RDP = 9 + + +class SectionType(IntEnum): + DTCM = 0x80 + ITCM = 0x81 + SRAM = 0x82 + PSRAM = 0x83 + LPDDR = 0x84 + XIP = 0x85 + + +class EncAlgo(IntEnum): + AES_EBC = 0 + AES_CBC = 1 + OTHER = 0xFF + + +class HashAlgo(IntEnum): + MD5 = 0 + SHA256 = 1 + OTHER = 0xFF + + +def index(func: Callable[[T], int], iterable: Iterable[T], default: T = None) -> T: + for idx, item in enumerate(iterable): + if func(item): + return idx + return default + + +class BitFlag(Adapter): + def encode(self, value: bool, ctx: Context) -> int: + return 0xFF if value else 0xFE + + def decode(self, value: int, ctx: Context) -> bool: + return value & 1 == 1 + + +def header_is_last(ctx: Context) -> bool: + header: SectionHeader = ctx.P.item.header + return header.next_offset == 0xFFFFFFFF + + +@dataclass +class Keyblock(DataStruct): + decryption: bytes = field("32s", default=FF_32) + hash: bytes = field("32s", default=FF_32) + + +@dataclass +class KeyblockOTA(DataStruct): + decryption: bytes = field("32s", default=FF_32) + reserved: List[bytes] = repeat(5)(field("32s", default=FF_32)) + + +@dataclass +class ImageHeader(DataStruct): + class Flags(IntFlag): + HAS_KEY1 = 1 << 0 + HAS_KEY2 = 1 << 1 + + length: int = built("I", lambda ctx: sizeof(ctx._.data)) + next_offset: int = field("I", default=0xFFFFFFFF) + type: ImageType = field("B") + is_encrypted: bool = field("?", default=False) + idx_pkey: int = field("B", default=0xFF) + flags: Flags = built( + "B", + lambda ctx: ImageHeader.Flags( + int(ctx.user_keys[0] != FF_32) + 2 * int(ctx.user_keys[1] != FF_32) + ), + ) + _1: ... = padding(8) + serial: int = field("I", default=0) + _2: ... = padding(8) + user_keys: List[bytes] = repeat(2)(field("32s", default=FF_32)) + + +@dataclass +class SectionHeader(DataStruct): + length: int = built("I", lambda ctx: sizeof(ctx._.entry) + sizeof(ctx._.data)) + next_offset: int = field("I", default=0xFFFFFFFF) + type: SectionType = field("B") + sce_enabled: bool = field("?", default=False) + xip_page_size: int = field("B", default=0) + xip_block_size: int = field("B", default=0) + _1: ... = padding(4) + valid_pattern: bytes = field("8s", default=bytes(range(8))) + sce_key_iv_valid: bool = adapter(BitFlag())( + built("B", lambda ctx: ctx.sce_key != FF_16 and ctx.sce_iv != FF_16), + ) + _2: ... = padding(7) + sce_key: bytes = field("16s", default=FF_16) + sce_iv: bytes = field("16s", default=FF_16) + _3: ... = padding(32) + + +@dataclass +@datastruct(repeat_fill=True) +class EntryHeader(DataStruct): + length: int = built("I", lambda ctx: sizeof(ctx._.data)) + address: int = field("I") + entry_table: List[int] = repeat(6)(field("I", default=0xFFFFFFFF)) + + +@dataclass +class FST(DataStruct): + class Flags(IntFlag): + ENC_EN = 1 << 0 + HASH_EN = 2 << 0 + + enc_algo: EncAlgo = field("H", default=EncAlgo.AES_CBC) + hash_algo: HashAlgo = field("H", default=HashAlgo.SHA256) + part_size: int = field("I", default=0) + valid_pattern: bytes = field("8s", default=bytes(range(8))) + _1: ... = padding(4) + flags: Flags = field("B", default=Flags.HASH_EN) + cipher_key_iv_valid: bool = adapter(BitFlag())( + built("B", lambda ctx: ctx.cipher_key != FF_32 and ctx.cipher_iv != FF_16), + ) + _2: ... = padding(10) + cipher_key: bytes = field("32s", default=FF_32) + cipher_iv: bytes = field("16s", default=FF_16) + _3: ... = padding(16) + + +@dataclass +class TrapConfig: + is_valid: bool + level: int + port: int + pin: int + + +@dataclass +class PartitionRecord(DataStruct): + offset: int = field("I") + length: int = field("I") + type: PartitionType = field("B") + dbg_skip: bool = field("?", default=False) + _1: ... = padding(6) + hash_key_valid: bool = adapter(BitFlag())( + built("B", lambda ctx: ctx.type == PartitionType.BOOT or ctx.hash_key != FF_32), + ) + _2: ... = padding(15) + hash_key: bytes = field("32s", default=FF_32) + + +def find_partition_index(type: PartitionType): + return lambda ctx: index(lambda p: p.type == type, ctx.partitions, 255) + + +@dataclass +class PartitionTable(DataStruct): + class KeyExport(IntEnum): + NONE = 0 + LATEST = 1 + BOTH = 2 + + rma_w_state: int = field("B", default=0xF0) + rma_ov_state: int = field("B", default=0xF0) + e_fwv: int = field("B", default=0) + _1: ... = padding(1) + count: int = built("B", lambda ctx: len(ctx.partitions) - 1) + idx_fw1: int = built("B", find_partition_index(PartitionType.FW1)) + idx_fw2: int = built("B", find_partition_index(PartitionType.FW2)) + idx_var: int = built("B", find_partition_index(PartitionType.VAR)) + idx_mp: int = built("B", find_partition_index(PartitionType.MP)) + _2: ... = padding(1) + trap_ota: TrapConfig = bitfield("b1p6u1u3u5", TrapConfig, default=0) + trap_mp: TrapConfig = bitfield("b1p6u1u3u5", TrapConfig, default=0) + _3: ... = padding(1) + key_export: KeyExport = field("B", default=KeyExport.BOTH) + user_data_len: int = built("H", lambda ctx: len(ctx.user_data)) + _4: ... = padding(14) + partitions: List[PartitionRecord] = repeat(lambda ctx: ctx.count + 1)(subfield()) + user_data: bytes = field(lambda ctx: ctx.user_data_len, default=b"") + + +@dataclass +class Bootloader(DataStruct): + entry: EntryHeader = subfield() + data: bytes = field(lambda ctx: ctx.entry.length, default=b"") + _1: ... = align(0x20, False, pattern=b"\x00") + + +@dataclass +class Section(DataStruct): + # noinspection PyMethodParameters + def update(ctx: Context): + section: "Section" = ctx.self + if section.header.next_offset == 0: + # calculate next_offset + size = section.sizeof(**ctx.P.kwargs) + section.header.next_offset = size + + _0: ... = action(packing(update)) + header: SectionHeader = subfield() + entry: EntryHeader = subfield() + data: bytes = field(lambda ctx: ctx.entry.length, default=b"") + _1: ... = align(0x20, False, pattern=b"\x00") + + +@dataclass +class Firmware(DataStruct): + # noinspection PyMethodParameters + def update(ctx: Context): + firmware: "Firmware" = ctx.self + # set next_offset to 0 for all images but the last, + # to allow calculation by Section.update() + for section in firmware.sections: + section.header.next_offset = 0 + firmware.sections[-1].header.next_offset = 0xFFFFFFFF + + _0: ... = action(packing(update)) + fst: FST = subfield() + sections: List[Section] = repeat(last=header_is_last)(subfield()) + + +@dataclass +class Image(DataStruct): + # noinspection PyMethodParameters + def update(ctx: Context): + image: "Image" = ctx.self + if image.header.next_offset == 0: + # calculate next_offset + size = image.sizeof(**ctx.P.kwargs) + if ctx.is_first and ctx.is_ota: + size -= sizeof(image.ota_signature) + if ctx.is_first: + size -= sizeof(image.keyblock) + image.header.next_offset = size + if ctx.is_first and ctx.is_ota: + # calculate OTA signature (header hash) + header = image.header.pack(parent=image) + if ctx.hash_key: + image.ota_signature = HMAC( + key=ctx.hash_key, + msg=header, + digestmod=sha256, + ).digest() + else: + image.ota_signature = sha256(header).digest() + + _0: ... = action(packing(update)) + _hash: ... = checksum_start( + init=lambda ctx: ( + HMAC(ctx.hash_key, digestmod=sha256) if ctx.hash_key else sha256() + ), + update=lambda data, obj, ctx: obj.update(data), + end=lambda obj, ctx: obj.digest(), + ) + + # 'header' hash for firmware images + ota_signature: bytes = cond(lambda ctx: ctx.is_first and ctx.is_ota)( + field("32s", default=FF_32) + ) + # keyblock for first sub-image only + keyblock: Any = cond(lambda ctx: ctx.is_first)( + switch(lambda ctx: ctx.is_ota)( + false=(Keyblock, subfield()), + true=(KeyblockOTA, subfield()), + ) + ) + + header: ImageHeader = subfield() + data: Any = switch(lambda ctx: ctx.header.type)( + PARTAB=(PartitionTable, subfield()), + BOOT=(Bootloader, subfield()), + # OTA images + FWHS_S=(Firmware, subfield()), + FWHS_NS=(Firmware, subfield()), + FWLS=(Firmware, subfield()), + XIP=(Firmware, subfield()), + # something else? + default=(bytes, field(lambda ctx: ctx.header.length)), + ) + + _1: ... = checksum_end(_hash) + hash: bytes = checksum_field("Image hash")(field("32s", default=FF_32)) + # align to 0x4000 for images having next_offset, 0x40 otherwise + # skip offset for non-firmware images + _2: ... = cond(lambda ctx: ctx.is_ota)( + align( + lambda ctx: 0x40 if ctx.header.next_offset == 0xFFFFFFFF else 0x4000, + pattern=b"\x87", + ) + ) + + +# noinspection PyMethodParameters,PyAttributeOutsideInit +@dataclass +class Flash(DataStruct): + def update(ctx: Context): + flash: "Flash" = ctx.self + # set next_offset to 0 for all images but the last, + # to allow calculation by Image.update() + for image in flash.firmware: + image.header.idx_pkey = 0 + image.header.next_offset = 0 + flash.firmware[-1].header.next_offset = 0xFFFFFFFF + + def update_offsets(ctx: Context): + flash: "Flash" = ctx.self + ptable: PartitionTable = flash.ptable.data + ctx.boot_offset = ptable.partitions[0].offset + idx_fw1 = ptable.idx_fw1 + ctx.firmware_offset = ptable.partitions[idx_fw1].offset + if ptable.partitions[idx_fw1].hash_key_valid: + ctx.firmware_hash_key = ptable.partitions[idx_fw1].hash_key + else: + ctx.firmware_hash_key = None + + _0: ... = action(packing(update)) + calibration: bytes = field("16s", default=FLASH_CALIBRATION) + + _1: ... = alignto(0x20) + ptable: Image = subfield( + hash_key=lambda ctx: ctx.hash_key, + is_ota=False, + is_first=True, + ) + _2: ... = action(update_offsets) + + _3: ... = alignto(0x1000) + system: SystemData = subfield() + + _4: ... = alignto(lambda ctx: ctx.boot_offset) + boot: Image = subfield( + hash_key=lambda ctx: ctx.hash_key, + is_ota=False, + is_first=True, + ) + + _5: ... = alignto(lambda ctx: ctx.firmware_offset) + _sum32: ... = checksum_start( + init=lambda ctx: 0, + update=lambda data, obj, ctx: obj + sum(data), + end=lambda obj, ctx: obj & 0xFFFFFFFF, + ) + firmware: List[Image] = repeat(last=header_is_last)( + subfield( + hash_key=lambda ctx: ctx.firmware_hash_key, + is_ota=True, + is_first=lambda ctx: ctx.G.tell() == ctx.firmware_offset, + ) + ) + _6: ... = checksum_end(_sum32) + sum32: int = checksum_field("FW1 sum32")(field("I", default=0xFFFFFFFF)) + + +@dataclass +class ImageConfig: + @dataclass + class Keys: + decryption: bytes + keyblock: Dict[str, bytes] + hash_keys: Dict[str, bytes] + user_keys: Dict[str, bytes] + xip_sce_key: bytes + xip_sce_iv: bytes + + # noinspection PyTypeChecker + def __post_init__(self): + self.decryption = bytes.fromhex(self.decryption) + self.xip_sce_key = bytes.fromhex(self.xip_sce_key) + self.xip_sce_iv = bytes.fromhex(self.xip_sce_iv) + self.keyblock = {k: bytes.fromhex(v) for k, v in self.keyblock.items()} + self.hash_keys = {k: bytes.fromhex(v) for k, v in self.hash_keys.items()} + self.user_keys = {k: bytes.fromhex(v) for k, v in self.user_keys.items()} + + @dataclass + class Section: + name: str + type: SectionType + is_boot: bool = False + + # noinspection PyTypeChecker + def __post_init__(self): + self.type = str2enum(SectionType, self.type) + + @dataclass + class Image: + type: ImageType + sections: List["ImageConfig.Section"] + + # noinspection PyArgumentList,PyTypeChecker + def __post_init__(self): + self.type = str2enum(ImageType, self.type) + self.sections = [ImageConfig.Section(**v) for v in self.sections] + + keys: Keys + ptable: Dict[str, PartitionType] + boot: Section + fw: List[Image] + + # noinspection PyArgumentList,PyTypeChecker + def __post_init__(self): + self.keys = ImageConfig.Keys(**self.keys) + self.ptable = {k: str2enum(PartitionType, v) for k, v in self.ptable.items()} + self.boot = ImageConfig.Section(**self.boot) + self.fw = [ImageConfig.Image(**v) for v in self.fw] + + +def pad_data(data: bytes, n: int, char: int) -> bytes: + """Add 'char'-filled padding to 'data' to align to a 'n'-sized block.""" + if len(data) % n == 0: + return data + return data + (bytes([char]) * pad_up(len(data), n)) + + +def get_image_config(): + image = { + "keys": { + "decryption": "a0d6dae7e062ca94cbb294bf896b9f68cf8438774256ac7403ca4fd9a1c9564f", + "keyblock": { + "part_table": "882aa16c8c44a7760aa8c9ab22e3568c6fa16c2afa4f0cea29a10abcdf60e44f", + "boot": "882aa16c8c44a7760aa8c9ab22e3568c6fa16c2afa4f0cea29a10abcdf60e44f", + }, + "hash_keys": { + "part_table": "47e5661335a4c5e0a94d69f3c737d54f2383791332939753ef24279608f6d72b", + "boot": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "ota1": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e5f", + "ota2": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e5f", + }, + "user_keys": { + "boot": "aa0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "ota1": "bb0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "ota2": "bb0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + }, + "xip_sce_key": "a0d6dae7e062ca94cbb294bf896b9f68", + "xip_sce_iv": "94879487948794879487948794879487", + }, + "ptable": {"boot": "BOOT", "ota1": "FW1", "ota2": "FW2"}, + "boot": { + "name": "boot.sram", + "type": "SRAM", + "is_boot": True, + }, + "fw": [ + { + "type": "FWHS_S", + "sections": [ + { + "name": "fwhs.sram", + "type": "SRAM", + }, + { + "name": "fwhs.psram", + "type": "PSRAM", + }, + ], + }, + { + "type": "XIP", + "sections": [ + { + "name": "fwhs.xip_c", + "type": "XIP", + } + ], + }, + { + "type": "XIP", + "sections": [ + { + "name": "fwhs.xip_p", + "type": "XIP", + } + ], + }, + ], + } + return ImageConfig(**image) + + +def get_public_key(private: bytes) -> bytes: + return bytes.fromhex( + { + "a0d6dae7e062ca94cbb294bf896b9f68cf8438774256ac7403ca4fd9a1c9564f": "68513ef83e396b12ba059a900f36b6d31d11fe1c5d25eb8aa7c550307f9c2405", + "882aa16c8c44a7760aa8c9ab22e3568c6fa16c2afa4f0cea29a10abcdf60e44f": "48ad23ddbdac9e65719db7d394d44d62820d19e50d68376774237e98d2305e6a", + }[private.hex()] + ) + + +def get_entrypoint(name: str) -> int: + return { + "boot.sram": 0x10036100, + "fwhs.sram": 0x10000480, + "fwhs.psram": 0, + "fwhs.xip_c": 0x9B000140, + "fwhs.xip_p": 0x9B800140, + }[name] + + +def build_keyblock(config: ImageConfig, region: str): + if region in config.keys.keyblock: + return Keyblock( + decryption=get_public_key(config.keys.decryption), + hash=get_public_key(config.keys.keyblock[region]), + ) + return KeyblockOTA( + decryption=get_public_key(config.keys.decryption), + ) + + +def build_section(section: ImageConfig.Section): + # find entrypoint address + entrypoint = get_entrypoint(section.name) + # read the binary image + if section.name not in ["fwhs.psram"]: + data = read_data_file(TEST_DATA_URLS[f"raw.{section.name}.bin"]) + else: + data = b"" + # build EntryHeader + entry = EntryHeader( + address=entrypoint, + entry_table=[entrypoint] if section.type == SectionType.SRAM else [], + ) + # build Bootloader/Section struct + if section.is_boot: + data = pad_data(data, 0x20, 0x00) + return Bootloader( + entry=entry, + data=data, + ) + return Section( + header=SectionHeader(type=section.type), + entry=entry, + data=data, + ) diff --git a/tests/test_dhcp.py b/tests/test_dhcp.py new file mode 100644 index 0000000..3697aef --- /dev/null +++ b/tests/test_dhcp.py @@ -0,0 +1,494 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-11. + +import pytest +from base import TestBase, TestData +from test_dhcp_structs import * + +TEST_DATA = [ + pytest.param( + TestData( + data=( + b"\x01\x01\x06\x00\x7c\x55\x73\x6a\x00\x00\x80\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xad\xbe" + b"\xef\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63" + b"\x35\x01\x01\x3d\x07\x01\x00\xde\xad\xbe\xef\xaa\x0c\x07\x4b\x55" + b"\x42\x41\x2d\x50\x43\x3c\x08\x4d\x53\x46\x54\x20\x35\x2e\x30\x37" + b"\x0e\x01\x03\x06\x0f\x1f\x21\x2b\x2c\x2e\x2f\x77\x79\xf9\xfc\xff" + ), + obj_full=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REQUEST, + hardware_type=1, + hardware_alen=6, + hops=0, + transaction_id=2085974890, + seconds_elapsed=timedelta(0), + bootp_flags=DhcpBootpFlags.BROADCAST, + client_ip_address=IPv4Address("0.0.0.0"), + your_ip_address=IPv4Address("0.0.0.0"), + server_ip_address=IPv4Address("0.0.0.0"), + gateway_ip_address=IPv4Address("0.0.0.0"), + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + server_host_name="", + boot_file_name="", + magic_cookie=b"c\x82Sc", + options=[ + DhcpOption( + option=DhcpOptionType.MESSAGE_TYPE, + length=1, + data=DhcpMessageType.DISCOVER, + ), + DhcpOption( + option=DhcpOptionType.CLIENT_IDENTIFIER, + length=7, + data=DhcpClientIdentifier( + hardware_type=1, + mac_address=MAC("00-DE-AD-BE-EF-AA"), + ), + ), + DhcpOption( + option=DhcpOptionType.HOST_NAME, + length=7, + data="KUBA-PC", + ), + DhcpOption( + option=DhcpOptionType.VENDOR_CLASS_IDENTIFIER, + length=8, + data="MSFT 5.0", + ), + DhcpOption( + option=DhcpOptionType.PARAMETER_REQUEST_LIST, + length=14, + data=[ + DhcpOptionType.SUBNET_MASK, + DhcpOptionType.ROUTER, + DhcpOptionType.DNS_SERVERS, + DhcpOptionType.DOMAIN_NAME, + DhcpOptionType.PERFORM_ROUTER_DISCOVERY, + DhcpOptionType.STATIC_ROUTING_TABLE, + DhcpOptionType.VENDOR_SPECIFIC_INFORMATION, + DhcpOptionType.NETBIOS_NAME_SERVER, + DhcpOptionType.NETBIOS_NODE_TYPE, + DhcpOptionType.NETBIOS_SCOPE, + DhcpOptionType.DOMAIN_SEARCH, + DhcpOptionType.CLASSLESS_STATIC_ROUTE, + DhcpOptionType.PRIVATE_CLASSLESS_STATIC_ROUTE, + DhcpOptionType.PRIVATE_PROXY_AUTODISCOVERY, + ], + ), + DhcpOption(option=DhcpOptionType.END, length=0, data=None), + ], + ), + obj_simple=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REQUEST, + transaction_id=2085974890, + seconds_elapsed=timedelta(0), + bootp_flags=DhcpBootpFlags.BROADCAST, + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + options=[ + DhcpOption( + option=DhcpOptionType.MESSAGE_TYPE, + data=DhcpMessageType.DISCOVER, + ), + DhcpOption( + option=DhcpOptionType.CLIENT_IDENTIFIER, + data=DhcpClientIdentifier( + mac_address=MAC("00-DE-AD-BE-EF-AA"), + ), + ), + DhcpOption( + option=DhcpOptionType.HOST_NAME, + data="KUBA-PC", + ), + DhcpOption( + option=DhcpOptionType.VENDOR_CLASS_IDENTIFIER, + data="MSFT 5.0", + ), + DhcpOption( + option=DhcpOptionType.PARAMETER_REQUEST_LIST, + data=[ + DhcpOptionType.SUBNET_MASK, + DhcpOptionType.ROUTER, + DhcpOptionType.DNS_SERVERS, + DhcpOptionType.DOMAIN_NAME, + DhcpOptionType.PERFORM_ROUTER_DISCOVERY, + DhcpOptionType.STATIC_ROUTING_TABLE, + DhcpOptionType.VENDOR_SPECIFIC_INFORMATION, + DhcpOptionType.NETBIOS_NAME_SERVER, + DhcpOptionType.NETBIOS_NODE_TYPE, + DhcpOptionType.NETBIOS_SCOPE, + DhcpOptionType.DOMAIN_SEARCH, + DhcpOptionType.CLASSLESS_STATIC_ROUTE, + DhcpOptionType.PRIVATE_CLASSLESS_STATIC_ROUTE, + DhcpOptionType.PRIVATE_PROXY_AUTODISCOVERY, + ], + ), + DhcpOption(option=DhcpOptionType.END), + ], + ), + ), + # id + id="dhcp_discover_windows_7", + ), + pytest.param( + TestData( + data=( + b"\x02\x01\x06\x00\x7c\x55\x73\x6a\x00\x00\x80\x00\x00\x00\x00\x00" + b"\xc0\xa8\x00\x7a\xc0\xa8\x00\x01\x00\x00\x00\x00\x00\xde\xad\xbe" + b"\xef\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63" + b"\x35\x01\x02\x36\x04\xc0\xa8\x00\x01\x33\x04\x00\x00\xa8\xc0\x3a" + b"\x04\x00\x00\x54\x60\x3b\x04\x00\x00\x93\xa8\x01\x04\xff\xff\xff" + b"\x00\x1c\x04\xc0\xa8\x00\xff\x03\x04\xc0\xa8\x00\x01\x06\x04\xc0" + b"\xa8\x00\x01\x0f\x05\x6c\x6f\x63\x61\x6c\xff" + ), + obj_full=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REPLY, + hardware_type=1, + hardware_alen=6, + hops=0, + transaction_id=2085974890, + bootp_flags=DhcpBootpFlags.BROADCAST, + client_ip_address=IPv4Address("0.0.0.0"), + your_ip_address=IPv4Address("192.168.0.122"), + server_ip_address=IPv4Address("192.168.0.1"), + gateway_ip_address=IPv4Address("0.0.0.0"), + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + server_host_name="", + boot_file_name="", + magic_cookie=b"c\x82Sc", + options=[ + DhcpOption( + option=DhcpOptionType.MESSAGE_TYPE, + length=1, + data=DhcpMessageType.OFFER, + ), + DhcpOption( + option=DhcpOptionType.SERVER_IDENTIFIER, + length=4, + data=IPv4Address("192.168.0.1"), + ), + DhcpOption( + option=DhcpOptionType.IP_ADDRESS_LEASE_TIME, + length=4, + data=timedelta(seconds=43200), + ), + DhcpOption( + option=DhcpOptionType.RENEW_TIME_VALUE, + length=4, + data=timedelta(seconds=21600), + ), + DhcpOption( + option=DhcpOptionType.REBINDING_TIME_VALUE, + length=4, + data=timedelta(seconds=37800), + ), + DhcpOption( + option=DhcpOptionType.SUBNET_MASK, + length=4, + data=IPv4Address("255.255.255.0"), + ), + DhcpOption( + option=DhcpOptionType.BROADCAST_ADDRESS, + length=4, + data=IPv4Address("192.168.0.255"), + ), + DhcpOption( + option=DhcpOptionType.ROUTER, + length=4, + data=IPv4Address("192.168.0.1"), + ), + DhcpOption( + option=DhcpOptionType.DNS_SERVERS, + length=4, + data=IPv4Address("192.168.0.1"), + ), + DhcpOption( + option=DhcpOptionType.DOMAIN_NAME, + length=5, + data="local", + ), + DhcpOption(option=DhcpOptionType.END, length=0, data=None), + ], + ), + obj_simple=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REPLY, + transaction_id=2085974890, + bootp_flags=DhcpBootpFlags.BROADCAST, + your_ip_address=IPv4Address("192.168.0.122"), + server_ip_address=IPv4Address("192.168.0.1"), + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + options=[ + DhcpOption( + option=DhcpOptionType.MESSAGE_TYPE, + data=DhcpMessageType.OFFER, + ), + DhcpOption( + option=DhcpOptionType.SERVER_IDENTIFIER, + data=IPv4Address("192.168.0.1"), + ), + DhcpOption( + option=DhcpOptionType.IP_ADDRESS_LEASE_TIME, + data=timedelta(seconds=43200), + ), + DhcpOption( + option=DhcpOptionType.RENEW_TIME_VALUE, + data=timedelta(seconds=21600), + ), + DhcpOption( + option=DhcpOptionType.REBINDING_TIME_VALUE, + data=timedelta(seconds=37800), + ), + DhcpOption( + option=DhcpOptionType.SUBNET_MASK, + data=IPv4Address("255.255.255.0"), + ), + DhcpOption( + option=DhcpOptionType.BROADCAST_ADDRESS, + data=IPv4Address("192.168.0.255"), + ), + DhcpOption( + option=DhcpOptionType.ROUTER, + data=IPv4Address("192.168.0.1"), + ), + DhcpOption( + option=DhcpOptionType.DNS_SERVERS, + data=IPv4Address("192.168.0.1"), + ), + DhcpOption( + option=DhcpOptionType.DOMAIN_NAME, + data="local", + ), + DhcpOption(option=DhcpOptionType.END), + ], + ), + ), + # id + id="dhcp_offer_windows_7", + ), + pytest.param( + TestData( + data=( + b"\x01\x01\x06\x00\x9a\xe1\x6c\x9d\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xad\xbe" + b"\xef\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63" + b"\x35\x01\x03\x3d\x07\x01\x00\xde\xad\xbe\xef\xaa\x32\x04\xc0\xa8" + b"\x00\x2a\x39\x02\x05\xdc\x3c\x0f\x61\x6e\x64\x72\x6f\x69\x64\x2d" + b"\x64\x68\x63\x70\x2d\x31\x32\x0c\x20\x55\x72\x7a\x61\x64\x7a\x65" + b"\x6e\x69\x65\x2d\x4d\x32\x31\x2d\x75\x7a\x79\x74\x6b\x6f\x77\x6e" + b"\x69\x6b\x61\x2d\x41\x41\x41\x41\x41\x37\x0c\x01\x03\x06\x0f\x1a" + b"\x1c\x33\x3a\x3b\x2b\x72\x6c\xff" + ), + obj_full=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REQUEST, + hardware_type=1, + hardware_alen=6, + hops=0, + transaction_id=2598464669, + seconds_elapsed=timedelta(0), + bootp_flags=DhcpBootpFlags(0), + client_ip_address=IPv4Address("0.0.0.0"), + your_ip_address=IPv4Address("0.0.0.0"), + server_ip_address=IPv4Address("0.0.0.0"), + gateway_ip_address=IPv4Address("0.0.0.0"), + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + server_host_name="", + boot_file_name="", + magic_cookie=b"c\x82Sc", + options=[ + DhcpOption( + option=DhcpOptionType.MESSAGE_TYPE, + length=1, + data=DhcpMessageType.REQUEST, + ), + DhcpOption( + option=DhcpOptionType.CLIENT_IDENTIFIER, + length=7, + data=DhcpClientIdentifier( + mac_address=MAC("00-DE-AD-BE-EF-AA"), + ), + ), + DhcpOption( + option=DhcpOptionType.REQUESTED_IP_ADDRESS, + length=4, + data=IPv4Address("192.168.0.42"), + ), + DhcpOption( + option=DhcpOptionType.MAXIMUM_MESSAGE_SIZE, + length=2, + data=1500, + ), + DhcpOption( + option=DhcpOptionType.VENDOR_CLASS_IDENTIFIER, + length=15, + data="android-dhcp-12", + ), + DhcpOption( + option=DhcpOptionType.HOST_NAME, + length=32, + data="Urzadzenie-M21-uzytkownika-AAAAA", + ), + DhcpOption( + option=DhcpOptionType.PARAMETER_REQUEST_LIST, + length=12, + data=[ + DhcpOptionType.SUBNET_MASK, + DhcpOptionType.ROUTER, + DhcpOptionType.DNS_SERVERS, + DhcpOptionType.DOMAIN_NAME, + DhcpOptionType.INTERFACE_MTU_SIZE, + DhcpOptionType.BROADCAST_ADDRESS, + DhcpOptionType.IP_ADDRESS_LEASE_TIME, + DhcpOptionType.RENEW_TIME_VALUE, + DhcpOptionType.REBINDING_TIME_VALUE, + DhcpOptionType.VENDOR_SPECIFIC_INFORMATION, + DhcpOptionType.DHCP_CAPTIVE_PORTAL, + DhcpOptionType.IPV6_ONLY_PREFERRED, + ], + ), + DhcpOption(option=DhcpOptionType.END, length=0, data=None), + ], + ), + obj_simple=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REQUEST, + transaction_id=2598464669, + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + options=[ + DhcpOption( + option=DhcpOptionType.MESSAGE_TYPE, + data=DhcpMessageType.REQUEST, + ), + DhcpOption( + option=DhcpOptionType.CLIENT_IDENTIFIER, + data=DhcpClientIdentifier( + mac_address=MAC("00-DE-AD-BE-EF-AA"), + ), + ), + DhcpOption( + option=DhcpOptionType.REQUESTED_IP_ADDRESS, + data=IPv4Address("192.168.0.42"), + ), + DhcpOption( + option=DhcpOptionType.MAXIMUM_MESSAGE_SIZE, + data=1500, + ), + DhcpOption( + option=DhcpOptionType.VENDOR_CLASS_IDENTIFIER, + data="android-dhcp-12", + ), + DhcpOption( + option=DhcpOptionType.HOST_NAME, + data="Urzadzenie-M21-uzytkownika-AAAAA", + ), + DhcpOption( + option=DhcpOptionType.PARAMETER_REQUEST_LIST, + data=[ + DhcpOptionType.SUBNET_MASK, + DhcpOptionType.ROUTER, + DhcpOptionType.DNS_SERVERS, + DhcpOptionType.DOMAIN_NAME, + DhcpOptionType.INTERFACE_MTU_SIZE, + DhcpOptionType.BROADCAST_ADDRESS, + DhcpOptionType.IP_ADDRESS_LEASE_TIME, + DhcpOptionType.RENEW_TIME_VALUE, + DhcpOptionType.REBINDING_TIME_VALUE, + DhcpOptionType.VENDOR_SPECIFIC_INFORMATION, + DhcpOptionType.DHCP_CAPTIVE_PORTAL, + DhcpOptionType.IPV6_ONLY_PREFERRED, + ], + ), + DhcpOption(option=DhcpOptionType.END), + ], + ), + ), + id="dhcp_request_android", + ), + pytest.param( + TestData( + data=( + b"\x01\x01\x06\x00\xde\xad\xbe\xef\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\xad\xbe" + b"\xef\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63" + b"\xff" + ), + obj_full=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REQUEST, + hardware_type=1, + hardware_alen=6, + hops=0, + transaction_id=3735928559, + seconds_elapsed=timedelta(0), + bootp_flags=DhcpBootpFlags(0), + client_ip_address=IPv4Address("0.0.0.0"), + your_ip_address=IPv4Address("0.0.0.0"), + server_ip_address=IPv4Address("0.0.0.0"), + gateway_ip_address=IPv4Address("0.0.0.0"), + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + server_host_name="", + boot_file_name="", + magic_cookie=b"c\x82Sc", + options=[DhcpOption(option=DhcpOptionType.END, length=0, data=None)], + ), + obj_simple=DhcpPacket( + packet_type=DhcpPacketType.BOOT_REQUEST, + transaction_id=0xDEADBEEF, + client_mac_address=MAC("00-DE-AD-BE-EF-AA"), + ), + ), + id="dhcp_minimal", + ), +] + + +@pytest.mark.parametrize("test", TEST_DATA) +class TestDhcp(TestBase): + pass + + +del TestBase diff --git a/tests/test_dhcp_structs.py b/tests/test_dhcp_structs.py new file mode 100644 index 0000000..0b1f5fb --- /dev/null +++ b/tests/test_dhcp_structs.py @@ -0,0 +1,233 @@ +# Copyright (c) Kuba Szczodrzyński 2023-9-10. +# https://github.com/kuba2k2/pynetkit/blob/master/pynetkit/modules/dhcp/structs.py + +from dataclasses import dataclass +from datetime import timedelta +from enum import IntEnum, IntFlag +from ipaddress import IPv4Address +from random import randint +from typing import Any + +from macaddress import MAC + +from datastruct import NETWORK, DataStruct, datastruct +from datastruct.adapters.network import ipv4_field, mac_field +from datastruct.adapters.time import timedelta_field +from datastruct.fields import ( + built, + cond, + const, + field, + padding, + repeat, + subfield, + switch, + text, + varlist, + vartext, +) + + +class DhcpPacketType(IntEnum): + BOOT_REQUEST = 1 + BOOT_REPLY = 2 + + +class DhcpMessageType(IntEnum): + DISCOVER = 1 + OFFER = 2 + REQUEST = 3 + DECLINE = 4 + ACK = 5 + NAK = 6 + RELEASE = 7 + INFORM = 8 + FORCERENEW = 9 + LEASEQUERY = 10 + LEASEUNASSIGNED = 11 + LEASEUNKNOWN = 12 + LEASEACTIVE = 13 + BULKLEASEQUERY = 14 + LEASEQUERYDONE = 15 + ACTIVELEASEQUERY = 16 + LEASEQUERYSTATUS = 17 + TLS = 18 + + +class DhcpBootpFlags(IntFlag): + BROADCAST = 1 << 15 + + +class DhcpOptionType(IntEnum): + SUBNET_MASK = 1 + TIME_OFFSET = 2 + ROUTER = 3 + TIME_SERVERS = 4 + NAME_SERVERS = 5 + DNS_SERVERS = 6 + LOG_SERVERS = 7 + COOKIE_SERVERS = 8 + LPR_SERVERS = 9 + IMPRESS_SERVERS = 10 + RLP_SERVERS = 11 + HOST_NAME = 12 + BOOT_FILE_SIZE = 13 + MERIT_DUMP_FILE = 14 + DOMAIN_NAME = 15 + SWAP_SERVER = 16 + ROOT_PATH = 17 + EXTENSION_FILE = 18 + IP_LAYER_FORWARDING_ = 19 + SRC_ROUTE_ENABLER = 20 + POLICY_FILTER = 21 + MAXIMUM_DG_REASSEMBLY_SIZE = 22 + DEFAULT_IP_TTL = 23 + PATH_MTU_AGING_TIMEOUT = 24 + MTU_PLATEAU_ = 25 + INTERFACE_MTU_SIZE = 26 + ALL_SUBNETS_ARE_LOCAL = 27 + BROADCAST_ADDRESS = 28 + PERFORM_MASK_DISCOVERY = 29 + PROVIDE_MASK_TO_OTHERS = 30 + PERFORM_ROUTER_DISCOVERY = 31 + ROUTER_SOLICITATION_ADDRESS = 32 + STATIC_ROUTING_TABLE = 33 + TRAILER_ENCAPSULATION = 34 + ARP_CACHE_TIMEOUT = 35 + ETHERNET_ENCAPSULATION = 36 + DEFAULT_TCP_TIME_TO_LIVE = 37 + TCP_KEEPALIVE_INTERVAL = 38 + TCP_KEEPALIVE_GARBAGE = 39 + NIS_DOMAIN_NAME = 40 + NIS_SERVER_ADDRESSES = 41 + NTP_SERVERS_ADDRESSES = 42 + VENDOR_SPECIFIC_INFORMATION = 43 + NETBIOS_NAME_SERVER = 44 + NETBIOS_DATAGRAM_DISTRIBUTION_ = 45 + NETBIOS_NODE_TYPE = 46 + NETBIOS_SCOPE = 47 + X_WINDOW_FONT_SERVER = 48 + X_WINDOW_DISPLAY_MANAGER = 49 + REQUESTED_IP_ADDRESS = 50 + IP_ADDRESS_LEASE_TIME = 51 + OPTION_OVERLOAD = 52 + MESSAGE_TYPE = 53 + SERVER_IDENTIFIER = 54 + PARAMETER_REQUEST_LIST = 55 + MESSAGE = 56 + MAXIMUM_MESSAGE_SIZE = 57 + RENEW_TIME_VALUE = 58 + REBINDING_TIME_VALUE = 59 + VENDOR_CLASS_IDENTIFIER = 60 + CLIENT_IDENTIFIER = 61 + NETWARE_IP_DOMAIN_NAME = 62 + NETWARE_IP_SUB_OPTIONS = 63 + NIS_V3_CLIENT_DOMAIN_NAME = 64 + NIS_V3_SERVER_ADDRESS = 65 + TFTP_SERVER_NAME = 66 + BOOT_FILE_NAME = 67 + HOME_AGENT_ADDRESSES = 68 + SIMPLE_MAIL_SERVER_ADDRESSES = 69 + POST_OFFICE_SERVER_ADDRESSES = 70 + NETWORK_NEWS_SERVER_ADDRESSES = 71 + WWW_SERVER_ADDRESSES = 72 + FINGER_SERVER_ADDRESSES = 73 + CHAT_SERVER_ADDRESSES = 74 + STREETTALK_SERVER_ADDRESSES = 75 + STREETTALK_DIRECTORY_ASSISTANCE_ADDRESSES = 76 + USER_CLASS_INFORMATION = 77 + SLP_DIRECTORY_AGENT = 78 + SLP_SERVICE_SCOPE = 79 + RAPID_COMMIT = 80 + FQDN = 81 + RELAY_AGENT_INFORMATION = 82 + INTERNET_STORAGE_NAME_SERVICE = 83 + NOVELL_DIRECTORY_SERVERS = 85 + NOVELL_DIRECTORY_SERVER_TREE_NAME = 86 + NOVELL_DIRECTORY_SERVER_CONTEXT = 87 + BCMCS_CONTROLLER_DOMAIN_NAME_LIST = 88 + BCMCS_CONTROLLER_IPV4_ADDRESS_LIST = 89 + AUTHENTICATION = 90 + CLIENT_SYSTEM = 93 + CLIENT_NETWORK_DEVICE_INTERFACE = 94 + LDAP_USE = 95 + UUID_BASED_CLIENT_IDENTIFIER = 97 + OPEN_GROUP_USER_AUTHENTICATION = 98 + IPV6_ONLY_PREFERRED = 108 + DHCP_CAPTIVE_PORTAL = 114 + DOMAIN_SEARCH = 119 + CLASSLESS_STATIC_ROUTE = 121 + PRIVATE = 224 + PRIVATE_CLASSLESS_STATIC_ROUTE = 249 + PRIVATE_PROXY_AUTODISCOVERY = 252 + END = 255 + + +@dataclass +class DhcpClientIdentifier(DataStruct): + hardware_type: int = const(1)(field("B")) + mac_address: MAC = mac_field() + + +@dataclass +@datastruct(endianness=NETWORK, padding_pattern=b"\x00") +class DhcpOption(DataStruct): + option: DhcpOptionType = field("B") + length: int = cond(lambda ctx: ctx.option != 255, if_not=0)( + built("B", lambda ctx: ctx.sizeof("data")) + ) + data: Any = cond(lambda ctx: ctx.option != 255, if_not=None)( + switch(lambda ctx: ctx.option)( + MESSAGE_TYPE=(DhcpMessageType, field("B")), + CLIENT_IDENTIFIER=(DhcpClientIdentifier, subfield()), + MAXIMUM_MESSAGE_SIZE=(int, field("H")), + INTERFACE_MTU_SIZE=(int, field("H")), + NETBIOS_NODE_TYPE=(int, field("B")), + # time values + IP_ADDRESS_LEASE_TIME=(timedelta, timedelta_field()), + RENEW_TIME_VALUE=(timedelta, timedelta_field()), + REBINDING_TIME_VALUE=(timedelta, timedelta_field()), + # text options + VENDOR_CLASS_IDENTIFIER=(str, vartext(lambda ctx: ctx.length)), + HOST_NAME=(str, vartext(lambda ctx: ctx.length)), + DOMAIN_NAME=(str, vartext(lambda ctx: ctx.length)), + # IP address options + REQUESTED_IP_ADDRESS=(IPv4Address, ipv4_field()), + SERVER_IDENTIFIER=(IPv4Address, ipv4_field()), + SUBNET_MASK=(IPv4Address, ipv4_field()), + BROADCAST_ADDRESS=(IPv4Address, ipv4_field()), + ROUTER=(IPv4Address, ipv4_field()), + DNS_SERVERS=(IPv4Address, ipv4_field()), + # other options + PARAMETER_REQUEST_LIST=( + list[DhcpOptionType], + varlist(lambda ctx: ctx.length)(field("B")), + ), + default=(bytes, field(lambda ctx: ctx.length)), + ) + ) + + +@dataclass +@datastruct(endianness=NETWORK, padding_pattern=b"\x00") +class DhcpPacket(DataStruct): + packet_type: DhcpPacketType = field("B") + hardware_type: int = const(1)(field("B")) + hardware_alen: int = const(6)(field("B")) + hops: int = field("b", default=0) + transaction_id: int = field("I", default_factory=lambda: randint(0, 0xFFFFFFFF)) + seconds_elapsed: timedelta = timedelta_field("H", default=timedelta(0)) + bootp_flags: DhcpBootpFlags = field("H", default=0) + client_ip_address: IPv4Address = ipv4_field(default=IPv4Address(0)) + your_ip_address: IPv4Address = ipv4_field(default=IPv4Address(0)) + server_ip_address: IPv4Address = ipv4_field(default=IPv4Address(0)) + gateway_ip_address: IPv4Address = ipv4_field(default=IPv4Address(0)) + client_mac_address: MAC = mac_field() + _1: ... = padding(10) + server_host_name: str = text(64, default="") + boot_file_name: str = text(128, default="") + magic_cookie: bytes = const(b"\x63\x82\x53\x63")(field(4)) + options: list[DhcpOption] = repeat( + last=lambda ctx: ctx.P.item.option == 255, + default_factory=lambda: [DhcpOption(DhcpOptionType.END)], + )(subfield()) diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 0000000..c0e0a3f --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,23 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-11. + +import pytest +from base import TestBase, TestData + +TEST_DATA = [ + pytest.param( + TestData( + data=None, + obj_full=None, + obj_simple=None, + ), + id="dummy", + ), +] + + +@pytest.mark.parametrize("test", TEST_DATA) +class TestDummy(TestBase): + pass + + +del TestBase diff --git a/tests/test_kvstorage.py b/tests/test_kvstorage.py new file mode 100644 index 0000000..470f1c1 --- /dev/null +++ b/tests/test_kvstorage.py @@ -0,0 +1,293 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-11. + +import pytest +from base import TestBase, TestData +from test_kvstorage_structs import * + +TEST_DATA = [ + pytest.param( + TestData( + cls=KVStorage, + data=( + b"\x54\x9d\xe2\x86\x1d\x2b\xa0\xf6\x06\xea\xde\x04\x08\xe8\x7a\x99" + b"\x8c\xf2\xb4\xdc\x3a\x15\xce\x76\x00\x54\xeb\xdc\x9e\x07\xf9\xcd" + + b"\x46\xdc\xed\x0e\x67\x2f\x3b\x70\xae\x12\x76\xa3\xf8\x71\x2e\x03" + * 254 + + b"\xdb\xca\x51\xcc\xda\x1a\xd2\x93\x80\xd0\x27\x53\x72\x2e\x87\x36" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\xc7\x59\xb3\xfc\x15\xd3\x54\x40\xc8\xf1\xec\xe6\x10\x14\x7e\x12" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 6 + + b"\x05\x7f\xa7\x9e\xb6\xd8\x0e\x80\xa6\x0b\x1a\x57\xe0\x60\x48\x2c" + b"\xe4\x22\x91\xe0\x79\x38\x2b\x36\x76\x1e\xca\xc4\xae\x83\x19\xc9" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 6 + + b"\xef\x37\x28\x62\x58\xd4\x2f\x50\x80\x1a\x29\x66\xa4\x41\x2c\xde" + b"\x65\xc3\x80\x09\x1d\xe6\xb4\xe9\x36\x8e\x64\xc9\x24\x92\x9a\x7d" + b"\x65\x46\x4d\xd7\x30\x87\xb1\xf4\x41\xb6\x19\xfe\xf8\x72\x23\x1c" + b"\x89\xbb\x8a\x2a\x97\xc5\x96\xb6\xe5\xec\xba\x26\xd7\xd5\x46\x7b" + b"\x27\x0a\x12\x74\x95\x8f\xb7\x76\x4c\x81\x99\xd1\xee\x84\x46\xd0" + b"\x86\x6d\x7c\x71\x68\x27\xe7\x95\x98\x2b\x17\xde\x41\x1d\xb1\x76" + b"\xf9\x28\x8b\xbf\x6e\x4c\xe3\x10\x61\x77\x9b\x0d\x2c\x5f\x04\x08" + b"\x4c\x19\xe7\xa2\x86\xd0\x35\xd0\xfe\xbf\xc1\x9b\x16\xfa\xb4\xc4" + b"\x34\x49\x0a\xd2\x1e\xe0\x78\x59\x39\x07\x31\x24\xe6\x8a\x1a\xb9" + b"\x25\x7d\x8b\x63\x0b\x65\x64\x51\x68\xa2\x19\x5e\x7d\xa1\xf4\xca" + b"\xc0\xd8\x20\x54\x91\xd7\xbb\x1b\xb8\xe1\x78\x70\x78\x11\xec\x0e" + b"\x37\xa6\xe8\x9f\x6e\xd4\x89\x2c\xcc\x8c\x10\x27\x7a\xd1\x47\x02" + b"\x77\xdb\x8a\x1b\x61\xc0\x6a\x2f\x12\x51\xfb\x44\xb9\x3f\x30\x85" + b"\x0b\x82\x4e\x62\x46\x2e\x51\x8e\xad\xb2\xee\xb4\x77\xe2\x47\x01" + b"\x23\x53\xa3\xd1\x15\xd3\x47\xa0\xda\x03\x51\xa3\x44\x7a\x09\x91" + b"\xf8\xc2\xab\x28\x3b\x80\x2a\x1a\x83\x99\xa4\xf6\x1d\x60\xaa\x59" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 223 + + b"\x34\x02\x0b\xae\x1a\xea\x33\x97\xbe\xa9\x9b\x27\xa5\xc4\x52\x0c" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 38 + + b"\x08\xd9\xf8\x57\x57\x4f\x5e\x81\xa7\x7c\xdf\xae\x48\xd3\xce\x92" + b"\x05\x9d\x19\x43\x93\x45\x9b\x55\x12\xd6\x59\x81\x4b\xfc\xdd\x95" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 6 + + b"\x62\x47\x69\x6e\xbc\xae\xca\x61\xf3\xe7\xc1\xa5\x1d\x4c\x3f\x1c" + b"\xaa\xa9\xfc\x2e\x9f\x85\x77\x93\xdc\xe6\xa8\x8f\xba\xf7\xc0\x1c" + b"\xd4\x15\xc2\x59\xba\x20\xff\xf9\x44\xab\xfe\x2b\x7c\xd3\xab\x68" + b"\x22\x97\x93\x73\xa4\x4a\xf4\x76\x13\x54\x6e\x7a\x86\x36\xe2\xa6" + b"\xc9\x57\x67\xf5\x29\xb8\x7d\x58\x6d\xd9\x37\x5b\x32\xe3\x17\x89" + b"\x53\xc4\xc3\x0a\xc2\x41\x0b\xab\x9d\x83\xe1\xde\x6b\x10\x0e\x08" + b"\xb6\xb1\xb8\x4e\xd8\x2c\x7f\xc7\x3a\xed\x02\x51\x0d\x25\x04\xde" + b"\x3e\x35\xe2\x5e\xe5\xda\x9c\x85\x0e\x44\x3d\xfb\xdb\x17\x56\x0e" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 7 + + b"\xef\x37\x28\x62\x58\xd4\x2f\x50\x80\x1a\x29\x66\xa4\x41\x2c\xde" + b"\x65\xc3\x80\x09\x1d\xe6\xb4\xe9\x36\x8e\x64\xc9\x24\x92\x9a\x7d" + b"\x65\x46\x4d\xd7\x30\x87\xb1\xf4\x41\xb6\x19\xfe\xf8\x72\x23\x1c" + b"\x89\xbb\x8a\x2a\x97\xc5\x96\xb6\xe5\xec\xba\x26\xd7\xd5\x46\x7b" + b"\x27\x0a\x12\x74\x95\x8f\xb7\x76\x4c\x81\x99\xd1\xee\x84\x46\xd0" + b"\x86\x6d\x7c\x71\x68\x27\xe7\x95\x98\x2b\x17\xde\x41\x1d\xb1\x76" + b"\xf9\x28\x8b\xbf\x6e\x4c\xe3\x10\x61\x77\x9b\x0d\x2c\x5f\x04\x08" + b"\x4c\x19\xe7\xa2\x86\xd0\x35\xd0\xfe\xbf\xc1\x9b\x16\xfa\xb4\xc4" + b"\x34\x49\x0a\xd2\x1e\xe0\x78\x59\x39\x07\x31\x24\xe6\x8a\x1a\xb9" + b"\x25\x7d\x8b\x63\x0b\x65\x64\x51\x68\xa2\x19\x5e\x7d\xa1\xf4\xca" + b"\xc0\xd8\x20\x54\x91\xd7\xbb\x1b\xb8\xe1\x78\x70\x78\x11\xec\x0e" + b"\x37\xa6\xe8\x9f\x6e\xd4\x89\x2c\xcc\x8c\x10\x27\x7a\xd1\x47\x02" + b"\x77\xdb\x8a\x1b\x61\xc0\x6a\x2f\x12\x51\xfb\x44\xb9\x3f\x30\x85" + b"\x0b\x82\x4e\x62\x46\x2e\x51\x8e\xad\xb2\xee\xb4\x77\xe2\x47\x01" + b"\x23\x53\xa3\xd1\x15\xd3\x47\xa0\xda\x03\x51\xa3\x44\x7a\x09\x91" + b"\xf8\xc2\xab\x28\x3b\x80\x2a\x1a\x83\x99\xa4\xf6\x1d\x60\xaa\x59" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 7 + + b"\x62\x47\x69\x6e\xbc\xae\xca\x61\xf3\xe7\xc1\xa5\x1d\x4c\x3f\x1c" + b"\xaa\xa9\xfc\x2e\x9f\x85\x77\x93\xdc\xe6\xa8\x8f\xba\xf7\xc0\x1c" + b"\xd4\x15\xc2\x59\xba\x20\xff\xf9\x44\xab\xfe\x2b\x7c\xd3\xab\x68" + b"\x22\x97\x93\x73\xa4\x4a\xf4\x76\x13\x54\x6e\x7a\x86\x36\xe2\xa6" + b"\xc9\x57\x67\xf5\x29\xb8\x7d\x58\x6d\xd9\x37\x5b\x32\xe3\x17\x89" + b"\x53\xc4\xc3\x0a\xc2\x41\x0b\xab\x9d\x83\xe1\xde\x6b\x10\x0e\x08" + b"\xb6\xb1\xb8\x4e\xd8\x2c\x7f\xc7\x3a\xed\x02\x51\x0d\x25\x04\xde" + b"\x3e\x35\xe2\x5e\xe5\xda\x9c\x85\x0e\x44\x3d\xfb\xdb\x17\x56\x0e" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 7 + + b"\xef\x37\x28\x62\x58\xd4\x2f\x50\x80\x1a\x29\x66\xa4\x41\x2c\xde" + b"\x65\xc3\x80\x09\x1d\xe6\xb4\xe9\x36\x8e\x64\xc9\x24\x92\x9a\x7d" + b"\x65\x46\x4d\xd7\x30\x87\xb1\xf4\x41\xb6\x19\xfe\xf8\x72\x23\x1c" + b"\x89\xbb\x8a\x2a\x97\xc5\x96\xb6\xe5\xec\xba\x26\xd7\xd5\x46\x7b" + b"\x27\x0a\x12\x74\x95\x8f\xb7\x76\x4c\x81\x99\xd1\xee\x84\x46\xd0" + b"\x86\x6d\x7c\x71\x68\x27\xe7\x95\x98\x2b\x17\xde\x41\x1d\xb1\x76" + b"\xf9\x28\x8b\xbf\x6e\x4c\xe3\x10\x61\x77\x9b\x0d\x2c\x5f\x04\x08" + b"\x4c\x19\xe7\xa2\x86\xd0\x35\xd0\xfe\xbf\xc1\x9b\x16\xfa\xb4\xc4" + b"\x34\x49\x0a\xd2\x1e\xe0\x78\x59\x39\x07\x31\x24\xe6\x8a\x1a\xb9" + b"\x25\x7d\x8b\x63\x0b\x65\x64\x51\x68\xa2\x19\x5e\x7d\xa1\xf4\xca" + b"\xc0\xd8\x20\x54\x91\xd7\xbb\x1b\xb8\xe1\x78\x70\x78\x11\xec\x0e" + b"\x93\x62\x36\x33\x9a\x61\xa3\xd0\xd1\x7f\x95\x5d\xb7\x3c\x15\x7e" + b"\x77\xdb\x8a\x1b\x61\xc0\x6a\x2f\x12\x51\xfb\x44\xb9\x3f\x30\x85" + b"\x0b\x82\x4e\x62\x46\x2e\x51\x8e\xad\xb2\xee\xb4\x77\xe2\x47\x01" + b"\x23\x53\xa3\xd1\x15\xd3\x47\xa0\xda\x03\x51\xa3\x44\x7a\x09\x91" + b"\xf8\xc2\xab\x28\x3b\x80\x2a\x1a\x83\x99\xa4\xf6\x1d\x60\xaa\x59" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 135 + + b"\xfb\xf3\x20\x0f\x3e\xa6\x26\x5c\x84\x22\x09\x03\x0d\x4b\x31\x7e" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\xfc\xfc\x9b\x91\xf4\x6b\x4a\x75\x59\x63\x7a\x57\xf9\xa3\xd3\x72" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\x5a\xfa\xf2\x3d\x3e\xa7\x9c\xa2\xe0\x94\xf1\x93\xa6\x05\xa6\xd7" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\x6c\xe5\xae\xc0\x9d\xaf\x24\x84\x7e\xe0\xaf\xa0\xdb\xca\x91\x9e" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\x3f\x05\x9f\x49\xea\x7f\x5c\x0f\x81\x76\x32\x1a\xc3\xb1\x6e\xcd" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\x9c\x81\xc6\x6e\x18\xf7\x08\xe9\x44\x4b\xbf\x80\x46\xb5\x9f\xf2" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\x8a\xf7\xab\x3f\x42\x30\x70\xe9\xc1\x81\x58\xe5\xc9\x34\x18\x00" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\x15\xfd\x5a\x09\x58\xe5\x13\x50\xb6\x8b\xc0\x68\x34\x10\x56\xdc" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\xd6\x7c\x7f\x85\x17\xc4\x64\xb9\x53\xe2\x02\x74\x8a\x54\xa3\xa7" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\xab\x80\x11\x81\x87\xb9\xe7\x02\x8d\xb1\xf2\x4c\x53\xa8\x30\xbd" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\xf1\xf4\x5a\x3c\x3d\xd0\x7f\x8a\xaf\x30\xc5\x36\x29\x42\x2b\x01" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 254 + + b"\xd0\x95\xed\xb4\x5e\x64\xe3\xfe\x3f\x14\x2b\x5b\xd7\x45\x11\xb2" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 6 + + b"\x38\x4c\x11\x67\x0f\x59\x65\xf2\xda\xd5\xdc\xc0\xe3\x1a\x51\x7a" + b"\x5b\x4e\xc2\x63\xaf\xba\x48\xf1\x7a\x84\x33\x38\x26\xba\x56\xb0" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 6 + + b"\xef\x37\x28\x62\x58\xd4\x2f\x50\x80\x1a\x29\x66\xa4\x41\x2c\xde" + b"\x65\xc3\x80\x09\x1d\xe6\xb4\xe9\x36\x8e\x64\xc9\x24\x92\x9a\x7d" + b"\x65\x46\x4d\xd7\x30\x87\xb1\xf4\x41\xb6\x19\xfe\xf8\x72\x23\x1c" + b"\x89\xbb\x8a\x2a\x97\xc5\x96\xb6\xe5\xec\xba\x26\xd7\xd5\x46\x7b" + b"\x27\x0a\x12\x74\x95\x8f\xb7\x76\x4c\x81\x99\xd1\xee\x84\x46\xd0" + b"\x86\x6d\x7c\x71\x68\x27\xe7\x95\x98\x2b\x17\xde\x41\x1d\xb1\x76" + b"\xf9\x28\x8b\xbf\x6e\x4c\xe3\x10\x61\x77\x9b\x0d\x2c\x5f\x04\x08" + b"\x4c\x19\xe7\xa2\x86\xd0\x35\xd0\xfe\xbf\xc1\x9b\x16\xfa\xb4\xc4" + b"\x34\x49\x0a\xd2\x1e\xe0\x78\x59\x39\x07\x31\x24\xe6\x8a\x1a\xb9" + b"\x25\x7d\x8b\x63\x0b\x65\x64\x51\x68\xa2\x19\x5e\x7d\xa1\xf4\xca" + b"\xc0\xd8\x20\x54\x91\xd7\xbb\x1b\xb8\xe1\x78\x70\x78\x11\xec\x0e" + b"\x37\xa6\xe8\x9f\x6e\xd4\x89\x2c\xcc\x8c\x10\x27\x7a\xd1\x47\x02" + b"\x77\xdb\x8a\x1b\x61\xc0\x6a\x2f\x12\x51\xfb\x44\xb9\x3f\x30\x85" + b"\x0b\x82\x4e\x62\x46\x2e\x51\x8e\xad\xb2\xee\xb4\x77\xe2\x47\x01" + b"\x23\x53\xa3\xd1\x15\xd3\x47\xa0\xda\x03\x51\xa3\x44\x7a\x09\x91" + b"\xf8\xc2\xab\x28\x3b\x80\x2a\x1a\x83\x99\xa4\xf6\x1d\x60\xaa\x59" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 223 + + b"\x52\xb0\x9a\xec\xf4\xe5\xf9\x36\x22\x69\x53\x53\x0a\x02\x22\xb5" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 38 + + b"\x2e\x35\xf9\x5d\x79\x34\xe4\xcf\x57\x79\x69\xde\x7a\xd6\xf5\x7c" + b"\x3d\xd9\x5c\x8a\x04\xe9\x48\x9c\x1f\xa3\x42\xc9\x7b\xa9\xf0\xbb" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 6 + + b"\x62\x47\x69\x6e\xbc\xae\xca\x61\xf3\xe7\xc1\xa5\x1d\x4c\x3f\x1c" + b"\xaa\xa9\xfc\x2e\x9f\x85\x77\x93\xdc\xe6\xa8\x8f\xba\xf7\xc0\x1c" + b"\xd4\x15\xc2\x59\xba\x20\xff\xf9\x44\xab\xfe\x2b\x7c\xd3\xab\x68" + b"\x22\x97\x93\x73\xa4\x4a\xf4\x76\x13\x54\x6e\x7a\x86\x36\xe2\xa6" + b"\xc9\x57\x67\xf5\x29\xb8\x7d\x58\x6d\xd9\x37\x5b\x32\xe3\x17\x89" + b"\x53\xc4\xc3\x0a\xc2\x41\x0b\xab\x9d\x83\xe1\xde\x6b\x10\x0e\x08" + b"\xb6\xb1\xb8\x4e\xd8\x2c\x7f\xc7\x3a\xed\x02\x51\x0d\x25\x04\xde" + b"\x3e\x35\xe2\x5e\xe5\xda\x9c\x85\x0e\x44\x3d\xfb\xdb\x17\x56\x0e" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 7 + + b"\xef\x37\x28\x62\x58\xd4\x2f\x50\x80\x1a\x29\x66\xa4\x41\x2c\xde" + b"\x65\xc3\x80\x09\x1d\xe6\xb4\xe9\x36\x8e\x64\xc9\x24\x92\x9a\x7d" + b"\x65\x46\x4d\xd7\x30\x87\xb1\xf4\x41\xb6\x19\xfe\xf8\x72\x23\x1c" + b"\x89\xbb\x8a\x2a\x97\xc5\x96\xb6\xe5\xec\xba\x26\xd7\xd5\x46\x7b" + b"\x27\x0a\x12\x74\x95\x8f\xb7\x76\x4c\x81\x99\xd1\xee\x84\x46\xd0" + b"\x86\x6d\x7c\x71\x68\x27\xe7\x95\x98\x2b\x17\xde\x41\x1d\xb1\x76" + b"\xf9\x28\x8b\xbf\x6e\x4c\xe3\x10\x61\x77\x9b\x0d\x2c\x5f\x04\x08" + b"\x4c\x19\xe7\xa2\x86\xd0\x35\xd0\xfe\xbf\xc1\x9b\x16\xfa\xb4\xc4" + b"\x34\x49\x0a\xd2\x1e\xe0\x78\x59\x39\x07\x31\x24\xe6\x8a\x1a\xb9" + b"\x25\x7d\x8b\x63\x0b\x65\x64\x51\x68\xa2\x19\x5e\x7d\xa1\xf4\xca" + b"\xc0\xd8\x20\x54\x91\xd7\xbb\x1b\xb8\xe1\x78\x70\x78\x11\xec\x0e" + b"\x37\xa6\xe8\x9f\x6e\xd4\x89\x2c\xcc\x8c\x10\x27\x7a\xd1\x47\x02" + b"\x77\xdb\x8a\x1b\x61\xc0\x6a\x2f\x12\x51\xfb\x44\xb9\x3f\x30\x85" + b"\x0b\x82\x4e\x62\x46\x2e\x51\x8e\xad\xb2\xee\xb4\x77\xe2\x47\x01" + b"\x23\x53\xa3\xd1\x15\xd3\x47\xa0\xda\x03\x51\xa3\x44\x7a\x09\x91" + b"\xf8\xc2\xab\x28\x3b\x80\x2a\x1a\x83\x99\xa4\xf6\x1d\x60\xaa\x59" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 175 + + b"\x10\xcc\x0f\x58\xdd\x28\xa8\x96\xcc\xc0\xfd\xc6\x7c\x3b\xe6\x4a" + b"\x78\x4a\x13\x07\x02\xd2\xbf\xf0\xff\x1d\x0a\xdc\x56\xa7\x94\x65" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 38 + + b"\x08\xd9\xf8\x57\x57\x4f\x5e\x81\xa7\x7c\xdf\xae\x48\xd3\xce\x92" + b"\x05\x9d\x19\x43\x93\x45\x9b\x55\x12\xd6\x59\x81\x4b\xfc\xdd\x95" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 6 + + b"\x62\x47\x69\x6e\xbc\xae\xca\x61\xf3\xe7\xc1\xa5\x1d\x4c\x3f\x1c" + b"\xaa\xa9\xfc\x2e\x9f\x85\x77\x93\xdc\xe6\xa8\x8f\xba\xf7\xc0\x1c" + b"\xd4\x15\xc2\x59\xba\x20\xff\xf9\x44\xab\xfe\x2b\x7c\xd3\xab\x68" + b"\x22\x97\x93\x73\xa4\x4a\xf4\x76\x13\x54\x6e\x7a\x86\x36\xe2\xa6" + b"\xc9\x57\x67\xf5\x29\xb8\x7d\x58\x6d\xd9\x37\x5b\x32\xe3\x17\x89" + b"\x53\xc4\xc3\x0a\xc2\x41\x0b\xab\x9d\x83\xe1\xde\x6b\x10\x0e\x08" + b"\xb6\xb1\xb8\x4e\xd8\x2c\x7f\xc7\x3a\xed\x02\x51\x0d\x25\x04\xde" + b"\x3e\x35\xe2\x5e\xe5\xda\x9c\x85\x0e\x44\x3d\xfb\xdb\x17\x56\x0e" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 7 + + b"\xef\x37\x28\x62\x58\xd4\x2f\x50\x80\x1a\x29\x66\xa4\x41\x2c\xde" + b"\x65\xc3\x80\x09\x1d\xe6\xb4\xe9\x36\x8e\x64\xc9\x24\x92\x9a\x7d" + b"\x65\x46\x4d\xd7\x30\x87\xb1\xf4\x41\xb6\x19\xfe\xf8\x72\x23\x1c" + b"\x89\xbb\x8a\x2a\x97\xc5\x96\xb6\xe5\xec\xba\x26\xd7\xd5\x46\x7b" + b"\x27\x0a\x12\x74\x95\x8f\xb7\x76\x4c\x81\x99\xd1\xee\x84\x46\xd0" + b"\x86\x6d\x7c\x71\x68\x27\xe7\x95\x98\x2b\x17\xde\x41\x1d\xb1\x76" + b"\xf9\x28\x8b\xbf\x6e\x4c\xe3\x10\x61\x77\x9b\x0d\x2c\x5f\x04\x08" + b"\x4c\x19\xe7\xa2\x86\xd0\x35\xd0\xfe\xbf\xc1\x9b\x16\xfa\xb4\xc4" + b"\x34\x49\x0a\xd2\x1e\xe0\x78\x59\x39\x07\x31\x24\xe6\x8a\x1a\xb9" + b"\x25\x7d\x8b\x63\x0b\x65\x64\x51\x68\xa2\x19\x5e\x7d\xa1\xf4\xca" + b"\xc0\xd8\x20\x54\x91\xd7\xbb\x1b\xb8\xe1\x78\x70\x78\x11\xec\x0e" + b"\x37\xa6\xe8\x9f\x6e\xd4\x89\x2c\xcc\x8c\x10\x27\x7a\xd1\x47\x02" + b"\x77\xdb\x8a\x1b\x61\xc0\x6a\x2f\x12\x51\xfb\x44\xb9\x3f\x30\x85" + b"\x0b\x82\x4e\x62\x46\x2e\x51\x8e\xad\xb2\xee\xb4\x77\xe2\x47\x01" + b"\x23\x53\xa3\xd1\x15\xd3\x47\xa0\xda\x03\x51\xa3\x44\x7a\x09\x91" + b"\xf8\xc2\xab\x28\x3b\x80\x2a\x1a\x83\x99\xa4\xf6\x1d\x60\xaa\x59" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 7 + + b"\x62\x47\x69\x6e\xbc\xae\xca\x61\xf3\xe7\xc1\xa5\x1d\x4c\x3f\x1c" + b"\xaa\xa9\xfc\x2e\x9f\x85\x77\x93\xdc\xe6\xa8\x8f\xba\xf7\xc0\x1c" + b"\xd4\x15\xc2\x59\xba\x20\xff\xf9\x44\xab\xfe\x2b\x7c\xd3\xab\x68" + b"\x22\x97\x93\x73\xa4\x4a\xf4\x76\x13\x54\x6e\x7a\x86\x36\xe2\xa6" + b"\xc9\x57\x67\xf5\x29\xb8\x7d\x58\x6d\xd9\x37\x5b\x32\xe3\x17\x89" + b"\x53\xc4\xc3\x0a\xc2\x41\x0b\xab\x9d\x83\xe1\xde\x6b\x10\x0e\x08" + b"\xb6\xb1\xb8\x4e\xd8\x2c\x7f\xc7\x3a\xed\x02\x51\x0d\x25\x04\xde" + b"\x3e\x35\xe2\x5e\xe5\xda\x9c\x85\x0e\x44\x3d\xfb\xdb\x17\x56\x0e" + b"\x8b\x65\x3e\xdb\xd8\x71\x50\xd7\x32\xf0\xee\x22\x9b\x40\x5c\x34" + + b"\x00\x88\xb7\x5c\x59\xda\x9b\xe1\xa6\xa1\xad\x02\xee\x27\x65\x54" + * 159 + ), + obj_full=None, + obj_simple=None, + context=dict( + aes=AES.new(key=KEY_MASTER, mode=AES.MODE_ECB), + ), + full_after_packing=False, + unpack_then_pack=False, + pack_then_unpack=False, + ), + id="kvstorage", + ), +] + + +@pytest.mark.parametrize("test", TEST_DATA) +class TestKVStorage(TestBase): + pass + + +del TestBase diff --git a/tests/test_kvstorage_structs.py b/tests/test_kvstorage_structs.py new file mode 100644 index 0000000..dfbf76b --- /dev/null +++ b/tests/test_kvstorage_structs.py @@ -0,0 +1,300 @@ +# Copyright (c) Kuba Szczodrzyński 2023-11-12. +# https://github.com/tuya-cloudcutter/bk7231tools/blob/main/bk7231tools/analysis/kvstorage.py + +import json +from dataclasses import dataclass +from io import SEEK_END, SEEK_SET +from json import JSONDecodeError +from logging import warning +from typing import Dict, List, Union + +from Crypto.Cipher import AES + +from datastruct import Context, DataStruct, datastruct +from datastruct.fields import ( + built, + checksum_end, + checksum_field, + checksum_start, + crypt, + crypt_end, + eval_into, + field, + repeat, + subfield, + switch, + tell, + text, + validate, + varlist, + virtual, +) + +KEY_MASTER = b"qwertyuiopasdfgh" +KEY_PART_1 = b"8710_2M" +KEY_PART_2 = b"HHRRQbyemofrtytf" +MAGIC_PROTECTED = 0x13579753 +MAGIC_KEY = 0x13579753 +MAGIC_DATA_1 = 0x98761234 +MAGIC_DATA_2 = 0x135726AB +ASCII = bytes(range(32, 128)) + b"\r\n" + +BLOCK_CRYPT = crypt( + block_size=16, + init=lambda ctx: ctx.G.root.aes, + decrypt=lambda data, obj, ctx: obj and obj.decrypt(data) or data, + encrypt=lambda data, obj, ctx: obj and obj.encrypt(data) or data, +) +BLOCK_CHECKSUM = checksum_field("block checksum")(field("I", default=0)) +BLOCK_CHECKSUM_CALC = checksum_start( + init=lambda ctx: 0, + update=lambda value, obj, ctx: (obj + sum(value)) & 0xFFFFFFFF, + end=lambda obj, ctx: obj, + target=BLOCK_CHECKSUM, +) +BLOCK_PADDING = field(lambda ctx: 4096 - ctx.P.tell()) +PAGE_PADDING = field(lambda ctx: 128 - ctx.P.tell()) + + +def block_magic(value: int): + return built("I", lambda ctx: value, always=False) + + +def block_magic_check(*values: int): + return validate( + check=lambda ctx: ctx.magic in values, + doc="block magic", + ) + + +def make_data_aes(inner_key: bytes) -> AES: + data_key = bytearray(16) + for i in range(0, 16): + data_key[i] = KEY_PART_1[i & 0b11] + KEY_PART_2[i] + for i in range(16): + data_key[i] = (data_key[i] + inner_key[i]) % 256 + return AES.new(key=data_key, mode=AES.MODE_ECB) + + +@dataclass +class ProtectedBlock(DataStruct): + @dataclass + @datastruct(padding_pattern=b"\x00") + class Data(DataStruct): + # sf_protected_data_s + length: int = built("I", lambda ctx: len(ctx.value)) + key: str = text(32) + value: bytes = field(lambda ctx: ctx.length) + + # sf_protected_s + _crypt: ... = BLOCK_CRYPT + magic: int = block_magic(MAGIC_PROTECTED) + _magic: ... = block_magic_check(MAGIC_PROTECTED) + checksum: int = BLOCK_CHECKSUM + _checksum: ... = BLOCK_CHECKSUM_CALC + + key: bytes = field(16) + data_length: int = built("I", lambda ctx: ctx.self.sizeof("data")) + reserved: int = field("I", default=0xFFFFFFFF) + + _crypt_inner: ... = crypt( + block_size=16, + init=lambda ctx: make_data_aes(ctx.key), + decrypt=lambda data, obj, ctx: obj.decrypt(data), + encrypt=lambda data, obj, ctx: obj.encrypt(data), + ) + data_start: int = tell() + data: List[Data] = varlist( + when=lambda ctx: ctx.P.tell() < ctx.data_start + ctx.data_length, + )(subfield()) + block_padding: bytes = BLOCK_PADDING + _crypt_inner_end: ... = crypt_end(_crypt_inner) + + _checksum_end: ... = checksum_end(_checksum) + _crypt_end: ... = crypt_end(_crypt) + + +@dataclass +class KeyBlock(DataStruct): + _crypt: ... = BLOCK_CRYPT + magic: int = block_magic(MAGIC_KEY) + _magic: ... = block_magic_check(MAGIC_KEY) + checksum: int = BLOCK_CHECKSUM + _checksum: ... = BLOCK_CHECKSUM_CALC + + key: bytes = field(16) + + _checksum_end: ... = checksum_end(_checksum) + block_padding: bytes = BLOCK_PADDING + _crypt_end: ... = crypt_end(_crypt) + + +@dataclass +class DataBlock(DataStruct): + # noinspection PyProtectedMember + @dataclass + @datastruct(padding_pattern=b"\x00") + class IndexPage(DataStruct): + @dataclass + class Part(DataStruct): + block_id: int = field("H") + page_id_start: int = field("B") + page_id_end: int = field("B") + + crc: int = field("I") + length: int = field("I") + block_id: int = field("H") + page_id: int = field("B") + parts_size: int = field("H") + element: int = field("I") + name_len: int = built("B", lambda ctx: len(ctx.name) + 1) + name: str = text(lambda ctx: ctx.name_len) + parts_data: List[Part] = repeat(lambda ctx: ctx.parts_size)(subfield()) + page_padding: bytes = PAGE_PADDING + + _id_check: ... = validate( + check=lambda ctx: ctx.block_id == ctx._.block_id + and ctx.page_id == ctx._.P.i + 1, + doc="block and page ID", + ) + + @dataclass + class DataPage(DataStruct): + data: bytes = field(128) + + _crypt: ... = BLOCK_CRYPT + magic: int = block_magic(MAGIC_DATA_1) + _magic: ... = block_magic_check(MAGIC_DATA_1, MAGIC_DATA_2) + checksum: int = BLOCK_CHECKSUM + _checksum: ... = BLOCK_CHECKSUM_CALC + + block_id: int = field("H") + unknown: int = field("I") + map_size: int = field("B") + map_data: List[int] = repeat(lambda ctx: ctx.map_size)(field("B")) + page_padding: bytes = PAGE_PADDING + + @staticmethod + def is_index_page(ctx: Context) -> bool: + i = ctx.P.i + 1 + return bool(ctx.map_data[i // 8] & (1 << i % 8)) + + pages: List[Union[DataPage, IndexPage]] = repeat(count=4096 // 128 - 1)( + switch(is_index_page)( + _True=(IndexPage, subfield()), + _False=(DataPage, subfield()), + ) + ) + + _checksum_end: ... = checksum_end(_checksum) + _crypt_end: ... = crypt_end(_crypt) + + +# noinspection PyProtectedMember +@dataclass +class KVStorage(DataStruct): + key_block: KeyBlock = subfield() + _data_aes: ... = eval_into( + "aes", lambda ctx: ctx.aes and make_data_aes(ctx.key_block.key) + ) + + @staticmethod + def check_end(ctx: Context) -> int: + pos = ctx.G.tell() + ctx.G.seek(0, SEEK_END) + end = ctx.G.tell() + ctx.G.seek(pos, SEEK_SET) + return end + + _data_end: int = virtual(check_end) + data_blocks: List[DataBlock] = repeat( + when=lambda ctx: ctx.G.tell() < ctx._data_end, + )(subfield()) + + def __post_init__(self) -> None: + self.blocks: Dict[ + int, Dict[int, Union[DataBlock.IndexPage, DataBlock.DataPage]] + ] = {} + self.indexes: Dict[str, DataBlock.IndexPage] = {} + + for block in self.data_blocks: + block_id = block.block_id + if block_id in self.blocks: + # skip swap blocks + continue + + block_pages = self.blocks[block_id] = {} + for i, page in enumerate(block.pages): + page_id = i + 1 + block_pages[page_id] = page + if not isinstance(page, DataBlock.IndexPage): + continue + + if block_id != page.block_id: + raise RuntimeError( + f"Block ID mismatch: in_block={page_id}, " + f"in_page={page.block_id}" + ) + if page_id != page.page_id: + raise RuntimeError( + f"Page ID mismatch: index={page_id}, id={page.page_id}" + ) + + if page.name in self.indexes: + warning(f"Duplicate index for '{page.name}': {page}") + continue + self.indexes[page.name] = page + + # added for test + self.read_all_values() + self.read_all_values_parsed() + + def read_value(self, index: DataBlock.IndexPage) -> bytes: + value = b"" + for part in index.parts_data: + block_id = part.block_id + if block_id not in self.blocks: + warning(f"Block by ID {block_id} does not exist, returning empty") + return value + for page_id in range(part.page_id_start, part.page_id_end + 1): + page = self.blocks[block_id].get(page_id, None) + if page is None: + raise RuntimeError( + f"Page by ID {page_id} does not exist in block {block_id}" + ) + if not isinstance(page, DataBlock.DataPage): + raise RuntimeError(f"Page is not a DataPage, but {type(page)}") + value += page.data + return value[0 : index.length] + + def read_value_parsed(self, index: DataBlock.IndexPage) -> Union[str, dict, list]: + value = self.read_value(index) + # non-binary string + value = value.rstrip(b"\x00") + if all(c in ASCII for c in value): + value = value.decode() + else: + return f"HEX:{value.hex()}" + # standard JSON + try: + return json.loads(value) + except JSONDecodeError: + pass + # something else? + return value + + def read_all_values(self) -> Dict[str, bytes]: + result = {} + for name, index in self.indexes.items(): + result[name] = self.read_value(index) + return result + + def read_all_values_parsed(self) -> Dict[str, Union[str, dict, list]]: + result = {} + for name, index in self.indexes.items(): + result[name] = self.read_value_parsed(index) + return result + + @property + def length(self) -> int: + return 0x1000 + len(self.blocks) * 0x1000 diff --git a/tests/test_ritool.py b/tests/test_ritool.py new file mode 100644 index 0000000..08103ce --- /dev/null +++ b/tests/test_ritool.py @@ -0,0 +1,136 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-11. + +import pytest +from base import TestBase, TestData +from test_ritool_structs import * + +TEST_DATA = [ + pytest.param( + TestData( + data=( + b"\x30\x31\x41\x4c\x43\x4c\x30\x32\x33\x46\x45\x35\x36\x33\x38\x39" + b"\x41\x42\x42\x44\x30\x31\x20\x20\x20\x20\x20\x20\x20\x20\x44\x45" + b"\x41\x44\x42\x45\x45\x46\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x47\x2d\x30\x31\x30\x47\x2d\x41\x31\x39\x30\x38\x31\x37\x00\xde" + b"\xad\xbe\xef\xaa\x00\x00\x00\x5e\x00\x01\x20\x20\x20\x20\x00\x00" + b"\x00\x00\x00\x01\x23\x45\x67\x89\xde\xad\xbe\xef\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x33\x46\x45\x35\x36\x33\x38\x39\x41\x42\x42\x41" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8f\xe7\x00\x00" + b"\x20\x20\x20\x20\x20\x20\x20\x20\x75\x73\x72\x61\x64\x6d\x69\x6e" + b"\x75\x73\x72\x61\x64\x6d\x69\x6e\x20\x20\x20\x20\x20\x20\x61\x64" + b"\x6d\x69\x6e\x61\x64\x6d\x69\x6e\x20\x41\x4c\x43\x23\x46\x47\x55" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x58\x58\x58\x58\x00\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x30\x30\x30\x30\x30\x31\x00\x00\x00\x00\x00\x00\xe2\xe3\x00\x00" + ), + obj_full=RiData( + format="01", + mfr_id="ALCL", + factory_code="02", + hw_version="3FE56389ABBD", + ics="01", + yp_serial_num=" DEADBEEF", + clei_code="0000000000", + mnemonic="G-010G-A", + prog_date="190817", + mac_address=MAC("00-DE-AD-BE-EF-AA"), + device_id_pref="0000", + sw_image="005e", + onu_mode="0001", + mnemonic2=" ", + password="00000000000123456789", + g894_serial="deadbeef", + hw_configuration="0000000000000000", + part_number="3FE56389ABBA", + spare4="000000000000000000000000", + checksum="8fe7", + inservice_reg="0000", + user_name=" usradmin", + user_password="usradmin", + mgnt_user_name=" adminadmin", + mgnt_user_password=" ALC#FGU", + ssid1_name="0000000000000000", + ssid1_password="00000000", + ssid2_name="0000000000000000", + ssid2_password="00000000", + operator_id="XXXX", + slid="00303030303030303030303030303030", + country_id="01", + spare_5="000000000000", + checksum1="e2e3", + spare6="0000", + ), + ), + id="ritool_1", + ), + pytest.param( + TestData( + data=( + b"\x30\x31\x41\x4c\x43\x4c\x30\x32\x33\x46\x45\x34\x36\x32\x35\x36" + b"\x41\x41\x41\x42\x30\x31\x20\x20\x20\x20\x20\x20\x20\x20\x44\x45" + b"\x41\x44\x42\x45\x45\x46\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x47\x2d\x32\x34\x30\x57\x2d\x43\x31\x39\x30\x33\x32\x39\x00\xde" + b"\xad\xbe\xef\xaa\x00\x00\x30\x30\x00\x03\x20\x20\x20\x20\x30\x30" + b"\x30\x30\x30\x30\x30\x30\x30\x30\xde\xad\xbe\xef\x30\x30\x30\x30" + b"\x30\x30\x30\x30\x33\x46\x45\x34\x36\x32\x35\x37\x41\x41\x41\x42" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6e\xe5\x30\x30" + b"\x20\x20\x20\x20\x20\x20\x20\x75\x73\x65\x72\x41\x64\x6d\x69\x6e" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x20\x20\x20\x20\x20\x20\x61\x64" + b"\x6d\x69\x6e\x61\x64\x6d\x69\x6e\x20\x41\x4c\x43\x23\x46\x47\x55" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x41\x4c\x43\x4c\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30" + b"\x30\x30\x30\x30\x65\x75\x30\x30\x30\x30\x30\x30\x1b\xe4\x30\x30" + ), + obj_full=RiData( + format="01", + mfr_id="ALCL", + factory_code="02", + hw_version="3FE46256AAAB", + ics="01", + yp_serial_num=" DEADBEEF", + clei_code="0000000000", + mnemonic="G-240W-C", + prog_date="190329", + mac_address=MAC("00-DE-AD-BE-EF-AA"), + device_id_pref="0000", + sw_image="3030", + onu_mode="0003", + mnemonic2=" ", + password="30303030303030303030", + g894_serial="deadbeef", + hw_configuration="3030303030303030", + part_number="3FE46257AAAB", + spare4="303030303030303030303030", + checksum="6ee5", + inservice_reg="3030", + user_name=" userAdmin", + user_password="00000000", + mgnt_user_name=" adminadmin", + mgnt_user_password=" ALC#FGU", + ssid1_name="0000000000000000", + ssid1_password="00000000", + ssid2_name="0000000000000000", + ssid2_password="00000000", + operator_id="ALCL", + slid="30303030303030303030303030303030", + country_id="eu", + spare_5="303030303030", + checksum1="1be4", + spare6="3030", + ), + ), + id="ritool_2", + ), +] + + +@pytest.mark.parametrize("test", TEST_DATA) +class TestRitool(TestBase): + pass + + +del TestBase diff --git a/tests/test_ritool_structs.py b/tests/test_ritool_structs.py new file mode 100644 index 0000000..cf11e69 --- /dev/null +++ b/tests/test_ritool_structs.py @@ -0,0 +1,56 @@ +# Copyright (c) Kuba Szczodrzyński 2023-11-23. + +from dataclasses import MISSING, dataclass + +from macaddress import MAC + +from datastruct import DataStruct +from datastruct.adapters.network import mac_field +from datastruct.fields import adapter, field, text +from datastruct.types import FormatType + + +def hexfield(fmt: FormatType, sep: str = "", *, default=..., default_factory=MISSING): + return adapter( + encode=lambda value, ctx: bytes.fromhex(value), + decode=lambda value, ctx: value.hex(), + )(field(fmt, default=default, default_factory=default_factory)) + + +@dataclass +class RiData(DataStruct): + format: str = text(2) + mfr_id: str = text(4) + factory_code: str = text(2) + hw_version: str = text(12) + ics: str = text(2) + yp_serial_num: str = text(16) + clei_code: str = text(10) + mnemonic: str = text(8) + prog_date: str = text(6) + mac_address: MAC = mac_field() + device_id_pref: str = hexfield(2) + sw_image: str = hexfield(2) + onu_mode: str = hexfield(2) + mnemonic2: str = text(4) + password: str = hexfield(10) + g894_serial: str = hexfield(4) + hw_configuration: str = hexfield(8) + part_number: str = text(12) + spare4: str = hexfield(12) + checksum: str = hexfield(2) + inservice_reg: str = hexfield(2) + user_name: str = text(16) + user_password: str = text(8) + mgnt_user_name: str = text(16) + mgnt_user_password: str = text(8) + ssid1_name: str = text(16) + ssid1_password: str = text(8) + ssid2_name: str = text(16) + ssid2_password: str = text(8) + operator_id: str = text(4) + slid: str = hexfield(16) + country_id: str = text(2) + spare_5: str = hexfield(6) + checksum1: str = hexfield(2) + spare6: str = hexfield(2) diff --git a/tests/test_tls.py b/tests/test_tls.py new file mode 100644 index 0000000..27fe8d6 --- /dev/null +++ b/tests/test_tls.py @@ -0,0 +1,305 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-12. + +import pytest +from base import TestBase, TestData +from test_tls_structs import * + +TEST_DATA = [ + pytest.param( + TestData( + data=( + b"\x17\x03\x03\x00\x22\xb8\x7a\x2b\x56\x5c\x56\xde\xce\x51\x6c\xe7" + b"\x60\xc6\x6d\xcc\xbb\xc2\xca\x71\x5d\xa3\x93\x96\x33\xfc\xd8\xed" + b"\x3f\xb6\x2b\xef\x16\x49\xa0" + ), + obj_full=TlsRecord( + type=TlsRecord.Type.APPLICATION_DATA, + version=TlsVersion.TLSv1_2, + length=34, + data=( + b"\xb8\x7a\x2b\x56\x5c\x56\xde\xce\x51\x6c\xe7\x60\xc6\x6d\xcc\xbb" + b"\xc2\xca\x71\x5d\xa3\x93\x96\x33\xfc\xd8\xed\x3f\xb6\x2b\xef\x16" + b"\x49\xa0" + ), + ), + ), + id="tls_data", + ), + pytest.param( + TestData( + data=( + b"\x16\x03\x03\x01\x7e\x01\x00\x01\x7a\x03\x03\xa7\x0f\xc2\x76\x65" + b"\xa7\x18\xfb\x88\xe9\x93\x97\x2b\x02\x70\x1b\x10\xf3\x32\x05\xb8" + b"\x8a\x5c\xe8\x4d\x33\xad\xa3\x55\xdf\xaa\x54\x20\x71\x79\x89\xf3" + b"\x72\xa9\x5c\x95\x7c\xc0\x9a\x2f\xc1\x39\xab\x4b\x77\xbf\x71\x90" + b"\xee\x6d\xba\x84\x83\x87\xe0\x6b\x30\x39\x9e\xf9\x00\x26\xc0\x2b" + b"\xc0\x2f\xc0\x2c\xc0\x30\xcc\xa9\xcc\xa8\xc0\x09\xc0\x13\xc0\x0a" + b"\xc0\x14\x00\x9c\x00\x9d\x00\x2f\x00\x35\xc0\x12\x00\x0a\x13\x01" + b"\x13\x02\x13\x03\x01\x00\x01\x0b\x00\x00\x00\x15\x00\x13\x00\x00" + b"\x10\x61\x70\x69\x2e\x77\x61\x6b\x61\x74\x69\x6d\x65\x2e\x63\x6f" + b"\x6d\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x0a\x00\x0a\x00\x08" + b"\x00\x1d\x00\x17\x00\x18\x00\x19\x00\x0b\x00\x02\x01\x00\x00\x0d" + b"\x00\x1a\x00\x18\x08\x04\x04\x03\x08\x07\x08\x05\x08\x06\x04\x01" + b"\x05\x01\x06\x01\x05\x03\x06\x03\x02\x01\x02\x03\xff\x01\x00\x01" + b"\x00\x00\x17\x00\x00\x00\x10\x00\x0e\x00\x0c\x02\x68\x32\x08\x68" + b"\x74\x74\x70\x2f\x31\x2e\x31\x00\x12\x00\x00\x00\x2b\x00\x05\x04" + b"\x03\x04\x03\x03\x00\x33\x00\x8b\x00\x89\x00\x19\x00\x85\x04\x00" + b"\xd2\xbd\xd4\x25\xc3\x68\x78\xb0\x02\x20\xaa\xf3\x07\xe7\x16\xca" + b"\x10\x8b\x56\x43\x47\x89\x47\x4a\xaf\x65\x0f\x61\xe1\x58\x9d\x72" + b"\xb9\x80\x85\x6e\xd8\xfd\x98\x2b\x21\x6c\x49\xb6\xb7\x6f\x14\xc2" + b"\x39\x8e\x9d\x23\x12\x1a\xc8\xa2\x5b\xff\x52\x9a\x08\x50\xd4\x44" + b"\xfe\x01\x64\xd2\xa6\x09\x99\xc6\x97\xce\x3e\xab\xdf\x6d\x39\x3c" + b"\x99\x16\x9a\x93\x47\x02\x6c\xd5\xbb\x87\x5b\xbd\xe4\x33\xda\x17" + b"\xd0\x35\x65\x5c\x07\x52\x58\xdb\xb0\xfb\xcd\x4e\xb7\x5c\xea\xd7" + b"\x8a\xd7\xa7\xdb\x40\xcd\xe4\xd4\xde\x98\x36\x0b\xca\xf1\xd0\xd5" + b"\xe7\xaf\x18" + ), + obj_full=TlsRecord( + type=TlsRecord.Type.HANDSHAKE, + version=TlsVersion.TLSv1_2, + length=382, + data=TlsHandshake( + type=TlsHandshake.Type.CLIENT_HELLO, + length=378, + data=TlsHandshakeHello( + version=TlsVersion.TLSv1_2, + random=( + b"\xa7\x0f\xc2\x76\x65\xa7\x18\xfb" + b"\x88\xe9\x93\x97\x2b\x02\x70\x1b" + b"\x10\xf3\x32\x05\xb8\x8a\x5c\xe8" + b"\x4d\x33\xad\xa3\x55\xdf\xaa\x54" + ), + session_id_length=32, + session_id=( + b"\x71\x79\x89\xf3\x72\xa9\x5c\x95" + b"\x7c\xc0\x9a\x2f\xc1\x39\xab\x4b" + b"\x77\xbf\x71\x90\xee\x6d\xba\x84" + b"\x83\x87\xe0\x6b\x30\x39\x9e\xf9" + ), + cipher_suites_length=38, + cipher_suites=[ + 49195, + 49199, + 49196, + 49200, + 52393, + 52392, + 49161, + 49171, + 49162, + 49172, + 156, + 157, + 47, + 53, + 49170, + 10, + 4865, + 4866, + 4867, + ], + compression_methods_length=1, + compression_methods=[0], + extensions_length=267, + extensions=[ + TlsExtension( + type=0, + length=21, + data=TlsExtension.ServerName( + names_length=19, + names=[ + TlsExtension.ServerName.Name( + type=0, + length=16, + value="api.wakatime.com", + ) + ], + ), + ), + TlsExtension( + type=5, length=5, data=b"\x01\x00\x00\x00\x00" + ), + TlsExtension( + type=10, + length=10, + data=b"\x00\x08\x00\x1d\x00\x17\x00\x18\x00\x19", + ), + TlsExtension(type=11, length=2, data=b"\x01\x00"), + TlsExtension( + type=13, + length=26, + data=( + b"\x00\x18\x08\x04\x04\x03\x08\x07" + b"\x08\x05\x08\x06\x04\x01\x05\x01" + b"\x06\x01\x05\x03\x06\x03\x02\x01" + b"\x02\x03" + ), + ), + TlsExtension(type=65281, length=1, data=b"\x00"), + TlsExtension(type=23, length=0, data=b""), + TlsExtension( + type=16, + length=14, + data=b"\x00\x0c\x02h2\x08http/1.1", + ), + TlsExtension(type=18, length=0, data=b""), + TlsExtension( + type=43, length=5, data=b"\x04\x03\x04\x03\x03" + ), + TlsExtension( + type=51, + length=139, + data=( + b"\x00\x89\x00\x19\x00\x85\x04\x00" + b"\xd2\xbd\xd4\x25\xc3\x68\x78\xb0" + b"\x02\x20\xaa\xf3\x07\xe7\x16\xca" + b"\x10\x8b\x56\x43\x47\x89\x47\x4a" + b"\xaf\x65\x0f\x61\xe1\x58\x9d\x72" + b"\xb9\x80\x85\x6e\xd8\xfd\x98\x2b" + b"\x21\x6c\x49\xb6\xb7\x6f\x14\xc2" + b"\x39\x8e\x9d\x23\x12\x1a\xc8\xa2" + b"\x5b\xff\x52\x9a\x08\x50\xd4\x44" + b"\xfe\x01\x64\xd2\xa6\x09\x99\xc6" + b"\x97\xce\x3e\xab\xdf\x6d\x39\x3c" + b"\x99\x16\x9a\x93\x47\x02\x6c\xd5" + b"\xbb\x87\x5b\xbd\xe4\x33\xda\x17" + b"\xd0\x35\x65\x5c\x07\x52\x58\xdb" + b"\xb0\xfb\xcd\x4e\xb7\x5c\xea\xd7" + b"\x8a\xd7\xa7\xdb\x40\xcd\xe4\xd4" + b"\xde\x98\x36\x0b\xca\xf1\xd0\xd5" + b"\xe7\xaf\x18" + ), + ), + ], + ), + ), + ), + ), + id="tls_client_hello", + ), + pytest.param( + TestData( + data=( + b"\x16\x03\x03\x00\xdf\x02\x00\x00\xdb\x03\x03\xc9\xe8\x60\x6c\xd5" + b"\x71\x68\x28\xef\x70\x6c\x02\x91\xd2\x7f\x7e\xf0\x91\xc4\x31\xdc" + b"\x82\x73\x8c\x78\x09\x6a\xf9\xba\xe3\x1d\x98\x20\x71\x79\x89\xf3" + b"\x72\xa9\x5c\x95\x7c\xc0\x9a\x2f\xc1\x39\xab\x4b\x77\xbf\x71\x90" + b"\xee\x6d\xba\x84\x83\x87\xe0\x6b\x30\x39\x9e\xf9\x13\x02\x00\x00" + b"\x93\x00\x2b\x00\x02\x03\x04\x00\x33\x00\x89\x00\x19\x00\x85\x04" + b"\x01\x06\x7e\x15\xbc\x80\x5b\xd1\x91\xe0\xb1\x46\x67\x99\xad\xed" + b"\x21\x68\xd8\x14\x85\x7f\x08\xcf\x76\x58\x70\x00\xed\xf4\xcd\x43" + b"\x59\xf7\xde\xd5\x12\xa1\x09\x3b\xd5\xcf\xa9\xfd\xf4\x5d\xc6\x2f" + b"\x80\x7b\x1a\x77\x0d\x7c\x0e\x9a\x71\x52\x2f\x19\x91\x8b\x17\x35" + b"\xa6\x1a\x00\x3d\x65\x0d\xd7\xb6\xb8\x92\x39\x28\x57\x2f\x46\x7a" + b"\x00\x6d\xb8\x8c\x09\x83\xaa\xab\x67\xb9\x25\x3d\xc5\x42\x87\x4d" + b"\x04\xb5\x7d\xa8\x78\xf7\x86\xb7\x68\xa1\x42\x03\x82\xf6\x97\x08" + b"\x6e\x13\x5e\x27\xdc\x5c\x78\xaf\x38\x1a\x9f\x89\x14\x8d\xb9\xba" + b"\x2e\xde\x90\x8d" + ), + obj_full=TlsRecord( + type=TlsRecord.Type.HANDSHAKE, + version=TlsVersion.TLSv1_2, + length=223, + data=TlsHandshake( + type=TlsHandshake.Type.SERVER_HELLO, + length=219, + data=TlsHandshakeHello( + version=TlsVersion.TLSv1_2, + random=( + b"\xc9\xe8\x60\x6c\xd5\x71\x68\x28" + b"\xef\x70\x6c\x02\x91\xd2\x7f\x7e" + b"\xf0\x91\xc4\x31\xdc\x82\x73\x8c" + b"\x78\x09\x6a\xf9\xba\xe3\x1d\x98" + ), + session_id_length=32, + session_id=( + b"\x71\x79\x89\xf3\x72\xa9\x5c\x95" + b"\x7c\xc0\x9a\x2f\xc1\x39\xab\x4b" + b"\x77\xbf\x71\x90\xee\x6d\xba\x84" + b"\x83\x87\xe0\x6b\x30\x39\x9e\xf9" + ), + cipher_suites_length=2, + cipher_suites=[4866], + compression_methods_length=1, + compression_methods=[0], + extensions_length=147, + extensions=[ + TlsExtension(type=43, length=2, data=b"\x03\x04"), + TlsExtension( + type=51, + length=137, + data=( + b"\x00\x19\x00\x85\x04\x01\x06\x7e" + b"\x15\xbc\x80\x5b\xd1\x91\xe0\xb1" + b"\x46\x67\x99\xad\xed\x21\x68\xd8" + b"\x14\x85\x7f\x08\xcf\x76\x58\x70" + b"\x00\xed\xf4\xcd\x43\x59\xf7\xde" + b"\xd5\x12\xa1\x09\x3b\xd5\xcf\xa9" + b"\xfd\xf4\x5d\xc6\x2f\x80\x7b\x1a" + b"\x77\x0d\x7c\x0e\x9a\x71\x52\x2f" + b"\x19\x91\x8b\x17\x35\xa6\x1a\x00" + b"\x3d\x65\x0d\xd7\xb6\xb8\x92\x39" + b"\x28\x57\x2f\x46\x7a\x00\x6d\xb8" + b"\x8c\x09\x83\xaa\xab\x67\xb9\x25" + b"\x3d\xc5\x42\x87\x4d\x04\xb5\x7d" + b"\xa8\x78\xf7\x86\xb7\x68\xa1\x42" + b"\x03\x82\xf6\x97\x08\x6e\x13\x5e" + b"\x27\xdc\x5c\x78\xaf\x38\x1a\x9f" + b"\x89\x14\x8d\xb9\xba\x2e\xde\x90" + b"\x8d" + ), + ), + ], + ), + ), + ), + ), + id="tls_server_hello", + ), + pytest.param( + TestData( + data=b"\x14\x03\x03\x00\x01\x01", + obj_full=TlsRecord( + type=TlsRecord.Type.CHANGE_CIPHER_SPEC, + version=TlsVersion.TLSv1_2, + length=1, + data=b"\x01", + ), + ), + id="tls_change_cipher_spec", + ), + pytest.param( + TestData( + data=( + b"\x15\x03\x03\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x20\x85\x7e\x51" + b"\xe6\x1b\x07\x94\xb7\x29\x8b\x47\xbe\x05\x0d\xff\x61\x7b\x88" + ), + obj_full=TlsRecord( + type=TlsRecord.Type.ALERT, + version=TlsVersion.TLSv1_2, + length=26, + data=( + b"\x00\x00\x00\x00\x00\x00\x00\x20\x85\x7e\x51\xe6\x1b\x07\x94\xb7" + b"\x29\x8b\x47\xbe\x05\x0d\xff\x61\x7b\x88" + ), + ), + ), + id="tls_encrypted_alert", + ), + pytest.param( + TestData( + cls=TlsRecord, + data=b"\x16\x03\x03\x00\x04\x0e\x00\x00\x00", + obj_full=None, + ), + id="tls_server_hello_done", + ), +] + + +@pytest.mark.parametrize("test", TEST_DATA) +class TestTls(TestBase): + pass + + +del TestBase diff --git a/tests/test_tls_structs.py b/tests/test_tls_structs.py new file mode 100644 index 0000000..6b07395 --- /dev/null +++ b/tests/test_tls_structs.py @@ -0,0 +1,197 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-12. +# https://github.com/kuba2k2/pynetkit/blob/master/pynetkit/modules/proxy/structs.py + +from dataclasses import dataclass +from enum import Enum, IntEnum + +from datastruct import NETWORK, DataStruct, datastruct +from datastruct.fields import cond, field, padding, repeat, subfield, switch, text + + +class TlsVersion(IntEnum): + SSLv3 = 0x300 + TLSv1 = 0x301 + TLSv1_1 = 0x302 + TLSv1_2 = 0x303 + TLSv1_3 = 0x304 + + +@dataclass +@datastruct(endianness=NETWORK, padding_pattern=b"\x00") +class TlsExtension(DataStruct): + class Type(IntEnum): + SERVER_NAME = 0 + MAX_FRAGMENT_LENGTH = 1 + CLIENT_CERTIFICATE_URL = 2 + TRUSTED_CA_KEYS = 3 + TRUNCATED_HMAC = 4 + STATUS_REQUEST = 5 + USER_MAPPING = 6 + CLIENT_AUTHZ = 7 + SERVER_AUTHZ = 8 + CERT_TYPE = 9 + SUPPORTED_GROUPS = 10 + EC_POINT_FORMATS = 11 + SRP = 12 + SIGNATURE_ALGORITHMS = 13 + USE_SRTP = 14 + HEARTBEAT = 15 + APPLICATION_LAYER_PROTOCOL_NEGOTIATION = 16 + STATUS_REQUEST_V2 = 17 + SIGNED_CERTIFICATE_TIMESTAMP = 18 + CLIENT_CERTIFICATE_TYPE = 19 + SERVER_CERTIFICATE_TYPE = 20 + PADDING = 21 + ENCRYPT_THEN_MAC = 22 + EXTENDED_MASTER_SECRET = 23 + TOKEN_BINDING = 24 + CACHED_INFO = 25 + TLS_LTS = 26 + COMPRESS_CERTIFICATE = 27 + RECORD_SIZE_LIMIT = 28 + PWD_PROTECT = 29 + PWD_CLEAR = 30 + PASSWORD_SALT = 31 + TICKET_PINNING = 32 + TLS_CERT_WITH_EXTERN_PSK = 33 + DELEGATED_CREDENTIAL = 34 + SESSION_TICKET = 35 + TLMSP = 36 + TLMSP_PROXYING = 37 + TLMSP_DELEGATE = 38 + SUPPORTED_EKT_CIPHERS = 39 + PRE_SHARED_KEY = 41 + EARLY_DATA = 42 + SUPPORTED_VERSIONS = 43 + COOKIE = 44 + PSK_KEY_EXCHANGE_MODES = 45 + CERTIFICATE_AUTHORITIES = 47 + OID_FILTERS = 48 + POST_HANDSHAKE_AUTH = 49 + SIGNATURE_ALGORITHMS_CERT = 50 + KEY_SHARE = 51 + TRANSPARENCY_INFO = 52 + CONNECTION_ID_DEPRECATED = 53 + CONNECTION_ID = 54 + EXTERNAL_ID_HASH = 55 + EXTERNAL_SESSION_ID = 56 + QUIC_TRANSPORT_PARAMETERS = 57 + TICKET_REQUEST = 58 + DNSSEC_CHAIN = 59 + SEQUENCE_NUMBER_ENCRYPTION_ALGORITHMS = 60 + RRC = 61 + ECH_OUTER_EXTENSIONS = 64768 + ENCRYPTED_CLIENT_HELLO = 65037 + RENEGOTIATION_INFO = 65281 + + @dataclass + @datastruct(endianness=NETWORK, padding_pattern=b"\x00") + class ServerName(DataStruct): + @dataclass + @datastruct(endianness=NETWORK, padding_pattern=b"\x00") + class Name(DataStruct): + type: int = field("B") + length: int = field("H") + value: str = text(lambda ctx: ctx.length) + + names_length: int = field("H") + names: list[Name] = repeat( + length=lambda ctx: ctx.names_length, + )(subfield()) + + type: int = field("H") + length: int = field("H") + data: bytes | ServerName = switch(lambda ctx: bool(ctx.length) and ctx.type)( + _0=(ServerName, subfield()), + default=(bytes, field(lambda ctx: ctx.length)), + ) + + +@dataclass +@datastruct(endianness=NETWORK, padding_pattern=b"\x00") +class TlsHandshakeHello(DataStruct): + version: TlsVersion = field("H") + random: bytes = field(32) + session_id_length: int = field("B") + session_id: bytes = field(lambda ctx: ctx.session_id_length) + cipher_suites_length: int = cond( + lambda ctx: ctx._.type == TlsHandshake.Type.CLIENT_HELLO, + if_not=2, + )(field("H")) + cipher_suites: list[int] = repeat( + lambda ctx: ctx.cipher_suites_length // 2, + )(field("H")) + compression_methods_length: int = cond( + lambda ctx: ctx._.type == TlsHandshake.Type.CLIENT_HELLO, + if_not=1, + )(field("B")) + compression_methods: list[int] = repeat( + lambda ctx: ctx.compression_methods_length, + )(field("B")) + extensions_length: int = field("H") + extensions: list[TlsExtension] = repeat( + length=lambda ctx: ctx.extensions_length, + )(subfield()) + + +@dataclass +@datastruct(endianness=NETWORK, padding_pattern=b"\x00") +class TlsHandshakeCertificate(DataStruct): + @dataclass + @datastruct(endianness=NETWORK, padding_pattern=b"\x00") + class Certificate(DataStruct): + _1: ... = padding(1) + length: int = field("H") + data: bytes = field(lambda ctx: ctx.length) + + _1: ... = padding(1) + certificates_length: int = field("H") + certificates: list[Certificate] = repeat( + length=lambda ctx: ctx.certificates_length, + )(subfield()) + + +@dataclass +@datastruct(endianness=NETWORK, padding_pattern=b"\x00") +class TlsHandshake(DataStruct): + class Type(Enum): + CLIENT_HELLO = 1 + SERVER_HELLO = 2 + CERTIFICATE = 11 + + @classmethod + def _missing_(cls, value): + unknown = object.__new__(TlsHandshake.Type) + unknown._name_ = f"UNKNOWN_{value}" + unknown._value_ = value + return unknown + + type: Type = field("B") + _1: ... = padding(1) + length: int = field("H") + data: bytes | TlsHandshakeHello | TlsHandshakeCertificate = switch( + lambda ctx: ctx.type + )( + CLIENT_HELLO=(TlsHandshakeHello, subfield()), + SERVER_HELLO=(TlsHandshakeHello, subfield()), + CERTIFICATE=(TlsHandshakeCertificate, subfield()), + default=(bytes, field(lambda ctx: ctx.length)), + ) + + +@dataclass +@datastruct(endianness=NETWORK, padding_pattern=b"\x00") +class TlsRecord(DataStruct): + class Type(IntEnum): + CHANGE_CIPHER_SPEC = 20 + ALERT = 21 + HANDSHAKE = 22 + APPLICATION_DATA = 23 + + type: Type = field("B") + version: TlsVersion = field("H") + length: int = field("H") + data: bytes | TlsHandshake = switch(lambda ctx: ctx.type)( + HANDSHAKE=(TlsHandshake, subfield()), + default=(bytes, field(lambda ctx: ctx.length)), + ) diff --git a/tests/test_uf2.py b/tests/test_uf2.py new file mode 100644 index 0000000..3da7304 --- /dev/null +++ b/tests/test_uf2.py @@ -0,0 +1,91 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-11. + +import pytest +from base import TestBase, TestData +from test_uf2_structs import * + +TEST_DATA = [ + pytest.param( + TestData( + data=( + b"\x30\x31\x50\x45\x61\x70\x70\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x66\x6c\x61\x73\x68\x30\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x10\x01\x00\x00\x10\x12\x00\x00\x00\x00\x00" + b"\x30\x31\x50\x45\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x66\x6c\x61\x73\x68\x30\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x20\x13\x00\x00\x60\x0a\x00\x00\x00\x00\x00" + ), + obj_full=PartitionTable( + partitions=[ + Partition( + magic_word=1162883376, + name="app", + flash_name="flash0", + offset=69632, + length=1183744, + ), + Partition( + magic_word=1162883376, + name="download", + flash_name="flash0", + offset=1253376, + length=679936, + ), + ] + ), + obj_simple=PartitionTable( + partitions=[ + Partition( + name="app", + flash_name="flash0", + offset=69632, + length=1183744, + ), + Partition( + name="download", + flash_name="flash0", + offset=1253376, + length=679936, + ), + ] + ), + context=dict( + name_len=16, + length=0x30 * 2, + ), + ), + id="uf2_partition_table", + ), + pytest.param( + TestData( + data=( + b"\x30\x31\x50\x45\x61\x70\x70\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x66\x6c\x61\x73" + b"\x68\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x10\x01\x00\x00\x10\x12\x00\x00\x00\x00\x00" + ), + obj_full=Partition( + magic_word=1162883376, + name="app", + flash_name="flash0", + offset=69632, + length=1183744, + ), + obj_simple=Partition( + name="app", + flash_name="flash0", + offset=69632, + length=1183744, + ), + ), + id="uf2_partition", + ), +] + + +@pytest.mark.parametrize("test", TEST_DATA) +class TestUF2(TestBase): + pass + + +del TestBase diff --git a/tests/test_uf2_structs.py b/tests/test_uf2_structs.py new file mode 100644 index 0000000..4e33c1d --- /dev/null +++ b/tests/test_uf2_structs.py @@ -0,0 +1,26 @@ +# Copyright (c) Kuba Szczodrzyński 2023-3-25. +# https://github.com/libretiny-eu/ltchiptool/blob/master/uf2tool/models/partition.py + +from dataclasses import dataclass +from typing import List + +from datastruct import DataStruct, Endianness, datastruct +from datastruct.fields import const, field, padding, repeat, subfield, text + + +@dataclass +@datastruct(endianness=Endianness.LITTLE, padding_pattern=b"\x00") +class Partition(DataStruct): + magic_word: int = const(0x45503130)(field("I")) + name: str = text(lambda ctx: ctx.G.root.name_len or 24) + flash_name: str = text(lambda ctx: ctx.G.root.name_len or 24) + offset: int = field("I") + length: int = field("I") + _1: ... = padding(4) + + +@dataclass +class PartitionTable(DataStruct): + partitions: List[Partition] = repeat(when=lambda ctx: ctx.G.tell() < ctx.length)( + subfield() + ) diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..b88aedb --- /dev/null +++ b/tests/util.py @@ -0,0 +1,22 @@ +# Copyright (c) Kuba Szczodrzyński 2024-10-12. + + +def read_data_file(name_or_url: str, gzipped: bool = False) -> bytes: + if name_or_url.startswith("http"): + import requests + + print(f"Downloading data from '{name_or_url}'") + with requests.get(name_or_url) as r: + data = r.content + else: + from pathlib import Path + + print(f"Reading data from '{name_or_url}'") + path = Path(__file__).with_name(name_or_url) + data = path.read_bytes() + + if gzipped: + import gzip + + return gzip.decompress(data) + return data