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 →