diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f37d0a..4671890 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,3 +21,10 @@ jobs: - run: zig fmt --check . - run: zig build test --summary all - run: zig build conformance + # The string scans depend on how a bool vector maps to an integer, + # which differs on big-endian targets. + - name: Test on big-endian (s390x) + run: | + sudo apt-get update + sudo apt-get install -y qemu-user-static + zig build test -Dtarget=s390x-linux -fqemu --summary all diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c80ad0..8c3cb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for Zig 0.17, alongside 0.16 +### Fixed +- Strings were scanned wrong on big-endian targets with Zig 0.16, so decoding failed and encoding could leave characters unescaped + ## [0.1.0] - 2026-09-16 Initial release. diff --git a/src/compat.zig b/src/compat.zig index 8ec67db..d83e7f0 100644 --- a/src/compat.zig +++ b/src/compat.zig @@ -1,8 +1,8 @@ -//! Type reflection helpers that work on both Zig 0.16 and 0.17. +//! Helpers that work on both Zig 0.16 and 0.17. //! //! Zig 0.17 changed `@typeInfo` for containers from a slice of field structs //! (`fields`) to parallel slices (`field_names`, `field_types`, -//! `field_attrs`). These helpers present the 0.16 shape on both. +//! `field_attrs`). The reflection helpers present the 0.16 shape on both. const std = @import("std"); @@ -58,3 +58,14 @@ pub fn unionFields(comptime T: type) []const UnionField { break :blk &final; }; } + +/// Bit-casts a bool vector to an integer with element `i` in bit `i`, so that +/// `@ctz` finds the first true element. Zig 0.16 bit-casts element 0 to the +/// most significant bit on big-endian targets, while 0.17 always puts it in +/// the least significant one. +pub inline fn vectorMask(comptime n: usize, v: @Vector(n, bool)) @Int(.unsigned, n) { + const Mask = @Int(.unsigned, n); + const reversed = comptime @as(Mask, @bitCast(std.simd.iota(u8, n) == @as(@Vector(n, u8), @splat(0)))) != 1; + const mask: Mask = @bitCast(v); + return if (reversed) @bitReverse(mask) else mask; +} diff --git a/src/decode.zig b/src/decode.zig index cc0c95e..76858d2 100644 --- a/src/decode.zig +++ b/src/decode.zig @@ -641,7 +641,7 @@ pub const Decoder = struct { while (i + 16 <= window.len) : (i += 16) { const chunk: V = window[i..][0..16].*; acc |= chunk; - const hits: u16 = @bitCast((chunk == quote) | (chunk == backslash) | (chunk < space)); + const hits = compat.vectorMask(16, (chunk == quote) | (chunk == backslash) | (chunk < space)); if (hits != 0) return .{ .end = i + @ctz(hits), .non_ascii = @reduce(.Or, acc) >= 0x80 }; } var tail: u8 = @reduce(.Or, acc); diff --git a/src/encode.zig b/src/encode.zig index fdb97dd..9a02570 100644 --- a/src/encode.zig +++ b/src/encode.zig @@ -322,7 +322,7 @@ pub fn writeString(str: []const u8, w: *Writer) Writer.Error!void { var i: usize = 0; while (i + 16 <= str.len) { const chunk: V = str[i..][0..16].*; - const hits: u16 = @bitCast((chunk == quote) | (chunk == backslash) | (chunk < space)); + const hits = compat.vectorMask(16, (chunk == quote) | (chunk == backslash) | (chunk < space)); if (hits == 0) { i += 16; continue;