From fbda4dca29a619aa3704cb69c77e49e040e4a912 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 28 Aug 2026 21:09:17 -0500 Subject: [PATCH] Add error-path test coverage and reject X in ISBN-13 input Add tests for malformed input, invalid check digits, the '0' check-digit branches of both formats, and the 979 prefix. Writing them surfaced a parser bug: 'X' was accepted as the 10th digit of a 13-digit input, and since 'X' - '0' is 40, which at weight 3 contributes 120 = 0 (mod 10), a string like 978-0-306-40X15-5 passed the checksum and parsed as a valid ISBN-13 (the non-SIMD build would hit a debug assertion instead). parse now rejects any 13-digit input containing 'X'. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016uAPiLrcvxP4hyvSLokRrp --- src/isbn.zig | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/isbn.zig b/src/isbn.zig index e49f2ab..57af202 100644 --- a/src/isbn.zig +++ b/src/isbn.zig @@ -52,6 +52,9 @@ pub const ISBN = union(enum) { return tmp; }, 13 => { + // 'X' is only valid as the check digit of an ISBN-10; the + // scan loop can only have accepted it at index 9. + if (buf[9] == 'X') return error.ParseError; var tmp: ISBN = .{ .@"13" = undefined, }; @@ -226,6 +229,53 @@ test "13 1" { try std.testing.expectEqual('7', c); } +test "13 2" { + const x: ISBN = try .parse("979-10-90636-07-1"); + try std.testing.expect(x == .@"13"); + try std.testing.expectEqualSlices(u8, "9791090636071", &x.@"13"); + const c = x.computeCheckDigit(); + try std.testing.expectEqual('1', c); +} + +test "10 check digit 0" { + const x: ISBN = try .parse("0-00-000000-0"); + try std.testing.expect(x == .@"10"); + const c = x.computeCheckDigit(); + try std.testing.expectEqual('0', c); +} + +test "13 check digit 0" { + const x: ISBN = try .parse("978-0-00-000020-0"); + try std.testing.expect(x == .@"13"); + const c = x.computeCheckDigit(); + try std.testing.expectEqual('0', c); +} + +test "parse rejects malformed input" { + try std.testing.expectError(error.ParseError, ISBN.parse("")); + try std.testing.expectError(error.ParseError, ISBN.parse("---")); + try std.testing.expectError(error.ParseError, ISBN.parse("0-306-40615")); // 9 digits + try std.testing.expectError(error.ParseError, ISBN.parse("0-306-40615-22")); // 11 digits + try std.testing.expectError(error.ParseError, ISBN.parse("978-0-306-40615")); // 12 digits + try std.testing.expectError(error.ParseError, ISBN.parse("978-0-306-40615-72")); // 14 digits + try std.testing.expectError(error.ParseError, ISBN.parse("0-306-4X615-2")); // X in wrong position + try std.testing.expectError(error.ParseError, ISBN.parse("0-306-40615-A")); // invalid character + try std.testing.expectError(error.ParseError, ISBN.parse("0 306 40615 2")); // space separator +} + +test "parse rejects X in an ISBN-13" { + // 'X' - '0' is 40, which at position 9 (weight 3) contributes + // 120 = 0 (mod 10), so this string is checksum-valid and must be + // rejected before the check-digit comparison. + try std.testing.expectError(error.ParseError, ISBN.parse("978-0-306-40X15-5")); +} + +test "parse rejects invalid check digits" { + try std.testing.expectError(error.InvalidCheckDigit, ISBN.parse("0-306-40615-3")); + try std.testing.expectError(error.InvalidCheckDigit, ISBN.parse("0-8044-2957-4")); // expects X + try std.testing.expectError(error.InvalidCheckDigit, ISBN.parse("978-0-306-40615-8")); +} + test "upgrade and eql" { const ten: ISBN = try .parse("0-306-40615-2"); const thirteen: ISBN = try .parse("978-0-306-40615-7"); -- 2.51.2