commit a7881f740a6177d20ef3447286810118737e9a84 Author: Caleb Denio Date: Tue May 27 18:07:49 2025 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b21f7ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +.zig-cache/ \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..86ef093 --- /dev/null +++ b/build.zig @@ -0,0 +1,57 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe_mod = b.createModule(.{ + .root_source_file = if (target.result.cpu.arch == .wasm32) b.path("src/wasm.zig") else b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe_mod.export_symbol_names = &.{ "malloc", "free", "decodeStringWasm" }; + + const exe = b.addExecutable(.{ + .name = "zig_imb", + .root_module = exe_mod, + }); + + exe.entry = .disabled; + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..ed8bd63 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,86 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .zig_imb, + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0x761a5f11ac79281a, // Changing this has security and trust implications. + + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.14.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/imb.zig b/src/imb.zig new file mode 100644 index 0000000..ef400dc --- /dev/null +++ b/src/imb.zig @@ -0,0 +1,287 @@ +const std = @import("std"); + +const bar_positions = [_][4]u4{ + .{ 7, 2, 4, 3 }, + .{ 1, 10, 0, 0 }, + .{ 9, 12, 2, 8 }, + .{ 5, 5, 6, 11 }, + .{ 8, 9, 3, 1 }, + .{ 0, 1, 5, 12 }, + .{ 2, 5, 1, 8 }, + .{ 4, 4, 9, 11 }, + .{ 6, 3, 8, 10 }, + .{ 3, 9, 7, 6 }, + .{ 5, 11, 1, 4 }, + .{ 8, 5, 2, 12 }, + .{ 9, 10, 0, 2 }, + .{ 7, 1, 6, 7 }, + .{ 3, 6, 4, 9 }, + .{ 0, 3, 8, 6 }, + .{ 6, 4, 2, 7 }, + .{ 1, 1, 9, 9 }, + .{ 7, 10, 5, 2 }, + .{ 4, 0, 3, 8 }, + .{ 6, 2, 0, 4 }, + .{ 8, 11, 1, 0 }, + .{ 9, 8, 3, 12 }, + .{ 2, 6, 7, 7 }, + .{ 5, 1, 4, 10 }, + .{ 1, 12, 6, 9 }, + .{ 7, 3, 8, 0 }, + .{ 5, 8, 9, 7 }, + .{ 4, 6, 2, 10 }, + .{ 3, 4, 0, 5 }, + .{ 8, 4, 5, 7 }, + .{ 7, 11, 1, 9 }, + .{ 6, 0, 9, 6 }, + .{ 0, 6, 4, 8 }, + .{ 2, 1, 3, 2 }, + .{ 5, 9, 8, 12 }, + .{ 4, 11, 6, 1 }, + .{ 9, 5, 7, 4 }, + .{ 3, 3, 1, 2 }, + .{ 0, 7, 2, 0 }, + .{ 1, 3, 4, 1 }, + .{ 6, 10, 3, 5 }, + .{ 8, 7, 9, 4 }, + .{ 2, 11, 5, 6 }, + .{ 0, 8, 7, 12 }, + .{ 4, 2, 8, 1 }, + .{ 5, 10, 3, 0 }, + .{ 9, 3, 0, 9 }, + .{ 6, 5, 2, 4 }, + .{ 7, 8, 1, 7 }, + .{ 5, 0, 4, 5 }, + .{ 2, 3, 0, 10 }, + .{ 6, 12, 9, 2 }, + .{ 3, 11, 1, 6 }, + .{ 8, 8, 7, 9 }, + .{ 5, 4, 0, 11 }, + .{ 1, 5, 2, 2 }, + .{ 9, 1, 4, 12 }, + .{ 8, 3, 6, 6 }, + .{ 7, 0, 3, 7 }, + .{ 4, 7, 7, 5 }, + .{ 0, 12, 1, 11 }, + .{ 2, 9, 9, 0 }, + .{ 6, 8, 5, 3 }, + .{ 3, 10, 8, 2 }, +}; + +fn generateCharacterTable(n: u8, comptime len: usize) [len]u13 { + @setEvalBranchQuota(150000); + + var table: [len]u13 = undefined; + + var lower_index = 0; + var upper_index = len - 1; + + for (0..0x2000) |i| { + const b = bitsSet(u13, i); + if (b != n) { + continue; + } + + const reverse = @bitReverse(@as(u13, i)); + + if (reverse < i) { + continue; + } + + if (reverse == i) { + table[upper_index] = i; + upper_index -= 1; + } else { + table[lower_index] = i; + lower_index += 1; + table[lower_index] = reverse; + lower_index += 1; + } + } + + return table; +} + +const character_table_5 = generateCharacterTable(5, 1287); +const character_table_2 = generateCharacterTable(2, 77); + +const BarType = enum { + descending, + ascending, + tracking, + full, +}; + +pub const BarcodeResult = struct { + tracking_code: [20]u8, + routing_code: [11]u8, +}; + +pub const Error = error{ + InvalidCharacter, + DecodingError, + InternalError, + InvalidChecksum, +}; + +fn bitsSet(comptime T: type, value: T) u8 { + var count: u8 = 0; + + for (0..@typeInfo(T).int.bits) |i| { + count += @intCast((value >> @intCast(i)) & 1); + } + + return count; +} + +fn findCodeword(character: u13) u13 { + const b = bitsSet(u13, character); + if (b == 2) { + for (character_table_2, 0..) |item, codeword| { + if (character == item) { + return @intCast(codeword + 1287); + } + } + } else { + for (character_table_5, 0..) |item, codeword| { + if (character == item) { + return @intCast(codeword); + } + } + } + unreachable; +} + +fn generateChecksum(data: [13]u8) u16 { + const generator_polynomial: u16 = 0x0F35; + var checksum: u16 = 0x07ff; + + var byte: u16 = data[0] << 5; + + for (2..8) |_| { + if (((checksum ^ byte) & 0x400) != 0) { + checksum = (checksum << 1) ^ generator_polynomial; + } else { + checksum <<= 1; + } + + checksum &= 0x7FF; + byte <<= 1; + } + + for (data[1..]) |b| { + byte = @as(u16, b) << 3; + + for (0..8) |_| { + if (((checksum ^ byte) & 0x400) != 0) { + checksum = (checksum << 1) ^ generator_polynomial; + } else { + checksum <<= 1; + } + + checksum &= 0x7FF; + byte <<= 1; + } + } + + return checksum; +} + +fn decode(bars: [65]BarType) Error!BarcodeResult { + var characters = [_]u13{0} ** 10; + + for (bars, 0..) |bar, i| { + const positions = bar_positions[i]; + if (bar == .descending or bar == .full) { + characters[positions[0]] |= (@as(u13, 1) << positions[1]); + } + if (bar == .ascending or bar == .full) { + characters[positions[2]] |= (@as(u13, 1) << positions[3]); + } + } + + var checksum: u11 = 0; + + for (&characters, 0..) |*character, i| { + switch (bitsSet(u13, character.*)) { + 8, 11 => { + character.* ^= 0b1111111111111; + checksum |= (@as(u11, 1) << @intCast(i)); + checksum |= (@as(u11, 1) << @intCast(i)); + }, + 2, 5 => {}, + else => return error.DecodingError, + } + + character.* = findCodeword(character.*); + } + + characters[9] /= 2; + if (characters[0] >= 659) { + characters[0] -= 659; + checksum |= 0b10000000000; + } + + var bindata: u104 = 0; + + bindata = @as(u104, characters[0]) * 1365 + characters[1]; + bindata = bindata * 1365 + characters[2]; + bindata = bindata * 1365 + characters[3]; + bindata = bindata * 1365 + characters[4]; + bindata = bindata * 1365 + characters[5]; + bindata = bindata * 1365 + characters[6]; + bindata = bindata * 1365 + characters[7]; + bindata = bindata * 1365 + characters[8]; + bindata = bindata * 636 + characters[9]; + + if (generateChecksum(@bitCast(@byteSwap(bindata))) != checksum) { // lol, only works on little-endian systems #WONTFIX + return error.InvalidChecksum; + } + + var tracking_code: [20]u8 = undefined; + var routing_code = [_]u8{0} ** 11; + + var i: u8 = 19; + while (i >= 2) : (i -= 1) { + tracking_code[i] = std.fmt.digitToChar(@intCast(bindata % 10), .lower); + bindata /= 10; + } + tracking_code[i] = std.fmt.digitToChar(@intCast(bindata % 5), .lower); + bindata /= 5; + i -= 1; + tracking_code[i] = std.fmt.digitToChar(@intCast(bindata % 10), .lower); + bindata /= 10; + + switch (bindata) { + 1...100000 => { + _ = std.fmt.bufPrint(&routing_code, "{d:05}", .{bindata - 1}) catch return error.InternalError; + }, + 100001...1000199999 => { + _ = std.fmt.bufPrint(&routing_code, "{d:09}", .{bindata - 100000 - 1}) catch return error.InternalError; + }, + else => { + _ = std.fmt.bufPrint(&routing_code, "{d:011}", .{bindata - 1000000000 - 100000 - 1}) catch return error.InternalError; + }, + } + + return .{ + .tracking_code = tracking_code, + .routing_code = routing_code, + }; +} + +pub fn decodeString(str: *const [65:0]u8) Error!BarcodeResult { + var bars: [65]BarType = undefined; + for (&bars, 0..) |*pt, i| { + pt.* = switch (str[i]) { + 'A', 'a' => .ascending, + 'D', 'd' => .descending, + 'F', 'f' => .full, + 'T', 't' => .tracking, + else => return error.InvalidCharacter, + }; + } + const thing = try decode(bars); + + return thing; +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..4976aa4 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,8 @@ +const std = @import("std"); +const imb = @import("./imb.zig"); + +pub fn main() !void { + const stuff = try imb.decodeString("AADTFFDFTDADTAADAATFDTDDAAADDTDTTDAFADADDDTFFFDDTTTADFAAADFTDAADA"); + std.log.info("{s}", .{stuff.tracking_code}); + std.log.info("{s}", .{stuff.routing_code}); +} diff --git a/src/wasm.zig b/src/wasm.zig new file mode 100644 index 0000000..e700532 --- /dev/null +++ b/src/wasm.zig @@ -0,0 +1,26 @@ +const std = @import("std"); +const imb = @import("./imb.zig"); + +const allocator = std.heap.wasm_allocator; + +export fn decodeStringWasm(str: *const [65:0]u8) usize { + if (imb.decodeString(str)) |v| { + const ptr = allocator.create([31]u8) catch unreachable; + const result = v.tracking_code ++ v.routing_code; + @memcpy(ptr, &result); + return @intFromPtr(ptr); + } else |e| switch (e) { + error.InvalidCharacter => return 0, + error.DecodingError => return 1, + error.InternalError => return 2, + error.InvalidChecksum => return 3, + } +} + +export fn malloc(len: u8) [*]u8 { + return (allocator.alloc(u8, len) catch unreachable).ptr; +} + +export fn free(ptr: [*]u8, len: u8) void { + allocator.free(ptr[0..len]); +} diff --git a/web/imb.wasm b/web/imb.wasm new file mode 100755 index 0000000..b36e8c3 Binary files /dev/null and b/web/imb.wasm differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..ce2e356 --- /dev/null +++ b/web/index.html @@ -0,0 +1,119 @@ + + + +
+ + +
+ +

+
+    
+  
+