use std::collections::hash_map::DefaultHasher;
use std::fs::{File, OpenOptions};
use std::hash::{Hash, Hasher};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use ossctl_core::ports::{
Clock, CommandOutput, CommandRunner, Fs, GitRepo, IdGen, JournalLock, JournalStore,
RegistryQuery, Tagger,
};
pub struct RealCommandRunner;
impl CommandRunner for RealCommandRunner {
fn run(&self, program: &str, args: &[&str], cwd: &Path) -> io::Result<CommandOutput> {
let out = Command::new(program)
.args(args)
.current_dir(cwd)
.stdin(Stdio::null())
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "true")
.env("GH_PROMPT_DISABLED", "1")
.env("GH_NO_UPDATE_NOTIFIER", "1")
.output()?;
Ok(CommandOutput {
status: out.status.code(),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
})
}
}
pub struct RealFs;
impl Fs for RealFs {
fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn is_dir(&self, path: &Path) -> bool {
path.is_dir()
}
fn is_file(&self, path: &Path) -> bool {
path.is_file()
}
fn read_dir(&self, dir: &Path) -> io::Result<Vec<String>> {
let mut names = Vec::new();
for entry in std::fs::read_dir(dir)? {
names.push(entry?.file_name().to_string_lossy().into_owned());
}
names.sort();
Ok(names)
}
}
pub struct RealGitRepo {
root: PathBuf,
}
impl RealGitRepo {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn git(&self, args: &[&str]) -> io::Result<std::process::Output> {
Command::new("git")
.arg("-C")
.arg(&self.root)
.args(args)
.stdin(Stdio::null())
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "true")
.env("GIT_OPTIONAL_LOCKS", "0")
.output()
}
fn git_stdout(&self, args: &[&str]) -> io::Result<String> {
let out = self.git(args)?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
} else {
Err(io::Error::other(format!(
"git {} exited {:?}",
args.join(" "),
out.status.code()
)))
}
}
}
impl GitRepo for RealGitRepo {
fn head_commit(&self) -> io::Result<String> {
let head = self.git_stdout(&["rev-parse", "HEAD"])?.trim().to_string();
if head.is_empty() {
return Err(io::Error::other("git rev-parse HEAD produced no output"));
}
Ok(head)
}
fn is_work_tree(&self) -> bool {
self.git(&["rev-parse", "--is-inside-work-tree"])
.is_ok_and(|o| o.status.success())
}
fn shortlog(&self, since: Option<&str>) -> io::Result<String> {
let mut args: Vec<String> = vec!["shortlog".into(), "-sne".into(), "--all".into()];
if let Some(s) = since {
args.push(format!("--since={s}"));
}
args.push("HEAD".into());
let borrowed: Vec<&str> = args.iter().map(String::as_str).collect();
self.git_stdout(&borrowed)
}
fn tags(&self) -> io::Result<Vec<String>> {
Ok(self
.git_stdout(&["tag", "--list"])?
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect())
}
fn git_common_dir(&self) -> io::Result<PathBuf> {
let raw = self.git_stdout(&["rev-parse", "--git-common-dir"])?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(io::Error::other(
"git rev-parse --git-common-dir produced no output",
));
}
let path = Path::new(trimmed);
Ok(if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
})
}
}
pub struct RealClock;
impl Clock for RealClock {
fn now_unix(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
}
pub struct RealIdGen;
static ID_COUNTER: AtomicU64 = AtomicU64::new(0);
impl IdGen for RealIdGen {
fn new_id(&self) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let ms = u64::try_from(now.as_millis()).unwrap_or(u64::MAX) & ((1 << 48) - 1);
let counter = ID_COUNTER.fetch_add(1, Ordering::Relaxed);
let anchor = 0u8;
let addr = std::ptr::addr_of!(anchor) as usize;
let pid = std::process::id();
let mut h1 = DefaultHasher::new();
(now.subsec_nanos(), counter, ms, addr, pid).hash(&mut h1);
let mut h2 = DefaultHasher::new();
(counter, h1.finish(), addr, ms, pid).hash(&mut h2);
let rand80 = (u128::from(h1.finish()) << 16) | u128::from(h2.finish() & 0xffff);
let value = (u128::from(ms) << 80) | (rand80 & ((1 << 80) - 1));
crockford_u128(value)
}
}
fn crockford_u128(mut value: u128) -> String {
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
let mut buf = [0u8; 26];
for slot in buf.iter_mut().rev() {
*slot = ALPHABET[(value & 0x1f) as usize];
value >>= 5;
}
String::from_utf8(buf.to_vec()).expect("crockford alphabet is ASCII")
}
pub struct RealRegistryQuery;
struct VersionKeys(Vec<String>);
impl<'de> serde::Deserialize<'de> for VersionKeys {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct KeysVisitor;
impl<'de> serde::de::Visitor<'de> for KeysVisitor {
type Value = Vec<String>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a JSON object mapping version strings to metadata")
}
fn visit_map<M: serde::de::MapAccess<'de>>(
self,
mut map: M,
) -> Result<Self::Value, M::Error> {
let mut keys = Vec::new();
while let Some((key, _)) = map.next_entry::<String, serde::de::IgnoredAny>()? {
keys.push(key);
}
Ok(keys)
}
}
deserializer.deserialize_map(KeysVisitor).map(VersionKeys)
}
}
impl RealRegistryQuery {
fn http_get(url: &str) -> io::Result<(u16, Vec<u8>)> {
let agent = ureq::Agent::config_builder()
.timeout_global(Some(Duration::from_secs(30)))
.user_agent(concat!("ossctl/", env!("CARGO_PKG_VERSION")))
.http_status_as_error(false)
.max_redirects(0)
.build()
.new_agent();
let mut resp = agent
.get(url)
.call()
.map_err(|e| io::Error::other(format!("HTTP GET {url} failed: {e}")))?;
let status = resp.status().as_u16();
let body = resp
.body_mut()
.with_config()
.limit(Self::MAX_BODY_BYTES)
.read_to_vec()
.map_err(|e| io::Error::other(format!("reading HTTP body from {url} failed: {e}")))?;
Ok((status, body))
}
const MAX_BODY_BYTES: u64 = 10 * 1024 * 1024;
const MAX_NPM_NAME_LEN: usize = 214;
fn validate_npm_package(package: &str) -> io::Result<()> {
let invalid = |msg: String| Err(io::Error::new(io::ErrorKind::InvalidInput, msg));
if package.is_empty() {
return invalid("refusing to query npm for an empty package name".to_string());
}
if package.len() > Self::MAX_NPM_NAME_LEN {
return invalid(format!(
"refusing to query npm for an over-long package name ({} > {} bytes)",
package.len(),
Self::MAX_NPM_NAME_LEN
));
}
if package.starts_with('-') {
return invalid(format!(
"refusing to query npm for a package name that looks like a flag: {package:?}"
));
}
let scoped = package.starts_with('@');
let slashes = package.bytes().filter(|&b| b == b'/').count();
let components: Vec<&str> = if scoped {
let rest = &package[1..];
match rest.split_once('/') {
Some((scope, name)) if !scope.is_empty() && !name.is_empty() && slashes == 1 => {
vec![scope, name]
}
_ => {
return invalid(format!(
"refusing to query npm for a malformed scoped package name: {package:?}"
));
}
}
} else if slashes != 0 {
return invalid(format!(
"refusing to query npm for an unscoped package name containing '/': {package:?}"
));
} else {
vec![package]
};
for component in components {
if component.starts_with('.') || component.starts_with('_') {
return invalid(format!(
"refusing to query npm for a package name whose component starts with '.' or '_': {package:?}"
));
}
}
for (i, &b) in package.as_bytes().iter().enumerate() {
let ok = b.is_ascii_alphanumeric()
|| matches!(b, b'-' | b'_' | b'.')
|| (b == b'@' && i == 0)
|| (b == b'/' && scoped);
if !ok {
return invalid(format!(
"refusing to query npm for a suspicious package name: {package:?}"
));
}
}
Ok(())
}
fn npm_registry_url(package: &str) -> String {
format!("https://registry.npmjs.org/{}", package.replace('/', "%2F"))
}
fn parse_npm_versions(body: &[u8], expected_name: &str) -> io::Result<Vec<String>> {
#[derive(serde::Deserialize)]
struct Packument {
name: String,
versions: VersionKeys,
}
let pack: Packument = serde_json::from_slice(body).map_err(|e| {
io::Error::other(format!("npm registry body was not a valid packument: {e}"))
})?;
if !pack.name.eq_ignore_ascii_case(expected_name) {
return Err(io::Error::other(format!(
"npm registry body was for package {:?}, not the requested {expected_name:?}",
pack.name
)));
}
Ok(pack.versions.0)
}
fn classify_npm_response(
status: u16,
body: &[u8],
expected_name: &str,
) -> io::Result<Vec<String>> {
match status {
200 => {
let versions = Self::parse_npm_versions(body, expected_name)?;
if versions.is_empty() {
return Err(io::Error::other(
"npm registry returned HTTP 200 with no versions; \
treating as unknown rather than 'not published'",
));
}
Ok(versions)
}
404 => Ok(Vec::new()),
other => Err(io::Error::other(format!(
"npm registry returned an unexpected HTTP status {other}"
))),
}
}
fn npm_versions(package: &str) -> io::Result<Vec<String>> {
Self::validate_npm_package(package)?;
let url = Self::npm_registry_url(package);
let (status, body) = Self::http_get(&url)?;
Self::classify_npm_response(status, &body, package)
}
fn sparse_index_path(crate_name: &str) -> String {
let name = crate_name.to_ascii_lowercase();
match name.len() {
0 => name,
1 => format!("1/{name}"),
2 => format!("2/{name}"),
3 => format!("3/{}/{}", &name[0..1], name),
_ => format!("{}/{}/{}", &name[0..2], &name[2..4], name),
}
}
const MAX_CRATE_NAME_LEN: usize = 64;
fn validate_crate_name(crate_name: &str) -> io::Result<()> {
if crate_name.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"refusing to query crates.io for an empty crate name",
));
}
if crate_name.len() > Self::MAX_CRATE_NAME_LEN {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"refusing to query crates.io for an over-long crate name ({} > {} bytes)",
crate_name.len(),
Self::MAX_CRATE_NAME_LEN
),
));
}
if crate_name.starts_with('-')
|| !crate_name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("refusing to query crates.io for a suspicious crate name: {crate_name:?}"),
));
}
Ok(())
}
fn parse_sparse_index(body: &str, expected_name: &str) -> io::Result<Vec<String>> {
#[derive(serde::Deserialize)]
struct SparseEntry {
name: String,
vers: String,
}
let mut versions = Vec::new();
for line in body.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let entry: SparseEntry = serde_json::from_str(line).map_err(|e| {
io::Error::other(format!(
"crates.io sparse-index line was not valid JSON: {e}"
))
})?;
if !entry.name.eq_ignore_ascii_case(expected_name) {
return Err(io::Error::other(format!(
"crates.io sparse-index body was for crate {:?}, not the requested {expected_name:?}",
entry.name
)));
}
versions.push(entry.vers);
}
Ok(versions)
}
fn classify_sparse_response(
status: u16,
body: &[u8],
expected_name: &str,
) -> io::Result<Vec<String>> {
match status {
200 => {
let body = std::str::from_utf8(body).map_err(|e| {
io::Error::other(format!(
"crates.io sparse index returned a non-UTF-8 body: {e}"
))
})?;
let versions = Self::parse_sparse_index(body, expected_name)?;
if versions.is_empty() {
return Err(io::Error::other(
"crates.io sparse index returned HTTP 200 with no release records; \
treating as unknown rather than 'not published'",
));
}
Ok(versions)
}
404 => Ok(Vec::new()),
other => Err(io::Error::other(format!(
"crates.io sparse index returned an unexpected HTTP status {other}"
))),
}
}
fn crates_io_versions(crate_name: &str) -> io::Result<Vec<String>> {
Self::validate_crate_name(crate_name)?;
let url = format!(
"https://index.crates.io/{}",
Self::sparse_index_path(crate_name)
);
let (status, body) = Self::http_get(&url)?;
Self::classify_sparse_response(status, &body, crate_name)
}
}
impl RegistryQuery for RealRegistryQuery {
fn published_versions(&self, ecosystem: &str, package: &str) -> io::Result<Vec<String>> {
match ecosystem {
"node" => Self::npm_versions(package),
"rust" => Self::crates_io_versions(package),
other => Err(io::Error::other(format!(
"no registry query wired for ecosystem '{other}' yet"
))),
}
}
}
pub struct ReadOnlyJournalStore;
impl ReadOnlyJournalStore {
fn read_only(op: &str) -> io::Error {
io::Error::new(
io::ErrorKind::Unsupported,
format!("{op} is not available on the read-only journal store"),
)
}
}
impl JournalStore for ReadOnlyJournalStore {
fn lock_exclusive(&self, _lock_path: &Path) -> io::Result<Box<dyn JournalLock>> {
Err(Self::read_only("lock_exclusive"))
}
fn append_line(&self, _path: &Path, _line: &str) -> io::Result<()> {
Err(Self::read_only("append_line"))
}
fn read_lines(&self, path: &Path) -> io::Result<Vec<String>> {
match std::fs::read_to_string(path) {
Ok(contents) => {
let end = contents.rfind('\n').map_or(0, |i| i + 1);
Ok(contents[..end].lines().map(str::to_string).collect())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
fn read(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn write_atomic(&self, _path: &Path, _bytes: &[u8]) -> io::Result<()> {
Err(Self::read_only("write_atomic"))
}
fn list_dir(&self, dir: &Path) -> io::Result<Vec<String>> {
let mut names = Vec::new();
match std::fs::read_dir(dir) {
Ok(entries) => {
for entry in entries {
names.push(entry?.file_name().to_string_lossy().into_owned());
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e),
}
names.sort();
Ok(names)
}
}
pub struct RealTagger {
root: PathBuf,
}
impl RealTagger {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn run(&self, program: &str, args: &[&str]) -> io::Result<std::process::Output> {
Command::new(program)
.args(args)
.current_dir(&self.root)
.stdin(Stdio::null())
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "true")
.env("GH_PROMPT_DISABLED", "1")
.env("GH_NO_UPDATE_NOTIFIER", "1")
.output()
}
fn check(out: std::process::Output, what: &str) -> io::Result<std::process::Output> {
if out.status.success() {
return Ok(out);
}
let stderr = String::from_utf8_lossy(&out.stderr);
let detail = if stderr.trim().is_empty() {
String::from_utf8_lossy(&out.stdout).into_owned()
} else {
stderr.into_owned()
};
Err(io::Error::other(format!(
"{what} exited {:?}: {}",
out.status.code(),
detail.trim()
)))
}
fn tag_commit(&self, tag: &str) -> Option<String> {
let out = self
.run(
"git",
&["rev-parse", "--verify", "-q", &format!("{tag}^{{commit}}")],
)
.ok()?;
if !out.status.success() {
return None;
}
let sha = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!sha.is_empty()).then_some(sha)
}
}
impl Tagger for RealTagger {
fn create_tag(&self, tag: &str, commit: &str, message: &str) -> io::Result<()> {
if let Some(existing) = self.tag_commit(tag) {
if existing == commit || existing.starts_with(commit) || commit.starts_with(&existing) {
return Ok(());
}
return Err(io::Error::other(format!(
"tag `{tag}` already exists at {existing}, not the sealed commit {commit}"
)));
}
let out = self.run("git", &["tag", "-a", tag, commit, "-m", message])?;
Self::check(out, "git tag")?;
Ok(())
}
fn push_tag(&self, tag: &str) -> io::Result<()> {
let refspec = format!("refs/tags/{tag}:refs/tags/{tag}");
let out = self.run("git", &["push", "origin", &refspec])?;
Self::check(out, "git push")?;
Ok(())
}
fn create_github_release(&self, tag: &str, title: &str) -> io::Result<Option<String>> {
let view = self.run(
"gh",
&["release", "view", tag, "--json", "url", "-q", ".url"],
)?;
if view.status.success() {
let url = String::from_utf8_lossy(&view.stdout).trim().to_string();
return Ok((!url.is_empty()).then_some(url));
}
let out = self.run(
"gh",
&[
"release",
"create",
tag,
"--title",
title,
"--generate-notes",
],
)?;
let out = Self::check(out, "gh release create")?;
let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
Ok((!url.is_empty()).then_some(url))
}
}
pub struct RealJournalStore;
struct RealJournalLock {
path: PathBuf,
}
impl JournalLock for RealJournalLock {}
impl Drop for RealJournalLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn fsync_dir(dir: &Path) {
let _ = File::open(dir).and_then(|f| f.sync_all());
}
impl JournalStore for RealJournalStore {
fn lock_exclusive(&self, lock_path: &Path) -> io::Result<Box<dyn JournalLock>> {
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
match OpenOptions::new()
.write(true)
.create_new(true)
.open(lock_path)
{
Ok(mut f) => {
let _ = writeln!(f, "{}", std::process::id());
let _ = f.sync_all();
Ok(Box::new(RealJournalLock {
path: lock_path.to_path_buf(),
}))
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new(
io::ErrorKind::WouldBlock,
"another release cut/resume holds the single-active-cut lock",
)),
Err(e) => Err(e),
}
}
fn append_line(&self, path: &Path, line: &str) -> io::Result<()> {
let created_parent = match path.parent() {
Some(p) if !p.exists() => {
std::fs::create_dir_all(p)?;
true
}
_ => false,
};
let mut f = OpenOptions::new().create(true).append(true).open(path)?;
f.write_all(line.as_bytes())?;
f.write_all(b"\n")?;
f.sync_all()?;
if created_parent {
if let Some(p) = path.parent() {
fsync_dir(p);
}
}
Ok(())
}
fn read_lines(&self, path: &Path) -> io::Result<Vec<String>> {
match std::fs::read_to_string(path) {
Ok(s) => Ok(s.lines().map(str::to_string).collect()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
fn read(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(b) => Ok(Some(b)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn write_atomic(&self, path: &Path, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "manifest path has no parent")
})?;
std::fs::create_dir_all(parent)?;
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("manifest.json");
let counter = ID_COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp = parent.join(format!(".{file_name}.{}.{counter}.tmp", std::process::id()));
{
let mut f = File::create(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
}
std::fs::rename(&tmp, path)?;
fsync_dir(parent);
Ok(())
}
fn list_dir(&self, dir: &Path) -> io::Result<Vec<String>> {
match std::fs::read_dir(dir) {
Ok(entries) => {
let mut names = Vec::new();
for entry in entries {
names.push(entry?.file_name().to_string_lossy().into_owned());
}
names.sort();
Ok(names)
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sparse_index_path_follows_cargo_prefix_convention() {
assert_eq!(RealRegistryQuery::sparse_index_path("a"), "1/a");
assert_eq!(RealRegistryQuery::sparse_index_path("no"), "2/no");
assert_eq!(RealRegistryQuery::sparse_index_path("cfg"), "3/c/cfg");
assert_eq!(RealRegistryQuery::sparse_index_path("serde"), "se/rd/serde");
assert_eq!(
RealRegistryQuery::sparse_index_path("ossctl-core"),
"os/sc/ossctl-core"
);
assert_eq!(
RealRegistryQuery::sparse_index_path("Ossctl-Core"),
"os/sc/ossctl-core"
);
}
#[test]
fn parse_sparse_index_collects_versions_including_yanked() {
let body = concat!(
r#"{"name":"tool","vers":"0.9.0","yanked":false}"#,
"\n",
r#"{"name":"tool","vers":"1.0.0","yanked":true}"#,
"\n",
r#"{"name":"tool","vers":"1.1.0","yanked":false}"#,
"\n",
);
let versions = RealRegistryQuery::parse_sparse_index(body, "tool").unwrap();
assert_eq!(versions, vec!["0.9.0", "1.0.0", "1.1.0"]);
}
#[test]
fn parse_sparse_index_blank_lines_are_skipped() {
let body = "\n{\"name\":\"tool\",\"vers\":\"1.0.0\",\"yanked\":false}\n\n";
assert_eq!(
RealRegistryQuery::parse_sparse_index(body, "tool").unwrap(),
vec!["1.0.0"]
);
}
#[test]
fn parse_sparse_index_rejects_malformed_line() {
let body = r#"{"name":"tool","yanked":false}"#;
assert!(RealRegistryQuery::parse_sparse_index(body, "tool").is_err());
let body = r#"{"vers":"1.0.0","yanked":false}"#;
assert!(RealRegistryQuery::parse_sparse_index(body, "tool").is_err());
assert!(RealRegistryQuery::parse_sparse_index("not json", "tool").is_err());
}
#[test]
fn parse_sparse_index_rejects_a_wrong_crate_body() {
let body = r#"{"name":"other","vers":"1.0.0","yanked":false}"#;
assert!(RealRegistryQuery::parse_sparse_index(body, "tool").is_err());
let body = r#"{"name":"Tool","vers":"1.0.0","yanked":false}"#;
assert_eq!(
RealRegistryQuery::parse_sparse_index(body, "tool").unwrap(),
vec!["1.0.0"]
);
}
#[test]
fn classify_sparse_response_200_returns_versions() {
let body = br#"{"name":"tool","vers":"1.0.0","yanked":false}"#;
assert_eq!(
RealRegistryQuery::classify_sparse_response(200, body, "tool").unwrap(),
vec!["1.0.0"]
);
}
#[test]
fn classify_sparse_response_404_is_empty_not_error() {
assert_eq!(
RealRegistryQuery::classify_sparse_response(404, b"not found", "tool").unwrap(),
Vec::<String>::new()
);
}
#[test]
fn classify_sparse_response_empty_200_fails_closed() {
assert!(RealRegistryQuery::classify_sparse_response(200, b"", "tool").is_err());
assert!(RealRegistryQuery::classify_sparse_response(200, b" \n ", "tool").is_err());
}
#[test]
fn classify_sparse_response_non_utf8_200_fails_closed() {
assert!(RealRegistryQuery::classify_sparse_response(200, &[0xff, 0xfe], "tool").is_err());
}
#[test]
fn classify_sparse_response_unexpected_status_fails_closed() {
assert!(RealRegistryQuery::classify_sparse_response(503, b"", "tool").is_err());
assert!(RealRegistryQuery::classify_sparse_response(410, b"", "tool").is_err());
assert!(RealRegistryQuery::classify_sparse_response(301, b"", "tool").is_err());
}
#[test]
fn classify_npm_response_200_returns_versions() {
let body = br#"{"name":"tool","versions":{"1.0.0":{},"1.1.0":{}}}"#;
let mut versions = RealRegistryQuery::classify_npm_response(200, body, "tool").unwrap();
versions.sort();
assert_eq!(versions, vec!["1.0.0", "1.1.0"]);
}
#[test]
fn classify_npm_response_404_is_empty_not_error() {
let body = br#"{"error":"Not found"}"#;
assert_eq!(
RealRegistryQuery::classify_npm_response(404, body, "tool").unwrap(),
Vec::<String>::new()
);
}
#[test]
fn classify_npm_response_empty_and_unexpected_fail_closed() {
let body = br#"{"name":"tool","versions":{}}"#;
assert!(RealRegistryQuery::classify_npm_response(200, body, "tool").is_err());
let body = br#"{"name":"other","versions":{"1.0.0":{}}}"#;
assert!(RealRegistryQuery::classify_npm_response(200, body, "tool").is_err());
assert!(RealRegistryQuery::classify_npm_response(200, b"not json", "tool").is_err());
assert!(RealRegistryQuery::classify_npm_response(503, b"", "tool").is_err());
let body = br#"{"name":"Tool","versions":{"1.0.0":{}}}"#;
assert_eq!(
RealRegistryQuery::classify_npm_response(200, body, "tool").unwrap(),
vec!["1.0.0"]
);
}
#[test]
fn npm_registry_url_encodes_scoped_slash() {
assert_eq!(
RealRegistryQuery::npm_registry_url("left-pad"),
"https://registry.npmjs.org/left-pad"
);
assert_eq!(
RealRegistryQuery::npm_registry_url("@scope/pkg"),
"https://registry.npmjs.org/@scope%2Fpkg"
);
}
#[test]
fn validate_npm_package_rejects_suspicious_input() {
assert!(RealRegistryQuery::validate_npm_package("left-pad").is_ok());
assert!(RealRegistryQuery::validate_npm_package("lodash.merge").is_ok());
assert!(RealRegistryQuery::validate_npm_package("@babel/core").is_ok());
assert!(RealRegistryQuery::validate_npm_package(
&"a".repeat(RealRegistryQuery::MAX_NPM_NAME_LEN)
)
.is_ok());
assert!(RealRegistryQuery::validate_npm_package(
&"a".repeat(RealRegistryQuery::MAX_NPM_NAME_LEN + 1)
)
.is_err());
assert!(RealRegistryQuery::validate_npm_package("").is_err());
assert!(RealRegistryQuery::validate_npm_package("-oops").is_err());
assert!(RealRegistryQuery::validate_npm_package("a b").is_err());
assert!(RealRegistryQuery::validate_npm_package("a/b").is_err()); assert!(RealRegistryQuery::validate_npm_package("@scope/a/b").is_err()); assert!(RealRegistryQuery::validate_npm_package("@scope").is_err()); assert!(RealRegistryQuery::validate_npm_package("@/pkg").is_err()); assert!(RealRegistryQuery::validate_npm_package("pkg@1").is_err()); assert!(RealRegistryQuery::validate_npm_package(".").is_err());
assert!(RealRegistryQuery::validate_npm_package("..").is_err());
assert!(RealRegistryQuery::validate_npm_package(".hidden").is_err());
assert!(RealRegistryQuery::validate_npm_package("_priv").is_err());
assert!(RealRegistryQuery::validate_npm_package("@scope/.").is_err());
assert!(RealRegistryQuery::validate_npm_package("@scope/..").is_err());
assert!(RealRegistryQuery::validate_npm_package("@.scope/pkg").is_err());
assert!(RealRegistryQuery::validate_npm_package("@_scope/pkg").is_err());
assert!(RealRegistryQuery::validate_npm_package("lodash.merge").is_ok());
assert!(RealRegistryQuery::validate_npm_package("read_file").is_ok());
}
#[test]
fn validate_crate_name_rejects_suspicious_input() {
assert!(RealRegistryQuery::validate_crate_name("ossctl-core").is_ok());
assert!(RealRegistryQuery::validate_crate_name("serde_json").is_ok());
assert!(RealRegistryQuery::validate_crate_name(
&"a".repeat(RealRegistryQuery::MAX_CRATE_NAME_LEN)
)
.is_ok());
assert!(RealRegistryQuery::validate_crate_name(
&"a".repeat(RealRegistryQuery::MAX_CRATE_NAME_LEN + 1)
)
.is_err());
assert!(RealRegistryQuery::validate_crate_name("").is_err());
assert!(RealRegistryQuery::validate_crate_name("-oops").is_err());
assert!(RealRegistryQuery::validate_crate_name("a/b").is_err());
assert!(RealRegistryQuery::validate_crate_name("a b").is_err());
assert!(RealRegistryQuery::validate_crate_name("a.b").is_err());
}
fn serve_once(response: &'static [u8]) -> String {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf);
let _ = stream.write_all(response);
let _ = stream.flush();
}
});
format!("http://{addr}/")
}
#[test]
fn http_get_delivers_200_status_and_body() {
let url = serve_once(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello");
let (status, body) = RealRegistryQuery::http_get(&url).unwrap();
assert_eq!(status, 200);
assert_eq!(body, b"hello");
}
#[test]
fn http_get_delivers_404_as_ok_status_not_error() {
let url = serve_once(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n");
let (status, body) = RealRegistryQuery::http_get(&url).unwrap();
assert_eq!(status, 404);
assert!(body.is_empty());
}
#[test]
fn http_get_delivers_5xx_as_ok_status() {
let url = serve_once(b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\n\r\n");
let (status, _) = RealRegistryQuery::http_get(&url).unwrap();
assert_eq!(status, 503);
}
#[test]
fn http_get_does_not_follow_redirects() {
let url = serve_once(
b"HTTP/1.1 301 Moved Permanently\r\nLocation: http://127.0.0.1:1/x\r\nContent-Length: 0\r\n\r\n",
);
let (status, _) = RealRegistryQuery::http_get(&url).unwrap();
assert_eq!(status, 301);
}
}