#[macro_use]
extern crate log;
pub mod dirs;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha512};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct Store {
root: PathBuf,
cache_dir: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredFile {
pub hex_hash: String,
pub store_path: PathBuf,
pub executable: bool,
}
pub type PackageIndex = BTreeMap<String, StoredFile>;
impl Store {
pub fn default_location() -> Result<Self, Error> {
let root = dirs::store_dir().ok_or(Error::NoHome)?;
let cache_dir = dirs::cache_dir().ok_or(Error::NoHome)?;
Ok(Self { root, cache_dir })
}
pub fn with_root(root: PathBuf) -> Result<Self, Error> {
let cache_dir = dirs::cache_dir().ok_or(Error::NoHome)?;
Ok(Self { root, cache_dir })
}
pub fn at(root: PathBuf) -> Self {
let cache_dir = root.parent().unwrap_or(&root).join("aube-cache");
Self { root, cache_dir }
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn index_dir(&self) -> PathBuf {
self.cache_dir.join("index")
}
pub fn virtual_store_dir(&self) -> PathBuf {
self.cache_dir.join("virtual-store")
}
pub fn packument_cache_dir(&self) -> PathBuf {
self.cache_dir.join("packuments-v1")
}
pub fn packument_full_cache_dir(&self) -> PathBuf {
self.cache_dir.join("packuments-full-v1")
}
pub fn has(&self, integrity: &str) -> bool {
self.file_path_from_integrity(integrity)
.is_some_and(|p| p.exists())
}
pub fn file_path_from_integrity(&self, integrity: &str) -> Option<PathBuf> {
let hex_hash = integrity_to_hex(integrity)?;
Some(self.file_path_from_hex(&hex_hash))
}
pub fn file_path_from_hex(&self, hex_hash: &str) -> PathBuf {
let (shard, rest) = hex_hash.split_at(2);
self.root.join(shard).join(rest)
}
pub fn load_index(&self, name: &str, version: &str) -> Option<PackageIndex> {
self.load_index_inner(name, version, false)
}
pub fn load_index_verified(&self, name: &str, version: &str) -> Option<PackageIndex> {
self.load_index_inner(name, version, true)
}
fn load_index_inner(
&self,
name: &str,
version: &str,
verify_files: bool,
) -> Option<PackageIndex> {
let safe_name = name.replace('/', "__");
let index_path = self.index_dir().join(format!("{safe_name}@{version}.json"));
let content = xx::file::read_to_string(&index_path).ok()?;
let index: PackageIndex = serde_json::from_str(&content).ok()?;
if verify_files {
if !index.values().all(|f| f.store_path.exists()) {
trace!("cache stale: {name}@{version}");
let _ = xx::file::remove_file(&index_path);
return None;
}
} else {
if let Some(f) = index.values().next()
&& !f.store_path.exists()
{
trace!("cache stale: {name}@{version}");
let _ = xx::file::remove_file(&index_path);
return None;
}
}
trace!("cache hit: {name}@{version}");
Some(index)
}
pub fn save_index(&self, name: &str, version: &str, index: &PackageIndex) -> Result<(), Error> {
let safe_name = name.replace('/', "__");
let index_path = self.index_dir().join(format!("{safe_name}@{version}.json"));
let json =
serde_json::to_string(index).map_err(|e| Error::Tar(format!("serialize: {e}")))?;
xx::file::write(&index_path, json).map_err(|e| Error::Xx(e.to_string()))?;
trace!("cached index: {name}@{version}");
Ok(())
}
pub fn ensure_shards_exist(&self) -> Result<(), Error> {
std::fs::create_dir_all(&self.root).map_err(|e| Error::Io(self.root.clone(), e))?;
let mut buf = [0u8; 2];
for hi in 0u8..16 {
for lo in 0u8..16 {
buf[0] = hex_digit(hi);
buf[1] = hex_digit(lo);
let shard = std::str::from_utf8(&buf).unwrap();
let path = self.root.join(shard);
std::fs::create_dir_all(&path).map_err(|e| Error::Io(path, e))?;
}
}
Ok(())
}
pub fn import_bytes(&self, content: &[u8], executable: bool) -> Result<StoredFile, Error> {
let hex_hash = blake3::hash(content).to_hex().to_string();
let store_path = self.file_path_from_hex(&hex_hash);
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&store_path)
{
Ok(mut file) => {
use std::io::Write;
file.write_all(content)
.map_err(|e| Error::Io(store_path.clone(), e))?;
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
xx::file::write(&store_path, content).map_err(|e| Error::Xx(e.to_string()))?;
}
Err(e) => return Err(Error::Io(store_path.clone(), e)),
}
if executable {
let exec_marker = PathBuf::from(format!("{}-exec", store_path.display()));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&exec_marker)
{
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
xx::file::write(&exec_marker, "").map_err(|e| Error::Xx(e.to_string()))?;
}
Err(e) => return Err(Error::Io(exec_marker, e)),
}
}
Ok(StoredFile {
hex_hash,
store_path,
executable,
})
}
pub fn import_directory(&self, dir: &Path) -> Result<PackageIndex, Error> {
let mut index = BTreeMap::new();
self.import_directory_recursive(dir, dir, &mut index)?;
Ok(index)
}
fn import_directory_recursive(
&self,
base: &Path,
current: &Path,
index: &mut PackageIndex,
) -> Result<(), Error> {
let entries = std::fs::read_dir(current)
.map_err(|e| Error::Tar(format!("read_dir {}: {e}", current.display())))?;
for entry in entries {
let entry =
entry.map_err(|e| Error::Tar(format!("read_dir {}: {e}", current.display())))?;
let file_type = entry
.file_type()
.map_err(|e| Error::Tar(format!("file_type: {e}")))?;
let name_os = entry.file_name();
let name_str = name_os.to_string_lossy();
if matches!(name_str.as_ref(), ".git" | "node_modules") {
continue;
}
let path = entry.path();
if file_type.is_dir() {
self.import_directory_recursive(base, &path, index)?;
continue;
}
if !file_type.is_file() {
continue;
}
let content = std::fs::read(&path)
.map_err(|e| Error::Tar(format!("read {}: {e}", path.display())))?;
#[cfg(unix)]
let executable = {
use std::os::unix::fs::PermissionsExt;
let meta = entry
.metadata()
.map_err(|e| Error::Tar(format!("metadata: {e}")))?;
meta.permissions().mode() & 0o111 != 0
};
#[cfg(not(unix))]
let executable = false;
let stored = self.import_bytes(&content, executable)?;
let rel = path
.strip_prefix(base)
.map_err(|e| Error::Tar(format!("strip_prefix: {e}")))?
.to_string_lossy()
.replace('\\', "/");
index.insert(rel, stored);
}
Ok(())
}
pub fn import_tarball(&self, tarball_bytes: &[u8]) -> Result<PackageIndex, Error> {
let gz = flate2::read::GzDecoder::new(tarball_bytes);
let mut archive = tar::Archive::new(gz);
let mut index = BTreeMap::new();
for entry in archive.entries().map_err(|e| Error::Tar(e.to_string()))? {
let mut entry = entry.map_err(|e| Error::Tar(e.to_string()))?;
if entry.header().entry_type().is_dir() {
continue;
}
let raw_path = entry
.path()
.map_err(|e| Error::Tar(e.to_string()))?
.to_path_buf();
let rel_path = {
let mut components = raw_path.components();
components.next(); let stripped: PathBuf = components.collect();
let s = if stripped.as_os_str().is_empty() {
raw_path.to_string_lossy().to_string()
} else {
stripped.to_string_lossy().to_string()
};
s.replace('\\', "/")
};
let mut content = Vec::new();
std::io::Read::read_to_end(&mut entry, &mut content)
.map_err(|e| Error::Tar(e.to_string()))?;
let mode = entry.header().mode().unwrap_or(0o644);
let executable = mode & 0o111 != 0;
let stored = self.import_bytes(&content, executable)?;
index.insert(rel_path, stored);
}
Ok(index)
}
}
fn hex_digit(n: u8) -> u8 {
match n {
0..=9 => b'0' + n,
10..=15 => b'a' + n - 10,
_ => unreachable!(),
}
}
pub fn verify_integrity(data: &[u8], expected: &str) -> Result<(), Error> {
let Some(expected_b64) = expected.strip_prefix("sha512-") else {
return Err(Error::Integrity(format!(
"unsupported integrity format (expected sha512-...): {expected}"
)));
};
let mut hasher = Sha512::new();
hasher.update(data);
let actual_bytes = hasher.finalize();
use base64::Engine;
let actual_b64 = base64::engine::general_purpose::STANDARD.encode(actual_bytes);
if actual_b64 == expected_b64 {
Ok(())
} else {
Err(Error::Integrity(format!(
"integrity mismatch: expected sha512-{expected_b64}, got sha512-{actual_b64}"
)))
}
}
pub fn validate_pkg_content(
index: &PackageIndex,
expected_name: &str,
expected_version: &str,
) -> Result<(), Error> {
let stored = index
.get("package.json")
.ok_or_else(|| Error::Tar("package.json missing from tarball".to_string()))?;
let bytes =
std::fs::read(&stored.store_path).map_err(|e| Error::Io(stored.store_path.clone(), e))?;
let v: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| Error::Tar(format!("invalid package.json: {e}")))?;
let actual_name = v.get("name").and_then(|n| n.as_str()).unwrap_or("");
let actual_version = v.get("version").and_then(|v| v.as_str()).unwrap_or("");
if actual_name != expected_name || actual_version != expected_version {
return Err(Error::PkgContentMismatch {
actual: format!("{actual_name}@{actual_version}"),
});
}
Ok(())
}
pub fn integrity_to_hex(integrity: &str) -> Option<String> {
let b64 = integrity.strip_prefix("sha512-")?;
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD.decode(b64).ok()?;
Some(hex::encode(bytes))
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("HOME environment variable not set")]
NoHome,
#[error("I/O error at {0}: {1}")]
Io(PathBuf, std::io::Error),
#[error("file error: {0}")]
Xx(String),
#[error("tarball extraction error: {0}")]
Tar(String),
#[error("integrity verification failed: {0}")]
Integrity(String),
#[error("package.json content mismatch: tarball declares {actual}")]
PkgContentMismatch { actual: String },
#[error("git error: {0}")]
Git(String),
}
pub fn git_resolve_ref(url: &str, committish: Option<&str>) -> Result<String, Error> {
if let Some(c) = committish
&& c.len() == 40
&& c.chars().all(|ch| ch.is_ascii_hexdigit())
{
return Ok(c.to_ascii_lowercase());
}
let out = std::process::Command::new("git")
.arg("ls-remote")
.arg(url)
.output()
.map_err(|e| Error::Git(format!("spawn git ls-remote {url}: {e}")))?;
if !out.status.success() {
return Err(Error::Git(format!(
"git ls-remote {url} failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
)));
}
let stdout = String::from_utf8_lossy(&out.stdout);
let mut head: Option<String> = None;
let mut main_branch: Option<String> = None;
let mut master_branch: Option<String> = None;
let mut tag_match: Option<String> = None;
let mut head_match: Option<String> = None;
let mut first: Option<String> = None;
for line in stdout.lines() {
let mut parts = line.split('\t');
let sha = parts.next().unwrap_or("").trim();
let name = parts.next().unwrap_or("").trim();
if sha.is_empty() || name.is_empty() {
continue;
}
if first.is_none() {
first = Some(sha.to_string());
}
match name {
"HEAD" => head = Some(sha.to_string()),
"refs/heads/main" => main_branch = Some(sha.to_string()),
"refs/heads/master" => master_branch = Some(sha.to_string()),
_ => {}
}
if let Some(want) = committish {
if name == format!("refs/tags/{want}") || name == format!("refs/tags/{want}^{{}}") {
tag_match = Some(sha.to_string());
} else if name == format!("refs/heads/{want}") {
head_match = Some(sha.to_string());
}
}
}
if let Some(want) = committish {
if let Some(sha) = tag_match.or(head_match) {
return Ok(sha);
}
let looks_hex =
want.len() >= 4 && want.len() < 40 && want.chars().all(|c| c.is_ascii_hexdigit());
if looks_hex {
return Err(Error::Git(format!(
"git ls-remote {url}: `#{want}` looks like an abbreviated commit SHA — aube requires a full 40-character SHA, or a branch/tag name"
)));
}
Err(Error::Git(format!(
"git ls-remote {url}: no ref matched {want}"
)))
} else {
head.or(main_branch)
.or(master_branch)
.or(first)
.ok_or_else(|| Error::Git(format!("git ls-remote {url}: no refs advertised")))
}
}
pub fn git_host_in_list(url: &str, hosts: &[String]) -> bool {
let Some(host) = git_url_host(url) else {
return false;
};
hosts.iter().any(|h| h == host)
}
pub fn git_url_host(url: &str) -> Option<&str> {
let rest = url.strip_prefix("git+").unwrap_or(url);
let after_scheme = match rest.split_once("://") {
Some((_, r)) => r,
None => {
let (userhost, _) = rest.split_once(':')?;
let host = userhost
.rsplit_once('@')
.map(|(_, h)| h)
.unwrap_or(userhost);
if host.is_empty() || host.contains('/') {
return None;
}
return Some(host);
}
};
let authority = after_scheme
.split_once('/')
.map(|(a, _)| a)
.unwrap_or(after_scheme);
let host_with_port = authority
.rsplit_once('@')
.map(|(_, h)| h)
.unwrap_or(authority);
let host = if let Some(inner) = host_with_port.strip_prefix('[') {
inner.split_once(']').map(|(h, _)| h).unwrap_or(inner)
} else {
host_with_port
.rsplit_once(':')
.map(|(h, _)| h)
.unwrap_or(host_with_port)
};
if host.is_empty() { None } else { Some(host) }
}
pub fn git_shallow_clone(url: &str, commit: &str, shallow: bool) -> Result<PathBuf, Error> {
use std::process::Command;
let mut hasher = blake3::Hasher::new();
hasher.update(url.as_bytes());
hasher.update(b"\0");
hasher.update(commit.as_bytes());
let digest = hasher.finalize();
let key: String = digest
.as_bytes()
.iter()
.take(8)
.map(|b| format!("{b:02x}"))
.collect();
let commit_short = commit.get(..commit.len().min(12)).unwrap_or(commit);
let target = std::env::temp_dir().join(format!("aube-git-{key}-{commit_short}"));
if target.join(".git").is_dir()
&& let Ok(out) = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&target)
.output()
&& out.status.success()
&& String::from_utf8_lossy(&out.stdout).trim() == commit
{
return Ok(target);
}
let scratch = std::env::temp_dir().join(format!(
"aube-git-{key}-{commit_short}.tmp.{}",
std::process::id()
));
if scratch.exists() {
let _ = std::fs::remove_dir_all(&scratch);
}
std::fs::create_dir_all(&scratch).map_err(|e| Error::Io(scratch.clone(), e))?;
let run_in = |dir: &Path, args: &[&str]| -> Result<(), Error> {
let out = Command::new("git")
.args(args)
.current_dir(dir)
.output()
.map_err(|e| Error::Git(format!("spawn git {args:?}: {e}")))?;
if !out.status.success() {
return Err(Error::Git(format!(
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(())
};
let do_clone = || -> Result<(), Error> {
run_in(&scratch, &["init", "-q"])?;
run_in(&scratch, &["remote", "add", "origin", url])?;
let shallow_ok =
shallow && run_in(&scratch, &["fetch", "--depth", "1", "-q", "origin", commit]).is_ok();
if !shallow_ok {
run_in(&scratch, &["fetch", "-q", "origin"])?;
}
run_in(&scratch, &["checkout", "-q", commit])?;
Ok(())
};
if let Err(e) = do_clone() {
let _ = std::fs::remove_dir_all(&scratch);
return Err(e);
}
match std::fs::rename(&scratch, &target) {
Ok(()) => Ok(target),
Err(_) => {
if target.join(".git").is_dir()
&& let Ok(out) = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&target)
.output()
&& out.status.success()
&& String::from_utf8_lossy(&out.stdout).trim() == commit
{
let _ = std::fs::remove_dir_all(&scratch);
return Ok(target);
}
let _ = std::fs::remove_dir_all(&target);
std::fs::rename(&scratch, &target).map_err(|e| {
let _ = std::fs::remove_dir_all(&scratch);
Error::Git(format!("rename clone into place: {e}"))
})?;
Ok(target)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_integrity_to_hex() {
let integrity = "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
let result = integrity_to_hex(integrity);
assert!(result.is_some());
let hex = result.unwrap();
assert_eq!(hex.len(), 128);
assert!(hex.chars().all(|c| c == '0'));
}
#[test]
fn test_integrity_to_hex_invalid() {
assert!(integrity_to_hex("md5-abc").is_none());
assert!(integrity_to_hex("notahash").is_none());
assert!(integrity_to_hex("sha256-abc").is_none());
assert!(integrity_to_hex("").is_none());
}
#[test]
fn test_file_path_from_hex_sharding() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let path = store.file_path_from_hex("abcdef1234567890");
let sep = std::path::MAIN_SEPARATOR;
assert!(path.to_string_lossy().contains(&format!("{sep}ab{sep}")));
assert!(path.to_string_lossy().ends_with("cdef1234567890"));
}
#[test]
fn test_import_bytes() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let content = b"hello world";
let stored = store.import_bytes(content, false).unwrap();
assert!(stored.store_path.exists());
assert_eq!(std::fs::read(&stored.store_path).unwrap(), content);
assert!(!stored.executable);
let stored2 = store.import_bytes(content, false).unwrap();
assert_eq!(stored.hex_hash, stored2.hex_hash);
}
#[test]
fn test_verify_integrity_valid() {
let data = b"hello world";
let mut hasher = Sha512::new();
hasher.update(data);
let hash = hasher.finalize();
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(hash);
let integrity = format!("sha512-{b64}");
assert!(verify_integrity(data, &integrity).is_ok());
}
#[test]
fn test_verify_integrity_mismatch() {
let data = b"hello world";
let wrong = "sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
let result = verify_integrity(data, wrong);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("integrity mismatch")
);
}
#[test]
fn test_verify_integrity_unsupported_format() {
let result = verify_integrity(b"test", "md5-abc123");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unsupported"));
}
#[test]
fn test_import_bytes_executable() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let content = b"#!/bin/sh\necho hello";
let stored = store.import_bytes(content, true).unwrap();
assert!(stored.executable);
let exec_marker = PathBuf::from(format!("{}-exec", stored.store_path.display()));
assert!(exec_marker.exists());
}
#[test]
fn test_import_bytes_different_content_different_hash() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let stored1 = store.import_bytes(b"content a", false).unwrap();
let stored2 = store.import_bytes(b"content b", false).unwrap();
assert_ne!(stored1.hex_hash, stored2.hex_hash);
}
#[test]
fn test_index_cache_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let content = b"test file";
let stored = store.import_bytes(content, false).unwrap();
let mut index = BTreeMap::new();
index.insert("index.js".to_string(), stored);
store.save_index("test-pkg", "1.0.0", &index).unwrap();
let loaded = store.load_index("test-pkg", "1.0.0");
assert!(loaded.is_some());
let loaded = loaded.unwrap();
assert_eq!(loaded.len(), 1);
assert!(loaded.contains_key("index.js"));
}
#[test]
fn test_index_cache_scoped_package() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let stored = store.import_bytes(b"scoped content", false).unwrap();
let mut index = BTreeMap::new();
index.insert("index.js".to_string(), stored);
store.save_index("@scope/pkg", "1.0.0", &index).unwrap();
let loaded = store.load_index("@scope/pkg", "1.0.0");
assert!(loaded.is_some());
}
#[test]
fn test_index_cache_stale_detection() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let stored = store.import_bytes(b"content", false).unwrap();
let store_path = stored.store_path.clone();
let mut index = BTreeMap::new();
index.insert("index.js".to_string(), stored);
store.save_index("pkg", "1.0.0", &index).unwrap();
std::fs::remove_file(&store_path).unwrap();
let loaded = store.load_index("pkg", "1.0.0");
assert!(loaded.is_none());
}
#[test]
fn test_index_cache_miss() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
assert!(store.load_index("nonexistent", "1.0.0").is_none());
}
fn index_with_manifest(store: &Store, name: &str, version: &str) -> PackageIndex {
let manifest =
serde_json::json!({"name": name, "version": version, "main": "index.js"}).to_string();
let stored = store.import_bytes(manifest.as_bytes(), false).unwrap();
let mut index = BTreeMap::new();
index.insert("package.json".to_string(), stored);
index
}
#[test]
fn test_validate_pkg_content_match() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let index = index_with_manifest(&store, "lodash", "4.17.21");
assert!(validate_pkg_content(&index, "lodash", "4.17.21").is_ok());
}
#[test]
fn test_validate_pkg_content_name_mismatch() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let index = index_with_manifest(&store, "evil-pkg", "1.0.0");
let err = validate_pkg_content(&index, "lodash", "1.0.0").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("content mismatch"), "{msg}");
assert!(msg.contains("declares evil-pkg@1.0.0"), "{msg}");
}
#[test]
fn test_validate_pkg_content_version_mismatch() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let index = index_with_manifest(&store, "lodash", "9.9.9");
let err = validate_pkg_content(&index, "lodash", "4.17.21").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("content mismatch"), "{msg}");
assert!(msg.contains("declares lodash@9.9.9"), "{msg}");
}
#[test]
fn test_validate_pkg_content_missing_manifest() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let stored = store.import_bytes(b"module.exports = 1;", false).unwrap();
let mut index = PackageIndex::new();
index.insert("index.js".to_string(), stored);
let err = validate_pkg_content(&index, "lodash", "4.17.21").unwrap_err();
assert!(err.to_string().contains("package.json missing"), "{err}",);
}
#[test]
fn test_validate_pkg_content_unparseable_manifest() {
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let stored = store.import_bytes(b"{not json", false).unwrap();
let mut index = PackageIndex::new();
index.insert("package.json".to_string(), stored);
let err = validate_pkg_content(&index, "lodash", "4.17.21").unwrap_err();
assert!(err.to_string().contains("invalid package.json"), "{err}");
}
#[test]
fn test_import_tarball() {
let mut builder = tar::Builder::new(Vec::new());
let content = b"module.exports = 42;\n";
let mut header = tar::Header::new_gnu();
header.set_size(content.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder
.append_data(&mut header, "package/index.js", &content[..])
.unwrap();
let bin_content = b"#!/usr/bin/env node\nconsole.log('hi');\n";
let mut bin_header = tar::Header::new_gnu();
bin_header.set_size(bin_content.len() as u64);
bin_header.set_mode(0o755);
bin_header.set_cksum();
builder
.append_data(&mut bin_header, "package/bin/cli.js", &bin_content[..])
.unwrap();
let tar_bytes = builder.into_inner().unwrap();
use flate2::write::GzEncoder;
use std::io::Write;
let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::fast());
encoder.write_all(&tar_bytes).unwrap();
let tgz_bytes = encoder.finish().unwrap();
let dir = tempfile::tempdir().unwrap();
let store = Store::at(dir.path().join("files"));
let index = store.import_tarball(&tgz_bytes).unwrap();
assert_eq!(index.len(), 2);
assert!(index.contains_key("index.js"));
assert!(index.contains_key("bin/cli.js"));
let idx_stored = &index["index.js"];
assert!(!idx_stored.executable);
assert_eq!(std::fs::read(&idx_stored.store_path).unwrap(), content);
let bin_stored = &index["bin/cli.js"];
assert!(bin_stored.executable);
assert_eq!(std::fs::read(&bin_stored.store_path).unwrap(), bin_content);
}
#[test]
fn test_git_url_host_https() {
assert_eq!(
git_url_host("https://github.com/user/repo.git"),
Some("github.com")
);
assert_eq!(
git_url_host("git+https://github.com/user/repo.git#main"),
Some("github.com")
);
assert_eq!(
git_url_host("git://git.example.com/repo.git"),
Some("git.example.com")
);
}
#[test]
fn test_git_url_host_ssh() {
assert_eq!(
git_url_host("git+ssh://git@github.com/user/repo.git"),
Some("github.com")
);
assert_eq!(
git_url_host("ssh://git@gitlab.com:2222/user/repo.git"),
Some("gitlab.com")
);
assert_eq!(
git_url_host("git@github.com:user/repo.git"),
Some("github.com")
);
}
#[test]
fn test_git_url_host_ipv6() {
assert_eq!(git_url_host("https://[::1]/repo.git"), Some("::1"));
assert_eq!(git_url_host("https://[::1]:8443/repo.git"), Some("::1"));
assert_eq!(
git_url_host("ssh://git@[2001:db8::1]:2222/user/repo.git"),
Some("2001:db8::1")
);
}
#[test]
fn test_git_url_host_rejects_garbage() {
assert_eq!(git_url_host(""), None);
assert_eq!(git_url_host("not a url"), None);
assert_eq!(git_url_host("/just/a/path"), None);
}
#[test]
fn test_git_host_in_list_exact_match() {
let hosts = vec![
"github.com".to_string(),
"gitlab.com".to_string(),
"bitbucket.org".to_string(),
];
assert!(git_host_in_list("https://github.com/user/repo.git", &hosts));
assert!(git_host_in_list(
"git+ssh://git@gitlab.com/user/repo.git",
&hosts
));
assert!(!git_host_in_list(
"https://api.github.com/user/repo.git",
&hosts
));
assert!(!git_host_in_list(
"https://self-hosted.example/user/repo.git",
&hosts
));
}
#[test]
fn test_git_host_in_list_empty_list() {
let hosts: Vec<String> = vec![];
assert!(!git_host_in_list(
"https://github.com/user/repo.git",
&hosts
));
}
}