Something went wrong. Try again.
A Zig library and command line tool for the classic Unix uuencode format.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Vectorised kernels for whole uuencode lines, kept beside the scalar code//! in `uuencode.zig` and `uudecode.zig` so the two can be compared.//!//! uuencode suits SIMD better than base64 does. Base64 needs a lookup to map//! a six-bit group onto its alphabet; uuencode's alphabet is contiguous, so//! the map is `group + 0x20` and the only special case is the zero group,//! which is one blend. What is left is a shuffle and a handful of shifts.//!//! Everything here works on a full 45 byte line, which is exactly 15 groups//! of three bytes and 60 characters. Short lines, meaning the last one, stay//! on the scalar path where the padding rules live.
const std = @import("std");const builtin = @import("builtin");const build_options = @import("build_options");const assert = std.debug.assert;
const uuencode = @import("uuencode.zig");const uudecode = @import("uudecode.zig");
/// Whether the vector kernels are worth using on the target being compiled/// for.////// `@Vector` compiles everywhere: on a target with no vector registers it/// lowers to scalar code that is no faster than the ordinary loop, and can be/// slower once the shuffles are expanded by hand. So this is a question about/// the machine rather than about the language, and the answer is comptime,/// which folds the unused path away entirely.////// The thresholds here were measured rather than guessed, on a Ryzen 7 5800X/// with `zig build bench -Dcpu=...`. Encoding a 16 MiB payload against the/// scalar path:////// baseline (SSE2 only) 2.25x/// x86_64_v2 (SSE4.2) 2.07x/// x86_64_v3 (AVX2) 2.58x////// SSE2 alone already pays for itself, and SSE2 is part of the x86_64/// baseline, so there is nothing to gate on there. Architectures that have/// not been measured default to the scalar path.////// `-Dsimd=false` forces the scalar path regardless. The build flag can only/// take the kernels away, never add them to a target that cannot run them.pub const available = build_options.simd and target_supported;
/// Whether the target could use the kernels, ignoring the build flag.pub const target_supported = switch (builtin.cpu.arch) { // SSE2 is guaranteed by the x86_64 baseline. .x86_64 => true, .x86 => std.Target.x86.featureSetHas(builtin.cpu.features, .sse2), .aarch64, .aarch64_be => std.Target.aarch64.featureSetHas(builtin.cpu.features, .neon), .wasm32, .wasm64 => std.Target.wasm.featureSetHas(builtin.cpu.features, .simd128), else => false,};
pub const bytes_per_line = uuencode.bytes_per_line;pub const chars_per_full_line = 4 * (bytes_per_line / 3);
const Line = @Vector(bytes_per_line, u8);const Chars = @Vector(chars_per_full_line, u8);
/// For output character `p`, the byte of its three byte group at offset `k`.fn encodeGather(comptime k: i32) @Vector(chars_per_full_line, i32) { var mask: [chars_per_full_line]i32 = undefined; for (&mask, 0..) |*entry, p| entry.* = 3 * @as(i32, @intCast(p / 4)) + k; return mask;}
/// Lanes holding the `k`th character of each four character group.fn encodeLanes(comptime k: usize) @Vector(chars_per_full_line, bool) { var mask: [chars_per_full_line]bool = undefined; for (&mask, 0..) |*entry, p| entry.* = p % 4 == k; return mask;}
/// Encodes exactly one full line. `charset` is comptime so the zero group/// blend folds away entirely for `.space`, where no blend is needed at all.pub fn encodeFullLine( dest: *[chars_per_full_line]u8, source: *const [bytes_per_line]u8, comptime charset: uuencode.Charset,) void { const in: Line = source.*;
// Three copies of the input, aligned so that lane `p` of each holds the // first, second and third byte of the group that character `p` comes from. const a = @shuffle(u8, in, undefined, encodeGather(0)); const b = @shuffle(u8, in, undefined, encodeGather(1)); const c = @shuffle(u8, in, undefined, encodeGather(2));
const two: @Vector(chars_per_full_line, u3) = @splat(2); const four: @Vector(chars_per_full_line, u3) = @splat(4); const six: @Vector(chars_per_full_line, u3) = @splat(6);
// Every lane computes all four possible characters; the blends below keep // the one that belongs to it. Branchless, and the masks are comptime. const first = a >> two; const second = ((a & @as(Chars, @splat(0x03))) << four) | (b >> four); const third = ((b & @as(Chars, @splat(0x0f))) << two) | (c >> six); const fourth = c & @as(Chars, @splat(0x3f));
const sextets = @select( u8, comptime encodeLanes(0), first, @select( u8, comptime encodeLanes(1), second, @select(u8, comptime encodeLanes(2), third, fourth), ), );
const shifted = sextets + @as(Chars, @splat(' ')); const chars = switch (charset) { .space => shifted, .backtick => @select( u8, sextets == @as(Chars, @splat(0)), @as(Chars, @splat('`')), shifted, ), };
dest.* = chars;}
/// For output byte `q`, the sextet contributing its high bits, and then the/// one contributing its low bits.fn decodeGather(comptime k: i32) @Vector(bytes_per_line, i32) { var mask: [bytes_per_line]i32 = undefined; for (&mask, 0..) |*entry, q| { const group: i32 = @intCast(q / 3); const within: i32 = @intCast(q % 3); entry.* = 4 * group + within + k; } return mask;}
/// How far the high and low contributors are shifted for each output byte.fn decodeShifts(comptime which: enum { high, low }) @Vector(bytes_per_line, u3) { var mask: [bytes_per_line]u3 = undefined; for (&mask, 0..) |*entry, q| { const within = q % 3; entry.* = switch (which) { .high => @intCast(2 * (within + 1)), .low => @intCast(4 - 2 * within), }; } return mask;}
/// Decodes exactly one full line, returning false without touching `dest` if/// any character is outside the uuencode alphabet. A false return means the/// caller should rerun the line on the scalar path, which reports which/// character was wrong.pub fn decodeFullLine( dest: *[bytes_per_line]u8, source: *const [chars_per_full_line]u8,) bool { const in: Chars = source.*;
// One wrapping subtract turns the range check into a single unsigned // comparison: characters below a space wrap up past 0xc0, and anything // above a backtick lands above 0x40, so both fail the same test. const offset = in -% @as(Chars, @splat(' ')); if (@reduce(.Or, offset > @as(Chars, @splat(0x40)))) return false;
// Masking to six bits is what makes the backtick and the space both mean // zero: 0x60 - 0x20 is 64, which is 0 once the seventh bit is dropped. const sextets = offset & @as(Chars, @splat(0x3f));
const high = @shuffle(u8, sextets, undefined, decodeGather(0)); const low = @shuffle(u8, sextets, undefined, decodeGather(1));
dest.* = (high << comptime decodeShifts(.high)) | (low >> comptime decodeShifts(.low)); return true;}
const testing = std.testing;
test "encodeFullLine matches the scalar encoder" { var prng: std.Random.DefaultPrng = .init(0x5114_D000); const random = prng.random();
var source: [bytes_per_line]u8 = undefined; var wide: [chars_per_full_line]u8 = undefined; var scalar: [uuencode.chars_per_line]u8 = undefined;
for (0..2000) |i| { switch (i % 4) { 0 => random.bytes(&source), 1 => @memset(&source, 0), 2 => @memset(&source, 0xff), else => for (&source) |*byte| { byte.* = if (random.uintLessThan(u8, 4) == 0) random.int(u8) else 0; }, }
inline for (.{ .backtick, .space }) |charset| { encodeFullLine(&wide, &source, charset); const expected = uuencode.encodeLineScalar(&scalar, &source, .{ .charset = charset }); // The scalar encoder also writes the length character first. try testing.expectEqualSlices(u8, expected[1..], &wide); } }}
test "decodeFullLine matches the scalar decoder" { var prng: std.Random.DefaultPrng = .init(0xD3C0_DE00); const random = prng.random();
var source: [bytes_per_line]u8 = undefined; var encoded: [uuencode.chars_per_line]u8 = undefined; var wide: [bytes_per_line]u8 = undefined; var scalar: [bytes_per_line]u8 = undefined;
for (0..2000) |_| { random.bytes(&source); const line = uuencode.encodeLineScalar(&encoded, &source, .{ .charset = if (random.boolean()) .backtick else .space, });
try testing.expect(decodeFullLine(&wide, line[1..][0..chars_per_full_line])); try testing.expectEqualSlices(u8, &source, &wide);
const written = try uudecode.decodeLineScalar(&scalar, line, .{}); try testing.expectEqual(bytes_per_line, written); try testing.expectEqualSlices(u8, &scalar, &wide); }}
test "decodeFullLine rejects characters outside the alphabet" { var source: [bytes_per_line]u8 = undefined; @memset(&source, 'x'); var encoded: [uuencode.chars_per_line]u8 = undefined; const line = uuencode.encodeLineScalar(&encoded, &source, .{});
var dest: [bytes_per_line]u8 = undefined; try testing.expect(decodeFullLine(&dest, line[1..][0..chars_per_full_line]));
for ([_]u8{ 0, '\n', 0x1f, 'a', 0x7f, 0xff }) |bad| { var broken = line[1..][0..chars_per_full_line].*; broken[7] = bad; try testing.expect(!decodeFullLine(&dest, &broken)); }}