From 38bfde3ad5c0ab6f560d4289fa305b3a990f6e6c Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Thu, 21 Aug 2025 13:14:29 +0200 Subject: [PATCH 01/26] Allocate blocklist ArrayList in one go --- src/root.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/root.zig b/src/root.zig index 5562e01..14a4f2e 100644 --- a/src/root.zig +++ b/src/root.zig @@ -104,7 +104,7 @@ fn blocklist_from_words( const lowercase_alphabet = try std.ascii.allocLowerString(allocator, alphabet); defer allocator.free(lowercase_alphabet); - var filtered_blocklist = ArrayList([]const u8).init(allocator); + var filtered_blocklist = try ArrayList([]const u8).initCapacity(allocator, words.len); for (words) |word| { if (word.len < 3) { @@ -115,7 +115,7 @@ fn blocklist_from_words( allocator.free(lowercased_word); continue; } - try filtered_blocklist.append(lowercased_word); + filtered_blocklist.appendAssumeCapacity(lowercased_word); } return try filtered_blocklist.toOwnedSlice(); From 1b365132105d5674196acbaed4baaf3299377388 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Thu, 21 Aug 2025 18:17:32 +0200 Subject: [PATCH 02/26] Keep stack allocated the working alphabet --- src/root.zig | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/root.zig b/src/root.zig index 14a4f2e..54ec2e9 100644 --- a/src/root.zig +++ b/src/root.zig @@ -139,13 +139,15 @@ fn encodeNumbers( min_length: u64, blocklist: []const []const u8, ) ![]u8 { - var alphabet = try allocator.dupe(u8, original_alphabet); - defer allocator.free(alphabet); - - if (increment > alphabet.len) { + if (increment > original_alphabet.len) { return Error.ReachedMaxAttempts; } + // Everything is ASCII, so the alphabet is 256 character at max. + var buffer: [256]u8 = undefined; + @memcpy(buffer[0..original_alphabet.len], original_alphabet); + var alphabet = buffer[0..original_alphabet.len]; + // Get semi-random offset. var offset: u64 = numbers.len; for (numbers, 0..) |n, i| { @@ -254,8 +256,11 @@ fn decodeID( return &.{}; } - const alphabet = try allocator.dupe(u8, decoding_alphabet); - defer allocator.free(alphabet); + // Everything is ASCII, so the alphabet is 256 character at max. + var buffer: [256]u8 = undefined; + @memcpy(buffer[0..decoding_alphabet.len], decoding_alphabet); + var alphabet = buffer[0..decoding_alphabet.len]; + shuffle(alphabet); // If a character is not in the alphabet, return an empty array. From e8104ed3faa4a1e430c43c28ac8a775d11ea1d90 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 22 Aug 2025 08:54:55 +0200 Subject: [PATCH 03/26] Remove toID and inline the ID generation This saves allocation of a new buffer just for the current number encoding. --- src/root.zig | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/root.zig b/src/root.zig index 54ec2e9..d72e868 100644 --- a/src/root.zig +++ b/src/root.zig @@ -169,9 +169,18 @@ fn encodeNumbers( try ret.append(prefix); for (numbers, 0..) |n, i| { - const x = try toID(allocator, n, alphabet[1..]); - defer allocator.free(x); - try ret.appendSlice(x); + // NOTE(lvignoli): In the reference implementation, the ID letters are inserted + // at index 0 in a helper buffer, which then extend the main squid ID buffer. + // Here, we append them to the squid ID buffer for efficiency, so we reverse the + // slice corresponding to the current number at the end. + const start = ret.items.len; + var result = n; + while (true) { + try ret.append(alphabet[1 + result % (alphabet.len - 1)]); + result = result / (alphabet.len - 1); + if (result == 0) break; + } + mem.reverse(u8, ret.items[start..]); if (i < numbers.len - 1) { try ret.append(alphabet[0]); @@ -309,29 +318,6 @@ fn decodeID( return try ret.toOwnedSlice(); } -/// toID generates a new ID string for number using alphabet. -fn toID( - allocator: mem.Allocator, - number: u64, - alphabet: []const u8, -) ![]const u8 { - // NOTE(lvignoli): In the reference implementation, the letters are inserted at index 0. - // Here we append them for efficiency, so we reverse the ID at the end. - var result: u64 = number; - var id = std.ArrayList(u8).init(allocator); - - while (true) { - try id.append(alphabet[result % alphabet.len]); - result = result / alphabet.len; - if (result == 0) break; - } - - const value: []u8 = try id.toOwnedSlice(); - mem.reverse(u8, value); - - return value; -} - /// toNumber converts a string to an integer using the given alphabet. fn toNumber(s: []const u8, alphabet: []const u8) u64 { var num: u64 = 0; From cdee75d0d13f0f6d41c2221fd159a2acfbfdb66c Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 22 Aug 2025 08:54:55 +0200 Subject: [PATCH 04/26] Add buffer size estimation --- src/root.zig | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/root.zig b/src/root.zig index d72e868..3950ef4 100644 --- a/src/root.zig +++ b/src/root.zig @@ -163,10 +163,11 @@ fn encodeNumbers( mem.reverse(u8, alphabet); // Build the ID. - var ret = ArrayList(u8).init(allocator); - defer ret.deinit(); + const estimated_buffer_size = estimateEncodingBufferSize(alphabet, numbers, min_length); + var ret = try ArrayList(u8).initCapacity(allocator, estimated_buffer_size); + errdefer ret.deinit(); - try ret.append(prefix); + ret.appendAssumeCapacity(prefix); for (numbers, 0..) |n, i| { // NOTE(lvignoli): In the reference implementation, the ID letters are inserted @@ -176,25 +177,25 @@ fn encodeNumbers( const start = ret.items.len; var result = n; while (true) { - try ret.append(alphabet[1 + result % (alphabet.len - 1)]); + ret.appendAssumeCapacity(alphabet[1 + result % (alphabet.len - 1)]); result = result / (alphabet.len - 1); if (result == 0) break; } mem.reverse(u8, ret.items[start..]); if (i < numbers.len - 1) { - try ret.append(alphabet[0]); + ret.appendAssumeCapacity(alphabet[0]); shuffle(alphabet); } } // Handle min_length requirements. if (min_length > ret.items.len) { - try ret.append(alphabet[0]); + ret.appendAssumeCapacity(alphabet[0]); while (min_length > ret.items.len) { shuffle(alphabet); const n = @min(min_length - ret.items.len, alphabet.len); - try ret.appendSlice(alphabet[0..n]); + ret.appendSliceAssumeCapacity(alphabet[0..n]); } } @@ -216,6 +217,35 @@ fn encodeNumbers( return ID; } +/// Estimate the size of the buffer necessary for encoding. +/// It is a an overestimation, so it is safe to assume capacity when constructing +/// the ID. +/// +/// Ported from github.com/sqids/sqids-c, by Latchezar Tzvetkoff. +fn estimateEncodingBufferSize( + alphabet: []const u8, + numbers: []const u64, + min_length: u64, +) usize { + var r: f64 = 0; // f64 as working type up to final usize cast + + const log2len = @log2(@as(f64, @floatFromInt(alphabet.len)) - 1); + + for (numbers) |n| { + const x = @as(f64, @floatFromInt(n)); + switch (n) { + 0 => r += 2, + std.math.maxInt(u64) => r += @ceil(@log2(x) / log2len) + 1, + else => r += @ceil(@log2(x + 1) / log2len) + 1, + } + } + + var res = @as(usize, @intFromFloat(r)); + res = @max(res, min_length) + 1; + + return res; +} + /// isBlockedID returns true if id collides with the blocklist. fn isBlockedID( allocator: mem.Allocator, From f07c23809866c817a286a690418c2b6c38c23ee8 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 22 Aug 2025 08:54:55 +0200 Subject: [PATCH 05/26] Fix array list not deallocated after error --- src/root.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/root.zig b/src/root.zig index 3950ef4..7e8ad00 100644 --- a/src/root.zig +++ b/src/root.zig @@ -105,6 +105,7 @@ fn blocklist_from_words( defer allocator.free(lowercase_alphabet); var filtered_blocklist = try ArrayList([]const u8).initCapacity(allocator, words.len); + errdefer filtered_blocklist.deinit(); for (words) |word| { if (word.len < 3) { From 2709fc5b41843b4e79b850015eab10c7d3978448 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Fri, 22 Aug 2025 11:38:38 +0200 Subject: [PATCH 06/26] Refactor internal encoding to reuse the same memory --- src/root.zig | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/root.zig b/src/root.zig index 7e8ad00..36469be 100644 --- a/src/root.zig +++ b/src/root.zig @@ -3,6 +3,7 @@ const std = @import("std"); const mem = std.mem; const testing = std.testing; const ArrayList = std.ArrayList; +const ArrayListUnmanaged = std.ArrayListUnmanaged; pub const Error = error{ TooShortAlphabet, @@ -70,18 +71,35 @@ pub const Sqids = struct { if (numbers.len == 0) { return ""; } + // Allocate ID buffer and working alphabet. + const estimated_buffer_size = estimateEncodingBufferSize(self.alphabet, numbers, self.min_length); + const buf = try self.allocator.alloc(u8, estimated_buffer_size); + errdefer self.allocator.free(buf); + const alphabet = try self.allocator.dupe(u8, self.alphabet); defer self.allocator.free(alphabet); shuffle(alphabet); + const increment = 0; - return try encodeNumbers( + + // We ignore the returned value, as we know we have allocated the correct length. + const n = try encodeNumbers( self.allocator, + buf, numbers, alphabet, increment, self.min_length, self.blocklist, ); + if (n != estimated_buffer_size) { + @branchHint(.cold); + @panic("This should not happenned"); + // I am not quite sure it is unreachable, Latchezar labelled his function an + // estimation..., so we panic here for now, but we should do better. + } + + return buf; } /// Decodes an ID into numbers using alphabet. Caller owns the memory. @@ -134,20 +152,21 @@ fn validInAlphabet(word: []const u8, alphabet: []const u8) bool { /// encodeNumbers performs the actual encoding processing. fn encodeNumbers( allocator: mem.Allocator, + buf: []u8, numbers: []const u64, original_alphabet: []const u8, increment: u64, min_length: u64, blocklist: []const []const u8, -) ![]u8 { +) !usize { if (increment > original_alphabet.len) { return Error.ReachedMaxAttempts; } - // Everything is ASCII, so the alphabet is 256 character at max. - var buffer: [256]u8 = undefined; - @memcpy(buffer[0..original_alphabet.len], original_alphabet); - var alphabet = buffer[0..original_alphabet.len]; + // Everything is ASCII, so the alphabet is 256 characters at max. + var alphabet_buffer: [256]u8 = undefined; + @memcpy(alphabet_buffer[0..original_alphabet.len], original_alphabet); + var alphabet = alphabet_buffer[0..original_alphabet.len]; // Get semi-random offset. var offset: u64 = numbers.len; @@ -164,9 +183,7 @@ fn encodeNumbers( mem.reverse(u8, alphabet); // Build the ID. - const estimated_buffer_size = estimateEncodingBufferSize(alphabet, numbers, min_length); - var ret = try ArrayList(u8).initCapacity(allocator, estimated_buffer_size); - errdefer ret.deinit(); + var ret = ArrayListUnmanaged(u8).initBuffer(buf); ret.appendAssumeCapacity(prefix); @@ -200,14 +217,16 @@ fn encodeNumbers( } } - var ID = try ret.toOwnedSlice(); + const ID = ret.items; + var len = ID.len; // Handle blocklist. const blocked = try isBlockedID(allocator, blocklist, ID); if (blocked) { - allocator.free(ID); // Freeing the old ID string. - ID = try encodeNumbers( + @memset(buf, undefined); + len = try encodeNumbers( allocator, + buf, numbers, original_alphabet, increment + 1, @@ -215,7 +234,7 @@ fn encodeNumbers( blocklist, ); } - return ID; + return len; } /// Estimate the size of the buffer necessary for encoding. @@ -242,7 +261,7 @@ fn estimateEncodingBufferSize( } var res = @as(usize, @intFromFloat(r)); - res = @max(res, min_length) + 1; + res = @max(res, min_length); return res; } From 87f439276ccbf94a35bc1f15a28e66ffd33a0f47 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Sun, 24 Aug 2025 12:07:01 +0200 Subject: [PATCH 07/26] Remove allocation when checking for blocked ID --- src/root.zig | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/root.zig b/src/root.zig index 36469be..c77b931 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,6 +1,7 @@ //! Module sqids-zig implements encoding and decoding of sqids identifiers. See sqids.org. const std = @import("std"); const mem = std.mem; +const ascii = std.ascii; const testing = std.testing; const ArrayList = std.ArrayList; const ArrayListUnmanaged = std.ArrayListUnmanaged; @@ -221,7 +222,7 @@ fn encodeNumbers( var len = ID.len; // Handle blocklist. - const blocked = try isBlockedID(allocator, blocklist, ID); + const blocked = try isBlockedID(blocklist, ID); if (blocked) { @memset(buf, undefined); len = try encodeNumbers( @@ -267,31 +268,23 @@ fn estimateEncodingBufferSize( } /// isBlockedID returns true if id collides with the blocklist. -fn isBlockedID( - allocator: mem.Allocator, - blocklist: []const []const u8, - id: []const u8, -) !bool { - const lower_id = try std.ascii.allocLowerString(allocator, id); - defer allocator.free(lower_id); - +fn isBlockedID(blocklist: []const []const u8, id: []const u8) !bool { for (blocklist) |word| { - if (word.len > lower_id.len) { + if (word.len > id.len) { continue; } - if (lower_id.len <= 3 or word.len <= 3) { + if (id.len <= 3 or word.len <= 3) { if (mem.eql(u8, id, word)) { return true; } } else if (containsNumber(word)) { - if (mem.startsWith(u8, lower_id, word) or mem.endsWith(u8, lower_id, word)) { + if (ascii.startsWithIgnoreCase(id, word) or ascii.endsWithIgnoreCase(id, word)) { return true; } - } else if (mem.indexOf(u8, lower_id, word)) |_| { + } else if (ascii.indexOfIgnoreCase(id, word)) |_| { return true; } } - return false; } From ea9ab64d7eab997d64206f87f2c0f6aaea594102 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Sun, 24 Aug 2025 12:18:36 +0200 Subject: [PATCH 08/26] Allocate working alphabet on the stack when encoding We know we'll have less than 128 characters in the alphabet. It is an overestimation, as some ASCII characters are not printable and will probably never been used in an alphabet. Also fixes the previous wrong 256 value here and there. --- benchmark/gen_bench_encoding_exes.py | 2 +- src/root.zig | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/benchmark/gen_bench_encoding_exes.py b/benchmark/gen_bench_encoding_exes.py index 0ebe562..8c0d5b0 100644 --- a/benchmark/gen_bench_encoding_exes.py +++ b/benchmark/gen_bench_encoding_exes.py @@ -47,6 +47,6 @@ def run(cmd): msg += "sudo poop \\\n" for cid in commit_ids: - msg += f"\tzig-out/bin/benchmark_random_uuids-{cid} \\\n" + msg += f"\tzig-out/bin/bench-encode-random-uuids-{cid} \\\n" print(msg) diff --git a/src/root.zig b/src/root.zig index c77b931..bb519ea 100644 --- a/src/root.zig +++ b/src/root.zig @@ -54,6 +54,7 @@ pub const Sqids = struct { // We use an arena to manage the memory of the blocklist var arena = std.heap.ArenaAllocator.init(allocator); const b = try blocklist_from_words(arena.allocator(), opts.alphabet, opts.blocklist); + return Sqids{ .allocator = allocator, .alphabet = opts.alphabet, @@ -77,15 +78,15 @@ pub const Sqids = struct { const buf = try self.allocator.alloc(u8, estimated_buffer_size); errdefer self.allocator.free(buf); - const alphabet = try self.allocator.dupe(u8, self.alphabet); - defer self.allocator.free(alphabet); + var alphabet_buffer: [128]u8 = undefined; + @memcpy(alphabet_buffer[0..self.alphabet.len], self.alphabet); + const alphabet = alphabet_buffer[0..self.alphabet.len]; shuffle(alphabet); const increment = 0; // We ignore the returned value, as we know we have allocated the correct length. const n = try encodeNumbers( - self.allocator, buf, numbers, alphabet, @@ -152,7 +153,6 @@ fn validInAlphabet(word: []const u8, alphabet: []const u8) bool { /// encodeNumbers performs the actual encoding processing. fn encodeNumbers( - allocator: mem.Allocator, buf: []u8, numbers: []const u64, original_alphabet: []const u8, @@ -164,8 +164,7 @@ fn encodeNumbers( return Error.ReachedMaxAttempts; } - // Everything is ASCII, so the alphabet is 256 characters at max. - var alphabet_buffer: [256]u8 = undefined; + var alphabet_buffer: [128]u8 = undefined; @memcpy(alphabet_buffer[0..original_alphabet.len], original_alphabet); var alphabet = alphabet_buffer[0..original_alphabet.len]; @@ -226,7 +225,6 @@ fn encodeNumbers( if (blocked) { @memset(buf, undefined); len = try encodeNumbers( - allocator, buf, numbers, original_alphabet, @@ -308,8 +306,8 @@ fn decodeID( return &.{}; } - // Everything is ASCII, so the alphabet is 256 character at max. - var buffer: [256]u8 = undefined; + // Everything is ASCII, so the alphabet is 128 character at max. + var buffer: [128]u8 = undefined; @memcpy(buffer[0..decoding_alphabet.len], decoding_alphabet); var alphabet = buffer[0..decoding_alphabet.len]; From 02c1405c9b4bac8d776b235d813f576837a4c4fb Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Wed, 20 Aug 2025 15:00:31 +0200 Subject: [PATCH 09/26] Draft blocklist object --- src/blocklist.zig | 1175 +++++++++++++++++++++++---------------------- src/main.zig | 27 ++ 2 files changed, 641 insertions(+), 561 deletions(-) create mode 100644 src/main.zig diff --git a/src/blocklist.zig b/src/blocklist.zig index 557762a..6a798b1 100644 --- a/src/blocklist.zig +++ b/src/blocklist.zig @@ -1,562 +1,615 @@ -pub const default_blocklist = .{ - "0rgasm", - "1d10t", - "1d1ot", - "1di0t", - "1diot", - "1eccacu10", - "1eccacu1o", - "1eccacul0", - "1eccaculo", - "1mbec11e", - "1mbec1le", - "1mbeci1e", - "1mbecile", - "a11upat0", - "a11upato", - "a1lupat0", - "a1lupato", - "aand", - "ah01e", - "ah0le", - "aho1e", - "ahole", - "al1upat0", - "al1upato", - "allupat0", - "allupato", - "ana1", - "ana1e", - "anal", - "anale", - "anus", - "arrapat0", - "arrapato", - "arsch", - "arse", - "ass", - "b00b", - "b00be", - "b01ata", - "b0ceta", - "b0iata", - "b0ob", - "b0obe", - "b0sta", - "b1tch", - "b1te", - "b1tte", - "ba1atkar", - "balatkar", - "bastard0", - "bastardo", - "batt0na", - "battona", - "bitch", - "bite", - "bitte", - "bo0b", - "bo0be", - "bo1ata", - "boceta", - "boiata", - "boob", - "boobe", - "bosta", - "bran1age", - "bran1er", - "bran1ette", - "bran1eur", - "bran1euse", - "branlage", - "branler", - "branlette", - "branleur", - "branleuse", - "c0ck", - "c0g110ne", - "c0g11one", - "c0g1i0ne", - "c0g1ione", - "c0gl10ne", - "c0gl1one", - "c0gli0ne", - "c0glione", - "c0na", - "c0nnard", - "c0nnasse", - "c0nne", - "c0u111es", - "c0u11les", - "c0u1l1es", - "c0u1lles", - "c0ui11es", - "c0ui1les", - "c0uil1es", - "c0uilles", - "c11t", - "c11t0", - "c11to", - "c1it", - "c1it0", - "c1ito", - "cabr0n", - "cabra0", - "cabrao", - "cabron", - "caca", - "cacca", - "cacete", - "cagante", - "cagar", - "cagare", - "cagna", - "cara1h0", - "cara1ho", - "caracu10", - "caracu1o", - "caracul0", - "caraculo", - "caralh0", - "caralho", - "cazz0", - "cazz1mma", - "cazzata", - "cazzimma", - "cazzo", - "ch00t1a", - "ch00t1ya", - "ch00tia", - "ch00tiya", - "ch0d", - "ch0ot1a", - "ch0ot1ya", - "ch0otia", - "ch0otiya", - "ch1asse", - "ch1avata", - "ch1er", - "ch1ng0", - "ch1ngadaz0s", - "ch1ngadazos", - "ch1ngader1ta", - "ch1ngaderita", - "ch1ngar", - "ch1ngo", - "ch1ngues", - "ch1nk", - "chatte", - "chiasse", - "chiavata", - "chier", - "ching0", - "chingadaz0s", - "chingadazos", - "chingader1ta", - "chingaderita", - "chingar", - "chingo", - "chingues", - "chink", - "cho0t1a", - "cho0t1ya", - "cho0tia", - "cho0tiya", - "chod", - "choot1a", - "choot1ya", - "chootia", - "chootiya", - "cl1t", - "cl1t0", - "cl1to", - "clit", - "clit0", - "clito", - "cock", - "cog110ne", - "cog11one", - "cog1i0ne", - "cog1ione", - "cogl10ne", - "cogl1one", - "cogli0ne", - "coglione", - "cona", - "connard", - "connasse", - "conne", - "cou111es", - "cou11les", - "cou1l1es", - "cou1lles", - "coui11es", - "coui1les", - "couil1es", - "couilles", - "cracker", - "crap", - "cu10", - "cu1att0ne", - "cu1attone", - "cu1er0", - "cu1ero", - "cu1o", - "cul0", - "culatt0ne", - "culattone", - "culer0", - "culero", - "culo", - "cum", - "cunt", - "d11d0", - "d11do", - "d1ck", - "d1ld0", - "d1ldo", - "damn", - "de1ch", - "deich", - "depp", - "di1d0", - "di1do", - "dick", - "dild0", - "dildo", - "dyke", - "encu1e", - "encule", - "enema", - "enf01re", - "enf0ire", - "enfo1re", - "enfoire", - "estup1d0", - "estup1do", - "estupid0", - "estupido", - "etr0n", - "etron", - "f0da", - "f0der", - "f0ttere", - "f0tters1", - "f0ttersi", - "f0tze", - "f0utre", - "f1ca", - "f1cker", - "f1ga", - "fag", - "fica", - "ficker", - "figa", - "foda", - "foder", - "fottere", - "fotters1", - "fottersi", - "fotze", - "foutre", - "fr0c10", - "fr0c1o", - "fr0ci0", - "fr0cio", - "fr0sc10", - "fr0sc1o", - "fr0sci0", - "fr0scio", - "froc10", - "froc1o", - "froci0", - "frocio", - "frosc10", - "frosc1o", - "frosci0", - "froscio", - "fuck", - "g00", - "g0o", - "g0u1ne", - "g0uine", - "gandu", - "go0", - "goo", - "gou1ne", - "gouine", - "gr0gnasse", - "grognasse", - "haram1", - "harami", - "haramzade", - "hund1n", - "hundin", - "id10t", - "id1ot", - "idi0t", - "idiot", - "imbec11e", - "imbec1le", - "imbeci1e", - "imbecile", - "j1zz", - "jerk", - "jizz", - "k1ke", - "kam1ne", - "kamine", - "kike", - "leccacu10", - "leccacu1o", - "leccacul0", - "leccaculo", - "m1erda", - "m1gn0tta", - "m1gnotta", - "m1nch1a", - "m1nchia", - "m1st", - "mam0n", - "mamahuev0", - "mamahuevo", - "mamon", - "masturbat10n", - "masturbat1on", - "masturbate", - "masturbati0n", - "masturbation", - "merd0s0", - "merd0so", - "merda", - "merde", - "merdos0", - "merdoso", - "mierda", - "mign0tta", - "mignotta", - "minch1a", - "minchia", - "mist", - "musch1", - "muschi", - "n1gger", - "neger", - "negr0", - "negre", - "negro", - "nerch1a", - "nerchia", - "nigger", - "orgasm", - "p00p", - "p011a", - "p01la", - "p0l1a", - "p0lla", - "p0mp1n0", - "p0mp1no", - "p0mpin0", - "p0mpino", - "p0op", - "p0rca", - "p0rn", - "p0rra", - "p0uff1asse", - "p0uffiasse", - "p1p1", - "p1pi", - "p1r1a", - "p1rla", - "p1sc10", - "p1sc1o", - "p1sci0", - "p1scio", - "p1sser", - "pa11e", - "pa1le", - "pal1e", - "palle", - "pane1e1r0", - "pane1e1ro", - "pane1eir0", - "pane1eiro", - "panele1r0", - "panele1ro", - "paneleir0", - "paneleiro", - "patakha", - "pec0r1na", - "pec0rina", - "pecor1na", - "pecorina", - "pen1s", - "pendej0", - "pendejo", - "penis", - "pip1", - "pipi", - "pir1a", - "pirla", - "pisc10", - "pisc1o", - "pisci0", - "piscio", - "pisser", - "po0p", - "po11a", - "po1la", - "pol1a", - "polla", - "pomp1n0", - "pomp1no", - "pompin0", - "pompino", - "poop", - "porca", - "porn", - "porra", - "pouff1asse", - "pouffiasse", - "pr1ck", - "prick", - "pussy", - "put1za", - "puta", - "puta1n", - "putain", - "pute", - "putiza", - "puttana", - "queca", - "r0mp1ba11e", - "r0mp1ba1le", - "r0mp1bal1e", - "r0mp1balle", - "r0mpiba11e", - "r0mpiba1le", - "r0mpibal1e", - "r0mpiballe", - "rand1", - "randi", - "rape", - "recch10ne", - "recch1one", - "recchi0ne", - "recchione", - "retard", - "romp1ba11e", - "romp1ba1le", - "romp1bal1e", - "romp1balle", - "rompiba11e", - "rompiba1le", - "rompibal1e", - "rompiballe", - "ruff1an0", - "ruff1ano", - "ruffian0", - "ruffiano", - "s1ut", - "sa10pe", - "sa1aud", - "sa1ope", - "sacanagem", - "sal0pe", - "salaud", - "salope", - "saugnapf", - "sb0rr0ne", - "sb0rra", - "sb0rrone", - "sbattere", - "sbatters1", - "sbattersi", - "sborr0ne", - "sborra", - "sborrone", - "sc0pare", - "sc0pata", - "sch1ampe", - "sche1se", - "sche1sse", - "scheise", - "scheisse", - "schlampe", - "schwachs1nn1g", - "schwachs1nnig", - "schwachsinn1g", - "schwachsinnig", - "schwanz", - "scopare", - "scopata", - "sexy", - "sh1t", - "shit", - "slut", - "sp0mp1nare", - "sp0mpinare", - "spomp1nare", - "spompinare", - "str0nz0", - "str0nza", - "str0nzo", - "stronz0", - "stronza", - "stronzo", - "stup1d", - "stupid", - "succh1am1", - "succh1ami", - "succhiam1", - "succhiami", - "sucker", - "t0pa", - "tapette", - "test1c1e", - "test1cle", - "testic1e", - "testicle", - "tette", - "topa", - "tr01a", - "tr0ia", - "tr0mbare", - "tr1ng1er", - "tr1ngler", - "tring1er", - "tringler", - "tro1a", - "troia", - "trombare", - "turd", - "twat", - "vaffancu10", - "vaffancu1o", - "vaffancul0", - "vaffanculo", - "vag1na", - "vagina", - "verdammt", - "verga", - "w1chsen", - "wank", - "wichsen", - "x0ch0ta", - "x0chota", - "xana", - "xoch0ta", - "xochota", - "z0cc01a", - "z0cc0la", - "z0cco1a", - "z0ccola", - "z1z1", - "z1zi", - "ziz1", - "zizi", - "zocc01a", - "zocc0la", - "zocco1a", - "zoccola", +const std = @import("std"); +const mem = std.mem; + +pub const Blocklist = struct { + alphabet: []const u8, + words: []const []const u8, + + fn new( + allocator: mem.Allocator, + alphabet: []const u8, + words: []const []const u8, + ) !Blocklist { + // Clean up blocklist: + // 1. all blocklist words should be lowercase, + // 2. no words less than 3 chars, + // 3. if some words contain chars that are not in the alphabet, remove those. + + const lowercase_alphabet = try std.ascii.allocLowerString(allocator, alphabet); + defer allocator.free(lowercase_alphabet); + + var filtered_blocklist = try std.ArrayList([]const u8).initCapacity(allocator, words.len); + + for (words) |word| { + if (word.len < 3) { + continue; + } + const lowercased_word = try std.ascii.allocLowerString(allocator, word); + if (!validInAlphabet(lowercased_word, lowercase_alphabet)) { + allocator.free(lowercased_word); + continue; + } + try filtered_blocklist.appendAssumeCapacity(lowercased_word); + } + + return try filtered_blocklist.toOwnedSlice(); + } + + fn deinit(s: *Blocklist) void { + s.allocator.free(s.words); + } +}; + +fn validInAlphabet(word: []const u8, alphabet: []const u8) bool { + for (word) |c| { + if (mem.indexOf(u8, alphabet, &.{c}) == null) { + return false; + } + } + return true; +} + +pub const default_blocklist = Blocklist{ + .words = .{ + "0rgasm", + "1d10t", + "1d1ot", + "1di0t", + "1diot", + "1eccacu10", + "1eccacu1o", + "1eccacul0", + "1eccaculo", + "1mbec11e", + "1mbec1le", + "1mbeci1e", + "1mbecile", + "a11upat0", + "a11upato", + "a1lupat0", + "a1lupato", + "aand", + "ah01e", + "ah0le", + "aho1e", + "ahole", + "al1upat0", + "al1upato", + "allupat0", + "allupato", + "ana1", + "ana1e", + "anal", + "anale", + "anus", + "arrapat0", + "arrapato", + "arsch", + "arse", + "ass", + "b00b", + "b00be", + "b01ata", + "b0ceta", + "b0iata", + "b0ob", + "b0obe", + "b0sta", + "b1tch", + "b1te", + "b1tte", + "ba1atkar", + "balatkar", + "bastard0", + "bastardo", + "batt0na", + "battona", + "bitch", + "bite", + "bitte", + "bo0b", + "bo0be", + "bo1ata", + "boceta", + "boiata", + "boob", + "boobe", + "bosta", + "bran1age", + "bran1er", + "bran1ette", + "bran1eur", + "bran1euse", + "branlage", + "branler", + "branlette", + "branleur", + "branleuse", + "c0ck", + "c0g110ne", + "c0g11one", + "c0g1i0ne", + "c0g1ione", + "c0gl10ne", + "c0gl1one", + "c0gli0ne", + "c0glione", + "c0na", + "c0nnard", + "c0nnasse", + "c0nne", + "c0u111es", + "c0u11les", + "c0u1l1es", + "c0u1lles", + "c0ui11es", + "c0ui1les", + "c0uil1es", + "c0uilles", + "c11t", + "c11t0", + "c11to", + "c1it", + "c1it0", + "c1ito", + "cabr0n", + "cabra0", + "cabrao", + "cabron", + "caca", + "cacca", + "cacete", + "cagante", + "cagar", + "cagare", + "cagna", + "cara1h0", + "cara1ho", + "caracu10", + "caracu1o", + "caracul0", + "caraculo", + "caralh0", + "caralho", + "cazz0", + "cazz1mma", + "cazzata", + "cazzimma", + "cazzo", + "ch00t1a", + "ch00t1ya", + "ch00tia", + "ch00tiya", + "ch0d", + "ch0ot1a", + "ch0ot1ya", + "ch0otia", + "ch0otiya", + "ch1asse", + "ch1avata", + "ch1er", + "ch1ng0", + "ch1ngadaz0s", + "ch1ngadazos", + "ch1ngader1ta", + "ch1ngaderita", + "ch1ngar", + "ch1ngo", + "ch1ngues", + "ch1nk", + "chatte", + "chiasse", + "chiavata", + "chier", + "ching0", + "chingadaz0s", + "chingadazos", + "chingader1ta", + "chingaderita", + "chingar", + "chingo", + "chingues", + "chink", + "cho0t1a", + "cho0t1ya", + "cho0tia", + "cho0tiya", + "chod", + "choot1a", + "choot1ya", + "chootia", + "chootiya", + "cl1t", + "cl1t0", + "cl1to", + "clit", + "clit0", + "clito", + "cock", + "cog110ne", + "cog11one", + "cog1i0ne", + "cog1ione", + "cogl10ne", + "cogl1one", + "cogli0ne", + "coglione", + "cona", + "connard", + "connasse", + "conne", + "cou111es", + "cou11les", + "cou1l1es", + "cou1lles", + "coui11es", + "coui1les", + "couil1es", + "couilles", + "cracker", + "crap", + "cu10", + "cu1att0ne", + "cu1attone", + "cu1er0", + "cu1ero", + "cu1o", + "cul0", + "culatt0ne", + "culattone", + "culer0", + "culero", + "culo", + "cum", + "cunt", + "d11d0", + "d11do", + "d1ck", + "d1ld0", + "d1ldo", + "damn", + "de1ch", + "deich", + "depp", + "di1d0", + "di1do", + "dick", + "dild0", + "dildo", + "dyke", + "encu1e", + "encule", + "enema", + "enf01re", + "enf0ire", + "enfo1re", + "enfoire", + "estup1d0", + "estup1do", + "estupid0", + "estupido", + "etr0n", + "etron", + "f0da", + "f0der", + "f0ttere", + "f0tters1", + "f0ttersi", + "f0tze", + "f0utre", + "f1ca", + "f1cker", + "f1ga", + "fag", + "fica", + "ficker", + "figa", + "foda", + "foder", + "fottere", + "fotters1", + "fottersi", + "fotze", + "foutre", + "fr0c10", + "fr0c1o", + "fr0ci0", + "fr0cio", + "fr0sc10", + "fr0sc1o", + "fr0sci0", + "fr0scio", + "froc10", + "froc1o", + "froci0", + "frocio", + "frosc10", + "frosc1o", + "frosci0", + "froscio", + "fuck", + "g00", + "g0o", + "g0u1ne", + "g0uine", + "gandu", + "go0", + "goo", + "gou1ne", + "gouine", + "gr0gnasse", + "grognasse", + "haram1", + "harami", + "haramzade", + "hund1n", + "hundin", + "id10t", + "id1ot", + "idi0t", + "idiot", + "imbec11e", + "imbec1le", + "imbeci1e", + "imbecile", + "j1zz", + "jerk", + "jizz", + "k1ke", + "kam1ne", + "kamine", + "kike", + "leccacu10", + "leccacu1o", + "leccacul0", + "leccaculo", + "m1erda", + "m1gn0tta", + "m1gnotta", + "m1nch1a", + "m1nchia", + "m1st", + "mam0n", + "mamahuev0", + "mamahuevo", + "mamon", + "masturbat10n", + "masturbat1on", + "masturbate", + "masturbati0n", + "masturbation", + "merd0s0", + "merd0so", + "merda", + "merde", + "merdos0", + "merdoso", + "mierda", + "mign0tta", + "mignotta", + "minch1a", + "minchia", + "mist", + "musch1", + "muschi", + "n1gger", + "neger", + "negr0", + "negre", + "negro", + "nerch1a", + "nerchia", + "nigger", + "orgasm", + "p00p", + "p011a", + "p01la", + "p0l1a", + "p0lla", + "p0mp1n0", + "p0mp1no", + "p0mpin0", + "p0mpino", + "p0op", + "p0rca", + "p0rn", + "p0rra", + "p0uff1asse", + "p0uffiasse", + "p1p1", + "p1pi", + "p1r1a", + "p1rla", + "p1sc10", + "p1sc1o", + "p1sci0", + "p1scio", + "p1sser", + "pa11e", + "pa1le", + "pal1e", + "palle", + "pane1e1r0", + "pane1e1ro", + "pane1eir0", + "pane1eiro", + "panele1r0", + "panele1ro", + "paneleir0", + "paneleiro", + "patakha", + "pec0r1na", + "pec0rina", + "pecor1na", + "pecorina", + "pen1s", + "pendej0", + "pendejo", + "penis", + "pip1", + "pipi", + "pir1a", + "pirla", + "pisc10", + "pisc1o", + "pisci0", + "piscio", + "pisser", + "po0p", + "po11a", + "po1la", + "pol1a", + "polla", + "pomp1n0", + "pomp1no", + "pompin0", + "pompino", + "poop", + "porca", + "porn", + "porra", + "pouff1asse", + "pouffiasse", + "pr1ck", + "prick", + "pussy", + "put1za", + "puta", + "puta1n", + "putain", + "pute", + "putiza", + "puttana", + "queca", + "r0mp1ba11e", + "r0mp1ba1le", + "r0mp1bal1e", + "r0mp1balle", + "r0mpiba11e", + "r0mpiba1le", + "r0mpibal1e", + "r0mpiballe", + "rand1", + "randi", + "rape", + "recch10ne", + "recch1one", + "recchi0ne", + "recchione", + "retard", + "romp1ba11e", + "romp1ba1le", + "romp1bal1e", + "romp1balle", + "rompiba11e", + "rompiba1le", + "rompibal1e", + "rompiballe", + "ruff1an0", + "ruff1ano", + "ruffian0", + "ruffiano", + "s1ut", + "sa10pe", + "sa1aud", + "sa1ope", + "sacanagem", + "sal0pe", + "salaud", + "salope", + "saugnapf", + "sb0rr0ne", + "sb0rra", + "sb0rrone", + "sbattere", + "sbatters1", + "sbattersi", + "sborr0ne", + "sborra", + "sborrone", + "sc0pare", + "sc0pata", + "sch1ampe", + "sche1se", + "sche1sse", + "scheise", + "scheisse", + "schlampe", + "schwachs1nn1g", + "schwachs1nnig", + "schwachsinn1g", + "schwachsinnig", + "schwanz", + "scopare", + "scopata", + "sexy", + "sh1t", + "shit", + "slut", + "sp0mp1nare", + "sp0mpinare", + "spomp1nare", + "spompinare", + "str0nz0", + "str0nza", + "str0nzo", + "stronz0", + "stronza", + "stronzo", + "stup1d", + "stupid", + "succh1am1", + "succh1ami", + "succhiam1", + "succhiami", + "sucker", + "t0pa", + "tapette", + "test1c1e", + "test1cle", + "testic1e", + "testicle", + "tette", + "topa", + "tr01a", + "tr0ia", + "tr0mbare", + "tr1ng1er", + "tr1ngler", + "tring1er", + "tringler", + "tro1a", + "troia", + "trombare", + "turd", + "twat", + "vaffancu10", + "vaffancu1o", + "vaffancul0", + "vaffanculo", + "vag1na", + "vagina", + "verdammt", + "verga", + "w1chsen", + "wank", + "wichsen", + "x0ch0ta", + "x0chota", + "xana", + "xoch0ta", + "xochota", + "z0cc01a", + "z0cc0la", + "z0cco1a", + "z0ccola", + "z1z1", + "z1zi", + "ziz1", + "zizi", + "zocc01a", + "zocc0la", + "zocco1a", + "zoccola", + }, }; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..01a014a --- /dev/null +++ b/src/main.zig @@ -0,0 +1,27 @@ +const std = @import("std"); +const sqids = @import("sqids"); +const mem = std.mem; + +const Sqids = sqids.Sqids; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + const numbers = &.{ 1, 2, 3 }; + + const opts = sqids.Options{ + // .blocklist = new_blocklist(allocator, alphabet, blocked_words), + // .alphabet = alphabet, + }; + + // Using the default Sqids, encode the numbers to a Sqids ID. + const s = try Sqids.init(opts); + defer s.deinit(); + const id = try s.encode(numbers); + defer allocator.free(id); + + // Print to stdout. + const stdout = std.io.getStdOut().writer(); + try stdout.print("{s}\n", .{id}); +} From cee33308022ffc704bb69711b32678b7893ccd4c Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Thu, 4 Sep 2025 10:29:52 +0200 Subject: [PATCH 10/26] Better Blocklist --- src/blocklist.zig | 35 +++++++++++++++++++---------------- src/root.zig | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/blocklist.zig b/src/blocklist.zig index 6a798b1..95b283e 100644 --- a/src/blocklist.zig +++ b/src/blocklist.zig @@ -6,7 +6,6 @@ pub const Blocklist = struct { words: []const []const u8, fn new( - allocator: mem.Allocator, alphabet: []const u8, words: []const []const u8, ) !Blocklist { @@ -15,24 +14,27 @@ pub const Blocklist = struct { // 2. no words less than 3 chars, // 3. if some words contain chars that are not in the alphabet, remove those. - const lowercase_alphabet = try std.ascii.allocLowerString(allocator, alphabet); - defer allocator.free(lowercase_alphabet); - - var filtered_blocklist = try std.ArrayList([]const u8).initCapacity(allocator, words.len); - - for (words) |word| { - if (word.len < 3) { - continue; + for (alphabet) |c| { + if (!std.ascii.isAscii(c)) { + return error.NonASCIICharacter; + } + } + // Check that the words are valid in the provided alphabet, which is ASCII. + for (words) |w| { + if (w.len < 3) { + return error.WordTooShort; } - const lowercased_word = try std.ascii.allocLowerString(allocator, word); - if (!validInAlphabet(lowercased_word, lowercase_alphabet)) { - allocator.free(lowercased_word); - continue; + for (w) |c| { + if (!std.ascii.indexOfIgnoreCase(alphabet, c)) { + return error.InvalidInAlphabet; + } } - try filtered_blocklist.appendAssumeCapacity(lowercased_word); } - return try filtered_blocklist.toOwnedSlice(); + return Blocklist{ + .alphabet = alphabet, + .words = words, + }; } fn deinit(s: *Blocklist) void { @@ -50,7 +52,8 @@ fn validInAlphabet(word: []const u8, alphabet: []const u8) bool { } pub const default_blocklist = Blocklist{ - .words = .{ + .alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + .words = &.{ "0rgasm", "1d10t", "1d1ot", diff --git a/src/root.zig b/src/root.zig index bb519ea..c134247 100644 --- a/src/root.zig +++ b/src/root.zig @@ -22,7 +22,7 @@ pub const default_alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX /// Options controls the configuration of the sqid encoder. pub const Options = struct { alphabet: []const u8 = default_alphabet, - blocklist: []const []const u8 = &default_blocklist, + blocklist: []const []const u8 = default_blocklist.words, min_length: u8 = 0, }; From e8569ba7619a1ecaf1619357bb39372292c23da7 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Sat, 9 May 2026 08:41:16 +0200 Subject: [PATCH 11/26] Upgrade test runner to zig 0.16. Thanks a lot Karl Seguin. --- src/tests/test_runner.zig | 111 ++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/src/tests/test_runner.zig b/src/tests/test_runner.zig index 4e76de5..33062ed 100644 --- a/src/tests/test_runner.zig +++ b/src/tests/test_runner.zig @@ -1,6 +1,16 @@ -// This is for the development branch of Zig. -// See https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b for a version that works -// on Zig 0.14.1. +// This is for the Zig 0.16. + +// See https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b/eb15512d6ae49663fa9df6c7a9725b20dab43edd +// for a version that workson Zig 0.15.2. + +// See https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b/1f317ebc9cd09bc50fd5591d09c34255e15d1d85 +// for a version that workson Zig 0.14.1. + +// in your build.zig, you can specify a custom test runner: +// const tests = b.addTest(.{ +// .root_module = $MODULE_BEING_TESTED, +// .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple }, +// }); // in your build.zig, you can specify a custom test runner: // const tests = b.addTest(.{ @@ -9,6 +19,7 @@ // }); const std = @import("std"); +const Io = std.Io; const builtin = @import("builtin"); const Allocator = std.mem.Allocator; @@ -18,16 +29,23 @@ const BORDER = "=" ** 80; // use in custom panic handler var current_test: ?[]const u8 = null; -pub fn main() !void { +pub fn main(init: std.process.Init) !void { var mem: [8192]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&mem); const allocator = fba.allocator(); - const env = Env.init(allocator); - defer env.deinit(allocator); + const env = Env.init(init.environ_map); + + std.testing.io_instance = .init(init.gpa, .{ + .argv0 = .init(init.minimal.args), + .environ = init.minimal.environ, + }); + defer std.testing.io_instance.deinit(); - var slowest = SlowTracker.init(allocator, 5); + const io = std.testing.io; + + var slowest = SlowTracker.init(allocator, io, 5); defer slowest.deinit(); var pass: usize = 0; @@ -52,7 +70,7 @@ pub fn main() !void { } var status = Status.pass; - slowest.startTiming(); + slowest.startTiming(io); const is_unnamed_test = isUnnamed(t); if (env.filter) |f| { @@ -78,7 +96,7 @@ pub fn main() !void { const result = t.func(); current_test = null; - const ns_taken = slowest.endTiming(friendly_name); + const ns_taken = slowest.endTiming(io, friendly_name); if (std.testing.allocator_instance.deinit() == .leak) { leak += 1; @@ -97,7 +115,7 @@ pub fn main() !void { fail += 1; Printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER }); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpErrorReturnTrace(trace); } if (env.fail_first) { break; @@ -134,7 +152,7 @@ pub fn main() !void { Printer.fmt("\n", .{}); try slowest.display(); Printer.fmt("\n", .{}); - std.posix.exit(if (fail == 0) 0 else 1); + std.process.exit(if (fail == 0) 0 else 1); } const Printer = struct { @@ -161,19 +179,22 @@ const Status = enum { }; const SlowTracker = struct { - const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming); max: usize, slowest: SlowestQueue, - timer: std.time.Timer, + start: Io.Timestamp, + allocator: Allocator, - fn init(allocator: Allocator, count: u32) SlowTracker { - const timer = std.time.Timer.start() catch @panic("failed to start timer"); - var slowest = SlowestQueue.init(allocator, {}); - slowest.ensureTotalCapacity(count) catch @panic("OOM"); + const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming); + + fn init(allocator: Allocator, io: Io, count: u32) SlowTracker { + const timestamp = Io.Clock.awake.now(io); + var slowest: SlowestQueue = .empty; + slowest.ensureTotalCapacity(allocator, count) catch @panic("OOM"); return .{ .max = count, - .timer = timer, + .start = timestamp, .slowest = slowest, + .allocator = allocator, }; } @@ -182,24 +203,26 @@ const SlowTracker = struct { name: []const u8, }; - fn deinit(self: SlowTracker) void { - self.slowest.deinit(); + fn deinit(self: *SlowTracker) void { + self.slowest.deinit(self.allocator); } - fn startTiming(self: *SlowTracker) void { - self.timer.reset(); + fn startTiming(self: *SlowTracker, io: Io) void { + self.start = Io.Clock.awake.now(io); } - fn endTiming(self: *SlowTracker, test_name: []const u8) u64 { - var timer = self.timer; - const ns = timer.lap(); + fn endTiming(self: *SlowTracker, io: Io, test_name: []const u8) u64 { + const timestamp = Io.Clock.awake.now(io); + const start = self.start; + self.start = timestamp; + const ns: u64 = @intCast(start.durationTo(timestamp).toNanoseconds()); var slowest = &self.slowest; if (slowest.count() < self.max) { // Capacity is fixed to the # of slow tests we want to track // If we've tracked fewer tests than this capacity, than always add - slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); + slowest.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); return ns; } @@ -214,8 +237,8 @@ const SlowTracker = struct { } // the previous fastest of our slow tests, has been pushed off. - _ = slowest.removeMin(); - slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); + _ = slowest.popMin(); + slowest.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); return ns; } @@ -223,7 +246,7 @@ const SlowTracker = struct { var slowest = self.slowest; const count = slowest.count(); Printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" }); - while (slowest.removeMinOrNull()) |info| { + while (slowest.popMin()) |info| { const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0; Printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name }); } @@ -240,34 +263,20 @@ const Env = struct { fail_first: bool, filter: ?[]const u8, - fn init(allocator: Allocator) Env { + fn init(map: *const std.process.Environ.Map) Env { return .{ - .verbose = readEnvBool(allocator, "TEST_VERBOSE", true), - .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false), - .filter = readEnv(allocator, "TEST_FILTER"), + .verbose = readEnvBool(map, "TEST_VERBOSE", true), + .fail_first = readEnvBool(map, "TEST_FAIL_FIRST", false), + .filter = readEnv(map, "TEST_FILTER"), }; } - fn deinit(self: Env, allocator: Allocator) void { - if (self.filter) |f| { - allocator.free(f); - } - } - - fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 { - const v = std.process.getEnvVarOwned(allocator, key) catch |err| { - if (err == error.EnvironmentVariableNotFound) { - return null; - } - std.log.warn("failed to get env var {s} due to err {}", .{ key, err }); - return null; - }; - return v; + fn readEnv(map: *const std.process.Environ.Map, key: []const u8) ?[]const u8 { + return map.get(key); } - fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool { - const value = readEnv(allocator, key) orelse return deflt; - defer allocator.free(value); + fn readEnvBool(map: *const std.process.Environ.Map, key: []const u8, deflt: bool) bool { + const value = readEnv(map, key) orelse return deflt; return std.ascii.eqlIgnoreCase(value, "true"); } }; From edb2f7e84c02c78e194afb5e6b36deb583d7789a Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Sat, 9 May 2026 08:41:16 +0200 Subject: [PATCH 12/26] Upgrade to zig 0.16. --- build.zig.zon | 2 +- src/root.zig | 18 +++++++++--------- src/tests/minlength.zig | 29 +++++++++++++++-------------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index dfb7e9a..f77b3ab 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .sqids, .fingerprint = 0x2448d471e1087204, .version = "0.3.0", - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.16.0", .dependencies = .{}, .paths = .{ "build.zig", diff --git a/src/root.zig b/src/root.zig index c134247..e1da759 100644 --- a/src/root.zig +++ b/src/root.zig @@ -42,7 +42,7 @@ pub const Sqids = struct { return Error.TooShortAlphabet; } for (opts.alphabet) |c| { - if (!std.ascii.isASCII(c)) { + if (!std.ascii.isAscii(c)) { return Error.NonASCIICharacter; } if (mem.count(u8, opts.alphabet, &.{c}) > 1) { @@ -125,7 +125,7 @@ fn blocklist_from_words( defer allocator.free(lowercase_alphabet); var filtered_blocklist = try ArrayList([]const u8).initCapacity(allocator, words.len); - errdefer filtered_blocklist.deinit(); + errdefer filtered_blocklist.deinit(allocator); for (words) |word| { if (word.len < 3) { @@ -139,7 +139,7 @@ fn blocklist_from_words( filtered_blocklist.appendAssumeCapacity(lowercased_word); } - return try filtered_blocklist.toOwnedSlice(); + return try filtered_blocklist.toOwnedSlice(allocator); } fn validInAlphabet(word: []const u8, alphabet: []const u8) bool { @@ -183,7 +183,7 @@ fn encodeNumbers( mem.reverse(u8, alphabet); // Build the ID. - var ret = ArrayListUnmanaged(u8).initBuffer(buf); + var ret: ArrayList(u8) = .initBuffer(buf); ret.appendAssumeCapacity(prefix); @@ -328,8 +328,8 @@ fn decodeID( mem.rotate(u8, alphabet, offset); mem.reverse(u8, alphabet); - var ret = ArrayList(u64).init(allocator); - defer ret.deinit(); + var ret: ArrayList(u64) = .empty; + defer ret.deinit(allocator); while (id.len > 0) { const separator = alphabet[0]; @@ -342,10 +342,10 @@ fn decodeID( // If empty, we are done (the rest is junk characters). if (left.len == 0) { - return try ret.toOwnedSlice(); + return try ret.toOwnedSlice(allocator); } - try ret.append(toNumber(left, alphabet[1..])); + try ret.append(allocator, toNumber(left, alphabet[1..])); // If there is still numbers to decode from the ID, shuffle the alphabet. if (right.len > 0) { @@ -356,7 +356,7 @@ fn decodeID( id = right; } - return try ret.toOwnedSlice(); + return try ret.toOwnedSlice(allocator); } /// toNumber converts a string to an integer using the given alphabet. diff --git a/src/tests/minlength.zig b/src/tests/minlength.zig index 0cf6fc2..2a521b1 100644 --- a/src/tests/minlength.zig +++ b/src/tests/minlength.zig @@ -48,22 +48,23 @@ test "min length: incremental min length" { } test "min length: incremental numbers" { - const s = try Squids.init(testing_allocator, .{ .min_length = sqids.default_alphabet.len }); + const ta = testing_allocator; + const s = try Squids.init(ta, .{ .min_length = sqids.default_alphabet.len }); defer s.deinit(); - var ids = std.StringArrayHashMap([]const u64).init(testing_allocator); - defer ids.deinit(); - - try ids.put("SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu", &.{ 0, 0 }); - try ids.put("n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc", &.{ 0, 1 }); - try ids.put("tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ", &.{ 0, 2 }); - try ids.put("eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE", &.{ 0, 3 }); - try ids.put("rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX", &.{ 0, 4 }); - try ids.put("sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2", &.{ 0, 5 }); - try ids.put("uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0", &.{ 0, 6 }); - try ids.put("74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy", &.{ 0, 7 }); - try ids.put("30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS", &.{ 0, 8 }); - try ids.put("moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin", &.{ 0, 9 }); + var ids: std.array_hash_map.String([]const u64) = .empty; + defer ids.deinit(ta); + + try ids.put(ta, "SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu", &.{ 0, 0 }); + try ids.put(ta, "n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc", &.{ 0, 1 }); + try ids.put(ta, "tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ", &.{ 0, 2 }); + try ids.put(ta, "eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE", &.{ 0, 3 }); + try ids.put(ta, "rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX", &.{ 0, 4 }); + try ids.put(ta, "sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2", &.{ 0, 5 }); + try ids.put(ta, "uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0", &.{ 0, 6 }); + try ids.put(ta, "74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy", &.{ 0, 7 }); + try ids.put(ta, "30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS", &.{ 0, 8 }); + try ids.put(ta, "moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin", &.{ 0, 9 }); var it = ids.iterator(); while (it.next()) |e| { From 3c7839f0097e69b362228266ef899088cc33053e Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Sat, 9 May 2026 08:41:16 +0200 Subject: [PATCH 13/26] Make Sqid unmanaged BREAKING CHANGE: Sqid struct is now unmanaged. Sqid.init takes no allocator. Calls to encode and decode take an allocator. --- README.md | 19 +++++++------- benchmark/bench.zig | 2 +- src/blocklist.zig | 6 ++--- src/root.zig | 55 +++++++++++++++-------------------------- src/tests/alphabet.zig | 17 ++++++------- src/tests/blocklist.zig | 28 ++++++++------------- src/tests/encoding.zig | 28 +++++++++------------ src/tests/minlength.zig | 18 ++++++-------- src/tests/utils.zig | 23 +++++++++-------- 9 files changed, 80 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index d28c5ef..0e4ef6f 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,12 @@ const sqids = @import("sqids"); Simple encode & decode: ```zig -const s = try sqids.Sqids.init(allocator, .{}) -defer s.deinit(); +const s = try sqids.Sqids.init(.{}) -const id = try s.encode(&.{1, 2, 3}); +const id = try s.encode(allocator, &.{1, 2, 3}); defer allocator.free(id); // Caller owns the memory. -const numbers = try s.decode(id); +const numbers = try s.decode(allocator, id); defer allocator.free(numbers); // Caller owns the memory. ``` @@ -93,22 +92,22 @@ The `sqids.Options` struct is used at initialization to customize the encoder. Enforce a *minimum* length for IDs: ```zig -const s = try sqids.Sqids.init(allocator, .{.min_length = 10}); -const id = try s.encode(&.{1, 2, 3}); // "86Rf07xd4z" +const s = try sqids.Sqids.init(.{.min_length = 10}); +const id = try s.encode(allocator, &.{1, 2, 3}); // "86Rf07xd4z" ``` Randomize IDs by providing a custom alphabet: ```zig -const s = try sqids.Sqids.init(allocator, .{.alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE"}); -const id = try s.encode(&.{1, 2, 3}); // "B4aajs" +const s = try sqids.Sqids.init(.{.alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE"}); +const id = try s.encode(allocator, &.{1, 2, 3}); // "B4aajs" ``` Prevent specific words from appearing anywhere in the auto-generated IDs: ```zig -const s = try sqids.Sqids.init(allocator, .{.blocklist = &.{"86Rf07"}}); -const id = try s.encode(&.{1, 2, 3}); // "se8ojk" +const s = try sqids.Sqids.init(.{.blocklist = &.{"86Rf07"}}); +const id = try s.encode(allocator, &.{1, 2, 3}); // "se8ojk" ``` ## 📝 License diff --git a/benchmark/bench.zig b/benchmark/bench.zig index 57a1206..12c6a4e 100644 --- a/benchmark/bench.zig +++ b/benchmark/bench.zig @@ -16,7 +16,7 @@ pub fn main() !void { var ids = try allocator.alloc([]const u8, numbers.len); // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.init(allocator, opts); + const s = try Sqids.init(opts); defer s.deinit(); for (numbers, 0..) |ns, i| { diff --git a/src/blocklist.zig b/src/blocklist.zig index 95b283e..2ea0532 100644 --- a/src/blocklist.zig +++ b/src/blocklist.zig @@ -37,9 +37,9 @@ pub const Blocklist = struct { }; } - fn deinit(s: *Blocklist) void { - s.allocator.free(s.words); - } + // fn deinit(s: *Blocklist) void { + // s.allocator.free(s.words); + // } }; fn validInAlphabet(word: []const u8, alphabet: []const u8) bool { diff --git a/src/root.zig b/src/root.zig index e1da759..41f2508 100644 --- a/src/root.zig +++ b/src/root.zig @@ -29,13 +29,11 @@ pub const Options = struct { /// Sqids encoder. /// Must be initialized with init and free with deinit methods. pub const Sqids = struct { - allocator: mem.Allocator, alphabet: []const u8, - arena: std.heap.ArenaAllocator, blocklist: []const []const u8, min_length: u8, - pub fn init(allocator: mem.Allocator, opts: Options) !Sqids { + pub fn init(opts: Options) !Sqids { // Check alphabet. // TODO(lvignoli): it would be better to "parse not validate", for both the alphabet and the blocklist. if (opts.alphabet.len < 3) { @@ -50,33 +48,22 @@ pub const Sqids = struct { } } - // Create blocklist from provided words. - // We use an arena to manage the memory of the blocklist - var arena = std.heap.ArenaAllocator.init(allocator); - const b = try blocklist_from_words(arena.allocator(), opts.alphabet, opts.blocklist); - return Sqids{ - .allocator = allocator, .alphabet = opts.alphabet, - .arena = arena, - .blocklist = b, + .blocklist = opts.blocklist, .min_length = opts.min_length, }; } - pub fn deinit(self: Sqids) void { - self.arena.deinit(); - } - /// Encodes a list of numbers into a sqids ID. Caller owns the memory. - pub fn encode(self: Sqids, numbers: []const u64) ![]const u8 { + pub fn encode(self: Sqids, gpa: std.mem.Allocator, numbers: []const u64) ![]const u8 { if (numbers.len == 0) { return ""; } // Allocate ID buffer and working alphabet. const estimated_buffer_size = estimateEncodingBufferSize(self.alphabet, numbers, self.min_length); - const buf = try self.allocator.alloc(u8, estimated_buffer_size); - errdefer self.allocator.free(buf); + const buf = try gpa.alloc(u8, estimated_buffer_size); + errdefer gpa.free(buf); var alphabet_buffer: [128]u8 = undefined; @memcpy(alphabet_buffer[0..self.alphabet.len], self.alphabet); @@ -105,8 +92,8 @@ pub const Sqids = struct { } /// Decodes an ID into numbers using alphabet. Caller owns the memory. - pub fn decode(self: Sqids, id: []const u8) ![]const u64 { - return try decodeID(self.allocator, id, self.alphabet); + pub fn decode(self: Sqids, gpa: std.mem.Allocator, id: []const u8) ![]const u64 { + return try decodeID(gpa, id, self.alphabet); } }; @@ -297,7 +284,7 @@ fn containsNumber(s: []const u8) bool { /// decodeID decodes an ID into numbers using alphabet. Caller owns the memory. fn decodeID( - allocator: mem.Allocator, + gpa: mem.Allocator, to_decode_id: []const u8, decoding_alphabet: []const u8, ) ![]const u64 { @@ -329,7 +316,7 @@ fn decodeID( mem.reverse(u8, alphabet); var ret: ArrayList(u64) = .empty; - defer ret.deinit(allocator); + defer ret.deinit(gpa); while (id.len > 0) { const separator = alphabet[0]; @@ -342,10 +329,10 @@ fn decodeID( // If empty, we are done (the rest is junk characters). if (left.len == 0) { - return try ret.toOwnedSlice(allocator); + return try ret.toOwnedSlice(gpa); } - try ret.append(allocator, toNumber(left, alphabet[1..])); + try ret.append(gpa, toNumber(left, alphabet[1..])); // If there is still numbers to decode from the ID, shuffle the alphabet. if (right.len > 0) { @@ -356,7 +343,7 @@ fn decodeID( id = right; } - return try ret.toOwnedSlice(allocator); + return try ret.toOwnedSlice(gpa); } /// toNumber converts a string to an integer using the given alphabet. @@ -409,9 +396,9 @@ test "encode" { }; for (cases) |case| { - const sqids = try Sqids.init(allocator, .{ .alphabet = case.alphabet }); - defer sqids.deinit(); - const id = try sqids.encode(case.numbers); + const sqids = try Sqids.init(.{ .alphabet = case.alphabet }); + + const id = try sqids.encode(allocator, case.numbers); defer allocator.free(id); try testing.expectEqualStrings(case.expected, id); } @@ -421,24 +408,22 @@ test "non-empty blocklist" { const allocator = testing.allocator; const blocklist: []const []const u8 = &.{"ArUO"}; - const sqids = try Sqids.init(allocator, .{ .blocklist = blocklist }); - defer sqids.deinit(); + const sqids = try Sqids.init(.{ .blocklist = blocklist }); - const actual_numbers = try sqids.decode("ArUO"); + const actual_numbers = try sqids.decode(allocator, "ArUO"); defer allocator.free(actual_numbers); try testing.expectEqualSlices(u64, &.{100000}, actual_numbers); - const got_id = try sqids.encode(&.{100000}); + const got_id = try sqids.encode(allocator, &.{100000}); defer allocator.free(got_id); try testing.expectEqualStrings("QyG4", got_id); } test "decode" { const allocator = testing.allocator; - const sqids = try Sqids.init(allocator, .{ .alphabet = "0123456789abcdef" }); - defer sqids.deinit(); + const sqids = try Sqids.init(.{ .alphabet = "0123456789abcdef" }); - const numbers = try sqids.decode("489158"); + const numbers = try sqids.decode(allocator, "489158"); defer allocator.free(numbers); try testing.expectEqualSlices(u64, &.{ 1, 2, 3 }, numbers); } diff --git a/src/tests/alphabet.zig b/src/tests/alphabet.zig index a98c642..1f597f5 100644 --- a/src/tests/alphabet.zig +++ b/src/tests/alphabet.zig @@ -5,18 +5,16 @@ const testing = std.testing; const utils = @import("utils.zig"); const sqids = @import("sqids"); -const Squids = sqids.Sqids; +const Sqids = sqids.Sqids; const testing_allocator = testing.allocator; test "simple" { - const s = try Squids.init(testing_allocator, .{ .alphabet = "0123456789abcdef" }); - defer s.deinit(); + const s = try Sqids.init(.{ .alphabet = "0123456789abcdef" }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{ 1, 2, 3 }, "489158"); } test "short" { - const s = try Squids.init(testing_allocator, .{ .alphabet = "abc" }); - defer s.deinit(); + const s = try Sqids.init(.{ .alphabet = "abc" }); try utils.expectEncodeDecode(testing_allocator, s, &.{ 1, 2, 3 }); } @@ -24,22 +22,21 @@ test "long" { const alphabet = \\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:\'"/?.>,<`~ ; - const s = try Squids.init(testing_allocator, .{ .alphabet = alphabet }); - defer s.deinit(); + const s = try Sqids.init(.{ .alphabet = alphabet }); try utils.expectEncodeDecode(testing_allocator, s, &.{ 1, 2, 3 }); } test "multibyte alphabet" { - const err = Squids.init(testing_allocator, .{ .alphabet = "ë1092" }) catch |err| err; + const err = Sqids.init(.{ .alphabet = "ë1092" }) catch |err| err; try testing.expectError(sqids.Error.NonASCIICharacter, err); } test "repeating alphabet characters" { - const err = Squids.init(testing_allocator, .{ .alphabet = "aabcdefg" }) catch |err| err; + const err = Sqids.init(.{ .alphabet = "aabcdefg" }) catch |err| err; try testing.expectError(sqids.Error.RepeatingAlphabetCharacter, err); } test "too short of an alphabet" { - const err = Squids.init(testing_allocator, .{ .alphabet = "ab" }) catch |err| err; + const err = Sqids.init(.{ .alphabet = "ab" }) catch |err| err; try testing.expectError(sqids.Error.TooShortAlphabet, err); } diff --git a/src/tests/blocklist.zig b/src/tests/blocklist.zig index e6828bb..5d4e6c2 100644 --- a/src/tests/blocklist.zig +++ b/src/tests/blocklist.zig @@ -5,27 +5,24 @@ const testing = std.testing; const utils = @import("utils.zig"); const sqids = @import("sqids"); -const Squids = sqids.Sqids; +const Sqids = sqids.Sqids; const testing_allocator = testing.allocator; test "if no custom blocklist param, use the default blocklist" { - const s = try Squids.init(testing_allocator, .{}); - defer s.deinit(); + const s = try Sqids.init(.{}); try utils.expectDecode(testing_allocator, s, "aho1e", &.{4572721}); try utils.expectEncode(testing_allocator, s, &.{4572721}, "JExTR"); } test "if an empty blocklist param passed, don't use any blocklist" { - const s = try Squids.init(testing_allocator, .{ .blocklist = &.{} }); - defer s.deinit(); + const s = try Sqids.init(.{ .blocklist = &.{} }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{4572721}, "aho1e"); } test "if a non-empty blocklist param passed, use only that" { - const s = try Squids.init(testing_allocator, .{ .blocklist = &.{"ArUO"} }); - defer s.deinit(); + const s = try Sqids.init(.{ .blocklist = &.{"ArUO"} }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{4572721}, "aho1e"); @@ -35,7 +32,7 @@ test "if a non-empty blocklist param passed, use only that" { } test "blocklist" { - const s = try Squids.init(testing_allocator, .{ + const s = try Sqids.init(.{ .blocklist = &.{ "JSwXFaosAN", // normal result of 1st encoding, let's block that word on purpose "OCjV9JK64o", // result of 2nd encoding @@ -44,13 +41,12 @@ test "blocklist" { "7tE6", // result of 4th encoding is `7tE6jdAHLe`, let's block the prefix }, }); - defer s.deinit(); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{ 1_000_000, 2_000_000 }, "1aYeB7bRUt"); } test "decoding blocklist words should still work" { - const s = try Squids.init(testing_allocator, .{ + const s = try Sqids.init(.{ .blocklist = &.{ "86Rf07", "se8ojk", @@ -59,7 +55,6 @@ test "decoding blocklist words should still work" { "5sQRZO", }, }); - defer s.deinit(); try utils.expectDecode(testing_allocator, s, "86Rf07", &.{ 1, 2, 3 }); try utils.expectDecode(testing_allocator, s, "se8ojk", &.{ 1, 2, 3 }); @@ -69,33 +64,30 @@ test "decoding blocklist words should still work" { } test "match against a short blocklist word" { - const s = try Squids.init(testing_allocator, .{ + const s = try Sqids.init(.{ .blocklist = &.{"pnd"}, }); - defer s.deinit(); try utils.expectEncodeDecode(testing_allocator, s, &.{1000}); } test "blocklist filtering in constructor" { - const s = try Squids.init(testing_allocator, .{ + const s = try Sqids.init(.{ .alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", .blocklist = &.{"sxnzkl"}, }); - defer s.deinit(); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{ 1, 2, 3 }, "IBSHOZ"); // without blocklist, would've been "SXNZKL" } test "max encoding attempts" { // Setup encoder such that alphabet.len == min_length == blocklist.len - const s = try Squids.init(testing_allocator, .{ + const s = try Sqids.init(.{ .alphabet = "abc", .min_length = 3, .blocklist = &.{ "cab", "abc", "bca" }, }); - defer s.deinit(); - const err = s.encode(&.{0}) catch |err| err; + const err = s.encode(testing_allocator, &.{0}) catch |err| err; try testing.expectError(sqids.Error.ReachedMaxAttempts, err); } diff --git a/src/tests/encoding.zig b/src/tests/encoding.zig index 8a3defd..59fb561 100644 --- a/src/tests/encoding.zig +++ b/src/tests/encoding.zig @@ -4,13 +4,11 @@ const testing = std.testing; const utils = @import("utils.zig"); -const sqids = @import("sqids"); -const Squids = sqids.Sqids; +const Sqids = @import("sqids").Sqids; const testing_allocator = testing.allocator; test "default encoder: encode incremental numbers" { - const s = try Squids.init(testing_allocator, .{}); - defer s.deinit(); + const s = try Sqids.init(.{}); var cases = std.StringHashMap([]const u64).init(testing_allocator); defer cases.deinit(); @@ -64,8 +62,7 @@ test "default encoder: encode incremental numbers" { } test "default encoder: multi input" { - const s = try Squids.init(testing_allocator, .{}); - defer s.deinit(); + const s = try Sqids.init(.{}); const numbers = [2][]const u64{ &.{ 0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, std.math.maxInt(u64) }, @@ -84,9 +81,9 @@ test "default encoder: multi input" { }; for (numbers) |n| { - const tmp_id = try s.encode(n); + const tmp_id = try s.encode(testing_allocator, n); defer testing_allocator.free(tmp_id); - const dec_output = try s.decode(tmp_id); + const dec_output = try s.decode(testing_allocator, tmp_id); defer testing_allocator.free(dec_output); try testing.expectEqualSlices(u64, n, dec_output); @@ -94,28 +91,25 @@ test "default encoder: multi input" { } test "default encoder: encoding no numbers" { - const s = try Squids.init(testing_allocator, .{}); - defer s.deinit(); + const s = try Sqids.init(.{}); - const output = try s.encode(&.{}); + const output = try s.encode(testing_allocator, &.{}); try testing.expectEqualStrings("", output); testing_allocator.free(output); } test "default encoder: decoding empty string" { - const s = try Squids.init(testing_allocator, .{}); - defer s.deinit(); + const s = try Sqids.init(.{}); - const output = try s.decode(""); + const output = try s.decode(testing_allocator, ""); try testing.expectEqualSlices(u64, &.{}, output); testing_allocator.free(output); } test "default encoder: decoding ID with invalid character" { - const s = try Squids.init(testing_allocator, .{}); - defer s.deinit(); + const s = try Sqids.init(.{}); - const output = try s.decode("*"); + const output = try s.decode(testing_allocator, "*"); try testing.expectEqualSlices(u64, &.{}, output); testing_allocator.free(output); } diff --git a/src/tests/minlength.zig b/src/tests/minlength.zig index 2a521b1..14bc89d 100644 --- a/src/tests/minlength.zig +++ b/src/tests/minlength.zig @@ -5,7 +5,7 @@ const testing = std.testing; const utils = @import("utils.zig"); const sqids = @import("sqids"); -const Squids = sqids.Sqids; +const Sqids = sqids.Sqids; const testing_allocator = testing.allocator; test "min length: incremental min length" { @@ -36,10 +36,9 @@ test "min length: incremental min length" { while (it.next()) |e| { const min_length = e.key_ptr.*; const id = e.value_ptr.*; - const s = try Squids.init(testing_allocator, .{ .min_length = min_length }); - defer s.deinit(); + const s = try Sqids.init(.{ .min_length = min_length }); - const got_id = try s.encode(&numbers); + const got_id = try s.encode(testing_allocator, &numbers); defer testing_allocator.free(got_id); try testing.expect(min_length == got_id.len); @@ -49,8 +48,7 @@ test "min length: incremental min length" { test "min length: incremental numbers" { const ta = testing_allocator; - const s = try Squids.init(ta, .{ .min_length = sqids.default_alphabet.len }); - defer s.deinit(); + const s = try Sqids.init(.{ .min_length = sqids.default_alphabet.len }); var ids: std.array_hash_map.String([]const u64) = .empty; defer ids.deinit(ta); @@ -87,15 +85,15 @@ test "min length: various" { }; for (min_lengths) |min_length| { - const s = try Squids.init(testing_allocator, .{ .min_length = min_length }); - defer s.deinit(); + const s = try Sqids.init(.{ .min_length = min_length }); + for (numbers) |ns| { - const id = try s.encode(ns); + const id = try s.encode(testing_allocator, ns); defer testing_allocator.free(id); try testing.expect(id.len >= min_length); - const got_numbers = try s.decode(id); + const got_numbers = try s.decode(testing_allocator, id); defer testing_allocator.free(got_numbers); try testing.expectEqualSlices(u64, ns, got_numbers); diff --git a/src/tests/utils.zig b/src/tests/utils.zig index d1638fb..4f87583 100644 --- a/src/tests/utils.zig +++ b/src/tests/utils.zig @@ -2,16 +2,15 @@ const std = @import("std"); const mem = std.mem; const testing = std.testing; -const sqids = @import("sqids"); -const Squids = sqids.Sqids; +const Sqids = @import("sqids").Sqids; pub fn expectEncode( allocator: mem.Allocator, - s: Squids, + s: Sqids, numbers: []const u64, id: []const u8, ) !void { - const got = try s.encode(numbers); + const got = try s.encode(allocator, numbers); defer allocator.free(got); try testing.expectEqualStrings(id, got); @@ -19,11 +18,11 @@ pub fn expectEncode( pub fn expectDecode( allocator: mem.Allocator, - s: Squids, + s: Sqids, id: []const u8, numbers: []const u64, ) !void { - const got = try s.decode(id); + const got = try s.decode(allocator, id); defer allocator.free(got); try testing.expectEqualSlices(u64, numbers, got); @@ -31,12 +30,12 @@ pub fn expectDecode( pub fn expectEncodeDecode( allocator: mem.Allocator, - s: Squids, + s: Sqids, numbers: []const u64, ) !void { - const id = try s.encode(numbers); + const id = try s.encode(allocator, numbers); defer allocator.free(id); - const obtained_numbers = try s.decode(id); + const obtained_numbers = try s.decode(allocator, id); defer allocator.free(obtained_numbers); try testing.expectEqualSlices(u64, numbers, obtained_numbers); @@ -44,17 +43,17 @@ pub fn expectEncodeDecode( pub fn expectEncodeDecodeWithID( allocator: mem.Allocator, - s: Squids, + s: Sqids, numbers: []const u64, id: []const u8, ) !void { // Test encoding. - const obtained_id = try s.encode(numbers); + const obtained_id = try s.encode(allocator, numbers); defer allocator.free(obtained_id); try testing.expectEqualStrings(id, obtained_id); // Test decoding back. - const obtained_numbers = try s.decode(obtained_id); + const obtained_numbers = try s.decode(allocator, obtained_id); defer allocator.free(obtained_numbers); try testing.expectEqualSlices(u64, numbers, obtained_numbers); } From 2c49acae42ec49a8b0cf4ae7d9a44d127c8bbf9b Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Sat, 9 May 2026 09:38:33 +0200 Subject: [PATCH 14/26] Rename Sqid.init to Sqid.new init suggests that it must be deinit afterwards. Since Sqid is now an unmanaged struct, there is no deinit. A constructor is still required for validation of the options arguments, I call it new. --- README.md | 8 ++++---- benchmark/bench.zig | 2 +- src/main.zig | 2 +- src/root.zig | 12 +++++++----- src/tests/alphabet.zig | 12 ++++++------ src/tests/blocklist.zig | 16 ++++++++-------- src/tests/encoding.zig | 10 +++++----- src/tests/minlength.zig | 6 +++--- 8 files changed, 35 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 0e4ef6f..cf61db1 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ const sqids = @import("sqids"); Simple encode & decode: ```zig -const s = try sqids.Sqids.init(.{}) +const s = try sqids.Sqids.new(.{}) const id = try s.encode(allocator, &.{1, 2, 3}); defer allocator.free(id); // Caller owns the memory. @@ -92,21 +92,21 @@ The `sqids.Options` struct is used at initialization to customize the encoder. Enforce a *minimum* length for IDs: ```zig -const s = try sqids.Sqids.init(.{.min_length = 10}); +const s = try sqids.Sqids.new(.{.min_length = 10}); const id = try s.encode(allocator, &.{1, 2, 3}); // "86Rf07xd4z" ``` Randomize IDs by providing a custom alphabet: ```zig -const s = try sqids.Sqids.init(.{.alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE"}); +const s = try sqids.Sqids.new(.{.alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE"}); const id = try s.encode(allocator, &.{1, 2, 3}); // "B4aajs" ``` Prevent specific words from appearing anywhere in the auto-generated IDs: ```zig -const s = try sqids.Sqids.init(.{.blocklist = &.{"86Rf07"}}); +const s = try sqids.Sqids.new(.{.blocklist = &.{"86Rf07"}}); const id = try s.encode(allocator, &.{1, 2, 3}); // "se8ojk" ``` diff --git a/benchmark/bench.zig b/benchmark/bench.zig index 12c6a4e..161d834 100644 --- a/benchmark/bench.zig +++ b/benchmark/bench.zig @@ -16,7 +16,7 @@ pub fn main() !void { var ids = try allocator.alloc([]const u8, numbers.len); // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.init(opts); + const s = try Sqids.new(opts); defer s.deinit(); for (numbers, 0..) |ns, i| { diff --git a/src/main.zig b/src/main.zig index 01a014a..2b5dea8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -16,7 +16,7 @@ pub fn main() !void { }; // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.init(opts); + const s = try Sqids.new(opts); defer s.deinit(); const id = try s.encode(numbers); defer allocator.free(id); diff --git a/src/root.zig b/src/root.zig index 41f2508..b25739b 100644 --- a/src/root.zig +++ b/src/root.zig @@ -27,13 +27,15 @@ pub const Options = struct { }; /// Sqids encoder. -/// Must be initialized with init and free with deinit methods. +/// +/// Must be created with new to get valid instances. +/// Sqids impose constraints on the alphabet, blocklist and min_length. pub const Sqids = struct { alphabet: []const u8, blocklist: []const []const u8, min_length: u8, - pub fn init(opts: Options) !Sqids { + pub fn new(opts: Options) !Sqids { // Check alphabet. // TODO(lvignoli): it would be better to "parse not validate", for both the alphabet and the blocklist. if (opts.alphabet.len < 3) { @@ -396,7 +398,7 @@ test "encode" { }; for (cases) |case| { - const sqids = try Sqids.init(.{ .alphabet = case.alphabet }); + const sqids = try Sqids.new(.{ .alphabet = case.alphabet }); const id = try sqids.encode(allocator, case.numbers); defer allocator.free(id); @@ -408,7 +410,7 @@ test "non-empty blocklist" { const allocator = testing.allocator; const blocklist: []const []const u8 = &.{"ArUO"}; - const sqids = try Sqids.init(.{ .blocklist = blocklist }); + const sqids = try Sqids.new(.{ .blocklist = blocklist }); const actual_numbers = try sqids.decode(allocator, "ArUO"); defer allocator.free(actual_numbers); @@ -421,7 +423,7 @@ test "non-empty blocklist" { test "decode" { const allocator = testing.allocator; - const sqids = try Sqids.init(.{ .alphabet = "0123456789abcdef" }); + const sqids = try Sqids.new(.{ .alphabet = "0123456789abcdef" }); const numbers = try sqids.decode(allocator, "489158"); defer allocator.free(numbers); diff --git a/src/tests/alphabet.zig b/src/tests/alphabet.zig index 1f597f5..50f9221 100644 --- a/src/tests/alphabet.zig +++ b/src/tests/alphabet.zig @@ -9,12 +9,12 @@ const Sqids = sqids.Sqids; const testing_allocator = testing.allocator; test "simple" { - const s = try Sqids.init(.{ .alphabet = "0123456789abcdef" }); + const s = try Sqids.new(.{ .alphabet = "0123456789abcdef" }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{ 1, 2, 3 }, "489158"); } test "short" { - const s = try Sqids.init(.{ .alphabet = "abc" }); + const s = try Sqids.new(.{ .alphabet = "abc" }); try utils.expectEncodeDecode(testing_allocator, s, &.{ 1, 2, 3 }); } @@ -22,21 +22,21 @@ test "long" { const alphabet = \\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:\'"/?.>,<`~ ; - const s = try Sqids.init(.{ .alphabet = alphabet }); + const s = try Sqids.new(.{ .alphabet = alphabet }); try utils.expectEncodeDecode(testing_allocator, s, &.{ 1, 2, 3 }); } test "multibyte alphabet" { - const err = Sqids.init(.{ .alphabet = "ë1092" }) catch |err| err; + const err = Sqids.new(.{ .alphabet = "ë1092" }) catch |err| err; try testing.expectError(sqids.Error.NonASCIICharacter, err); } test "repeating alphabet characters" { - const err = Sqids.init(.{ .alphabet = "aabcdefg" }) catch |err| err; + const err = Sqids.new(.{ .alphabet = "aabcdefg" }) catch |err| err; try testing.expectError(sqids.Error.RepeatingAlphabetCharacter, err); } test "too short of an alphabet" { - const err = Sqids.init(.{ .alphabet = "ab" }) catch |err| err; + const err = Sqids.new(.{ .alphabet = "ab" }) catch |err| err; try testing.expectError(sqids.Error.TooShortAlphabet, err); } diff --git a/src/tests/blocklist.zig b/src/tests/blocklist.zig index 5d4e6c2..9cfb14e 100644 --- a/src/tests/blocklist.zig +++ b/src/tests/blocklist.zig @@ -9,20 +9,20 @@ const Sqids = sqids.Sqids; const testing_allocator = testing.allocator; test "if no custom blocklist param, use the default blocklist" { - const s = try Sqids.init(.{}); + const s = try Sqids.new(.{}); try utils.expectDecode(testing_allocator, s, "aho1e", &.{4572721}); try utils.expectEncode(testing_allocator, s, &.{4572721}, "JExTR"); } test "if an empty blocklist param passed, don't use any blocklist" { - const s = try Sqids.init(.{ .blocklist = &.{} }); + const s = try Sqids.new(.{ .blocklist = &.{} }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{4572721}, "aho1e"); } test "if a non-empty blocklist param passed, use only that" { - const s = try Sqids.init(.{ .blocklist = &.{"ArUO"} }); + const s = try Sqids.new(.{ .blocklist = &.{"ArUO"} }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{4572721}, "aho1e"); @@ -32,7 +32,7 @@ test "if a non-empty blocklist param passed, use only that" { } test "blocklist" { - const s = try Sqids.init(.{ + const s = try Sqids.new(.{ .blocklist = &.{ "JSwXFaosAN", // normal result of 1st encoding, let's block that word on purpose "OCjV9JK64o", // result of 2nd encoding @@ -46,7 +46,7 @@ test "blocklist" { } test "decoding blocklist words should still work" { - const s = try Sqids.init(.{ + const s = try Sqids.new(.{ .blocklist = &.{ "86Rf07", "se8ojk", @@ -64,7 +64,7 @@ test "decoding blocklist words should still work" { } test "match against a short blocklist word" { - const s = try Sqids.init(.{ + const s = try Sqids.new(.{ .blocklist = &.{"pnd"}, }); @@ -72,7 +72,7 @@ test "match against a short blocklist word" { } test "blocklist filtering in constructor" { - const s = try Sqids.init(.{ + const s = try Sqids.new(.{ .alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", .blocklist = &.{"sxnzkl"}, }); @@ -82,7 +82,7 @@ test "blocklist filtering in constructor" { test "max encoding attempts" { // Setup encoder such that alphabet.len == min_length == blocklist.len - const s = try Sqids.init(.{ + const s = try Sqids.new(.{ .alphabet = "abc", .min_length = 3, .blocklist = &.{ "cab", "abc", "bca" }, diff --git a/src/tests/encoding.zig b/src/tests/encoding.zig index 59fb561..482bd8b 100644 --- a/src/tests/encoding.zig +++ b/src/tests/encoding.zig @@ -8,7 +8,7 @@ const Sqids = @import("sqids").Sqids; const testing_allocator = testing.allocator; test "default encoder: encode incremental numbers" { - const s = try Sqids.init(.{}); + const s = try Sqids.new(.{}); var cases = std.StringHashMap([]const u64).init(testing_allocator); defer cases.deinit(); @@ -62,7 +62,7 @@ test "default encoder: encode incremental numbers" { } test "default encoder: multi input" { - const s = try Sqids.init(.{}); + const s = try Sqids.new(.{}); const numbers = [2][]const u64{ &.{ 0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, std.math.maxInt(u64) }, @@ -91,7 +91,7 @@ test "default encoder: multi input" { } test "default encoder: encoding no numbers" { - const s = try Sqids.init(.{}); + const s = try Sqids.new(.{}); const output = try s.encode(testing_allocator, &.{}); try testing.expectEqualStrings("", output); @@ -99,7 +99,7 @@ test "default encoder: encoding no numbers" { } test "default encoder: decoding empty string" { - const s = try Sqids.init(.{}); + const s = try Sqids.new(.{}); const output = try s.decode(testing_allocator, ""); try testing.expectEqualSlices(u64, &.{}, output); @@ -107,7 +107,7 @@ test "default encoder: decoding empty string" { } test "default encoder: decoding ID with invalid character" { - const s = try Sqids.init(.{}); + const s = try Sqids.new(.{}); const output = try s.decode(testing_allocator, "*"); try testing.expectEqualSlices(u64, &.{}, output); diff --git a/src/tests/minlength.zig b/src/tests/minlength.zig index 14bc89d..74528c3 100644 --- a/src/tests/minlength.zig +++ b/src/tests/minlength.zig @@ -36,7 +36,7 @@ test "min length: incremental min length" { while (it.next()) |e| { const min_length = e.key_ptr.*; const id = e.value_ptr.*; - const s = try Sqids.init(.{ .min_length = min_length }); + const s = try Sqids.new(.{ .min_length = min_length }); const got_id = try s.encode(testing_allocator, &numbers); defer testing_allocator.free(got_id); @@ -48,7 +48,7 @@ test "min length: incremental min length" { test "min length: incremental numbers" { const ta = testing_allocator; - const s = try Sqids.init(.{ .min_length = sqids.default_alphabet.len }); + const s = try Sqids.new(.{ .min_length = sqids.default_alphabet.len }); var ids: std.array_hash_map.String([]const u64) = .empty; defer ids.deinit(ta); @@ -85,7 +85,7 @@ test "min length: various" { }; for (min_lengths) |min_length| { - const s = try Sqids.init(.{ .min_length = min_length }); + const s = try Sqids.new(.{ .min_length = min_length }); for (numbers) |ns| { const id = try s.encode(testing_allocator, ns); From a4a3892f8d0830b4940382789d4ed1c0f9e6208e Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Sat, 9 May 2026 09:45:35 +0200 Subject: [PATCH 15/26] build.zig: Add a runnable main program --- build.zig | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index 23d77ab..99408ca 100644 --- a/build.zig +++ b/build.zig @@ -38,12 +38,25 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&b.addRunArtifact(tests).step); test_step.dependOn(&b.addRunArtifact(root_tests).step); - const exe_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .optimize = optimize, - .target = target, + // const exe_module = b.createModule(.{ + // .root_source_file = b.path("src/main.zig"), + // .optimize = optimize, + // .target = target, + // }); + // exe_module.addImport("sqids", sqids_module); + const exe = b.addExecutable(.{ + .name = "main", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .optimize = optimize, + .target = target, + .imports = &.{.{ .name = "sqids", .module = sqids_module }}, + }), }); - exe_module.addImport("sqids", sqids_module); + const run_step = b.step("run", "Run the demo program"); + // const exe_artifact = b.addInstallArtifact(exe, .{}); + const exe_artifact = b.addRunArtifact(exe); + run_step.dependOn(&exe_artifact.step); const bench_module = b.createModule(.{ .root_source_file = b.path("benchmark/bench.zig"), .target = target, .optimize = optimize }); bench_module.addImport("sqids", sqids_module); From decdcf3cf0b6495eac0abbe6ef648409125424c1 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 11:17:00 +0200 Subject: [PATCH 16/26] Remove Blocklist object Prefer a plain list of words, and let the user handle optimizations. --- src/blocklist.zig | 1178 +++++++++++++++++++++------------------------ src/root.zig | 16 +- 2 files changed, 570 insertions(+), 624 deletions(-) diff --git a/src/blocklist.zig b/src/blocklist.zig index 2ea0532..0782883 100644 --- a/src/blocklist.zig +++ b/src/blocklist.zig @@ -1,618 +1,562 @@ -const std = @import("std"); -const mem = std.mem; - -pub const Blocklist = struct { - alphabet: []const u8, - words: []const []const u8, - - fn new( - alphabet: []const u8, - words: []const []const u8, - ) !Blocklist { - // Clean up blocklist: - // 1. all blocklist words should be lowercase, - // 2. no words less than 3 chars, - // 3. if some words contain chars that are not in the alphabet, remove those. - - for (alphabet) |c| { - if (!std.ascii.isAscii(c)) { - return error.NonASCIICharacter; - } - } - // Check that the words are valid in the provided alphabet, which is ASCII. - for (words) |w| { - if (w.len < 3) { - return error.WordTooShort; - } - for (w) |c| { - if (!std.ascii.indexOfIgnoreCase(alphabet, c)) { - return error.InvalidInAlphabet; - } - } - } - - return Blocklist{ - .alphabet = alphabet, - .words = words, - }; - } - - // fn deinit(s: *Blocklist) void { - // s.allocator.free(s.words); - // } -}; - -fn validInAlphabet(word: []const u8, alphabet: []const u8) bool { - for (word) |c| { - if (mem.indexOf(u8, alphabet, &.{c}) == null) { - return false; - } - } - return true; -} - -pub const default_blocklist = Blocklist{ - .alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", - .words = &.{ - "0rgasm", - "1d10t", - "1d1ot", - "1di0t", - "1diot", - "1eccacu10", - "1eccacu1o", - "1eccacul0", - "1eccaculo", - "1mbec11e", - "1mbec1le", - "1mbeci1e", - "1mbecile", - "a11upat0", - "a11upato", - "a1lupat0", - "a1lupato", - "aand", - "ah01e", - "ah0le", - "aho1e", - "ahole", - "al1upat0", - "al1upato", - "allupat0", - "allupato", - "ana1", - "ana1e", - "anal", - "anale", - "anus", - "arrapat0", - "arrapato", - "arsch", - "arse", - "ass", - "b00b", - "b00be", - "b01ata", - "b0ceta", - "b0iata", - "b0ob", - "b0obe", - "b0sta", - "b1tch", - "b1te", - "b1tte", - "ba1atkar", - "balatkar", - "bastard0", - "bastardo", - "batt0na", - "battona", - "bitch", - "bite", - "bitte", - "bo0b", - "bo0be", - "bo1ata", - "boceta", - "boiata", - "boob", - "boobe", - "bosta", - "bran1age", - "bran1er", - "bran1ette", - "bran1eur", - "bran1euse", - "branlage", - "branler", - "branlette", - "branleur", - "branleuse", - "c0ck", - "c0g110ne", - "c0g11one", - "c0g1i0ne", - "c0g1ione", - "c0gl10ne", - "c0gl1one", - "c0gli0ne", - "c0glione", - "c0na", - "c0nnard", - "c0nnasse", - "c0nne", - "c0u111es", - "c0u11les", - "c0u1l1es", - "c0u1lles", - "c0ui11es", - "c0ui1les", - "c0uil1es", - "c0uilles", - "c11t", - "c11t0", - "c11to", - "c1it", - "c1it0", - "c1ito", - "cabr0n", - "cabra0", - "cabrao", - "cabron", - "caca", - "cacca", - "cacete", - "cagante", - "cagar", - "cagare", - "cagna", - "cara1h0", - "cara1ho", - "caracu10", - "caracu1o", - "caracul0", - "caraculo", - "caralh0", - "caralho", - "cazz0", - "cazz1mma", - "cazzata", - "cazzimma", - "cazzo", - "ch00t1a", - "ch00t1ya", - "ch00tia", - "ch00tiya", - "ch0d", - "ch0ot1a", - "ch0ot1ya", - "ch0otia", - "ch0otiya", - "ch1asse", - "ch1avata", - "ch1er", - "ch1ng0", - "ch1ngadaz0s", - "ch1ngadazos", - "ch1ngader1ta", - "ch1ngaderita", - "ch1ngar", - "ch1ngo", - "ch1ngues", - "ch1nk", - "chatte", - "chiasse", - "chiavata", - "chier", - "ching0", - "chingadaz0s", - "chingadazos", - "chingader1ta", - "chingaderita", - "chingar", - "chingo", - "chingues", - "chink", - "cho0t1a", - "cho0t1ya", - "cho0tia", - "cho0tiya", - "chod", - "choot1a", - "choot1ya", - "chootia", - "chootiya", - "cl1t", - "cl1t0", - "cl1to", - "clit", - "clit0", - "clito", - "cock", - "cog110ne", - "cog11one", - "cog1i0ne", - "cog1ione", - "cogl10ne", - "cogl1one", - "cogli0ne", - "coglione", - "cona", - "connard", - "connasse", - "conne", - "cou111es", - "cou11les", - "cou1l1es", - "cou1lles", - "coui11es", - "coui1les", - "couil1es", - "couilles", - "cracker", - "crap", - "cu10", - "cu1att0ne", - "cu1attone", - "cu1er0", - "cu1ero", - "cu1o", - "cul0", - "culatt0ne", - "culattone", - "culer0", - "culero", - "culo", - "cum", - "cunt", - "d11d0", - "d11do", - "d1ck", - "d1ld0", - "d1ldo", - "damn", - "de1ch", - "deich", - "depp", - "di1d0", - "di1do", - "dick", - "dild0", - "dildo", - "dyke", - "encu1e", - "encule", - "enema", - "enf01re", - "enf0ire", - "enfo1re", - "enfoire", - "estup1d0", - "estup1do", - "estupid0", - "estupido", - "etr0n", - "etron", - "f0da", - "f0der", - "f0ttere", - "f0tters1", - "f0ttersi", - "f0tze", - "f0utre", - "f1ca", - "f1cker", - "f1ga", - "fag", - "fica", - "ficker", - "figa", - "foda", - "foder", - "fottere", - "fotters1", - "fottersi", - "fotze", - "foutre", - "fr0c10", - "fr0c1o", - "fr0ci0", - "fr0cio", - "fr0sc10", - "fr0sc1o", - "fr0sci0", - "fr0scio", - "froc10", - "froc1o", - "froci0", - "frocio", - "frosc10", - "frosc1o", - "frosci0", - "froscio", - "fuck", - "g00", - "g0o", - "g0u1ne", - "g0uine", - "gandu", - "go0", - "goo", - "gou1ne", - "gouine", - "gr0gnasse", - "grognasse", - "haram1", - "harami", - "haramzade", - "hund1n", - "hundin", - "id10t", - "id1ot", - "idi0t", - "idiot", - "imbec11e", - "imbec1le", - "imbeci1e", - "imbecile", - "j1zz", - "jerk", - "jizz", - "k1ke", - "kam1ne", - "kamine", - "kike", - "leccacu10", - "leccacu1o", - "leccacul0", - "leccaculo", - "m1erda", - "m1gn0tta", - "m1gnotta", - "m1nch1a", - "m1nchia", - "m1st", - "mam0n", - "mamahuev0", - "mamahuevo", - "mamon", - "masturbat10n", - "masturbat1on", - "masturbate", - "masturbati0n", - "masturbation", - "merd0s0", - "merd0so", - "merda", - "merde", - "merdos0", - "merdoso", - "mierda", - "mign0tta", - "mignotta", - "minch1a", - "minchia", - "mist", - "musch1", - "muschi", - "n1gger", - "neger", - "negr0", - "negre", - "negro", - "nerch1a", - "nerchia", - "nigger", - "orgasm", - "p00p", - "p011a", - "p01la", - "p0l1a", - "p0lla", - "p0mp1n0", - "p0mp1no", - "p0mpin0", - "p0mpino", - "p0op", - "p0rca", - "p0rn", - "p0rra", - "p0uff1asse", - "p0uffiasse", - "p1p1", - "p1pi", - "p1r1a", - "p1rla", - "p1sc10", - "p1sc1o", - "p1sci0", - "p1scio", - "p1sser", - "pa11e", - "pa1le", - "pal1e", - "palle", - "pane1e1r0", - "pane1e1ro", - "pane1eir0", - "pane1eiro", - "panele1r0", - "panele1ro", - "paneleir0", - "paneleiro", - "patakha", - "pec0r1na", - "pec0rina", - "pecor1na", - "pecorina", - "pen1s", - "pendej0", - "pendejo", - "penis", - "pip1", - "pipi", - "pir1a", - "pirla", - "pisc10", - "pisc1o", - "pisci0", - "piscio", - "pisser", - "po0p", - "po11a", - "po1la", - "pol1a", - "polla", - "pomp1n0", - "pomp1no", - "pompin0", - "pompino", - "poop", - "porca", - "porn", - "porra", - "pouff1asse", - "pouffiasse", - "pr1ck", - "prick", - "pussy", - "put1za", - "puta", - "puta1n", - "putain", - "pute", - "putiza", - "puttana", - "queca", - "r0mp1ba11e", - "r0mp1ba1le", - "r0mp1bal1e", - "r0mp1balle", - "r0mpiba11e", - "r0mpiba1le", - "r0mpibal1e", - "r0mpiballe", - "rand1", - "randi", - "rape", - "recch10ne", - "recch1one", - "recchi0ne", - "recchione", - "retard", - "romp1ba11e", - "romp1ba1le", - "romp1bal1e", - "romp1balle", - "rompiba11e", - "rompiba1le", - "rompibal1e", - "rompiballe", - "ruff1an0", - "ruff1ano", - "ruffian0", - "ruffiano", - "s1ut", - "sa10pe", - "sa1aud", - "sa1ope", - "sacanagem", - "sal0pe", - "salaud", - "salope", - "saugnapf", - "sb0rr0ne", - "sb0rra", - "sb0rrone", - "sbattere", - "sbatters1", - "sbattersi", - "sborr0ne", - "sborra", - "sborrone", - "sc0pare", - "sc0pata", - "sch1ampe", - "sche1se", - "sche1sse", - "scheise", - "scheisse", - "schlampe", - "schwachs1nn1g", - "schwachs1nnig", - "schwachsinn1g", - "schwachsinnig", - "schwanz", - "scopare", - "scopata", - "sexy", - "sh1t", - "shit", - "slut", - "sp0mp1nare", - "sp0mpinare", - "spomp1nare", - "spompinare", - "str0nz0", - "str0nza", - "str0nzo", - "stronz0", - "stronza", - "stronzo", - "stup1d", - "stupid", - "succh1am1", - "succh1ami", - "succhiam1", - "succhiami", - "sucker", - "t0pa", - "tapette", - "test1c1e", - "test1cle", - "testic1e", - "testicle", - "tette", - "topa", - "tr01a", - "tr0ia", - "tr0mbare", - "tr1ng1er", - "tr1ngler", - "tring1er", - "tringler", - "tro1a", - "troia", - "trombare", - "turd", - "twat", - "vaffancu10", - "vaffancu1o", - "vaffancul0", - "vaffanculo", - "vag1na", - "vagina", - "verdammt", - "verga", - "w1chsen", - "wank", - "wichsen", - "x0ch0ta", - "x0chota", - "xana", - "xoch0ta", - "xochota", - "z0cc01a", - "z0cc0la", - "z0cco1a", - "z0ccola", - "z1z1", - "z1zi", - "ziz1", - "zizi", - "zocc01a", - "zocc0la", - "zocco1a", - "zoccola", - }, +pub const default_blocklist = [_][]const u8{ + "0rgasm", + "1d10t", + "1d1ot", + "1di0t", + "1diot", + "1eccacu10", + "1eccacu1o", + "1eccacul0", + "1eccaculo", + "1mbec11e", + "1mbec1le", + "1mbeci1e", + "1mbecile", + "a11upat0", + "a11upato", + "a1lupat0", + "a1lupato", + "aand", + "ah01e", + "ah0le", + "aho1e", + "ahole", + "al1upat0", + "al1upato", + "allupat0", + "allupato", + "ana1", + "ana1e", + "anal", + "anale", + "anus", + "arrapat0", + "arrapato", + "arsch", + "arse", + "ass", + "b00b", + "b00be", + "b01ata", + "b0ceta", + "b0iata", + "b0ob", + "b0obe", + "b0sta", + "b1tch", + "b1te", + "b1tte", + "ba1atkar", + "balatkar", + "bastard0", + "bastardo", + "batt0na", + "battona", + "bitch", + "bite", + "bitte", + "bo0b", + "bo0be", + "bo1ata", + "boceta", + "boiata", + "boob", + "boobe", + "bosta", + "bran1age", + "bran1er", + "bran1ette", + "bran1eur", + "bran1euse", + "branlage", + "branler", + "branlette", + "branleur", + "branleuse", + "c0ck", + "c0g110ne", + "c0g11one", + "c0g1i0ne", + "c0g1ione", + "c0gl10ne", + "c0gl1one", + "c0gli0ne", + "c0glione", + "c0na", + "c0nnard", + "c0nnasse", + "c0nne", + "c0u111es", + "c0u11les", + "c0u1l1es", + "c0u1lles", + "c0ui11es", + "c0ui1les", + "c0uil1es", + "c0uilles", + "c11t", + "c11t0", + "c11to", + "c1it", + "c1it0", + "c1ito", + "cabr0n", + "cabra0", + "cabrao", + "cabron", + "caca", + "cacca", + "cacete", + "cagante", + "cagar", + "cagare", + "cagna", + "cara1h0", + "cara1ho", + "caracu10", + "caracu1o", + "caracul0", + "caraculo", + "caralh0", + "caralho", + "cazz0", + "cazz1mma", + "cazzata", + "cazzimma", + "cazzo", + "ch00t1a", + "ch00t1ya", + "ch00tia", + "ch00tiya", + "ch0d", + "ch0ot1a", + "ch0ot1ya", + "ch0otia", + "ch0otiya", + "ch1asse", + "ch1avata", + "ch1er", + "ch1ng0", + "ch1ngadaz0s", + "ch1ngadazos", + "ch1ngader1ta", + "ch1ngaderita", + "ch1ngar", + "ch1ngo", + "ch1ngues", + "ch1nk", + "chatte", + "chiasse", + "chiavata", + "chier", + "ching0", + "chingadaz0s", + "chingadazos", + "chingader1ta", + "chingaderita", + "chingar", + "chingo", + "chingues", + "chink", + "cho0t1a", + "cho0t1ya", + "cho0tia", + "cho0tiya", + "chod", + "choot1a", + "choot1ya", + "chootia", + "chootiya", + "cl1t", + "cl1t0", + "cl1to", + "clit", + "clit0", + "clito", + "cock", + "cog110ne", + "cog11one", + "cog1i0ne", + "cog1ione", + "cogl10ne", + "cogl1one", + "cogli0ne", + "coglione", + "cona", + "connard", + "connasse", + "conne", + "cou111es", + "cou11les", + "cou1l1es", + "cou1lles", + "coui11es", + "coui1les", + "couil1es", + "couilles", + "cracker", + "crap", + "cu10", + "cu1att0ne", + "cu1attone", + "cu1er0", + "cu1ero", + "cu1o", + "cul0", + "culatt0ne", + "culattone", + "culer0", + "culero", + "culo", + "cum", + "cunt", + "d11d0", + "d11do", + "d1ck", + "d1ld0", + "d1ldo", + "damn", + "de1ch", + "deich", + "depp", + "di1d0", + "di1do", + "dick", + "dild0", + "dildo", + "dyke", + "encu1e", + "encule", + "enema", + "enf01re", + "enf0ire", + "enfo1re", + "enfoire", + "estup1d0", + "estup1do", + "estupid0", + "estupido", + "etr0n", + "etron", + "f0da", + "f0der", + "f0ttere", + "f0tters1", + "f0ttersi", + "f0tze", + "f0utre", + "f1ca", + "f1cker", + "f1ga", + "fag", + "fica", + "ficker", + "figa", + "foda", + "foder", + "fottere", + "fotters1", + "fottersi", + "fotze", + "foutre", + "fr0c10", + "fr0c1o", + "fr0ci0", + "fr0cio", + "fr0sc10", + "fr0sc1o", + "fr0sci0", + "fr0scio", + "froc10", + "froc1o", + "froci0", + "frocio", + "frosc10", + "frosc1o", + "frosci0", + "froscio", + "fuck", + "g00", + "g0o", + "g0u1ne", + "g0uine", + "gandu", + "go0", + "goo", + "gou1ne", + "gouine", + "gr0gnasse", + "grognasse", + "haram1", + "harami", + "haramzade", + "hund1n", + "hundin", + "id10t", + "id1ot", + "idi0t", + "idiot", + "imbec11e", + "imbec1le", + "imbeci1e", + "imbecile", + "j1zz", + "jerk", + "jizz", + "k1ke", + "kam1ne", + "kamine", + "kike", + "leccacu10", + "leccacu1o", + "leccacul0", + "leccaculo", + "m1erda", + "m1gn0tta", + "m1gnotta", + "m1nch1a", + "m1nchia", + "m1st", + "mam0n", + "mamahuev0", + "mamahuevo", + "mamon", + "masturbat10n", + "masturbat1on", + "masturbate", + "masturbati0n", + "masturbation", + "merd0s0", + "merd0so", + "merda", + "merde", + "merdos0", + "merdoso", + "mierda", + "mign0tta", + "mignotta", + "minch1a", + "minchia", + "mist", + "musch1", + "muschi", + "n1gger", + "neger", + "negr0", + "negre", + "negro", + "nerch1a", + "nerchia", + "nigger", + "orgasm", + "p00p", + "p011a", + "p01la", + "p0l1a", + "p0lla", + "p0mp1n0", + "p0mp1no", + "p0mpin0", + "p0mpino", + "p0op", + "p0rca", + "p0rn", + "p0rra", + "p0uff1asse", + "p0uffiasse", + "p1p1", + "p1pi", + "p1r1a", + "p1rla", + "p1sc10", + "p1sc1o", + "p1sci0", + "p1scio", + "p1sser", + "pa11e", + "pa1le", + "pal1e", + "palle", + "pane1e1r0", + "pane1e1ro", + "pane1eir0", + "pane1eiro", + "panele1r0", + "panele1ro", + "paneleir0", + "paneleiro", + "patakha", + "pec0r1na", + "pec0rina", + "pecor1na", + "pecorina", + "pen1s", + "pendej0", + "pendejo", + "penis", + "pip1", + "pipi", + "pir1a", + "pirla", + "pisc10", + "pisc1o", + "pisci0", + "piscio", + "pisser", + "po0p", + "po11a", + "po1la", + "pol1a", + "polla", + "pomp1n0", + "pomp1no", + "pompin0", + "pompino", + "poop", + "porca", + "porn", + "porra", + "pouff1asse", + "pouffiasse", + "pr1ck", + "prick", + "pussy", + "put1za", + "puta", + "puta1n", + "putain", + "pute", + "putiza", + "puttana", + "queca", + "r0mp1ba11e", + "r0mp1ba1le", + "r0mp1bal1e", + "r0mp1balle", + "r0mpiba11e", + "r0mpiba1le", + "r0mpibal1e", + "r0mpiballe", + "rand1", + "randi", + "rape", + "recch10ne", + "recch1one", + "recchi0ne", + "recchione", + "retard", + "romp1ba11e", + "romp1ba1le", + "romp1bal1e", + "romp1balle", + "rompiba11e", + "rompiba1le", + "rompibal1e", + "rompiballe", + "ruff1an0", + "ruff1ano", + "ruffian0", + "ruffiano", + "s1ut", + "sa10pe", + "sa1aud", + "sa1ope", + "sacanagem", + "sal0pe", + "salaud", + "salope", + "saugnapf", + "sb0rr0ne", + "sb0rra", + "sb0rrone", + "sbattere", + "sbatters1", + "sbattersi", + "sborr0ne", + "sborra", + "sborrone", + "sc0pare", + "sc0pata", + "sch1ampe", + "sche1se", + "sche1sse", + "scheise", + "scheisse", + "schlampe", + "schwachs1nn1g", + "schwachs1nnig", + "schwachsinn1g", + "schwachsinnig", + "schwanz", + "scopare", + "scopata", + "sexy", + "sh1t", + "shit", + "slut", + "sp0mp1nare", + "sp0mpinare", + "spomp1nare", + "spompinare", + "str0nz0", + "str0nza", + "str0nzo", + "stronz0", + "stronza", + "stronzo", + "stup1d", + "stupid", + "succh1am1", + "succh1ami", + "succhiam1", + "succhiami", + "sucker", + "t0pa", + "tapette", + "test1c1e", + "test1cle", + "testic1e", + "testicle", + "tette", + "topa", + "tr01a", + "tr0ia", + "tr0mbare", + "tr1ng1er", + "tr1ngler", + "tring1er", + "tringler", + "tro1a", + "troia", + "trombare", + "turd", + "twat", + "vaffancu10", + "vaffancu1o", + "vaffancul0", + "vaffanculo", + "vag1na", + "vagina", + "verdammt", + "verga", + "w1chsen", + "wank", + "wichsen", + "x0ch0ta", + "x0chota", + "xana", + "xoch0ta", + "xochota", + "z0cc01a", + "z0cc0la", + "z0cco1a", + "z0ccola", + "z1z1", + "z1zi", + "ziz1", + "zizi", + "zocc01a", + "zocc0la", + "zocco1a", + "zoccola", }; diff --git a/src/root.zig b/src/root.zig index b25739b..ecfe62b 100644 --- a/src/root.zig +++ b/src/root.zig @@ -13,23 +13,22 @@ pub const Error = error{ ReachedMaxAttempts, }; -const blocklist_module = @import("blocklist.zig"); -pub const default_blocklist = blocklist_module.default_blocklist; - /// The default alphabet for sqids. pub const default_alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +/// The default blocklist for sqids +pub const default_blocklist = &@import("blocklist.zig").default_blocklist; + /// Options controls the configuration of the sqid encoder. pub const Options = struct { alphabet: []const u8 = default_alphabet, - blocklist: []const []const u8 = default_blocklist.words, + blocklist: []const []const u8 = default_blocklist, min_length: u8 = 0, }; /// Sqids encoder. /// /// Must be created with new to get valid instances. -/// Sqids impose constraints on the alphabet, blocklist and min_length. pub const Sqids = struct { alphabet: []const u8, blocklist: []const []const u8, @@ -99,8 +98,10 @@ pub const Sqids = struct { } }; -/// blocklist_from_words constructs a sanitized blocklist from a list of words. -fn blocklist_from_words( +/// blocklist_from_words allocates a sanitized blocklist from a list of words. +/// +/// Caller owns the memory. +pub fn blocklist_from_words( allocator: mem.Allocator, alphabet: []const u8, words: []const []const u8, @@ -255,6 +256,7 @@ fn estimateEncodingBufferSize( } /// isBlockedID returns true if id collides with the blocklist. +/// Collisions ignore case. fn isBlockedID(blocklist: []const []const u8, id: []const u8) !bool { for (blocklist) |word| { if (word.len > id.len) { From ecf0c59769c05012d3d24740ac49fa03c4ab8a78 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 11:17:00 +0200 Subject: [PATCH 17/26] Update main to run --- src/main.zig | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/main.zig b/src/main.zig index 2b5dea8..e6238bf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,24 +4,17 @@ const mem = std.mem; const Sqids = sqids.Sqids; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); +pub fn main(init: std.process.Init) !void { + var arena = init.arena; + const allocator = arena.allocator(); const numbers = &.{ 1, 2, 3 }; - const opts = sqids.Options{ - // .blocklist = new_blocklist(allocator, alphabet, blocked_words), - // .alphabet = alphabet, - }; - // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.new(opts); - defer s.deinit(); - const id = try s.encode(numbers); + const s = try Sqids.new(.{}); + const id = try s.encode(allocator, numbers); defer allocator.free(id); // Print to stdout. - const stdout = std.io.getStdOut().writer(); - try stdout.print("{s}\n", .{id}); + std.debug.print("{s}\n", .{id}); } From 2093149d4c4bbdf64d5557e088ef1fe219c3f56c Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 11:06:16 +0200 Subject: [PATCH 18/26] Better initialization --- README.md | 8 ++++---- benchmark/bench.zig | 2 +- src/main.zig | 2 +- src/root.zig | 20 +++++++++++++++----- src/tests/alphabet.zig | 12 ++++++------ src/tests/blocklist.zig | 16 ++++++++-------- src/tests/encoding.zig | 10 +++++----- src/tests/minlength.zig | 6 +++--- 8 files changed, 43 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index cf61db1..7352529 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ const sqids = @import("sqids"); Simple encode & decode: ```zig -const s = try sqids.Sqids.new(.{}) +const s: sqids.Sqids = .default; const id = try s.encode(allocator, &.{1, 2, 3}); defer allocator.free(id); // Caller owns the memory. @@ -92,21 +92,21 @@ The `sqids.Options` struct is used at initialization to customize the encoder. Enforce a *minimum* length for IDs: ```zig -const s = try sqids.Sqids.new(.{.min_length = 10}); +const s = try sqids.Sqids.init(.{.min_length = 10}); const id = try s.encode(allocator, &.{1, 2, 3}); // "86Rf07xd4z" ``` Randomize IDs by providing a custom alphabet: ```zig -const s = try sqids.Sqids.new(.{.alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE"}); +const s = try sqids.Sqids.init(.{.alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE"}); const id = try s.encode(allocator, &.{1, 2, 3}); // "B4aajs" ``` Prevent specific words from appearing anywhere in the auto-generated IDs: ```zig -const s = try sqids.Sqids.new(.{.blocklist = &.{"86Rf07"}}); +const s = try sqids.Sqids.init(.{.blocklist = &.{"86Rf07"}}); const id = try s.encode(allocator, &.{1, 2, 3}); // "se8ojk" ``` diff --git a/benchmark/bench.zig b/benchmark/bench.zig index 161d834..955734d 100644 --- a/benchmark/bench.zig +++ b/benchmark/bench.zig @@ -16,7 +16,7 @@ pub fn main() !void { var ids = try allocator.alloc([]const u8, numbers.len); // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.new(opts); + const s = try Sqids.init(pts); defer s.deinit(); for (numbers, 0..) |ns, i| { diff --git a/src/main.zig b/src/main.zig index e6238bf..191e908 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,7 +11,7 @@ pub fn main(init: std.process.Init) !void { const numbers = &.{ 1, 2, 3 }; // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.new(.{}); + const s: Sqids = .default; const id = try s.encode(allocator, numbers); defer allocator.free(id); diff --git a/src/root.zig b/src/root.zig index ecfe62b..e9ec4b0 100644 --- a/src/root.zig +++ b/src/root.zig @@ -28,13 +28,23 @@ pub const Options = struct { /// Sqids encoder. /// -/// Must be created with new to get valid instances. +/// Use Sqids.default, or create with init to get a valid instance. +/// +/// Smaller blocklist makes encoding more efficient, as the blocklist is traversed for each +/// generated ID. To get a smaller blocklist with only words valid in the used alphabet, use +/// blocklist_from_words. pub const Sqids = struct { alphabet: []const u8, blocklist: []const []const u8, min_length: u8, - pub fn new(opts: Options) !Sqids { + pub const default = Sqids{ + .alphabet = default_alphabet, + .blocklist = default_blocklist, + .min_length = 0, + }; + + pub fn init(opts: Options) !Sqids { // Check alphabet. // TODO(lvignoli): it would be better to "parse not validate", for both the alphabet and the blocklist. if (opts.alphabet.len < 3) { @@ -400,7 +410,7 @@ test "encode" { }; for (cases) |case| { - const sqids = try Sqids.new(.{ .alphabet = case.alphabet }); + const sqids = try Sqids.init(.{ .alphabet = case.alphabet }); const id = try sqids.encode(allocator, case.numbers); defer allocator.free(id); @@ -412,7 +422,7 @@ test "non-empty blocklist" { const allocator = testing.allocator; const blocklist: []const []const u8 = &.{"ArUO"}; - const sqids = try Sqids.new(.{ .blocklist = blocklist }); + const sqids = try Sqids.init(.{ .blocklist = blocklist }); const actual_numbers = try sqids.decode(allocator, "ArUO"); defer allocator.free(actual_numbers); @@ -425,7 +435,7 @@ test "non-empty blocklist" { test "decode" { const allocator = testing.allocator; - const sqids = try Sqids.new(.{ .alphabet = "0123456789abcdef" }); + const sqids = try Sqids.init(.{ .alphabet = "0123456789abcdef" }); const numbers = try sqids.decode(allocator, "489158"); defer allocator.free(numbers); diff --git a/src/tests/alphabet.zig b/src/tests/alphabet.zig index 50f9221..1f597f5 100644 --- a/src/tests/alphabet.zig +++ b/src/tests/alphabet.zig @@ -9,12 +9,12 @@ const Sqids = sqids.Sqids; const testing_allocator = testing.allocator; test "simple" { - const s = try Sqids.new(.{ .alphabet = "0123456789abcdef" }); + const s = try Sqids.init(.{ .alphabet = "0123456789abcdef" }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{ 1, 2, 3 }, "489158"); } test "short" { - const s = try Sqids.new(.{ .alphabet = "abc" }); + const s = try Sqids.init(.{ .alphabet = "abc" }); try utils.expectEncodeDecode(testing_allocator, s, &.{ 1, 2, 3 }); } @@ -22,21 +22,21 @@ test "long" { const alphabet = \\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:\'"/?.>,<`~ ; - const s = try Sqids.new(.{ .alphabet = alphabet }); + const s = try Sqids.init(.{ .alphabet = alphabet }); try utils.expectEncodeDecode(testing_allocator, s, &.{ 1, 2, 3 }); } test "multibyte alphabet" { - const err = Sqids.new(.{ .alphabet = "ë1092" }) catch |err| err; + const err = Sqids.init(.{ .alphabet = "ë1092" }) catch |err| err; try testing.expectError(sqids.Error.NonASCIICharacter, err); } test "repeating alphabet characters" { - const err = Sqids.new(.{ .alphabet = "aabcdefg" }) catch |err| err; + const err = Sqids.init(.{ .alphabet = "aabcdefg" }) catch |err| err; try testing.expectError(sqids.Error.RepeatingAlphabetCharacter, err); } test "too short of an alphabet" { - const err = Sqids.new(.{ .alphabet = "ab" }) catch |err| err; + const err = Sqids.init(.{ .alphabet = "ab" }) catch |err| err; try testing.expectError(sqids.Error.TooShortAlphabet, err); } diff --git a/src/tests/blocklist.zig b/src/tests/blocklist.zig index 9cfb14e..c7dc4ce 100644 --- a/src/tests/blocklist.zig +++ b/src/tests/blocklist.zig @@ -9,20 +9,20 @@ const Sqids = sqids.Sqids; const testing_allocator = testing.allocator; test "if no custom blocklist param, use the default blocklist" { - const s = try Sqids.new(.{}); + const s: Sqids = .default; try utils.expectDecode(testing_allocator, s, "aho1e", &.{4572721}); try utils.expectEncode(testing_allocator, s, &.{4572721}, "JExTR"); } test "if an empty blocklist param passed, don't use any blocklist" { - const s = try Sqids.new(.{ .blocklist = &.{} }); + const s = try Sqids.init(.{ .blocklist = &.{} }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{4572721}, "aho1e"); } test "if a non-empty blocklist param passed, use only that" { - const s = try Sqids.new(.{ .blocklist = &.{"ArUO"} }); + const s = try Sqids.init(.{ .blocklist = &.{"ArUO"} }); try utils.expectEncodeDecodeWithID(testing_allocator, s, &.{4572721}, "aho1e"); @@ -32,7 +32,7 @@ test "if a non-empty blocklist param passed, use only that" { } test "blocklist" { - const s = try Sqids.new(.{ + const s = try Sqids.init(.{ .blocklist = &.{ "JSwXFaosAN", // normal result of 1st encoding, let's block that word on purpose "OCjV9JK64o", // result of 2nd encoding @@ -46,7 +46,7 @@ test "blocklist" { } test "decoding blocklist words should still work" { - const s = try Sqids.new(.{ + const s = try Sqids.init(.{ .blocklist = &.{ "86Rf07", "se8ojk", @@ -64,7 +64,7 @@ test "decoding blocklist words should still work" { } test "match against a short blocklist word" { - const s = try Sqids.new(.{ + const s = try Sqids.init(.{ .blocklist = &.{"pnd"}, }); @@ -72,7 +72,7 @@ test "match against a short blocklist word" { } test "blocklist filtering in constructor" { - const s = try Sqids.new(.{ + const s = try Sqids.init(.{ .alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", .blocklist = &.{"sxnzkl"}, }); @@ -82,7 +82,7 @@ test "blocklist filtering in constructor" { test "max encoding attempts" { // Setup encoder such that alphabet.len == min_length == blocklist.len - const s = try Sqids.new(.{ + const s = try Sqids.init(.{ .alphabet = "abc", .min_length = 3, .blocklist = &.{ "cab", "abc", "bca" }, diff --git a/src/tests/encoding.zig b/src/tests/encoding.zig index 482bd8b..349263e 100644 --- a/src/tests/encoding.zig +++ b/src/tests/encoding.zig @@ -8,7 +8,7 @@ const Sqids = @import("sqids").Sqids; const testing_allocator = testing.allocator; test "default encoder: encode incremental numbers" { - const s = try Sqids.new(.{}); + const s: Sqids = .default; var cases = std.StringHashMap([]const u64).init(testing_allocator); defer cases.deinit(); @@ -62,7 +62,7 @@ test "default encoder: encode incremental numbers" { } test "default encoder: multi input" { - const s = try Sqids.new(.{}); + const s: Sqids = .default; const numbers = [2][]const u64{ &.{ 0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, std.math.maxInt(u64) }, @@ -91,7 +91,7 @@ test "default encoder: multi input" { } test "default encoder: encoding no numbers" { - const s = try Sqids.new(.{}); + const s: Sqids = .default; const output = try s.encode(testing_allocator, &.{}); try testing.expectEqualStrings("", output); @@ -99,7 +99,7 @@ test "default encoder: encoding no numbers" { } test "default encoder: decoding empty string" { - const s = try Sqids.new(.{}); + const s: Sqids = .default; const output = try s.decode(testing_allocator, ""); try testing.expectEqualSlices(u64, &.{}, output); @@ -107,7 +107,7 @@ test "default encoder: decoding empty string" { } test "default encoder: decoding ID with invalid character" { - const s = try Sqids.new(.{}); + const s: Sqids = .default; const output = try s.decode(testing_allocator, "*"); try testing.expectEqualSlices(u64, &.{}, output); diff --git a/src/tests/minlength.zig b/src/tests/minlength.zig index 74528c3..14bc89d 100644 --- a/src/tests/minlength.zig +++ b/src/tests/minlength.zig @@ -36,7 +36,7 @@ test "min length: incremental min length" { while (it.next()) |e| { const min_length = e.key_ptr.*; const id = e.value_ptr.*; - const s = try Sqids.new(.{ .min_length = min_length }); + const s = try Sqids.init(.{ .min_length = min_length }); const got_id = try s.encode(testing_allocator, &numbers); defer testing_allocator.free(got_id); @@ -48,7 +48,7 @@ test "min length: incremental min length" { test "min length: incremental numbers" { const ta = testing_allocator; - const s = try Sqids.new(.{ .min_length = sqids.default_alphabet.len }); + const s = try Sqids.init(.{ .min_length = sqids.default_alphabet.len }); var ids: std.array_hash_map.String([]const u64) = .empty; defer ids.deinit(ta); @@ -85,7 +85,7 @@ test "min length: various" { }; for (min_lengths) |min_length| { - const s = try Sqids.new(.{ .min_length = min_length }); + const s = try Sqids.init(.{ .min_length = min_length }); for (numbers) |ns| { const id = try s.encode(testing_allocator, ns); From 1450a40f3e2b8d6d3713be3f1d4c1b7b375e984a Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 16:26:30 +0200 Subject: [PATCH 19/26] Upgrade CI to Zig 0.16 --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1f7f2e0..a2bc59a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - zig: [0.13.0, master] + zig: [0.16.0, master] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} name: Zig ${{ matrix.zig }} on ${{ matrix.os }} From b427cd179586ae31b56fa8192be7d4a6bfaeb9b4 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 16:26:30 +0200 Subject: [PATCH 20/26] Do not build docs on pull request --- .github/workflows/docs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 06cbd5a..3fc2e4e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,8 +3,6 @@ name: docs on: push: branches: ["main"] - pull_request: - branches: ["main"] workflow_dispatch: jobs: From dbee314a536648800a50e3a63dd1fd6d50faa059 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 16:26:30 +0200 Subject: [PATCH 21/26] Update CI for 0.16 only, change zig setup action --- .github/workflows/build-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index a2bc59a..2e4cee5 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -10,14 +10,14 @@ jobs: strategy: fail-fast: false matrix: - zig: [0.16.0, master] + zig: [0.16.0] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} name: Zig ${{ matrix.zig }} on ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup Zig - uses: korandoru/setup-zig@v1 + uses: mlugg/setup-zig@v2 with: zig-version: ${{ matrix.zig }} - run: zig build test From 570ac1052deb23d7d542947048b9256c98c24982 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 16:26:30 +0200 Subject: [PATCH 22/26] Update bench.zig to be runnable --- benchmark/bench.zig | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/benchmark/bench.zig b/benchmark/bench.zig index 955734d..1ea89a3 100644 --- a/benchmark/bench.zig +++ b/benchmark/bench.zig @@ -7,22 +7,21 @@ const numbers = numbers_file.numbers; const Sqids = sqids.Sqids; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - const opts = sqids.Options{ .blocklist = undefined }; +pub fn main(init: std.process.Init) !void { + const allocator = init.arena.allocator(); var ids = try allocator.alloc([]const u8, numbers.len); + defer allocator.free(ids); // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.init(pts); - defer s.deinit(); + const s = try Sqids.init(.{ .blocklist = undefined }); for (numbers, 0..) |ns, i| { - const id = try s.encode(&ns); + const id = try s.encode(allocator, &ns); ids[i] = id; } - allocator.free(ids); + for (ids) |id| { + allocator.free(id); + } } From 7b9ed6cce02f42dea220b3188a35c661e50f0c70 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 16:26:30 +0200 Subject: [PATCH 23/26] Bump version to 0.4 There were breaking changes on the API, and upgrade to Zig 0.16. --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index f77b3ab..3c795b4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .sqids, .fingerprint = 0x2448d471e1087204, - .version = "0.3.0", + .version = "0.4.0", .minimum_zig_version = "0.16.0", .dependencies = .{}, .paths = .{ From e0b1b8f2d08cbd233b58d482a7a04b3d655e3ee0 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Mon, 11 May 2026 16:26:30 +0200 Subject: [PATCH 24/26] Add a changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ba3e353 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## Unreleased + +## [v0.4.0] + +This release contains performance improvements, breaking changes in public API, and upgrades the +library to Zig 0.16. + +## Changed +- Use `Sqids.init` to create a new Sqids encoder/decoder instance. + There is no need for an allocator anymore. + User is responsible for managing the blocklist. + There is no need to deinit instances. +- `Sqids.encode` and `Sqids.decode` now both take an allocator. Caller owns the memory. + +## Added +- `blocklist_from_words` is now a public function, intended for users to create minimal blocklists for their input alphabet. +- `Sqids.default` is a handy shortcut to a Sqids instance with default alphabet, default blocklist + and no minimum length. + +## Removed +- `Sqids.deinit` + +[0.4.0]: https://github.com/sqids/sqids-zig/compare/v0.3.0...v0.4.0 From c0e0f081379a43ded90b1f232768316dd71cfdd5 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Thu, 14 May 2026 10:01:31 +0200 Subject: [PATCH 25/26] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7352529..77d85b2 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ const s = try sqids.Sqids.init(.{.blocklist = &.{"86Rf07"}}); const id = try s.encode(allocator, &.{1, 2, 3}); // "se8ojk" ``` +Use `sqids.blocklist_from_words` to create the minimal blocklist consistent with the working alphabet. + ## 📝 License [MIT](LICENSE) From 82c003cdf9c2897ef5578ca5cf76b1dce7b4e5f4 Mon Sep 17 00:00:00 2001 From: Louis Vignoli Date: Thu, 14 May 2026 10:16:08 +0200 Subject: [PATCH 26/26] Empty blocklist is slice of length 0, not undefined --- benchmark/bench.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/bench.zig b/benchmark/bench.zig index 1ea89a3..de90a64 100644 --- a/benchmark/bench.zig +++ b/benchmark/bench.zig @@ -14,7 +14,7 @@ pub fn main(init: std.process.Init) !void { defer allocator.free(ids); // Using the default Sqids, encode the numbers to a Sqids ID. - const s = try Sqids.init(.{ .blocklist = undefined }); + const s = try Sqids.init(.{ .blocklist = &.{} }); for (numbers, 0..) |ns, i| { const id = try s.encode(allocator, &ns);