From a7881f740a6177d20ef3447286810118737e9a84 Mon Sep 17 00:00:00 2001 From: Caleb Denio Date: Tue, 27 May 2025 18:07:49 -0400 Subject: [PATCH] Initial commit --- .gitignore | 2 + build.zig | 57 ++++++++++ build.zig.zon | 86 +++++++++++++++ src/imb.zig | 287 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 8 ++ src/wasm.zig | 26 +++++ web/imb.wasm | Bin 0 -> 10217 bytes web/index.html | 119 ++++++++++++++++++++ 8 files changed, 585 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/imb.zig create mode 100644 src/main.zig create mode 100644 src/wasm.zig create mode 100755 web/imb.wasm create mode 100644 web/index.html 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 0000000000000000000000000000000000000000..b36e8c360f15f571fe5f5d224d0043eeda353087 GIT binary patch literal 10217 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 BHLCys literal 0 HcmV?d00001 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 @@ + + + +
+ + +
+ +

+
+    
+  
+