Skip to content

Random Port Generator — Zig source

Generate one or many random TCP/UDP port numbers across registered, ephemeral, or the full range - optionally unique. Runs entirely in your browser with crypto-grade randomness.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// random-port-generator — Zig port: random/dynamic port picker.
//
// Display port of the CosmoDev Random Port Generator tool — same contract as
// cli/random-port-generator/random-port-generator.go (the live Go twin) and
// src/lib/random-port.ts (the canonical TypeScript implementation). The
// default rng is std.crypto.random (a CSPRNG standing in for the TS
// crypto.getRandomValues default); invalid custom ranges return
// error.InvalidPortRange instead of throwing.
const std = @import("std");

/// Named ranges — `Range` in the Go twin; registered is the zero-value
/// default, matching the Go twin and the TS default (registered ports).
pub const PortRange = enum { registered, any, ephemeral, custom };

/// PortOptions in the TS lib (count/unique cover randomPorts).
pub const PortOptions = struct {
    range: PortRange = .registered,
    min: i32 = 1, // custom-range lower bound (full-range default, like min ?? 1 in TS)
    max: i32 = 65535, // custom-range upper bound (max ?? 65535 in TS)
    count: usize = 1,
    unique: bool = false,
    rng: ?*const fn () f64 = null, // injectable random in [0, 1)
};

pub const PortError = error{ InvalidPortRange, OutOfMemory };

/// defaultRng() in the TS lib: a CSPRNG draw in [0, 1).
fn defaultRng() f64 {
    return std.crypto.random.float(f64);
}

/// resolveRange() in the TS lib.
pub fn resolveRange(opts: PortOptions) struct { lo: i32, hi: i32 } {
    return switch (opts.range) {
        .custom => .{ .lo = opts.min, .hi = opts.max },
        .any => .{ .lo = 1, .hi = 65535 },
        .ephemeral => .{ .lo = 49152, .hi = 65535 },
        .registered => .{ .lo = 1024, .hi = 49151 },
    };
}

/// assertRange() in the TS lib — the TS throw becomes error.InvalidPortRange.
fn assertRange(lo: i32, hi: i32) PortError!void {
    if (lo < 0 or lo > 65535 or hi > 65535 or lo > hi) return error.InvalidPortRange;
}

/// randomPort() in the TS lib — Port() in the Go twin.
pub fn randomPort(opts: PortOptions) PortError!i32 {
    const rng = opts.rng orelse defaultRng;
    const r = resolveRange(opts);
    try assertRange(r.lo, r.hi);
    const span: f64 = @floatFromInt(r.hi - r.lo + 1);
    return r.lo + @as(i32, @intFromFloat(@floor(rng() * span)));
}

/// randomPorts() in the TS lib — Ports() in the Go twin. Caller frees.
pub fn randomPorts(allocator: std.mem.Allocator, opts: PortOptions) PortError![]i32 {
    const count = @max(1, opts.count);
    const r = resolveRange(opts);
    try assertRange(r.lo, r.hi);
    if (!opts.unique) {
        const out = try allocator.alloc(i32, count);
        for (out) |*p| p.* = try randomPort(opts);
        return out;
    }

    // Fisher-Yates partial shuffle over the range to pick n unique ports.
    const capacity: usize = @intCast(r.hi - r.lo + 1);
    const n = @min(count, capacity);
    const rng = opts.rng orelse defaultRng;
    const pool = try allocator.alloc(i32, capacity);
    defer allocator.free(pool);
    for (pool, 0..) |*p, i| p.* = r.lo + @as(i32, @intCast(i));
    var i: usize = 0;
    while (i < n) : (i += 1) {
        const span: f64 = @floatFromInt(capacity - i);
        const j = i + @as(usize, @intFromFloat(@floor(rng() * span)));
        std.mem.swap(i32, &pool[i], &pool[j]);
    }
    const out = try allocator.alloc(i32, n);
    @memcpy(out, pool[0..n]);
    return out;
}

fn zeroRng() f64 {
    return 0.0;
}

fn halfRng() f64 {
    return 0.5;
}

fn pointNineRng() f64 {
    return 0.9;
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    // Vectors from src/lib/random-port.test.ts — a fixed rng pins every draw.
    try stdout.print("registered+0 = {d}\n", .{try randomPort(.{ .rng = zeroRng })}); // 1024
    try stdout.print("any+0 = {d}\n", .{try randomPort(.{ .range = .any, .rng = zeroRng })}); // 1
    try stdout.print("ephemeral+0 = {d}\n", .{try randomPort(.{ .range = .ephemeral, .rng = zeroRng })}); // 49152
    try stdout.print("custom 8000-8000 @0.9 = {d}\n", .{try randomPort(.{
        .range = .custom,
        .min = 8000,
        .max = 8000,
        .rng = pointNineRng,
    })}); // 8000
    const d = resolveRange(.{ .range = .custom });
    try stdout.print("resolveRange(custom defaults) = {d}..{d}\n", .{ d.lo, d.hi }); // 1..65535

    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const a = arena.allocator();

    const five = try randomPorts(a, .{ .count = 5, .rng = zeroRng });
    try stdout.print("count=5 @0 = {any}\n", .{five}); // 1024 x5
    const uniq = try randomPorts(a, .{
        .count = 3,
        .unique = true,
        .range = .custom,
        .min = 1,
        .max = 10,
        .rng = halfRng,
    });
    try stdout.print("unique 3 in 1..10 @0.5 = {any}\n", .{uniq}); // 6, 1, 7
    const capped = try randomPorts(a, .{
        .count = 5,
        .unique = true,
        .range = .custom,
        .min = 1,
        .max = 3,
        .rng = halfRng,
    });
    try stdout.print("unique 5 in 1..3 @0.5 = {any}\n", .{capped}); // capped at capacity 3

    const p = try randomPort(.{}); // default CSPRNG, registered range
    try stdout.print("default rng registered = {d} (in 1024..49151)\n", .{p});

    // Invalid custom ranges — the TS throw, as error values.
    if (randomPort(.{ .range = .custom, .min = 70000 })) |got| { // min above 65535
        try stdout.print("unexpected {d}\n", .{got});
    } else |err| try stdout.print("invalid: {s}\n", .{@errorName(err)});
    if (randomPort(.{ .range = .custom, .min = 100, .max = 50 })) |got| { // min above max
        try stdout.print("unexpected {d}\n", .{got});
    } else |err| try stdout.print("invalid: {s}\n", .{@errorName(err)});
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →