Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ pub fn build(b: *std.Build) void {
"tests/list_cli_test.zig",
"tests/tap_cli_test.zig",
"tests/uses_cli_test.zig",
"tests/text_replace_test.zig",
};

const test_step = b.step("test", "Run all unit tests");
Expand Down
43 changes: 2 additions & 41 deletions src/core/dsl/builtins/inreplace.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const values = @import("../values.zig");
const sandbox = @import("../sandbox.zig");
const pathname = @import("pathname.zig");
const output = @import("../../../ui/output.zig");
const text_replace = @import("../../../text_replace.zig");

const Value = values.Value;
const BuiltinError = pathname.BuiltinError;
Expand Down Expand Up @@ -43,7 +44,7 @@ pub fn inreplace(ctx: ExecCtx, _: ?Value, args: []const Value) BuiltinError!Valu

// Replace all occurrences
if (from.len == 0) return Value{ .nil = {} };
const new_content = replaceAll(ctx.allocator, content, from, to) catch {
const new_content = text_replace.replaceAll(ctx.allocator, content, from, to) catch {
return Value{ .nil = {} };
};

Expand All @@ -65,46 +66,6 @@ pub fn inreplace(ctx: ExecCtx, _: ?Value, args: []const Value) BuiltinError!Valu
return Value{ .nil = {} };
}

/// Replace all occurrences of `needle` with `replacement` in `haystack`.
fn replaceAll(allocator: std.mem.Allocator, haystack: []const u8, needle: []const u8, replacement: []const u8) ![]const u8 {
// Count occurrences
var count: usize = 0;
var pos: usize = 0;
while (pos <= haystack.len -| needle.len) {
if (std.mem.startsWith(u8, haystack[pos..], needle)) {
count += 1;
pos += needle.len;
} else {
pos += 1;
}
}

if (count == 0) return try allocator.dupe(u8, haystack);

const new_len = haystack.len - (count * needle.len) + (count * replacement.len);
const buf = try allocator.alloc(u8, new_len);

var src: usize = 0;
var dst: usize = 0;
while (src <= haystack.len -| needle.len) {
if (std.mem.startsWith(u8, haystack[src..], needle)) {
@memcpy(buf[dst..][0..replacement.len], replacement);
dst += replacement.len;
src += needle.len;
} else {
buf[dst] = haystack[src];
dst += 1;
src += 1;
}
}
// Copy remaining tail
if (src < haystack.len) {
@memcpy(buf[dst..][0 .. haystack.len - src], haystack[src..]);
}

return buf;
}

/// Write content atomically: write to temp file then rename over original.
/// Returns one of `AtomicWriteError` to identify the failing step; the
/// underlying error name is written to `underlying_name_out` so the caller
Expand Down
94 changes: 51 additions & 43 deletions src/core/dsl/builtins/string.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const std = @import("std");
const values = @import("../values.zig");
const pathname = @import("pathname.zig");
const text_replace = @import("../../../text_replace.zig");

const Value = values.Value;
const BuiltinError = pathname.BuiltinError;
Expand All @@ -23,48 +24,6 @@ fn receiverStr(allocator: std.mem.Allocator, receiver: ?Value) BuiltinError![]co
};
}

/// Replace all occurrences of `needle` with `replacement` in `haystack`.
fn replaceAll(allocator: std.mem.Allocator, haystack: []const u8, needle: []const u8, replacement: []const u8) BuiltinError![]const u8 {
if (needle.len == 0) return allocator.dupe(u8, haystack) catch return BuiltinError.OutOfMemory;

// Count occurrences to compute result size.
var count: usize = 0;
var pos: usize = 0;
while (pos <= haystack.len -| needle.len) {
if (std.mem.startsWith(u8, haystack[pos..], needle)) {
count += 1;
pos += needle.len;
} else {
pos += 1;
}
}

if (count == 0) return allocator.dupe(u8, haystack) catch return BuiltinError.OutOfMemory;

const new_len = haystack.len - (count * needle.len) + (count * replacement.len);
const buf = allocator.alloc(u8, new_len) catch return BuiltinError.OutOfMemory;

var src: usize = 0;
var dst: usize = 0;
while (src <= haystack.len -| needle.len) {
if (std.mem.startsWith(u8, haystack[src..], needle)) {
@memcpy(buf[dst..][0..replacement.len], replacement);
dst += replacement.len;
src += needle.len;
} else {
buf[dst] = haystack[src];
dst += 1;
src += 1;
}
}
// Copy remaining tail bytes (less than needle.len).
if (src < haystack.len) {
@memcpy(buf[dst..][0 .. haystack.len - src], haystack[src..]);
}

return buf;
}

/// Replace only the first occurrence of `needle` with `replacement`.
fn replaceFirst(allocator: std.mem.Allocator, haystack: []const u8, needle: []const u8, replacement: []const u8) BuiltinError![]const u8 {
if (needle.len == 0) return allocator.dupe(u8, haystack) catch return BuiltinError.OutOfMemory;
Expand All @@ -91,7 +50,10 @@ pub fn gsub(ctx: ExecCtx, receiver: ?Value, args: []const Value) BuiltinError!Va
if (args.len < 2) return Value{ .string = s };
const pattern = try args[0].asString(ctx.allocator);
const replacement = try args[1].asString(ctx.allocator);
return Value{ .string = try replaceAll(ctx.allocator, s, pattern, replacement) };
return Value{
.string = text_replace.replaceAll(ctx.allocator, s, pattern, replacement) catch
return BuiltinError.OutOfMemory,
};
}

