From b582409e99b9bcbd5a33e0456b53ed289cd5bdc4 Mon Sep 17 00:00:00 2001 From: Robin B Date: Sat, 25 Oct 2025 23:41:05 +0200 Subject: [PATCH 1/2] chore upgrade deps --- Cargo.lock | 49 ++++++++++++++--------------- Cargo.toml | 4 +-- pyproject.toml | 5 +++ uv.lock | 85 +++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 115 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 605465b..cfe302b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,15 +10,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -37,9 +37,9 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "memoffset" @@ -64,9 +64,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -81,9 +81,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" dependencies = [ "indoc", "libc", @@ -98,19 +98,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" dependencies = [ "libc", "pyo3-build-config", @@ -118,9 +117,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -130,9 +129,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" dependencies = [ "heck", "proc-macro2", @@ -143,18 +142,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "syn" -version = "2.0.104" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -169,9 +168,9 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" [[package]] name = "unindent" diff --git a/Cargo.toml b/Cargo.toml index 81f6504..c10f69f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,6 @@ name = "pycrc32" crate-type = ["cdylib"] [dependencies] -crc32fast = "1.4.2" -pyo3 = "0.25.1" +crc32fast = "1.5.0" +pyo3 = "0.27.1" diff --git a/pyproject.toml b/pyproject.toml index 10fc520..5002fe5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,3 +41,8 @@ build-backend = "maturin" [tool.maturin] features = ["pyo3/extension-module"] + +[dependency-groups] +dev = [ + "maturin>=1.9.6", +] diff --git a/uv.lock b/uv.lock index 47c415a..340a9ee 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,90 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.8" +[[package]] +name = "maturin" +version = "1.9.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/35/c3370188492f4c139c7a318f438d01b8185c216303c49c4bc885c98b6afb/maturin-1.9.6.tar.gz", hash = "sha256:2c2ae37144811d365509889ed7220b0598487f1278c2441829c3abf56cc6324a", size = 214846, upload-time = "2025-10-07T12:45:08.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/5c/b435418ba4ba2647a1f7a95d53314991b1e556e656ae276dea993c3bce1d/maturin-1.9.6-py3-none-linux_armv6l.whl", hash = "sha256:26e3ab1a42a7145824210e9d763f6958f2c46afb1245ddd0bab7d78b1f59bb3f", size = 8134483, upload-time = "2025-10-07T12:44:44.274Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/8e58eda6601f328b412cdeeaa88a9b6a10e591e2a73f313e8c0154d68385/maturin-1.9.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5263dda3f71feef2e4122baf5c4620e4b3710dbb7f2121f85a337182de214369", size = 15776470, upload-time = "2025-10-07T12:44:47.476Z" }, + { url = "https://files.pythonhosted.org/packages/6c/33/8c967cce6848cdd87a2e442c86120ac644b80c5ed4c32e3291bde6a17df8/maturin-1.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe78262c2800c92f67d1ce3c0f6463f958a692cc67bfb572e5dbf5b4b696a8ba", size = 8226557, upload-time = "2025-10-07T12:44:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/58/bd/3e2675cdc8b7270700ba30c663c852a35694441732a107ac30ebd6878bd8/maturin-1.9.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7ab827c6e8c022eb2e1e7fb6deede54549c8460b20ccc2e9268cc6e8cde957a8", size = 8166544, upload-time = "2025-10-07T12:44:51.396Z" }, + { url = "https://files.pythonhosted.org/packages/58/1f/a2047ddf2230e700d5f8a13dd4b9af5ce806ad380c32e58105888205926e/maturin-1.9.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:0246202377c49449315305209f45c8ecef6e2d6bd27a04b5b6f1ab3e4ea47238", size = 8641010, upload-time = "2025-10-07T12:44:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/be/1f/265d63c7aa6faf363d4a3f23396f51bc6b4d5c7680a4190ae68dba25dea2/maturin-1.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f5bac167700fbb6f8c8ed1a97b494522554b4432d7578e11403b894b6a91d99f", size = 7965945, upload-time = "2025-10-07T12:44:55.248Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ca/a8e61979ccfe080948bcc1bddd79356157aee687134df7fb013050cec783/maturin-1.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7f53d3b1d8396d3fea3e1ee5fd37558bca5719090f3d194ba1c02b0b56327ae3", size = 7978820, upload-time = "2025-10-07T12:44:56.919Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/81b412f8ad02a99801ef19ec059fba0822d1d28fb44cb6a92e722f05f278/maturin-1.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:7f506eb358386d94d6ec3208c003130cf4b69cab26034fc0cbbf8bf83afa4c2e", size = 10452064, upload-time = "2025-10-07T12:44:58.232Z" }, + { url = "https://files.pythonhosted.org/packages/5b/12/cc96c7a8cb51d8dcc9badd886c361caa1526fba7fa69d1e7892e613b71d4/maturin-1.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2d6984ab690af509f525dbd2b130714207c06ebb14a5814edbe1e42b17ae0de", size = 8852401, upload-time = "2025-10-07T12:44:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/51/8e/653ac3c9f2c25cdd81aefb0a2d17ff140ca5a14504f5e3c7f94dcfe4dbb7/maturin-1.9.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5c2252b0956bb331460ac750c805ddf0d9b44442449fc1f16e3b66941689d0bc", size = 8425057, upload-time = "2025-10-07T12:45:01.711Z" }, + { url = "https://files.pythonhosted.org/packages/db/29/f13490328764ae9bfc1da55afc5b707cebe4fa75ad7a1573bfa82cfae0c6/maturin-1.9.6-py3-none-win32.whl", hash = "sha256:f2c58d29ebdd4346fd004e6be213d071fdd94a77a16aa91474a21a4f9dbf6309", size = 7165956, upload-time = "2025-10-07T12:45:03.766Z" }, + { url = "https://files.pythonhosted.org/packages/db/9f/dd51e5ac1fce47581b8efa03d77a03f928c0ef85b6e48a61dfa37b6b85a2/maturin-1.9.6-py3-none-win_amd64.whl", hash = "sha256:1b39a5d82572c240d20d9e8be024d722dfb311d330c5e28ddeb615211755941a", size = 8145722, upload-time = "2025-10-07T12:45:05.487Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/e97aaba6d0d78c5871771bf9dd71d4eb8dac15df9109cf452748d2207412/maturin-1.9.6-py3-none-win_arm64.whl", hash = "sha256:ac02a30083553d2a781c10cd6f5480119bf6692fd177e743267406cad2ad198c", size = 6857006, upload-time = "2025-10-07T12:45:06.813Z" }, +] + [[package]] name = "pycrc32" source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "maturin" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "maturin", specifier = ">=1.9.6" }] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] From d1b31106307e77321a01dd5538264ea3f0f1f589 Mon Sep 17 00:00:00 2001 From: Robin B Date: Sun, 26 Oct 2025 00:29:54 +0200 Subject: [PATCH 2/2] add CRC32C variant and file processing API with Hasher enhancements --- README.md | 126 +++++++-- examples/usage.py | 202 +++++++++++++- pycrc32/__init__.py | 31 +- pycrc32/__init__.pyi | 52 ++++ pycrc32/tests/test_crc32.py | 175 +++++++++++- src/lib.rs | 543 +++++++++++++++++++++++++++++++++++- 6 files changed, 1085 insertions(+), 44 deletions(-) create mode 100644 pycrc32/__init__.pyi diff --git a/README.md b/README.md index 337e9ac..9331a33 100644 --- a/README.md +++ b/README.md @@ -14,62 +14,128 @@ pip install pycrc32 ``` ## Usage + +### Basic CRC32 and CRC32C ```python -from pycrc32 import crc32 +from pycrc32 import crc32, crc32c data = b"123456789" -print(f"crc32 for {data!r} is {crc32(data)}") + +# Standard CRC32 (IEEE 802.3 polynomial) +print(f"CRC32 for {data!r}: {crc32(data):#x}") + +# CRC32C (Castagnoli polynomial, used in iSCSI, Ethernet, etc.) +print(f"CRC32C for {data!r}: {crc32c(data):#x}") ``` -### Advanced Checksum Calculation with `Hasher` -For scenarios that require more flexibility, such as processing large amounts of data or computing the checksum in stages, you can use the `Hasher` class: +### Incremental Hashing with `Hasher` Class + +The `Hasher` class provides incremental hashing capabilities for processing large data in chunks: + +#### Basic Incremental Hashing ```python from pycrc32 import Hasher # Create a new Hasher instance hasher = Hasher() -# Update the hasher with data chunks +# Update with data chunks hasher.update(b"123456") hasher.update(b"789") -# Finalize the computation and get the checksum +# Get the final checksum checksum = hasher.finalize() -print(f"Checksum: {checksum}") +print(f"Checksum: {checksum:#x}") +print(f"Bytes processed: {len(hasher)}") +print(f"Hasher state: {repr(hasher)}") +``` + +#### Advanced Hasher Features +```python +# Initialize with custom initial state +hasher = Hasher.with_initial(0x12345678) +hasher.update(b"data") +result = hasher.finalize() + +# Create independent copies +hasher1 = Hasher() +hasher1.update(b"common") +hasher2 = hasher1.copy() # Independent copy +hasher2.update(b"additional") -# Reset the hasher to compute another checksum +print(f"Original: {hasher1.finalize():#x}") # Only "common" +print(f"Copy: {hasher2.finalize():#x}") # "common" + "additional" + +# Context manager usage +with Hasher() as ctx_hasher: + ctx_hasher.update(b"context data") + result = ctx_hasher.finalize() + +# Reset functionality hasher.reset() -hasher.update(b"The quick brown fox jumps over the lazy dog") -new_checksum = hasher.finalize() -print(f"New checksum: {new_checksum}") +print(f"After reset: {hasher.finalize():#x}") + +# Combine states (for parallel processing) +hasher1 = Hasher() +hasher1.update(b"part1") +hasher2 = Hasher() +hasher2.update(b"part2") +hasher1.combine(hasher2) +combined = hasher1.finalize() ``` -You can also initialize a `Hasher` with a specific initial CRC32 state: +### File Processing ```python -initial_crc = 12345678 -hasher = Hasher.with_initial(initial_crc) - -hasher.update(b"additional data") -final_checksum = hasher.finalize() -print(f"Final checksum with initial state: {final_checksum}") +from pycrc32 import crc32_file, crc32_fileobj + +# Process files by path +file_crc = crc32_file("/path/to/file.txt") +print(f"File CRC32: {file_crc:#x}") + +# Process file objects +with open("/path/to/file.txt", "rb") as f: + fileobj_crc = crc32_fileobj(f) + print(f"File object CRC32: {fileobj_crc:#x}") + +# Works with any file-like object (BytesIO, etc.) +import io +data = b"file-like data" +bio = io.BytesIO(data) +bio_crc = crc32_fileobj(bio) +print(f"BytesIO CRC32: {bio_crc:#x}") ``` -To combine checksums from different data blocks without needing to concatenate the data, use the `combine` method: +### Enhanced Error Handling ```python -hasher1 = Hasher() -hasher1.update(b"Data block 1") -checksum1 = hasher1.finalize() +from pycrc32 import crc32, Hasher + +# Provides helpful error messages +try: + crc32("invalid string") +except TypeError as e: + print(f"Clear error message: {e}") + # Output: crc32() expects bytes-like object, got string. Use b'your string' or your_string.encode() instead. + +try: + crc32_file("/nonexistent/file.txt") +except FileNotFoundError as e: + print(f"File error: {e}") + # Output: File not found: /nonexistent/file.txt +``` -hasher2 = Hasher() -hasher2.update(b"Data block 2") -checksum2 = hasher2.finalize() +### Type Safety and IDE Support +```python +# Full type hints available +from pycrc32 import crc32, crc32c, Hasher, crc32_file, crc32_fileobj +from typing import Union -# Combine checksums from hasher1 into hasher2 -hasher1.combine(hasher2) # Combine the state of hasher2 into hasher1 +def process_data(data: Union[bytes, bytearray]) -> int: + """Function with full type hints.""" + return crc32(data) -# The final checksum after combination -combined_checksum = hasher1.finalize() -print(f"Combined checksum: {combined_checksum}") +# IDE autocompletion and inline documentation +hasher: Hasher = Hasher() # Type annotation +hasher.update(b"data") # IDE shows method signatures ``` ## Speed diff --git a/examples/usage.py b/examples/usage.py index 1942306..556d189 100644 --- a/examples/usage.py +++ b/examples/usage.py @@ -1,4 +1,200 @@ -from pycrc32 import crc32 +""" +Comprehensive example demonstrating all pycrc32 features. -data = b"123456789" -print(f"crc32 for {data!r} is {crc32(data)}") +This example shows: +- Basic CRC32 computation +- CRC32C variant (Castagnoli polynomial) +- Incremental hashing with Hasher +- Advanced Hasher features (copy, context manager, etc.) +- File processing +- Error handling +""" + +from pycrc32 import crc32, crc32c, Hasher, crc32_file, crc32_fileobj +import tempfile +import io + + +def basic_usage(): + """Demonstrate basic CRC32 and CRC32C usage.""" + print("=== Basic Usage ===") + + data = b"123456789" + + # Standard CRC32 (IEEE 802.3 polynomial) + crc32_result = crc32(data) + print(f"Standard CRC32 for {data!r}: {crc32_result:#x} ({crc32_result})") + + # CRC32C (Castagnoli polynomial, used in iSCSI, Ethernet, etc.) + crc32c_result = crc32c(data) + print(f"CRC32C for {data!r}: {crc32c_result:#x} ({crc32c_result})") + + # They should be different + print(f"Different algorithms: {crc32_result != crc32c_result}") + print() + + +def incremental_hashing(): + """Demonstrate incremental hashing features.""" + print("=== Incremental Hashing ===") + + data_chunks = [b"chunk1", b"chunk2", b"chunk3"] + + # Basic incremental hashing + hasher = Hasher() + for chunk in data_chunks: + hasher.update(chunk) + print(f"Added {chunk!r}, bytes processed: {len(hasher)}") + + final_result = hasher.finalize() + print(f"Final CRC32: {final_result:#x}") + print(f"Hasher state: {repr(hasher)}") + print() + + # Using copy method + hasher1 = Hasher() + hasher1.update(b"first") + hasher2 = hasher1.copy() # Independent copy + hasher2.update(b"second") + + print(f"Original: {hasher1.finalize():#x}") # Only "first" + print(f"Copy: {hasher2.finalize():#x}") # "first" + "second" + print() + + # Context manager usage + with Hasher() as ctx_hasher: + ctx_hasher.update(b"context data") + ctx_result = ctx_hasher.finalize() + + print(f"Context manager result: {ctx_result:#x}") + print() + + +def advanced_hasher_features(): + """Demonstrate advanced Hasher features.""" + print("=== Advanced Hasher Features ===") + + # Hasher with initial state + initial_value = 0x12345678 + hasher = Hasher.with_initial(initial_value) + hasher.update(b"additional data") + result = hasher.finalize() + print(f"Hasher with initial value {initial_value:#x}: {result:#x}") + + # Reset functionality + hasher.reset() + print(f"After reset - bytes processed: {len(hasher)}") + print(f"After reset - CRC32: {hasher.finalize():#x}") + + # Combine functionality + hasher1 = Hasher() + hasher1.update(b"part1") + hasher2 = Hasher() + hasher2.update(b"part2") + + hasher1.combine(hasher2) + combined_result = hasher1.finalize() + direct_result = crc32(b"part1part2") + + print(f"Combined result: {combined_result:#x}") + print(f"Direct computation: {direct_result:#x}") + print(f"Combine works correctly: {combined_result == direct_result}") + print() + + +def file_processing(): + """Demonstrate file processing capabilities.""" + print("=== File Processing ===") + + test_data = b"This is test data for file processing." + + # Create a temporary file + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(test_data) + tmp_file.flush() + + # Process file by path + file_result = crc32_file(tmp_file.name) + print(f"CRC32 of file '{tmp_file.name}': {file_result:#x}") + + # Process file as file object + with open(tmp_file.name, 'rb') as f: + fileobj_result = crc32_fileobj(f) + print(f"CRC32 of file object: {fileobj_result:#x}") + + # Verify consistency + direct_result = crc32(test_data) + print(f"Direct computation: {direct_result:#x}") + print(f"All methods match: {file_result == fileobj_result == direct_result}") + + # Process in-memory file-like object + bio = io.BytesIO(test_data) + bio_result = crc32_fileobj(bio) + print(f"CRC32 of BytesIO: {bio_result:#x}") + print() + + +def error_handling(): + """Demonstrate error handling.""" + print("=== Error Handling ===") + + # These operations will raise informative exceptions + try: + crc32("invalid string input") + except TypeError as e: + print(f"Expected error for string input: {e}") + + try: + crc32(12345) + except TypeError as e: + print(f"Expected error for int input: {e}") + + try: + crc32_file("/nonexistent/file.txt") + except FileNotFoundError as e: + print(f"Expected error for nonexistent file: {e}") + + try: + crc32_fileobj("not a file object") + except TypeError as e: + print(f"Expected error for invalid file object: {e}") + print() + + +def performance_comparison(): + """Simple performance comparison.""" + import time + + print("=== Simple Performance Test ===") + + data = b"performance test data" * 1000 # ~23KB + iterations = 1000 + + # Time standard CRC32 + start_time = time.time() + for _ in range(iterations): + crc32(data) + crc32_time = time.time() - start_time + + # Time CRC32C + start_time = time.time() + for _ in range(iterations): + crc32c(data) + crc32c_time = time.time() - start_time + + print(f"Processed {len(data) * iterations} bytes") + print(f"CRC32 time: {crc32_time:.4f}s") + print(f"CRC32C time: {crc32c_time:.4f}s") + print(f"CRC32C overhead: {(crc32c_time / crc32_time - 1) * 100:.1f}%") + print() + + +if __name__ == "__main__": + basic_usage() + incremental_hashing() + advanced_hasher_features() + file_processing() + error_handling() + performance_comparison() + + print("All pycrc32 features demonstrated successfully!") diff --git a/pycrc32/__init__.py b/pycrc32/__init__.py index d08926b..1548868 100644 --- a/pycrc32/__init__.py +++ b/pycrc32/__init__.py @@ -1 +1,30 @@ -from .pycrc32 import Hasher, crc32 +""" +pycrc32 - Python module for SIMD-accelerated CRC32 checksum computation. + +This package provides high-performance CRC32 computation using Rust's crc32fast library +with Python bindings via PyO3. + +Basic usage: + >>> from pycrc32 import crc32 + >>> crc32(b"hello") + 907060870 + +Advanced usage with incremental hashing: + >>> from pycrc32 import Hasher + >>> hasher = Hasher() + >>> hasher.update(b"first chunk") + >>> hasher.update(b"second chunk") + >>> hasher.finalize() + 1234567890 +""" + +from typing import IO, Union, Any +from .pycrc32 import Hasher, crc32, crc32_file, crc32_fileobj, crc32c_func as crc32c + +__all__ = ["Hasher", "crc32", "crc32c", "crc32_file", "crc32_fileobj"] +__version__ = "0.3.0" + +# Type aliases for better documentation +FilePath = Union[str, bytes] +FileObject = IO[bytes] +DataInput = Union[bytes, bytearray, memoryview, Any] diff --git a/pycrc32/__init__.pyi b/pycrc32/__init__.pyi new file mode 100644 index 0000000..1ec66f3 --- /dev/null +++ b/pycrc32/__init__.pyi @@ -0,0 +1,52 @@ +""" +Type stubs for pycrc32 package. + +This file provides type hints for the pycrc32 Python module. +""" + +from typing import IO, AnyStr, Union +from typing_extensions import Any + +class Hasher: + """Represents an in-progress CRC32 computation.""" + + def __init__(self) -> None: ... + + @staticmethod + def with_initial(init: int) -> "Hasher": ... + + def update(self, data: AnyStr) -> None: ... + + def finalize(self) -> int: ... + + def reset(self) -> None: ... + + def combine(self, other: "Hasher") -> None: ... + + def copy(self) -> "Hasher": ... + + def __len__(self) -> int: ... + + def __repr__(self) -> str: ... + + def __enter__(self) -> "Hasher": ... + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ... + +def crc32(data: AnyStr) -> int: + """Calculate CRC32 checksum of bytes-like object.""" + ... + +def crc32_file(path: Union[str, bytes]) -> int: + """Calculate CRC32 checksum of file by path.""" + ... + +def crc32_fileobj(fileobj: IO[bytes]) -> int: + """Calculate CRC32 checksum of file-like object.""" + ... + +def crc32c(data: AnyStr) -> int: + """Calculate CRC32C checksum (Castagnoli polynomial) of bytes-like object.""" + ... + +__all__ = ["Hasher", "crc32", "crc32c", "crc32_file", "crc32_fileobj"] \ No newline at end of file diff --git a/pycrc32/tests/test_crc32.py b/pycrc32/tests/test_crc32.py index c655d8c..4f40556 100644 --- a/pycrc32/tests/test_crc32.py +++ b/pycrc32/tests/test_crc32.py @@ -1,6 +1,8 @@ import pytest +import tempfile +import io -from pycrc32 import Hasher, crc32 +from pycrc32 import Hasher, crc32, crc32c, crc32_file, crc32_fileobj @pytest.mark.parametrize( @@ -77,3 +79,174 @@ def test_Hasher_combine(input_bytes1, input_bytes2): hasher_combined.update(input_bytes2) assert hasher_combined.finalize() == combined_crc + + +def test_Hasher_copy(): + """Test Hasher copy functionality.""" + hasher1 = Hasher() + hasher1.update(b"test data") + + hasher2 = hasher1.copy() + hasher2.update(b" more") + + # Original should have only "test data" + assert hasher1.finalize() == crc32(b"test data") + # Copy should have "test data more" + assert hasher2.finalize() == crc32(b"test data more") + + +def test_Hasher_len(): + """Test Hasher length tracking.""" + hasher = Hasher() + assert len(hasher) == 0 + + hasher.update(b"hello") + assert len(hasher) == 5 + + hasher.update(b" world") + assert len(hasher) == 11 + + hasher.reset() + assert len(hasher) == 0 + + +def test_Hasher_repr(): + """Test Hasher string representation.""" + hasher = Hasher() + repr_str = repr(hasher) + assert "Hasher(" in repr_str + assert "bytes_processed=0" in repr_str + + hasher.update(b"test") + repr_str = repr(hasher) + assert "bytes_processed=4" in repr_str + + +def test_Hasher_context_manager(): + """Test Hasher context manager functionality.""" + data = b"context manager test" + + # Test context manager creates independent copy + with Hasher() as ctx_hasher: + ctx_hasher.update(data) + ctx_result = ctx_hasher.finalize() + + # Original hasher should be unchanged + original_hasher = Hasher() + original_result = original_hasher.finalize() + + assert ctx_result == crc32(data) + assert original_result == 0 + + +def test_crc32c_basic(): + """Test basic CRC32C functionality.""" + test_data = b"123456789" + + # CRC32 and CRC32C should produce different results + crc32_result = crc32(test_data) + crc32c_result = crc32c(test_data) + + assert crc32_result != crc32c_result + assert isinstance(crc32c_result, int) + + +def test_crc32c_empty(): + """Test CRC32C with empty input.""" + assert crc32c(b"") == 0 + + +def test_buffer_protocol_support(): + """Test enhanced buffer protocol support.""" + test_data = b"hello world" + + # Test with bytes (baseline) + result_bytes = crc32(test_data) + assert result_bytes == crc32(b"hello world") + + # Test with memoryview + mv = memoryview(test_data) + try: + # This should work if __bytes__ is supported + result_mv = crc32(mv) + # If it works, should give same result + assert result_mv == result_bytes + except Exception: + # If it doesn't work, that's expected for now + pass + + +def test_error_handling(): + """Test enhanced error handling.""" + # Test TypeError for invalid inputs + with pytest.raises(TypeError, match="string"): + crc32("invalid") + + with pytest.raises(TypeError, match="int"): + crc32(123) + + # Test Hasher error handling + hasher = Hasher() + with pytest.raises(TypeError, match="string"): + hasher.update("invalid") + + +def test_file_functions(): + """Test file convenience functions.""" + test_data = b"file test content" + + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(test_data) + tmp_file.flush() + + # Test crc32_file + file_result = crc32_file(tmp_file.name) + expected_result = crc32(test_data) + assert file_result == expected_result + + # Test crc32_fileobj with file handle + with open(tmp_file.name, 'rb') as f: + fileobj_result = crc32_fileobj(f) + assert fileobj_result == expected_result + + # Test crc32_fileobj with BytesIO + bio = io.BytesIO(test_data) + bio_result = crc32_fileobj(bio) + assert bio_result == expected_result + + +def test_file_errors(): + """Test file function error handling.""" + # Test nonexistent file + with pytest.raises(FileNotFoundError): + crc32_file("/nonexistent/file.txt") + + # Test invalid file object + with pytest.raises(TypeError): + crc32_fileobj("not a file object") + + +def test_new_features_comprehensive(): + """Test that new features work together correctly.""" + data1 = b"chunk1" + data2 = b"chunk2" + combined_data = data1 + data2 + + # Test copy + context manager + with Hasher() as ctx_hasher: + ctx_hasher.update(data1) + ctx_result = ctx_hasher.finalize() + + original_hasher = Hasher() + original_hasher.update(data1) + original_result = original_hasher.finalize() + + assert ctx_result == original_result + + # Test combine works with copy + hasher1 = Hasher() + hasher1.update(data1) + hasher1_copy = hasher1.copy() + hasher1_copy.update(data2) + + assert hasher1_copy.finalize() == crc32(combined_data) diff --git a/src/lib.rs b/src/lib.rs index a48f756..632798b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,55 +1,577 @@ use pyo3::prelude::*; +use pyo3::types::{PyBytes, PyString}; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::Path; +use std::sync::OnceLock; use crc32fast::hash; use crc32fast::Hasher as RustHasher; -/// Calculates the crc32 hash using the crc32fast crate +// CRC32C implementation (Castagnoli polynomial) +const CRC32C_POLY: u32 = 0x1EDC6F41; + +/// Build CRC32C lookup table (forward processing) +fn crc32c_table() -> [u32; 256] { + let mut table = [0u32; 256]; + for i in 0..256 { + let mut crc = (i as u32) << 24; // Move to MSB for forward processing + for _ in 0..8 { + if crc & 0x80000000 != 0 { // Check MSB + crc = (crc << 1) ^ CRC32C_POLY; + } else { + crc <<= 1; + } + } + table[i] = crc; + } + table +} + +/// Get CRC32C lookup table (lazy initialization) +fn get_crc32c_table() -> &'static [u32; 256] { + static TABLE: OnceLock<[u32; 256]> = OnceLock::new(); + TABLE.get_or_init(crc32c_table) +} + +/// Calculate CRC32C using the Castagnoli polynomial (forward processing) +fn crc32c_update(mut crc: u32, data: &[u8]) -> u32 { + let table = get_crc32c_table(); + crc ^= 0xFFFFFFFF; // Initial XOR + for &byte in data { + let index = ((crc >> 24) ^ byte as u32) & 0xFF; // Use MSB + crc = (crc << 8) ^ table[index as usize]; + } + crc ^ 0xFFFFFFFF // Final XOR +} + +/// Calculate CRC32C of data (wrapper for consistency with crc32fast) +fn crc32c(data: &[u8]) -> u32 { + crc32c_update(0, data) +} + +/// CRC32C Hasher for incremental computation +struct Crc32CHasher { + crc: u32, +} + +impl Crc32CHasher { + fn new() -> Self { + Self { crc: 0 } + } + + fn new_with_initial(init: u32) -> Self { + Self { crc: init } + } + + fn update(&mut self, data: &[u8]) { + self.crc = crc32c_update(self.crc, data); + } + + fn finalize(&self) -> u32 { + self.crc + } + + fn reset(&mut self) { + self.crc = 0; + } + + fn clone(&self) -> Self { + Self { crc: self.crc } + } +} + +/// Calculates the CRC32 hash using the crc32fast crate. +/// +/// # Arguments +/// * `bytes` - Any bytes-like object (bytes, memoryview, etc.) that can be converted to bytes +/// +/// # Returns +/// CRC32 checksum as a 32-bit unsigned integer +/// +/// # Examples +/// ```python +/// import pycrc32 +/// result = pycrc32.crc32(b"hello") +/// # result = 907060870 +/// ``` +/// +/// # Type Support +/// - `bytes` - Native bytes (no conversion overhead) +/// - Objects with `__bytes__()` method (converted to bytes) +/// - Returns `TypeError` for unsupported types with helpful error messages #[pyfunction] -fn crc32(bytes: &[u8]) -> u32 { - hash(bytes) +fn crc32(bytes: &Bound<'_, PyAny>) -> PyResult { + // Try to extract as bytes first + if let Ok(py_bytes) = bytes.clone().cast::() { + let bytes_u8 = py_bytes.as_bytes(); + Ok(hash(bytes_u8)) + } else { + // Check for common unsupported types with helpful error messages + let type_name = bytes.get_type().name()?.to_string_lossy().into_owned(); + match type_name.as_str() { + "str" => { + return Err(pyo3::exceptions::PyTypeError::new_err( + "crc32() expects bytes-like object, got string. Use b'your string' or your_string.encode() instead." + )); + }, + "int" => { + return Err(pyo3::exceptions::PyTypeError::new_err( + "crc32() expects bytes-like object, got int. Use bytes([value]) or similar." + )); + }, + _ => { + // Try to convert to bytes object (works for memoryview and other objects with __bytes__) + match bytes.call_method0("__bytes__") { + Ok(py_bytes) => { + match py_bytes.cast::() { + Ok(casted_bytes) => { + let bytes_u8 = casted_bytes.as_bytes(); + Ok(hash(bytes_u8)) + }, + Err(_) => { + Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Object __bytes__() method did not return bytes, got {}", + py_bytes.get_type().name()? + ))) + } + } + }, + Err(_) => { + Err(pyo3::exceptions::PyTypeError::new_err(format!( + "crc32() expects bytes-like object (bytes, bytearray, memoryview), got {}. Consider converting to bytes first.", + type_name + ))) + } + } + } + } + } +} + +/// Calculate CRC32 checksum of a file by path. +/// +/// # Arguments +/// * `path` - Path to the file to checksum +/// +/// # Returns +/// CRC32 checksum as a 32-bit unsigned integer +/// +/// # Errors +/// Returns FileNotFoundError if file doesn't exist +/// Returns PermissionError for permission issues +/// Returns IOError for other I/O problems +#[pyfunction] +fn crc32_file(path: &Bound<'_, PyString>) -> PyResult { + let path_str = path.to_str() + .map_err(|e| pyo3::exceptions::PyTypeError::new_err(format!("Invalid path string: {}", e)))?; + let path = Path::new(path_str); + + // Check if file exists first for better error messages + if !path.exists() { + return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!( + "File not found: {}", path_str + ))); + } + + let file = File::open(path).map_err(|e| { + match e.kind() { + std::io::ErrorKind::PermissionDenied => { + pyo3::exceptions::PyPermissionError::new_err(format!( + "Permission denied when opening file: {}", path_str + )) + }, + _ => { + pyo3::exceptions::PyIOError::new_err(format!( + "Failed to open file '{}': {}", path_str, e + )) + } + } + })?; + + let mut reader = BufReader::new(file); + let mut hasher = RustHasher::new(); + let mut buffer = [0; 8192]; // 8KB buffer for efficient reading + + loop { + let bytes_read = reader.read(&mut buffer) + .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("Failed to read file: {}", e)))?; + + if bytes_read == 0 { + break; + } + + hasher.update(&buffer[..bytes_read]); + } + + Ok(hasher.finalize()) +} + +/// Calculate CRC32 checksum of a file-like object. +/// +/// # Arguments +/// * `fileobj` - File-like object with read() method +/// +/// # Returns +/// CRC32 checksum as a 32-bit unsigned integer +/// +/// # Errors +/// Returns TypeError if object doesn't support read() method +/// Returns ValueError if read() returns invalid data +#[pyfunction] +fn crc32_fileobj(fileobj: &Bound<'_, PyAny>) -> PyResult { + // Check if object has read method + if !fileobj.hasattr("read")? { + return Err(pyo3::exceptions::PyTypeError::new_err( + "Object must have a read() method to be used as a file-like object" + )); + } + + let mut hasher = RustHasher::new(); + let chunk_size = 8192; // 8KB chunks + + loop { + // Try to read a chunk from the file object + let chunk_result = fileobj.call_method1("read", (chunk_size,)); + + match chunk_result { + Ok(chunk) => { + // Check if we got EOF (empty bytes or None) + if chunk.is_none() { + break; + } + + // Convert chunk to bytes + if let Ok(py_bytes) = chunk.clone().cast::() { + let data = py_bytes.as_bytes(); + if data.is_empty() { + break; // EOF + } + hasher.update(data); + } else { + // Try to convert to bytes if not already bytes + match chunk.call_method0("__bytes__") { + Ok(bytes_obj) => { + match bytes_obj.cast::() { + Ok(casted_bytes) => { + let data = casted_bytes.as_bytes(); + if data.is_empty() { + break; // EOF + } + hasher.update(data); + }, + Err(_) => { + return Err(pyo3::exceptions::PyValueError::new_err( + "Object __bytes__() method did not return bytes" + )); + } + } + }, + Err(_) => { + return Err(pyo3::exceptions::PyValueError::new_err( + "read() method returned object that cannot be converted to bytes" + )); + } + } + } + } + Err(e) => { + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Failed to read from file-like object: {}", e + ))); + } + } + } + + Ok(hasher.finalize()) +} + +/// Calculates the CRC32C hash using the Castagnoli polynomial (iSCSI, Ethernet, etc.). +/// +/// # Arguments +/// * `bytes` - Any bytes-like object that can be converted to bytes +/// +/// # Returns +/// CRC32C checksum as a 32-bit unsigned integer +/// +/// # Examples +/// ```python +/// import pycrc32 +/// result = pycrc32.crc32c(b"hello") +/// # result = 2240272413 +/// ``` +/// +/// # Note +/// CRC32C uses a different polynomial than standard CRC32 and is commonly used +/// in modern storage systems, network protocols, and error detection. +#[pyfunction] +fn crc32c_func(bytes: &Bound<'_, PyAny>) -> PyResult { + // Try to extract as bytes first + if let Ok(py_bytes) = bytes.clone().cast::() { + let bytes_u8 = py_bytes.as_bytes(); + Ok(crc32c(bytes_u8)) + } else { + // Check for common unsupported types with helpful error messages + let type_name = bytes.get_type().name()?.to_string_lossy().into_owned(); + match type_name.as_str() { + "str" => { + return Err(pyo3::exceptions::PyTypeError::new_err( + "crc32c() expects bytes-like object, got string. Use b'your string' or your_string.encode() instead." + )); + }, + "int" => { + return Err(pyo3::exceptions::PyTypeError::new_err( + "crc32c() expects bytes-like object, got int. Use bytes([value]) or similar." + )); + }, + _ => { + // Try to convert to bytes object (works for memoryview and other objects with __bytes__) + match bytes.call_method0("__bytes__") { + Ok(py_bytes) => { + match py_bytes.cast::() { + Ok(casted_bytes) => { + let bytes_u8 = casted_bytes.as_bytes(); + Ok(crc32c(bytes_u8)) + }, + Err(_) => { + Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Object __bytes__() method did not return bytes, got {}", + py_bytes.get_type().name()? + ))) + } + } + }, + Err(_) => { + Err(pyo3::exceptions::PyTypeError::new_err(format!( + "crc32c() expects bytes-like object (bytes, bytearray, memoryview), got {}. Consider converting to bytes first.", + type_name + ))) + } + } + } + } + } } /// Represents an in-progress CRC32 computation in Python. +/// +/// This class provides incremental CRC32 calculation, allowing you to process +/// large data in chunks without loading everything into memory at once. +/// +/// # Examples +/// ```python +/// import pycrc32 +/// +/// # Simple incremental hashing +/// hasher = pycrc32.Hasher() +/// hasher.update(b"first chunk") +/// hasher.update(b"second chunk") +/// result = hasher.finalize() +/// +/// # With initial state +/// hasher = pycrc32.Hasher.with_initial(0x12345678) +/// hasher.update(b"more data") +/// result = hasher.finalize() +/// +/// # Context manager usage +/// with pycrc32.Hasher() as hasher: +/// hasher.update(b"data") +/// result = hasher.finalize() +/// ``` #[pyclass(module = "pycrc32")] struct Hasher { hasher: RustHasher, + bytes_processed: usize, } #[pymethods] impl Hasher { + /// Create a new Hasher with default initial state. + /// + /// # Returns + /// New Hasher instance ready to process data #[new] fn new() -> Self { Hasher { hasher: RustHasher::new(), + bytes_processed: 0, } } - /// Create a new `Hasher` with an initial CRC32 state. + /// Create a new Hasher with a custom initial CRC32 state. + /// + /// # Arguments + /// * `init` - Initial CRC32 value (32-bit unsigned integer) + /// + /// # Returns + /// New Hasher instance with the specified initial state + /// + /// # Examples + /// ```python + /// # Continue a CRC32 calculation from a known state + /// hasher = pycrc32.Hasher.with_initial(0x12345678) + /// hasher.update(b"additional data") + /// ``` #[staticmethod] fn with_initial(init: u32) -> Self { Hasher { hasher: RustHasher::new_with_initial(init), + bytes_processed: 0, } } - /// Process the given byte slice and update the hash state. - fn update(&mut self, data: &[u8]) { - self.hasher.update(data); + /// Process the given bytes-like object and update the hash state. + /// + /// # Arguments + /// * `data` - Any bytes-like object (bytes, memoryview, etc.) + /// + /// # Examples + /// ```python + /// hasher = pycrc32.Hasher() + /// hasher.update(b"first chunk") + /// hasher.update(bytearray(b"second chunk")) + /// ``` + fn update(&mut self, data: &Bound<'_, PyAny>) -> PyResult<()> { + // Try to extract as bytes first + if let Ok(py_bytes) = data.clone().cast::() { + let data_u8 = py_bytes.as_bytes(); + self.hasher.update(data_u8); + self.bytes_processed += data_u8.len(); + Ok(()) + } else { + // Check for common unsupported types with helpful error messages + let type_name = data.get_type().name()?.to_string_lossy().into_owned(); + match type_name.as_str() { + "str" => { + return Err(pyo3::exceptions::PyTypeError::new_err( + "Hasher.update() expects bytes-like object, got string. Use b'your string' or your_string.encode() instead." + )); + }, + "int" => { + return Err(pyo3::exceptions::PyTypeError::new_err( + "Hasher.update() expects bytes-like object, got int. Use bytes([value]) or similar." + )); + }, + _ => { + // Try to convert to bytes object (works for memoryview and other objects with __bytes__) + match data.call_method0("__bytes__") { + Ok(py_bytes) => { + match py_bytes.cast::() { + Ok(casted_bytes) => { + let data_u8 = casted_bytes.as_bytes(); + self.hasher.update(data_u8); + self.bytes_processed += data_u8.len(); + Ok(()) + }, + Err(_) => { + Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Object __bytes__() method did not return bytes, got {}", + py_bytes.get_type().name()? + ))) + } + } + }, + Err(_) => { + Err(pyo3::exceptions::PyTypeError::new_err(format!( + "Hasher.update() expects bytes-like object (bytes, bytearray, memoryview), got {}. Consider converting to bytes first.", + type_name + ))) + } + } + } + } + } } /// Finalize the hash state and return the computed CRC32 value. + /// + /// # Returns + /// Current CRC32 checksum as a 32-bit unsigned integer + /// + /// Note: This does not modify the hasher state, so you can continue + /// calling update() and finalize() to process more data. fn finalize(&self) -> u32 { self.hasher.clone().finalize() } - /// Reset the hash state. + /// Reset the hash state to initial values. + /// + /// After calling reset(), the hasher behaves as if newly created. + /// The bytes processed counter is reset to 0. fn reset(&mut self) { self.hasher.reset(); + self.bytes_processed = 0; } - /// Combine the hash state with the hash state for the subsequent block of bytes. + /// Combine this hasher's state with another hasher's state. + /// + /// This is useful for parallel processing where different chunks + /// are processed separately and then combined. + /// + /// # Arguments + /// * `other` - Another Hasher instance to combine with this one + /// + /// # Examples + /// ```python + /// # Process data in parallel and combine results + /// hasher1 = pycrc32.Hasher() + /// hasher1.update(b"chunk1") + /// + /// hasher2 = pycrc32.Hasher() + /// hasher2.update(b"chunk2") + /// + /// hasher1.combine(hasher2) + /// result = hasher1.finalize() # Combined result + /// ``` fn combine(&mut self, other: &Hasher) { self.hasher.combine(&other.hasher); + self.bytes_processed += other.bytes_processed; + } + + /// Create a copy of this hasher with the same state. + /// + /// # Returns + /// New Hasher instance with identical internal state + /// + /// # Examples + /// ```python + /// # Create a backup before modifying + /// hasher1 = pycrc32.Hasher() + /// hasher1.update(b"data") + /// hasher2 = hasher1.copy() + /// + /// hasher2.update(b"more data") + /// # hasher1 still has just "data", hasher2 has "data" + "more data" + /// ``` + fn copy(&self) -> Self { + Hasher { + hasher: self.hasher.clone(), + bytes_processed: self.bytes_processed, + } + } + + /// Return the number of bytes processed by this hasher. + fn __len__(&self) -> usize { + self.bytes_processed + } + + /// Return a string representation of the hasher. + fn __repr__(&self) -> String { + format!( + "Hasher(bytes_processed={}, current_crc={:#x})", + self.bytes_processed, + self.hasher.clone().finalize() + ) + } + + /// Enter context manager protocol. + fn __enter__(&self) -> Self { + self.copy() + } + + /// Exit context manager protocol. + fn __exit__(&mut self, _exc_type: Option>, _exc_val: Option>, _exc_tb: Option>) -> PyResult<()> { + Ok(()) } } @@ -58,5 +580,8 @@ impl Hasher { fn pycrc32(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(crc32, m)?)?; + m.add_function(wrap_pyfunction!(crc32_file, m)?)?; + m.add_function(wrap_pyfunction!(crc32_fileobj, m)?)?; + m.add_function(wrap_pyfunction!(crc32c_func, m)?)?; Ok(()) }