Skip to content

Random Port Generator — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! random-port-generator — random/dynamic port picker over well-known ranges.
//!
//! Language: Rust (edition 2021, standard library only)
//! Source:   CosmoDev polyglot showcase port of the Random Port Generator tool,
//!           ported from src/lib/random-port.ts (the canonical TypeScript
//!           implementation) and kept in lock-step with cli/random-port-generator
//!           (the Go twin). Pure + deterministic via an injectable RNG; never
//!           panics (invalid custom ranges surface as an `Err`).
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//!   - Pure + deterministic; never panics. The only failure mode is an invalid
//!     custom range, which surfaces as `Result::Err` (mirrors the TS throw).
//!   - Functionally equivalent to the TS reference and the Go twin: same named
//!     ranges, same validation, same Fisher–Yates partial-shuffle unique pick.
//!   - Self-contained: std only. The default RNG is a non-crypto time source
//!     (see `default_rng`); tests inject a constant closure for deterministic
//!     outputs. Production should inject `getrandom` / `rand`.
//!
//! The injectable `rng` is the key that makes a *random* tool deterministic and
//! therefore testable: every showcase assertion below uses a fixed closure and
//! reproduces a vector from src/lib/random-port.test.ts exactly.

use std::time::{SystemTime, UNIX_EPOCH};

/// Well-known port ranges. Mirrors the TS `PortRange` union and the Go `Range`
/// enum. `Registered` is the default (matches TS + Go).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PortRange {
    /// IANA registered range, 1024–49151 (the default).
    #[default]
    Registered,
    /// Ephemeral/dynamic range, 49152–65535.
    Ephemeral,
    /// The whole valid port space, 1–65535.
    Any,
    /// Caller-supplied bounds via `Options::min` / `Options::max`.
    Custom,
}

/// Options mirror the TS `PortOptions` interface. Every field has a sensible
/// default, so callers can construct it incrementally with `..Default::default()`.
#[derive(Debug, Clone, Default)]
pub struct Options {
    /// Range preset. `None` -> `Registered` (the TS default).
    pub range: Option<PortRange>,
    /// Custom-range lower bound (used only when `range == Custom`). Mirrors the
    /// TS `min ?? 1` fallback; `None` means "unset -> 1".
    pub min: Option<i64>,
    /// Custom-range upper bound (used only when `range == Custom`). Mirrors the
    /// TS `max ?? 65535` fallback; `None` means "unset -> 65535".
    pub max: Option<i64>,
    /// How many ports `random_ports` returns. `None` -> 1.
    pub count: Option<usize>,
    /// Deduplicate the result of `random_ports` via a Fisher–Yates partial
    /// shuffle (capped at range capacity).
    pub unique: Option<bool>,
    /// Injectable RNG returning a float in [0, 1). `None` -> `default_rng`.
    /// Tests inject a constant closure for deterministic, reproducible outputs.
    pub rng: Option<fn() -> f64>,
}

/// Default RNG. Rust's std has no secure RNG (the `getrandom` / `rand` crates
/// provide one, but the brief forbids external deps), so we seed from monotonic
/// time and fold it into [0, 1). This is intentionally NOT cryptographic — it
/// exists only to give the default path a value; production code should inject
/// a real CSPRNG via `Options::rng`, matching the TS crypto-backed default.
fn default_rng() -> f64 {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.subsec_nanos() as f64)
        .unwrap_or(0.0);
    (nanos / 1_000_000_000.0).fract()
}

/// Resolve an options object to its `(lo, hi)` bounds. Mirrors the TS
/// `resolveRange` and the Go `bounds` (without the validation, which lives in
/// `assert_range`).
pub fn resolve_range(opts: &Options) -> (i64, i64) {
    match opts.range.unwrap_or_default() {
        PortRange::Any => (1, 65535),
        PortRange::Ephemeral => (49152, 65535),
        PortRange::Custom => (opts.min.unwrap_or(1), opts.max.unwrap_or(65535)),
        PortRange::Registered => (1024, 49151),
    }
}

/// Validate a resolved range. Mirrors the TS `assertRange` (the integer check is
/// implicit in the `i64` types, so only bounds/ordering remain).
fn assert_range(lo: i64, hi: i64) -> Result<(), String> {
    if lo < 0 || lo > 65535 || hi > 65535 || lo > hi {
        return Err(format!("Invalid port range {lo}-{hi}"));
    }
    Ok(())
}

