Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions docs/developer/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ absolute size bound. The application parser selects the checksum type from the
generic encoded length and validates the common header before verifying the
outer checksum. Only afterward may the HRP select a registered application
profile. The parser then checks its application length and payload semantics.
When the checksum fails at a length the application never uses, the parser
reports that length instead of the checksum; this only chooses the reason for
a rejection.
An unregistered HRP crosses the boundary only as an opaque base `Secret` or
`Share`, with normalized `hrp`, `profile is None`, and no invented payload meaning.

CLI input diagnostics preserve generic container, header, and length failures.
After a checksum failure, the CLI may report an unsupported profile text length
instead. This diagnostic does not validate the input or change parser ordering;
alignment correction remains available. Frozen-prefix hints do not override a
generic parse-length failure.
CLI input diagnostics preserve generic container, header, and length failures;
alignment correction remains available after a length failure. Frozen-prefix
hints do not override a generic parse-length failure.

### Specification-to-code map

Expand Down
15 changes: 1 addition & 14 deletions src/codex32/_cli_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
CodexError,
DuplicateShareIndex,
ExistingTargetIndex,
InvalidChecksum,
InvalidLength,
)
from codex32.profiles import Profile, _optional_profile_rules, _profile_rules
from codex32.profiles.ms32 import (
Expand Down Expand Up @@ -300,18 +298,7 @@ def _parse(value: str, profiles: tuple[Profile, ...] | None) -> Artifact:
try:
artifact = parse_codex32(value)
except CodexError as error:
message = str(error)
if isinstance(error, InvalidChecksum):
# Parsing has already checked the container and generic length.
# Explain an unsupported profile length without validating input
# or changing the parser's checksum-before-profile boundary.
try:
_profile_rules(value[: value.rfind("1")]).validate_text_length(len(value))
except InvalidLength as length_error:
message = str(length_error)
except CodexError:
pass
raise InputError(message) from error
raise InputError(str(error)) from error
if profiles is not None and artifact.profile not in profiles:
allowed = " or ".join(_profile_rules(profile).label for profile in profiles)
raise InputError(f"This command accepts only {allowed} input.")
Expand Down
5 changes: 4 additions & 1 deletion src/codex32/bip93.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,12 @@ def _decode_codex32(text: str) -> tuple[str, _ProfileRules | None, tuple[int, ..
checksum = _checksum_for_encoded_length(hrp, len(encoded))
body = tuple(encoded[: -checksum.length])
Header._from_symbols(body[:6])
profile_rules = _optional_profile_rules(hrp)
if not bech32_verify_checksum(hrp, encoded, checksum):
if profile_rules is not None:
# A length the application never uses is the clearer reason.
profile_rules.validate_text_length(len(text))
raise InvalidChecksum("The checksum does not match.")
profile_rules = _optional_profile_rules(hrp)
if profile_rules is not None:
profile_rules.validate_text_length(len(text))
profile_rules.validate_payload_length(len(body) - 6)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli_lengths.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"This input has 95 characters. A Bitcoin master-seed backup must have "
"exactly 48, 54, 61, 67, 74, or 127 characters."
),
InvalidChecksum,
InvalidLength,
),
),
)
Expand Down
11 changes: 4 additions & 7 deletions tests/test_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ def test_pr2258_supported_size_vectors(seed_hex: str, text: str) -> None:

@pytest.mark.parametrize("text", FORMERLY_VALID_UNSUPPORTED_MS)
def test_pr2258_rejects_formerly_valid_unsupported_sizes(text: str) -> None:
expected = InvalidChecksum if text in FORMERLY_VALID_UNSUPPORTED_MS[-2:] else InvalidLength
with pytest.raises(expected):
with pytest.raises(InvalidLength):
parse_codex32(text)


Expand Down Expand Up @@ -138,17 +137,15 @@ def test_common_header_is_validated_before_checksum() -> None:
("hrp", "payload_length"),
(("ms", 26), ("cl", 52), ("bip39_12w", 27), ("bip39_24w", 53)),
)
def test_checksum_precedes_profile_length(hrp: str, payload_length: int) -> None:
def test_unused_profile_length_is_reported_instead_of_checksum(hrp: str, payload_length: int) -> None:
valid = _oracle_encode(hrp, "0tests" + "q" * payload_length)
with pytest.raises(InvalidChecksum):
with pytest.raises(InvalidLength, match=f"This input has {len(valid) - 1} characters"):
parse_codex32(valid[:-1])


def test_generic_length_and_checksum_precede_ms_length() -> None:
def test_generic_length_precedes_ms_length() -> None:
with pytest.raises(InvalidLength, match="at least 21"):
parse_codex32("ms1")
with pytest.raises(InvalidChecksum):
parse_codex32("ms10tests" + "q" * 40)


def test_valid_profile_length_still_reports_checksum_failure() -> None:
Expand Down
Loading