os: add randomTmpPath for allocating temp paths (#12465)

Factor TempDir's name generation into a reusable `randomBasename` (16
random bytes, url-safe base64) and add `randomTmpPath` on top, which
composes `allocTmpDir` + `randomBasename` into a single allocated path
in the form `{TMPDIR}/{prefix}{random}` (`mktemp(1)`-ish).

This is convenient for callers who want a unique path under TMPDIR (for
a temporary file, socket, etc.) without having to think about basename
buffer sizing or path joining.

Also, use `std.base64.url_safe_no_pad.Encoder` instead of the custom
base64 alphabet, which is exactly equivalent.
This commit is contained in:
Mitchell Hashimoto
2026-04-25 13:14:30 -07:00
committed by GitHub
3 changed files with 68 additions and 22 deletions

View File

@@ -3,10 +3,8 @@
const TempDir = @This();
const std = @import("std");
const testing = std.testing;
const Dir = std.fs.Dir;
const allocTmpDir = @import("file.zig").allocTmpDir;
const freeTmpDir = @import("file.zig").freeTmpDir;
const file = @import("file.zig");
const log = std.log.scoped(.tempdir);
@@ -18,28 +16,26 @@ parent: Dir,
/// Name buffer that name points into. Generally do not use. To get the
/// name call the name() function.
name_buf: [TMP_PATH_LEN:0]u8,
name_buf: [file.RANDOM_BASENAME_LEN:0]u8,
/// Create the temporary directory.
pub fn init() !TempDir {
// Note: the tmp_path_buf sentinel is important because it ensures
// we actually always have TMP_PATH_LEN+1 bytes of available space. We
// need that so we can set the sentinel in the case we use all the
// possible length.
var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined;
var rand_buf: [RANDOM_BYTES]u8 = undefined;
// we actually always have RANDOM_BASENAME_LEN+1 bytes of available
// space. We need that so we can set the sentinel in the case we use
// all the possible length.
var tmp_path_buf: [file.RANDOM_BASENAME_LEN:0]u8 = undefined;
const dir = dir: {
const cwd = std.fs.cwd();
const tmp_dir = allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
defer freeTmpDir(std.heap.page_allocator, tmp_dir);
const tmp_dir = file.allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
defer file.freeTmpDir(std.heap.page_allocator, tmp_dir);
break :dir try cwd.openDir(tmp_dir, .{});
};
// We now loop forever until we can find a directory that we can create.
while (true) {
std.crypto.random.bytes(rand_buf[0..]);
const tmp_path = b64_encoder.encode(&tmp_path_buf, &rand_buf);
const tmp_path = try file.randomBasename(&tmp_path_buf);
tmp_path_buf[tmp_path.len] = 0;
dir.makeDir(tmp_path) catch |err| switch (err) {
@@ -69,16 +65,9 @@ pub fn deinit(self: *TempDir) void {
log.err("error deleting temp dir err={}", .{err});
}
// The amount of random bytes to get to determine our filename.
const RANDOM_BYTES = 16;
const TMP_PATH_LEN = b64_encoder.calcSize(RANDOM_BYTES);
// Base64 encoder, replacing the standard `+/` with `-_` so that it can
// be used in a file name on any filesystem.
const b64_encoder = std.base64.Base64Encoder.init(b64_alphabet, null);
const b64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".*;
test {
const testing = std.testing;
var td = try init();
errdefer td.deinit();

View File

@@ -79,3 +79,58 @@ pub fn freeTmpDir(allocator: std.mem.Allocator, dir: []const u8) void {
allocator.free(dir);
}
}
const random_basename_bytes = 16;
const b64_encoder = std.base64.url_safe_no_pad.Encoder;
pub const RandomBasenameError = error{BufferTooSmall};
/// Length of the basename produced by `randomBasename`.
pub const RANDOM_BASENAME_LEN = b64_encoder.calcSize(random_basename_bytes);
/// Write a random filesystem-safe base64 basename of length
/// `RANDOM_BASENAME_LEN` into `buf` and return a slice over the
/// written bytes. Returns `error.BufferTooSmall` if `buf` is too
/// short.
pub fn randomBasename(buf: []u8) RandomBasenameError![]const u8 {
if (buf.len < RANDOM_BASENAME_LEN) return error.BufferTooSmall;
var rand_buf: [random_basename_bytes]u8 = undefined;
std.crypto.random.bytes(&rand_buf);
return b64_encoder.encode(buf[0..RANDOM_BASENAME_LEN], &rand_buf);
}
/// Return a freshly-allocated path of the form `{TMPDIR}/{prefix}{random}`.
/// The caller owns the returned slice and must free it with `allocator`.
///
/// Nothing is created on disk; this only builds the path string. Useful
/// for one-shot temporary file/socket paths where a full `TempDir` is
/// overkill.
pub fn randomTmpPath(
allocator: std.mem.Allocator,
prefix: []const u8,
) std.mem.Allocator.Error![]u8 {
const tmp_dir = allocTmpDir(allocator) orelse "/tmp";
defer freeTmpDir(allocator, tmp_dir);
var name_buf: [RANDOM_BASENAME_LEN]u8 = undefined;
const basename = randomBasename(&name_buf) catch unreachable;
return std.fmt.allocPrint(
allocator,
"{s}{c}{s}{s}",
.{ tmp_dir, std.fs.path.sep, prefix, basename },
);
}
test randomBasename {
const testing = std.testing;
var buf: [RANDOM_BASENAME_LEN]u8 = undefined;
const name = try randomBasename(&buf);
try testing.expectEqual(RANDOM_BASENAME_LEN, name.len);
for (name) |c| {
const ok = std.ascii.isAlphanumeric(c) or c == '-' or c == '_';
try testing.expect(ok);
}
var small: [RANDOM_BASENAME_LEN - 1]u8 = undefined;
try testing.expectError(error.BufferTooSmall, randomBasename(&small));
}

View File

@@ -52,6 +52,7 @@ pub const fixMaxFiles = file.fixMaxFiles;
pub const restoreMaxFiles = file.restoreMaxFiles;
pub const allocTmpDir = file.allocTmpDir;
pub const freeTmpDir = file.freeTmpDir;
pub const randomTmpPath = file.randomTmpPath;
pub const isFlatpak = flatpak.isFlatpak;
pub const FlatpakHostCommand = flatpak.FlatpakHostCommand;
pub const home = homedir.home;
@@ -67,6 +68,7 @@ pub const ShellEscapeWriter = shell.ShellEscapeWriter;
pub const getKernelInfo = kernel_info.getKernelInfo;
test {
_ = file;
_ = i18n;
_ = path;
_ = uri;