/// Return a single random port within the resolved range. Mirrors the TS
/// `randomPort`. The only failure mode is an invalid custom range.
pub fn random_port(opts: &Options) -> Result<i64, String> {
    let rng = opts.rng.unwrap_or(default_rng);
    let (lo, hi) = resolve_range(opts);
    assert_range(lo, hi)?;
    Ok(lo + (rng() * (hi - lo + 1) as f64).floor() as i64)
}

/// Return `count` ports. When `unique`, a Fisher–Yates partial shuffle over the
/// range yields distinct values (capped at range capacity). Mirrors the TS
/// `randomPorts`.
pub fn random_ports(opts: &Options) -> Result<Vec<i64>, String> {
    let count = opts.count.unwrap_or(1).max(1);
    let (lo, hi) = resolve_range(opts);
    assert_range(lo, hi)?;
    let unique = opts.unique.unwrap_or(false);

    if !unique {
        // Mirrors TS: `Array.from({ length: count }, () => randomPort(opts))`.
        let mut out = Vec::with_capacity(count);
        for _ in 0..count {
            out.push(random_port(opts)?);
        }
        return Ok(out);
    }

    // Fisher–Yates partial shuffle over the range to pick `n` unique ports.
    let capacity = (hi - lo + 1) as usize;
    let n = count.min(capacity);
    let rng = opts.rng.unwrap_or(default_rng);
    let mut pool: Vec<i64> = (0..capacity).map(|i| lo + i as i64).collect();
    for i in 0..n {
        let span = (capacity - i) as f64;
        let j = i + (rng() * span).floor() as usize;
        pool.swap(i, j);
    }
    pool.truncate(n);
    Ok(pool)
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deterministic_with_injected_rng() {
        // rng=0 collapses to the low bound of the default registered range.
        let opts = Options { rng: Some(|| 0.0), ..Default::default() };
        assert_eq!(random_port(&opts).unwrap(), 1024);
    }

    #[test]
    fn well_known_ranges() {
        let any = Options { range: Some(PortRange::Any), rng: Some(|| 0.0), ..Default::default() };
        let ephem = Options { range: Some(PortRange::Ephemeral), rng: Some(|| 0.0), ..Default::default() };
        assert_eq!(random_port(&any).unwrap(), 1);
        assert_eq!(random_port(&ephem).unwrap(), 49152);
        assert_eq!(resolve_range(&Options::default()), (1024, 49151));
    }

    #[test]
    fn custom_range_and_defaults() {
        let single = Options {
            range: Some(PortRange::Custom),
            min: Some(8000),
            max: Some(8000),
            rng: Some(|| 0.9),
            ..Default::default()
        };
        assert_eq!(random_port(&single).unwrap(), 8000);
        // Custom with no bounds resolves to the full port range.
        let custom = Options { range: Some(PortRange::Custom), ..Default::default() };
        assert_eq!(resolve_range(&custom), (1, 65535));
    }

    #[test]
    fn rejects_invalid_custom_range() {
        let over = Options { range: Some(PortRange::Custom), min: Some(70000), ..Default::default() };
        assert!(random_port(&over).is_err());
        let inverted = Options { range: Some(PortRange::Custom), min: Some(100), max: Some(50), ..Default::default() };
        assert!(random_port(&inverted).is_err());
    }

    #[test]
    fn multiple_and_unique_ports() {
        // Non-unique with a constant rng repeats the low bound of the range.
        let many = Options { count: Some(5), rng: Some(|| 0.0), ..Default::default() };
        assert_eq!(random_ports(&many).unwrap(), vec![1024, 1024, 1024, 1024, 1024]);
        // Unique within a small range, capped at capacity.
        let uniq = Options {
            count: Some(5),
            unique: Some(true),
            range: Some(PortRange::Custom),
            min: Some(1),
            max: Some(3),
            rng: Some(|| 0.5),
            ..Default::default()
        };
        let mut got = random_ports(&uniq).unwrap();
        assert_eq!(got.len(), 3); // capped at capacity 3
        got.sort();
        assert_eq!(got, vec![1, 2, 3]); // all three present, no dupes
    }
}

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 →