nix-base32 0.4.2__py3-none-any.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.
base32/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """Pure-Python implementation of the Nix-style base32 codec.
2
+
3
+ The interface is two functions—:func:`encode` and :func:`decode`.
4
+ The codec generally follows upstream implementation (in Nix source tree)
5
+
6
+ The Nix base32 variant differs from RFC 4648 base32:
7
+ letters *e*, *o*, *u*, and *t* are omitted.
8
+
9
+ Example
10
+ -------
11
+
12
+ >>> from base32 import encode, decode
13
+ >>> s = encode(b"hello")
14
+ >>> s
15
+ NixBase32Str('nbswy3dp')
16
+ >>> decode(s)
17
+ b'hello'
18
+
19
+ The :class:`~base32.detail.types.NixBase32Str` type is returned to
20
+ ensure all encoded representations are valid according to Nix alphabet.
21
+ """
22
+
23
+ from importlib.metadata import PackageNotFoundError, version
24
+
25
+ try:
26
+ __version__ = version("nix-base32")
27
+ except PackageNotFoundError:
28
+ from typing import Final
29
+
30
+ __version__: Final[str] = "dev"
31
+
32
+
33
+ from .decode import decode, decode_iter, decode_stream, decode_to_stream
34
+ from .detail import NixBase32Str
35
+ from .encode import encode, encode_iter, encode_stream, encode_to_stream
36
+
37
+ __all__ = [
38
+ "NixBase32Str",
39
+ "decode",
40
+ "decode_iter",
41
+ "decode_stream",
42
+ "decode_to_stream",
43
+ "encode",
44
+ "encode_iter",
45
+ "encode_stream",
46
+ "encode_to_stream",
47
+ ]
base32/cli/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """CLI interface."""
2
+
3
+ import click
4
+
5
+ from .. import __version__
6
+ from . import ops
7
+
8
+
9
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True)
10
+ @click.version_option(__version__, "--version", "-v")
11
+ @click.option("-d", "decode", is_flag=True, help="Decode input.")
12
+ @click.option(
13
+ "--sri",
14
+ is_flag=True,
15
+ help="Decode input, producing output in SRI format if the digest matches a known hash length.",
16
+ )
17
+ @click.option(
18
+ "-i", "--input", is_flag=False, default="-", help='input file (default: "-" for stdin)'
19
+ )
20
+ @click.option(
21
+ "-o", "--output", is_flag=False, default="-", help='output file (default: "-" for stdout)'
22
+ )
23
+ def base32(input: str, output: str, decode: bool, sri: bool) -> None:
24
+ """
25
+ The base32 utility acts as a Nix base32 variant decoder when passed the --decode (or -d) flag and as
26
+ a Nix base32 encoder otherwise.
27
+ """
28
+
29
+ if decode or sri:
30
+ ops.decode(input, output, sri)
31
+ else:
32
+ ops.encode(input, output)
base32/cli/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Entry point for running the CLI as a module.
2
+
3
+ Allows invocation via: python -m base32.cli
4
+ """
5
+
6
+ from . import base32
7
+
8
+ if __name__ == "__main__":
9
+ base32()
base32/cli/io.py ADDED
@@ -0,0 +1,26 @@
1
+ import sys
2
+ from contextlib import contextmanager
3
+ from typing import TYPE_CHECKING, TextIO, cast
4
+
5
+ if TYPE_CHECKING:
6
+ from collections.abc import Iterator
7
+
8
+
9
+ @contextmanager
10
+ def infile(name: str) -> Iterator[TextIO]:
11
+ """Yield a readable text stream for a filename or stdin if '-'."""
12
+ if name == "-":
13
+ yield cast("TextIO", sys.stdin)
14
+ else:
15
+ with open(name, encoding="utf-8") as f:
16
+ yield f
17
+
18
+
19
+ @contextmanager
20
+ def outfile(name: str) -> Iterator[TextIO]:
21
+ """Yield a writable text stream for a filename or stdout if '-'."""
22
+ if name == "-":
23
+ yield cast("TextIO", sys.stdout)
24
+ else:
25
+ with open(name, "w", encoding="utf-8") as f:
26
+ yield f
base32/cli/ops.py ADDED
@@ -0,0 +1,100 @@
1
+ import re
2
+ import sys
3
+ import base64
4
+ import binascii
5
+
6
+ import click
7
+
8
+ import base32
9
+
10
+ from .io import infile, outfile
11
+
12
+ # Expected hash lengths for SRI validation
13
+ SRI_HASH_LENGTHS: dict[str, int] = {
14
+ "sha256": 32,
15
+ "sha384": 48,
16
+ "sha512": 64,
17
+ }
18
+
19
+
20
+ def encode(input: str, output: str) -> None:
21
+ """Reads input and writes Nix base32 output
22
+
23
+ :param input: input filename or '-' for stdin
24
+ :type input: str
25
+ :param output: output filename or '-' for stdout
26
+ :type output: str
27
+ """
28
+
29
+ data: str
30
+
31
+ with infile(input) as f:
32
+ data = f.read().strip()
33
+
34
+ # if data is SRI hash (e.g., sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=)
35
+ if m := re.match(
36
+ r"^(sha256|sha384|sha512)-((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$",
37
+ data,
38
+ ):
39
+ hash_type = m.group(1)
40
+ payload = base64.b64decode(m.group(2))
41
+
42
+ # Validate hash length matches algorithm
43
+ expected_len = SRI_HASH_LENGTHS[hash_type]
44
+ if len(payload) != expected_len:
45
+ click.echo(
46
+ f"error: {hash_type} hash must be {expected_len} bytes, got {len(payload)}",
47
+ err=True,
48
+ )
49
+ sys.exit(1)
50
+ else:
51
+ if not re.fullmatch(r"[0-9a-fA-F]+", data):
52
+ click.echo("error: only hex or SRI input supported", err=True)
53
+ sys.exit(1)
54
+ payload = binascii.unhexlify(data)
55
+
56
+ with outfile(output) as f:
57
+ f.write(base32.encode(payload))
58
+
59
+
60
+ def decode(input: str, output: str, sri: bool) -> None:
61
+ """Reads Nix base32 input and writes decoded output
62
+
63
+ :param input: input filename or '-' for stdin
64
+ :type input: str
65
+ :param output: output filename or '-' for stdout
66
+ :type output: str
67
+ :param sri: if set
68
+ :type sri: bool
69
+ """
70
+
71
+ data: str
72
+
73
+ with infile(input) as f:
74
+ data = f.read().strip()
75
+
76
+ try:
77
+ decoded = base32.decode(data)
78
+ except Exception as exc:
79
+ click.echo(f"decode error: {exc}", err=True)
80
+ sys.exit(1)
81
+
82
+ hexed = binascii.hexlify(decoded).decode("utf-8")
83
+
84
+ with outfile(output) as f:
85
+ if sri:
86
+ length = len(hexed)
87
+ prefix = {
88
+ 64: "sha256",
89
+ 96: "sha384",
90
+ 128: "sha512",
91
+ }.get(length)
92
+
93
+ if prefix is None:
94
+ click.echo("SRI mode supported only for SHA256/384/512 digests", err=True)
95
+ sys.exit(1)
96
+
97
+ b64 = base64.b64encode(decoded).decode("utf-8")
98
+ f.write(f"{prefix}-{b64}")
99
+ else:
100
+ f.write(hexed)
base32/decode.py ADDED
@@ -0,0 +1,148 @@
1
+ """base32 decoding utility following the Nix variant.
2
+
3
+ Implements :func:`decode`, the inverse of
4
+ :func:`base32.encode`.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, BinaryIO, TextIO
8
+
9
+ from .detail import NixBase32Str, max_decoded_length, reverse_lookup
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Iterator
13
+
14
+
15
+ def decode(s: str | NixBase32Str) -> bytes:
16
+ """Decode a Nix base32 string back into the original bytes.
17
+
18
+ The algorithm reverses :func:`base32.encode`, consuming bits
19
+ five at a time from LSB to MSB (right to left).
20
+
21
+ :param s: Nix base32 string to decode.
22
+ :type s: str | base32.detail.types.NixBase32Str
23
+ :returns: Original binary data represented by ``s``.
24
+ :rtype: bytes
25
+ :raises ValueError: If the string contains invalid base32 symbol(s).
26
+
27
+ :example:
28
+ >>> from base32 import encode, decode
29
+ >>> s = encode(b"hi")
30
+ >>> s
31
+ NixBase32Str('nbqwcid')
32
+ >>> decode(s)
33
+ b'hi'
34
+ """
35
+ if not s:
36
+ return b""
37
+
38
+ # Upper-bound capacity = ceil(len(s) * 5 / 8)
39
+ cap = max_decoded_length(len(s))
40
+ out = bytearray(cap)
41
+ used = 0
42
+
43
+ for n, ch in enumerate(reversed(s)):
44
+ digit = reverse_lookup(ch)
45
+ if digit is None:
46
+ raise ValueError(f"invalid character {ch!r}")
47
+
48
+ b = n * 5
49
+ i = b // 8
50
+ j = b % 8
51
+
52
+ out[i] = (out[i] | ((digit << j) & 0xFF)) & 0xFF
53
+ if used < i + 1:
54
+ used = i + 1
55
+
56
+ # If 5-bit group crosses byte boundary, spill over.
57
+ if j and (digit >> (8 - j)):
58
+ out[i + 1] = (out[i + 1] | (digit >> (8 - j))) & 0xFF
59
+ if used < i + 2:
60
+ used = i + 2
61
+
62
+ return bytes(out[:used])
63
+
64
+
65
+ def decode_iter(s: str | NixBase32Str, chunk_size: int = 8192) -> Iterator[bytes]:
66
+ """Decode a Nix base32 string and yield output in chunks.
67
+
68
+ This is useful for memory-efficient writing of large decoded data.
69
+ The decoding itself requires the full input, but output is yielded
70
+ in manageable chunks.
71
+
72
+ :param s: Nix base32 string to decode.
73
+ :param chunk_size: Maximum bytes per yielded chunk.
74
+ :yields: Bytes chunks of the decoded output.
75
+ :raises ValueError: If the string contains invalid base32 symbol(s).
76
+
77
+ :example:
78
+ >>> for chunk in decode_iter("0" * 1000, chunk_size=100):
79
+ ... print(len(chunk)) # Each chunk <= 100 bytes
80
+ """
81
+ decoded = decode(s)
82
+ for i in range(0, len(decoded), chunk_size):
83
+ yield decoded[i : i + chunk_size]
84
+
85
+
86
+ def decode_to_stream(
87
+ s: str | NixBase32Str,
88
+ writer: BinaryIO,
89
+ *,
90
+ chunk_size: int = 8192,
91
+ ) -> int:
92
+ """Decode a Nix base32 string and write to a binary stream.
93
+
94
+ :param s: Nix base32 string to decode.
95
+ :param writer: Binary stream to write decoded output to.
96
+ :param chunk_size: Write buffer size in bytes.
97
+ :returns: Number of bytes written.
98
+ :raises ValueError: If the string contains invalid base32 symbol(s).
99
+
100
+ :example:
101
+ >>> import io
102
+ >>> buf = io.BytesIO()
103
+ >>> decode_to_stream("nbswy3dp", buf)
104
+ 5
105
+ >>> buf.getvalue()
106
+ b'hello'
107
+ """
108
+ written = 0
109
+ for chunk in decode_iter(s, chunk_size):
110
+ writer.write(chunk)
111
+ written += len(chunk)
112
+ return written
113
+
114
+
115
+ def decode_stream(
116
+ reader: TextIO,
117
+ writer: BinaryIO,
118
+ *,
119
+ chunk_size: int = 8192,
120
+ strip: bool = True,
121
+ ) -> int:
122
+ """Read base32 text from a stream, decode, and write to binary stream.
123
+
124
+ .. note::
125
+ Due to the Nix base32 algorithm processing characters in reverse
126
+ order, the full input must be buffered before decoding.
127
+ This function provides streaming I/O but not streaming computation.
128
+
129
+ :param reader: Text stream to read base32 input from.
130
+ :param writer: Binary stream to write decoded output to.
131
+ :param chunk_size: I/O buffer size.
132
+ :param strip: Whether to strip whitespace from input (default True).
133
+ :returns: Number of bytes written.
134
+ :raises ValueError: If the input contains invalid base32 symbol(s).
135
+
136
+ :example:
137
+ >>> import io
138
+ >>> inp = io.StringIO("nbswy3dp")
139
+ >>> out = io.BytesIO()
140
+ >>> decode_stream(inp, out)
141
+ 5
142
+ >>> out.getvalue()
143
+ b'hello'
144
+ """
145
+ data = reader.read()
146
+ if strip:
147
+ data = data.strip()
148
+ return decode_to_stream(data, writer, chunk_size=chunk_size)
@@ -0,0 +1,32 @@
1
+ """Low-level internal utilities for Nix-style base32 encoding and decoding.
2
+
3
+ This subpackage provides fine-grained tools used by higher-level base32
4
+ logic. It encapsulates Nix-specific alphabet definitions, validation
5
+ types, length computations, and fast reverse-lookup functionality.
6
+
7
+ The exposed API is intentionally narrow—only stable primitives required
8
+ by the public encoder/decoder surface are exported.
9
+
10
+ Example usage::
11
+
12
+ from base32.detail import encoded_length, reverse_lookup
13
+
14
+ n = 16
15
+ print(f"{n} bytes -> roughly {encoded_length(n)} base32 chars")
16
+
17
+ digit = reverse_lookup("f")
18
+ print(f"Digit for 'f': {digit}")
19
+ """
20
+
21
+ from .lengths import encoded_length, max_decoded_length
22
+ from .reverse_lookup import reverse_lookup
23
+ from .types import INVALID, NixBase32Str, charset
24
+
25
+ __all__ = [
26
+ "INVALID",
27
+ "NixBase32Str",
28
+ "charset",
29
+ "encoded_length",
30
+ "max_decoded_length",
31
+ "reverse_lookup",
32
+ ]
@@ -0,0 +1,58 @@
1
+ """Length computation helpers for Nix base32 encoding.
2
+
3
+ These functions mirror the capacity and sizing formulas used by the
4
+ official Nix implementation. They allow callers to estimate the output
5
+ length of encoded data or the maximum number of bytes that can be safely
6
+ decoded from a given base32 representation.
7
+ """
8
+
9
+
10
+ def encoded_length(n: int) -> int:
11
+ """Compute the number of Nix base32 characters required to encode ``n`` bytes.
12
+
13
+ The calculation uses the formula::
14
+
15
+ ceil((n * 8) / 5)
16
+
17
+ expressed in integer arithmetic as ``(n * 8 - 1) // 5 + 1``.
18
+ It ensures sufficient capacity even when input length isn't a multiple
19
+ of 5-bits.
20
+
21
+ :param n: Number of bytes in the input data.
22
+ :type n: int
23
+ :returns: Number of base32 characters required to represent the input.
24
+ :rtype: int
25
+
26
+ :example:
27
+ >>> encoded_length(1)
28
+ 2
29
+ >>> encoded_length(5)
30
+ 8
31
+ """
32
+ return (n * 8 - 1) // 5 + 1
33
+
34
+
35
+ def max_decoded_length(n: int) -> int:
36
+ """Compute the maximum number of bytes that may decode from ``n`` base32 characters.
37
+
38
+ This mirrors the logic used in the Nix reference decoder:
39
+ ``ceil(n * 5 / 8)``.
40
+ It yields the upper bound of decoded bytes to aid buffer preallocation.
41
+
42
+ :param n: Number of base32 characters in the encoded input.
43
+ :type n: int
44
+ :returns: Maximum number of bytes that can be decoded.
45
+ :rtype: int
46
+
47
+ :example:
48
+ >>> max_decoded_length(2)
49
+ 1
50
+ >>> max_decoded_length(8)
51
+ 5
52
+
53
+ .. seealso::
54
+ **Nix reference implementation:**
55
+ https://github.com/NixOS/nix/blob/fb117e0cacc9b0bb29288ee9d3cb6dc0b5ff34a5/src/libutil/base-nix-32.cc#L45
56
+ """
57
+ # ceil(n * 5 / 8): capacity upper-bound used by the reference decoder
58
+ return (n * 5 + 7) // 8
@@ -0,0 +1,71 @@
1
+ """Reverse lookup utilities for the Nix-style base32 alphabet.
2
+
3
+ Provides a table-based constant-time mapping from base32 characters back
4
+ to their numeric digit values.
5
+ Invalid characters are consistently mapped to the :data:`INVALID`
6
+ sentinel value, enabling efficient checks during decoding.
7
+ """
8
+
9
+ from .types import INVALID, charset
10
+
11
+
12
+ def build_lookup_table() -> list[int]:
13
+ """Construct the ASCII lookup table for the Nix base32 alphabet.
14
+
15
+ Builds a 256-element array mapping every possible 8-bit character
16
+ to its corresponding base32 digit value or :data:`INVALID` if the
17
+ character is not part of the alphabet.
18
+
19
+ :returns:
20
+ Table of integer values indexed by character ordinal
21
+ (0..255).
22
+ :rtype: list[int]
23
+
24
+ .. important::
25
+ Called automatically at module import to initialize
26
+ :data:`lookup_table`. In normal use this function need not
27
+ be invoked manually.
28
+ """
29
+ result: list[int] = [INVALID] * 256
30
+ for i, ch in enumerate(charset):
31
+ result[ord(ch)] = i
32
+ return result
33
+
34
+
35
+ lookup_table: list[int] = build_lookup_table()
36
+ """Prebuilt lookup table for ASCII → base32 digit mapping.
37
+
38
+ This table is generated once at import time to provide constant-time
39
+ access for all subsequent reverse lookups.
40
+
41
+ :meta hide-value:
42
+ """
43
+
44
+
45
+ def reverse_lookup(ch: str) -> int | None:
46
+ """Convert a Nix base32 character to its numeric digit index.
47
+
48
+ Uses :data:`lookup_table` for a direct translation of the character
49
+ to its digit value. Returns ``None`` for invalid characters.
50
+
51
+ :param ch: Single base32 character to convert.
52
+ :type ch: str
53
+ :returns:
54
+ Integer digit index (0..31) or ``None`` if invalid.
55
+ :rtype: int | None
56
+
57
+ :example:
58
+ >>> reverse_lookup("a")
59
+ 10
60
+ >>> reverse_lookup("?")
61
+ None
62
+ """
63
+ if len(ch) != 1:
64
+ return None
65
+
66
+ codepoint = ord(ch)
67
+ if codepoint >= len(lookup_table):
68
+ return None
69
+
70
+ digit: int = lookup_table[codepoint]
71
+ return None if digit == INVALID else digit
base32/detail/types.py ADDED
@@ -0,0 +1,54 @@
1
+ # fmt: off
2
+ """Type definitions and constants for Nix-style base32 encoding.
3
+
4
+ Defines the Nix-specific alphabet, type aliases for valid characters and
5
+ strings, and validation logic that ensures base32 strings contain only
6
+ permitted symbols.
7
+
8
+ The alphabet used here omits ambiguous letters ("e", "o", "u", "t") to
9
+ reduce transcription errors, following the Nix convention.
10
+ """
11
+
12
+
13
+ from typing import ClassVar, Literal, get_args
14
+
15
+ INVALID: int = 0xFF
16
+ """Sentinel integer used for invalid base32 character mappings."""
17
+
18
+ # Original reference:
19
+ # https://github.com/NixOS/nix/blob/fb117e0cacc9b0bb29288ee9d3cb6dc0b5ff34a5/src/libutil/include/nix/util/base-nix-32.hh#L17
20
+ # Note: 'e', 'o', 'u', 't' - omitted to avoid ambiguity.
21
+
22
+ type NixBase32Charset = Literal["0123456789abcdfghijklmnpqrsvwxyz"]
23
+ """Literal of the full concatenated Nix base32 alphabet."""
24
+
25
+ charset: NixBase32Charset = get_args(NixBase32Charset.__value__)[0]
26
+ """Canonical string representation of the Nix base32 alphabet, in order."""
27
+
28
+
29
+ class NixBase32Str(str):
30
+ """Validated string subclass restricted to Nix base32 characters.
31
+
32
+ Instances of this class behave like built-in :class:`str` but cannot
33
+ contain invalid symbols. Construction will raise :class:`ValueError`
34
+ if the input includes any character not in the official Nix alphabet.
35
+
36
+ :param value: base32 string value to validate and store.
37
+ :type value: str
38
+ :raises ValueError: If the input contains any disallowed character.
39
+
40
+ :example:
41
+ >>> NixBase32Str("abc123")
42
+ 'abc123'
43
+ >>> NixBase32Str("abcd$") # invalid
44
+ Traceback (most recent call last):
45
+ ...
46
+ ValueError: Invalid Nix base32 string: abcd$
47
+ """
48
+
49
+ _allowed: ClassVar[set[str]] = set(charset)
50
+
51
+ def __new__(cls, value: str) -> NixBase32Str: # noqa: PYI034
52
+ if not set(value) <= cls._allowed:
53
+ raise ValueError(f"Invalid Nix base32 string: {value}")
54
+ return super().__new__(cls, value)
base32/encode.py ADDED
@@ -0,0 +1,136 @@
1
+ """base32 encoding following the Nix variant semantics.
2
+
3
+ This module defines :func:`encode`, which converts arbitrary byte
4
+ strings into a :class:`~base32.detail.types.NixBase32Str`.
5
+
6
+ The algorithm mirrors the Nix implementation.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING, BinaryIO, TextIO
10
+
11
+ from .detail import NixBase32Str, charset, encoded_length
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Iterator
15
+
16
+
17
+ def encode(bs: bytes) -> NixBase32Str:
18
+ """Encode a byte sequence into a Nix base32 string.
19
+
20
+ Each group of five bits in ``bs`` is mapped to a single
21
+ character from the Nix base32 alphabet. The result omits
22
+ padding characters and is guaranteed to roundtrip through
23
+ :func:`base32.decode`.
24
+
25
+ :param bs: Bytes to encode.
26
+ :type bs: bytes
27
+ :returns: base32 string representation.
28
+ :rtype: base32.detail.types.NixBase32Str
29
+
30
+ :example:
31
+ >>> from base32 import encode
32
+ >>> encode(b"foo")
33
+ NixBase32Str('mzxw6')
34
+ >>> encode(b"")
35
+ NixBase32Str('')
36
+ .. seealso::
37
+ **Nix reference implementation:**
38
+ https://github.com/NixOS/nix/blob/fb117e0cacc9b0bb29288ee9d3cb6dc0b5ff34a5/src/libutil/base-nix-32.cc#L20
39
+ """
40
+ if not bs:
41
+ return NixBase32Str("")
42
+
43
+ length = encoded_length(len(bs))
44
+ out: list[str] = []
45
+
46
+ # Walk 5-bit groups from MSB to LSB.
47
+ for n in reversed(range(length)):
48
+ b = n * 5
49
+ i = b // 8
50
+ j = b % 8
51
+
52
+ b1 = bs[i]
53
+ b2 = bs[i + 1] if i + 1 < len(bs) else 0
54
+ c = ((b1 >> j) | ((b2 << (8 - j)) & 0xFF)) & 0xFF
55
+ out.append(charset[c & 0x1F])
56
+
57
+ return NixBase32Str("".join(out))
58
+
59
+
60
+ def encode_iter(bs: bytes, chunk_size: int = 8192) -> Iterator[str]:
61
+ """Encode bytes and yield output in chunks.
62
+
63
+ This is useful for memory-efficient writing of large encoded data.
64
+ The encoding itself requires the full input, but output is yielded
65
+ in manageable chunks.
66
+
67
+ :param bs: Bytes to encode.
68
+ :param chunk_size: Maximum characters per yielded chunk.
69
+ :yields: String chunks of the encoded output.
70
+
71
+ :example:
72
+ >>> for chunk in encode_iter(b"hello" * 1000, chunk_size=100):
73
+ ... print(len(chunk)) # Each chunk <= 100 chars
74
+ """
75
+ encoded = encode(bs)
76
+ for i in range(0, len(encoded), chunk_size):
77
+ yield str(encoded[i : i + chunk_size])
78
+
79
+
80
+ def encode_to_stream(
81
+ bs: bytes,
82
+ writer: TextIO,
83
+ *,
84
+ chunk_size: int = 8192,
85
+ ) -> int:
86
+ """Encode bytes and write to a text stream.
87
+
88
+ :param bs: Bytes to encode.
89
+ :param writer: Text stream to write encoded output to.
90
+ :param chunk_size: Write buffer size in characters.
91
+ :returns: Number of characters written.
92
+
93
+ :example:
94
+ >>> import io
95
+ >>> buf = io.StringIO()
96
+ >>> encode_to_stream(b"hello", buf)
97
+ 8
98
+ >>> buf.getvalue()
99
+ 'nbswy3dp'
100
+ """
101
+ written = 0
102
+ for chunk in encode_iter(bs, chunk_size):
103
+ writer.write(chunk)
104
+ written += len(chunk)
105
+ return written
106
+
107
+
108
+ def encode_stream(
109
+ reader: BinaryIO,
110
+ writer: TextIO,
111
+ *,
112
+ chunk_size: int = 8192,
113
+ ) -> int:
114
+ """Read binary data from a stream, encode, and write to text stream.
115
+
116
+ .. note::
117
+ Due to the Nix base32 algorithm processing bits in reverse order
118
+ (MSB to LSB), the full input must be buffered before encoding.
119
+ This function provides streaming I/O but not streaming computation.
120
+
121
+ :param reader: Binary stream to read input from.
122
+ :param writer: Text stream to write encoded output to.
123
+ :param chunk_size: I/O buffer size.
124
+ :returns: Number of characters written.
125
+
126
+ :example:
127
+ >>> import io
128
+ >>> inp = io.BytesIO(b"hello")
129
+ >>> out = io.StringIO()
130
+ >>> encode_stream(inp, out)
131
+ 8
132
+ >>> out.getvalue()
133
+ 'nbswy3dp'
134
+ """
135
+ data = reader.read()
136
+ return encode_to_stream(data, writer, chunk_size=chunk_size)
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: nix-base32
3
+ Version: 0.4.2
4
+ Summary: Pure Python implementation of nix variant of base32
5
+ Author-email: "Peter A." <ink.splatters@pm.me>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Peter A.
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python :: 3.14
32
+ Classifier: Programming Language :: Python :: Implementation :: CPython
33
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
34
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
35
+ Requires-Python: >=3.14
36
+ Provides-Extra: cli
37
+ Requires-Dist: click>=8.3.0; extra == 'cli'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # nix‑base32
41
+
42
+ Pure‑Python implementation of the Nix‑specific base32 variant.
43
+
44
+ ______________________________________________________________________
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ uv tool install 'nix-base32[cli] @ git+https://github.com/ink-splatters/nix-base32'
50
+ ```
51
+
52
+ ______________________________________________________________________
53
+
54
+ ## Usage
55
+
56
+ ### Python
57
+
58
+ ```python
59
+ from base32 import encode, decode
60
+
61
+ data = b"hello"
62
+ encoded = encode(data)
63
+ print(encoded) # -> NixBase32Str('nbswy3dp')
64
+ print(decode(encoded)) # -> b'hello'
65
+ ```
66
+
67
+ ### CLI
68
+
69
+ the CLI was inspired by BSD bintrans' `base64` util (now part of standard macOS distribution).
70
+
71
+ _Examples:_
72
+
73
+ ```bash
74
+ ❯ echo de2fc4ce5252da49a272fb22e68c73dcfa12ef08077ac26b40ad3a40dd31376e | base32
75
+ 0vip67fl0fmd81mw4yh713pi5ynwff6fc8pvfai4knjjab7c8byy
76
+
77
+ ❯ echo 0vip67fl0fmd81mw4yh713pi5ynwff6fc8pvfai4knjjab7c8byy | base32 --sri
78
+ sha256-3i/EzlJS2kmicvsi5oxz3PoS7wgHesJrQK06QN0xN24=
79
+
80
+ ❯ echo de2fc4ce5252da49a272fb22e68c73dcfa12ef08077ac26b40ad3a40dd31376e | base32 | base32 -d
81
+ de2fc4ce5252da49a272fb22e68c73dcfa12ef08077ac26b40ad3a40dd31376e
82
+ ```
83
+
84
+ ______________________________________________________________________
85
+
86
+ ## License
87
+
88
+ MIT — see [LICENSE](LICENSE).
89
+
90
+ ## Contributions
91
+
92
+ Contributions are welcome at the author's discretion; please open an issue first.
93
+ See any standard open collaborative code of conduct such as
94
+ [Contributor Covenant](https://www.contributor-covenant.org/).
@@ -0,0 +1,16 @@
1
+ base32/__init__.py,sha256=BvePOqVgt46zTmg7LJp7PO2VvsMpyOWMSTH19qvKR30,1155
2
+ base32/decode.py,sha256=qp29n0yHEDbDa3fTX8sPOYBZQ6eFhXGwl_twcvH3zLY,4381
3
+ base32/encode.py,sha256=LWfjsb7JgwypuJvth6YdNUTbe3HxlMeUjEZZNzndB1c,3920
4
+ base32/cli/__init__.py,sha256=UDzlMCCYAy7o0VUF_GHRU96KdXJoTbsclcgcK-Plgpg,1005
5
+ base32/cli/__main__.py,sha256=mzo6ohPJvgG2zMydqiln_PAciPUryZoYmq8siMbJ4i4,160
6
+ base32/cli/io.py,sha256=u19qO2EDiCCeL_G5-2k8xBpN33V6uF1ymlYI4nlDohk,704
7
+ base32/cli/ops.py,sha256=x4uZT4jolIBileuB02-yzD-Yu2gaDeRtqHHHmb6F6Ms,2592
8
+ base32/detail/__init__.py,sha256=lsyKSdi9neCLBh0uPOk-uqev7A81WkPSbwXOYO3oQi8,931
9
+ base32/detail/lengths.py,sha256=vzWGO1xNymTd_astOuqB0O-hmmZQOinAm1RlhOHiiMQ,1788
10
+ base32/detail/reverse_lookup.py,sha256=KPfqgJTMkbZ3XIxsiTj8MWc_OxMZG1mgQlc6RUJHigc,2035
11
+ base32/detail/types.py,sha256=WxJiaiyazmEV7XOnumxYF978PBoihoxQNbIcY4S89bE,1966
12
+ nix_base32-0.4.2.dist-info/METADATA,sha256=RPgIMsoN37nGNJpb7W1Y9fBAf6_g-Kq3QcLzfCLuYeA,3395
13
+ nix_base32-0.4.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
14
+ nix_base32-0.4.2.dist-info/entry_points.txt,sha256=dFW5GNmB92k6Q-qAGYBsOueZPaJBAYjeS68gshohRCQ,45
15
+ nix_base32-0.4.2.dist-info/licenses/LICENSE,sha256=yCbF7A9Qpm2aPKI-pOyuORXIcq_TAxy-4pD8pOOhfbU,1065
16
+ nix_base32-0.4.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ base32 = base32.cli:base32
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peter A.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.