pith-cdc 0.1.1b1__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pith_cdc/__init__.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 pith-hash
|
|
3
|
+
"""pith-cdc SDK: FastCDC content-defined chunking through ctypes.
|
|
4
|
+
|
|
5
|
+
The single Rust core (the ``pith-cdc`` cdylib built by
|
|
6
|
+
``cargo build --release``) is loaded at runtime; this package carries
|
|
7
|
+
no third-party dependency — ``ctypes`` is the standard library.
|
|
8
|
+
|
|
9
|
+
Discovery order (the suite's cdylib convention):
|
|
10
|
+
|
|
11
|
+
1. ``PITH_CDYLIB`` — an explicit cdylib *file* path;
|
|
12
|
+
2. ``PITH_CDYLIB_DIR`` — a *directory* scanned for the cdylib names
|
|
13
|
+
(the CD pipeline points this at ``target/release``);
|
|
14
|
+
3. the package directory itself (the built wheel ships the cdylib as
|
|
15
|
+
package data);
|
|
16
|
+
4. ``<repo root>/target/release`` — the repository working-tree layout,
|
|
17
|
+
so a source checkout runs against a local cargo build with no
|
|
18
|
+
configuration.
|
|
19
|
+
|
|
20
|
+
The FFI surface is one chunking operation plus one free:
|
|
21
|
+
``pith_cdc_chunk`` runs the FastCDC scan over a byte slice and hands
|
|
22
|
+
back a flat ``(offset, length, hash)`` triple buffer (24 bytes per
|
|
23
|
+
chunk), and ``pith_cdc_free`` releases the handed-out buffer.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import ctypes
|
|
29
|
+
import os
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Chunk",
|
|
35
|
+
"FfiError",
|
|
36
|
+
"LibraryNotFoundError",
|
|
37
|
+
"find_cdylib",
|
|
38
|
+
"chunk",
|
|
39
|
+
"LEVEL_0",
|
|
40
|
+
"LEVEL_1",
|
|
41
|
+
"LEVEL_2",
|
|
42
|
+
"LEVEL_3",
|
|
43
|
+
"STATUS_OK",
|
|
44
|
+
"STATUS_INVALID",
|
|
45
|
+
"STATUS_REJECTED",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
#: Status: success.
|
|
49
|
+
STATUS_OK = 0
|
|
50
|
+
#: Status: a caller argument is invalid (a null pointer, or a level
|
|
51
|
+
#: code the ABI does not define — only 0..=3 exist).
|
|
52
|
+
STATUS_INVALID = -1
|
|
53
|
+
#: Status: the core chunker refused the parameters (a size outside its
|
|
54
|
+
#: range, or a ``min <= avg <= max`` violation).
|
|
55
|
+
STATUS_REJECTED = -2
|
|
56
|
+
|
|
57
|
+
#: Normalization-level wire code: level 0, no normalization.
|
|
58
|
+
LEVEL_0 = 0
|
|
59
|
+
#: Normalization-level wire code: level 1.
|
|
60
|
+
LEVEL_1 = 1
|
|
61
|
+
#: Normalization-level wire code: level 2, the crate default.
|
|
62
|
+
LEVEL_2 = 2
|
|
63
|
+
#: Normalization-level wire code: level 3.
|
|
64
|
+
LEVEL_3 = 3
|
|
65
|
+
|
|
66
|
+
#: Every cdylib file name cargo may drop into the build directory, per
|
|
67
|
+
#: platform (windows / linux / macOS).
|
|
68
|
+
CDYLIB_NAMES = ("pith_cdc.dll", "libpith_cdc.so", "libpith_cdc.dylib")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class Chunk:
|
|
73
|
+
"""One content-defined chunk: a ``(offset, length)`` span plus the
|
|
74
|
+
Gear fingerprint value at the cut point (the ``hash`` — a rolling
|
|
75
|
+
fingerprint, not a digest)."""
|
|
76
|
+
|
|
77
|
+
#: Byte offset of the chunk start within the source.
|
|
78
|
+
offset: int
|
|
79
|
+
#: Chunk length in bytes.
|
|
80
|
+
length: int
|
|
81
|
+
#: Gear fingerprint as of the end of the chunk.
|
|
82
|
+
hash: int
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class LibraryNotFoundError(OSError):
|
|
86
|
+
"""No cdylib was found through the discovery chain."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class FfiError(Exception):
|
|
90
|
+
"""A non-zero status code came back from the cdylib."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, op: str, status: int) -> None:
|
|
93
|
+
kind = {
|
|
94
|
+
STATUS_INVALID: "invalid argument",
|
|
95
|
+
STATUS_REJECTED: "parameters rejected",
|
|
96
|
+
}.get(status, "unknown failure")
|
|
97
|
+
super().__init__(f"{op} failed: {kind} (status {status})")
|
|
98
|
+
#: The raw status code the FFI returned.
|
|
99
|
+
self.status = status
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def find_cdylib() -> Path:
|
|
103
|
+
"""Locates the cdylib through the suite's discovery chain."""
|
|
104
|
+
explicit = os.environ.get("PITH_CDYLIB")
|
|
105
|
+
if explicit:
|
|
106
|
+
p = Path(explicit)
|
|
107
|
+
if p.is_file():
|
|
108
|
+
return p
|
|
109
|
+
env_dir = os.environ.get("PITH_CDYLIB_DIR")
|
|
110
|
+
candidates: list[Path] = []
|
|
111
|
+
if env_dir:
|
|
112
|
+
env_dir_path = Path(env_dir)
|
|
113
|
+
candidates.append(env_dir_path)
|
|
114
|
+
if not env_dir_path.is_absolute():
|
|
115
|
+
# CD and local runs invoke tools from the repository root or
|
|
116
|
+
# from sdk/<lang>; resolve the env value against both.
|
|
117
|
+
candidates.append(Path.cwd() / env_dir_path)
|
|
118
|
+
candidates.append(Path(__file__).resolve().parents[3] / env_dir_path)
|
|
119
|
+
candidates.append(Path(__file__).resolve().parent) # packaged wheel
|
|
120
|
+
candidates.append(Path(__file__).resolve().parents[3] / "target" / "release")
|
|
121
|
+
for directory in candidates:
|
|
122
|
+
for name in CDYLIB_NAMES:
|
|
123
|
+
p = directory / name
|
|
124
|
+
if p.is_file():
|
|
125
|
+
return p
|
|
126
|
+
raise LibraryNotFoundError(
|
|
127
|
+
"no pith-cdc cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, "
|
|
128
|
+
"the package directory and <repo>/target/release); "
|
|
129
|
+
"run `cargo build --release` first"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
_lib: ctypes.CDLL | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load() -> ctypes.CDLL:
|
|
137
|
+
global _lib
|
|
138
|
+
if _lib is None:
|
|
139
|
+
lib = ctypes.CDLL(str(find_cdylib()))
|
|
140
|
+
lib.pith_cdc_chunk.argtypes = [
|
|
141
|
+
ctypes.c_void_p, # data
|
|
142
|
+
ctypes.c_size_t, # len
|
|
143
|
+
ctypes.c_size_t, # min_size
|
|
144
|
+
ctypes.c_size_t, # avg_size
|
|
145
|
+
ctypes.c_size_t, # max_size
|
|
146
|
+
ctypes.c_uint32, # level
|
|
147
|
+
ctypes.POINTER(ctypes.c_void_p), # out buffer
|
|
148
|
+
ctypes.POINTER(ctypes.c_size_t), # out length (bytes)
|
|
149
|
+
]
|
|
150
|
+
lib.pith_cdc_chunk.restype = ctypes.c_int32
|
|
151
|
+
lib.pith_cdc_free.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
|
152
|
+
lib.pith_cdc_free.restype = None
|
|
153
|
+
_lib = lib
|
|
154
|
+
return _lib
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def chunk(
|
|
158
|
+
data: bytes,
|
|
159
|
+
min_size: int,
|
|
160
|
+
avg_size: int,
|
|
161
|
+
max_size: int,
|
|
162
|
+
level: int = LEVEL_2,
|
|
163
|
+
) -> list[Chunk]:
|
|
164
|
+
"""Chunks ``data`` with FastCDC and returns the chunk boundaries.
|
|
165
|
+
|
|
166
|
+
``min_size``/``avg_size``/``max_size`` are validated by the Rust
|
|
167
|
+
core (the spec's defaults are 2048/8192/32768); ``level`` is one of
|
|
168
|
+
the ``LEVEL_*`` wire codes (the default is :data:`LEVEL_2`, the
|
|
169
|
+
paper's sweet spot).
|
|
170
|
+
|
|
171
|
+
Raises :class:`FfiError` with ``status == STATUS_REJECTED`` for
|
|
172
|
+
out-of-range or misordered sizes and ``status ==
|
|
173
|
+
STATUS_INVALID`` for an unknown level code — never a crash; an
|
|
174
|
+
empty input simply returns ``[]``.
|
|
175
|
+
"""
|
|
176
|
+
# ctypes hands an empty bytes object as a valid buffer, but pass a
|
|
177
|
+
# real NULL for it so the (null, 0) contract is exercised exactly
|
|
178
|
+
# like the Go binding does.
|
|
179
|
+
buf = data if len(data) else None
|
|
180
|
+
out = ctypes.c_void_p()
|
|
181
|
+
out_len = ctypes.c_size_t()
|
|
182
|
+
status = _load().pith_cdc_chunk(
|
|
183
|
+
buf, len(data), min_size, avg_size, max_size, level,
|
|
184
|
+
ctypes.byref(out), ctypes.byref(out_len),
|
|
185
|
+
)
|
|
186
|
+
if status != STATUS_OK:
|
|
187
|
+
raise FfiError("pith_cdc_chunk", status)
|
|
188
|
+
try:
|
|
189
|
+
count = out_len.value // 24
|
|
190
|
+
words = (ctypes.c_uint64 * (count * 3)).from_address(out.value)
|
|
191
|
+
return [
|
|
192
|
+
Chunk(offset=words[i * 3], length=words[i * 3 + 1], hash=words[i * 3 + 2])
|
|
193
|
+
for i in range(count)
|
|
194
|
+
]
|
|
195
|
+
finally:
|
|
196
|
+
_load().pith_cdc_free(out, out_len.value)
|
pith_cdc/pith_cdc.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pith-cdc
|
|
3
|
+
Version: 0.1.1b1
|
|
4
|
+
Summary: FastCDC content-defined chunking SDK: normalized Gear-fingerprint chunk boundaries via the pith-cdc Rust cdylib (ctypes, zero-dep)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/pith-hash/pith-cdc
|
|
7
|
+
Project-URL: Homepage, https://github.com/pith-hash/pith-cdc
|
|
8
|
+
Keywords: fastcdc,chunking,deduplication,content-defined,hashing
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
12
|
+
Requires-Python: >=3.9
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pith_cdc/__init__.py,sha256=QqJcUO67UVvyTaybHqb33SEwy3JJDsuUtjbjHVfKEUs,6621
|
|
2
|
+
pith_cdc/pith_cdc.dll,sha256=0zzzVAZIVwI_awXlKAgWNABb0rxBvcL-mpSI6YwW0rA,119296
|
|
3
|
+
pith_cdc-0.1.1b1.dist-info/METADATA,sha256=9SH8YWigRuz2gotfmSB3-Af7sOPShhj4_P-oNFyJ8QM,582
|
|
4
|
+
pith_cdc-0.1.1b1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
|
|
5
|
+
pith_cdc-0.1.1b1.dist-info/top_level.txt,sha256=p17KSMz5mr8GxjQCAaGiG323RAViRSFwzXatPtYzhwo,9
|
|
6
|
+
pith_cdc-0.1.1b1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pith_cdc
|