/// gsub!(pattern, replacement) — in-place gsub (same as gsub for immutable strings)
Expand Down Expand Up @@ -321,3 +283,49 @@ fn parseLeadingInt(s: []const u8) i64 {
}
return if (saw_digit) n else 0;
}

// ---------------------------------------------------------------------------
// Inline tests
// ---------------------------------------------------------------------------

fn testCtx(allocator: std.mem.Allocator) ExecCtx {
return .{
.allocator = allocator,
// The string builtins only touch `allocator`; remaining fields are
// structurally required by ExecCtx but never read on these paths.
.io = undefined,
.environ = undefined,
.cellar_path = "",
.malt_prefix = "",
};
}

test "gsub rewrites every occurrence and routes through the shared replacer" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const ctx = testCtx(arena.allocator());

const args = [_]Value{ .{ .string = "AAA" }, .{ .string = "X" } };
const out = try gsub(ctx, .{ .string = "headAAAmidAAAtail" }, &args);
try std.testing.expectEqualStrings("headXmidXtail", out.string);
}

test "gsub with no match returns the receiver bytes unchanged" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const ctx = testCtx(arena.allocator());

const args = [_]Value{ .{ .string = "absent" }, .{ .string = "Z" } };
const out = try gsub(ctx, .{ .string = "no needles here" }, &args);
try std.testing.expectEqualStrings("no needles here", out.string);
}

test "gsub with empty replacement deletes every match" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const ctx = testCtx(arena.allocator());

const args = [_]Value{ .{ .string = "X-" }, .{ .string = "" } };
const out = try gsub(ctx, .{ .string = "remove-X-and-X-everything" }, &args);
try std.testing.expectEqualStrings("remove-and-everything", out.string);
}
1 change: 1 addition & 0 deletions src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub const perms = @import("core/perms.zig");
pub const sandbox_macos = @import("core/sandbox/macos.zig");
pub const main_mod = @import("main.zig");
pub const app_ctx = @import("app_ctx.zig");
pub const text_replace = @import("text_replace.zig");

test {
@import("std").testing.refAllDecls(@This());
Expand Down
59 changes: 2 additions & 57 deletions src/macho/patcher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

const std = @import("std");
const parser = @import("parser.zig");
const text_replace = @import("../text_replace.zig");

pub const PatchError = error{
PathTooLong,
Expand Down Expand Up @@ -403,60 +404,4 @@ fn hasPrefix(path: []const u8, prefix: []const u8) bool {
return std.mem.eql(u8, path[0..prefix.len], prefix);
}

/// Replace all occurrences of `needle` with `replacement` in `haystack`.
/// Returns the original slice (same pointer) if there were no matches, or
/// a caller-owned allocation with the substitution applied. Uses
/// `std.mem.findPos` which is memchr-based and significantly faster
/// than a naive byte-by-byte `mem.eql` loop for small needles — the old
/// implementation showed up as ~60 samples on `mem.eqlBytes` in the
/// warm-ffmpeg profile.
fn replaceAll(allocator: std.mem.Allocator, haystack: []const u8, needle: []const u8, replacement: []const u8) ![]const u8 {
if (needle.len == 0) return haystack;

// Fast path: no matches at all → return the original slice unchanged.
const first = std.mem.indexOf(u8, haystack, needle) orelse return haystack;

// Count the remaining matches so we can preallocate exactly.
// (indexOfPos is O(n) per call; the total work is one linear pass.)
var match_count: usize = 1;
var probe = first + needle.len;
while (std.mem.findPos(u8, haystack, probe, needle)) |p| {
match_count += 1;
probe = p + needle.len;
}

const rep_len = replacement.len;
const ndl_len = needle.len;
const new_len = haystack.len + match_count * rep_len - match_count * ndl_len;
const buf = try allocator.alloc(u8, new_len);
errdefer allocator.free(buf);

// Second pass: copy segments between matches and write the replacement
// at each match position. Uses indexOfPos for the fast scan.
var src: usize = 0;
var dst: usize = 0;
var match = first;
while (true) {
// Copy the segment leading up to `match`.
const segment_len = match - src;
if (segment_len > 0) {
@memcpy(buf[dst .. dst + segment_len], haystack[src..match]);
dst += segment_len;
}
// Emit replacement.
@memcpy(buf[dst .. dst + rep_len], replacement);
dst += rep_len;
src = match + ndl_len;

match = std.mem.findPos(u8, haystack, src, needle) orelse break;
}

// Tail: everything after the last match.
if (src < haystack.len) {
@memcpy(buf[dst .. dst + (haystack.len - src)], haystack[src..]);
dst += haystack.len - src;
}

std.debug.assert(dst == new_len);
return buf;
}
const replaceAll = text_replace.replaceAll;
Loading
Loading