const std = @import("std");const io_mod = @import("../shared/io.zig"); const Allocator = std.mem.Allocator;const session_id_random_bytes: usize = 8; pub fn sessionDirPath(alloc: Allocator, sessions_dir: []const u8, session_id: []const u8) ![]u8 { try validateSessionId(session_id); return std.fs.path.join(alloc, &.{ sessions_dir, session_id });} pub fn validateSessionId(session_id: []const u8) !void { if (session_id.len == 0 or session_id.len > 255 or std.mem.eql(u8, session_id, ".") or std.mem.eql(u8, session_id, "..")) { return error.InvalidSessionId; } for (session_id) |byte| { if (!std.ascii.isAlphanumeric(byte) and byte != '.' and byte != '_' and byte != '-') { return error.InvalidSessionId; } }} pub fn generateSessionId(alloc: Allocator) ![]u8 { var random_bytes: [session_id_random_bytes]u8 = undefined; io_mod.getIo().random(&random_bytes); const random_hex = std.fmt.bytesToHex(random_bytes, .lower); return std.fmt.allocPrint(alloc, "{d}-{d}-{s}", .{ io_mod.milliTimestamp(), io_mod.nanoTimestamp(), random_hex });} fn isLowerHex(text: []const u8) bool { for (text) |c| { if (!((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'))) return false; } return true;} test "generated session id includes random hex suffix" { const id = try generateSessionId(std.testing.allocator); defer std.testing.allocator.free(id); var segments = std.mem.splitScalar(u8, id, '-'); const millis = segments.next() orelse return error.TestExpectedEqual; const nanos = segments.next() orelse return error.TestExpectedEqual; const suffix = segments.next() orelse return error.TestExpectedEqual; try std.testing.expect(segments.next() == null); try std.testing.expect(millis.len > 0); try std.testing.expect(nanos.len > 0); _ = try std.fmt.parseInt(i64, millis, 10); _ = try std.fmt.parseInt(i128, nanos, 10); try std.testing.expectEqual(@as(usize, 16), suffix.len); try std.testing.expect(isLowerHex(suffix));} test "session directory path rejects unsafe ids" { const alloc = std.testing.allocator; inline for (.{ "", ".", "..", "../outside", "/tmp/outside", "nested/session", "nested\\session" }) |id| { try std.testing.expectError(error.InvalidSessionId, sessionDirPath(alloc, "/tmp/sessions", id)); } const unsupported = [_]u8{ 0xff, 'a' }; try std.testing.expectError( error.InvalidSessionId, sessionDirPath(alloc, "/tmp/sessions", &unsupported), ); var too_long: [256]u8 = undefined; @memset(&too_long, 'a'); try std.testing.expectError( error.InvalidSessionId, sessionDirPath(alloc, "/tmp/sessions", &too_long), ); const dotted = try sessionDirPath(alloc, "/tmp/sessions", "session.v3"); defer alloc.free(dotted); try std.testing.expectEqualStrings("/tmp/sessions/session.v3", dotted); inline for (.{ ".hidden-session", "session..branch", "last", "resume", }) |id| { const valid = try sessionDirPath(alloc, "/tmp/sessions", id); alloc.free(valid); } const nul_id = [_]u8{ 'a', 0, 'b' }; try std.testing.expectError( error.InvalidSessionId, sessionDirPath(alloc, "/tmp/sessions", &nul_id), ); var max_id: [255]u8 = undefined; @memset(&max_id, 'a'); const maximum = try sessionDirPath(alloc, "/tmp/sessions", &max_id); defer alloc.free(maximum); try std.testing.expect(std.mem.endsWith(u8, maximum, &max_id));}
const std = @import("std");
const io_mod = @import("../shared/io.zig");
const Allocator = std.mem.Allocator;
const session_id_random_bytes: usize = 8;
pub fn sessionDirPath(alloc: Allocator, sessions_dir: []const u8, session_id: []const u8) ![]u8 {
try validateSessionId(session_id);
return std.fs.path.join(alloc, &.{ sessions_dir, session_id });
}
pub fn validateSessionId(session_id: []const u8) !void {
if (session_id.len == 0 or session_id.len > 255 or
std.mem.eql(u8, session_id, ".") or std.mem.eql(u8, session_id, ".."))
{
return error.InvalidSessionId;
}
for (session_id) |byte| {
if (!std.ascii.isAlphanumeric(byte) and byte != '.' and byte != '_' and byte != '-') {
return error.InvalidSessionId;
}
}
}
pub fn generateSessionId(alloc: Allocator) ![]u8 {
var random_bytes: [session_id_random_bytes]u8 = undefined;
io_mod.getIo().random(&random_bytes);
const random_hex = std.fmt.bytesToHex(random_bytes, .lower);
return std.fmt.allocPrint(alloc, "{d}-{d}-{s}", .{ io_mod.milliTimestamp(), io_mod.nanoTimestamp(), random_hex });
}
fn isLowerHex(text: []const u8) bool {
for (text) |c| {
if (!((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'))) return false;
}
return true;
}
test "generated session id includes random hex suffix" {
const id = try generateSessionId(std.testing.allocator);
defer std.testing.allocator.free(id);
var segments = std.mem.splitScalar(u8, id, '-');
const millis = segments.next() orelse return error.TestExpectedEqual;
const nanos = segments.next() orelse return error.TestExpectedEqual;
const suffix = segments.next() orelse return error.TestExpectedEqual;
try std.testing.expect(segments.next() == null);
try std.testing.expect(millis.len > 0);
try std.testing.expect(nanos.len > 0);
_ = try std.fmt.parseInt(i64, millis, 10);
_ = try std.fmt.parseInt(i128, nanos, 10);
try std.testing.expectEqual(@as(usize, 16), suffix.len);
try std.testing.expect(isLowerHex(suffix));
}
test "session directory path rejects unsafe ids" {
const alloc = std.testing.allocator;
inline for (.{ "", ".", "..", "../outside", "/tmp/outside", "nested/session", "nested\\session" }) |id| {
try std.testing.expectError(error.InvalidSessionId, sessionDirPath(alloc, "/tmp/sessions", id));
}
const unsupported = [_]u8{ 0xff, 'a' };
try std.testing.expectError(
error.InvalidSessionId,
sessionDirPath(alloc, "/tmp/sessions", &unsupported),
);
var too_long: [256]u8 = undefined;
@memset(&too_long, 'a');
try std.testing.expectError(
error.InvalidSessionId,
sessionDirPath(alloc, "/tmp/sessions", &too_long),
);
const dotted = try sessionDirPath(alloc, "/tmp/sessions", "session.v3");
defer alloc.free(dotted);
try std.testing.expectEqualStrings("/tmp/sessions/session.v3", dotted);
inline for (.{
".hidden-session",
"session..branch",
"last",
"resume",
}) |id| {
const valid = try sessionDirPath(alloc, "/tmp/sessions", id);
alloc.free(valid);
}
const nul_id = [_]u8{ 'a', 0, 'b' };
try std.testing.expectError(
error.InvalidSessionId,
sessionDirPath(alloc, "/tmp/sessions", &nul_id),
);
var max_id: [255]u8 = undefined;
@memset(&max_id, 'a');
const maximum = try sessionDirPath(alloc, "/tmp/sessions", &max_id);
defer alloc.free(maximum);
try std.testing.expect(std.mem.endsWith(u8, maximum, &max_id));
}