use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
pub const PEERS_LOCK_SCHEMA: &str = "chio.conformance.peers/v1";
pub const PEERS_LOCK_FILENAME: &str = "peers.lock.toml";
pub const SUPPORTED_LANGUAGES: &[&str] = &["python", "js", "go", "cpp"];
#[derive(Debug, Clone, Deserialize)]
pub struct PeersLock {
pub schema: String,
#[serde(default, rename = "peer")]
pub peers: Vec<PeerEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PeerEntry {
pub language: String,
pub url: String,
pub sha256: String,
pub target: String,
pub binary: String,
#[serde(default = "default_published")]
pub published: bool,
}
fn default_published() -> bool {
true
}
#[derive(Debug, thiserror::Error)]
pub enum PeersLockError {
#[error("i/o error reading {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("toml parse error in {path}: {source}")]
Toml {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("invalid peers.lock.toml: {0}")]
Validation(String),
}
impl PeersLock {
pub fn load(path: &Path) -> Result<Self, PeersLockError> {
let bytes = fs::read_to_string(path).map_err(|source| PeersLockError::Io {
path: path.to_path_buf(),
source,
})?;
Self::parse_str(&bytes).map_err(|source| match source {
PeersLockError::Toml { source, .. } => PeersLockError::Toml {
path: path.to_path_buf(),
source,
},
other => other,
})
}
pub fn parse_str(content: &str) -> Result<Self, PeersLockError> {
toml::from_str::<PeersLock>(content).map_err(|source| PeersLockError::Toml {
path: PathBuf::from("<memory>"),
source,
})
}
pub fn validate(&self) -> Result<(), PeersLockError> {
if self.schema != PEERS_LOCK_SCHEMA {
return Err(PeersLockError::Validation(format!(
"unexpected schema `{}`, expected `{}`",
self.schema, PEERS_LOCK_SCHEMA
)));
}
if self.peers.is_empty() {
return Err(PeersLockError::Validation(
"lockfile has no [[peer]] entries".to_string(),
));
}
for (idx, entry) in self.peers.iter().enumerate() {
entry
.validate()
.map_err(|message| PeersLockError::Validation(format!("peer[{idx}]: {message}")))?;
}
Ok(())
}
pub fn entries_for(&self, language: &str, target: &str) -> Vec<&PeerEntry> {
self.peers
.iter()
.filter(|entry| language.is_empty() || entry.language == language)
.filter(|entry| target.is_empty() || entry.target == target)
.collect()
}
pub fn entries_for_language(&self, language: &str) -> Vec<&PeerEntry> {
self.entries_for(language, "")
}
pub fn partition_by_published<'a>(
entries: &[&'a PeerEntry],
) -> (Vec<&'a PeerEntry>, Vec<&'a PeerEntry>) {
let mut published = Vec::new();
let mut skipped = Vec::new();
for entry in entries {
if entry.published {
published.push(*entry);
} else {
skipped.push(*entry);
}
}
(published, skipped)
}
}
impl PeerEntry {
fn validate(&self) -> Result<(), String> {
if !SUPPORTED_LANGUAGES.contains(&self.language.as_str()) {
return Err(format!(
"unsupported language `{}`; expected one of {:?}",
self.language, SUPPORTED_LANGUAGES,
));
}
if !self.url.starts_with("https://") {
return Err(format!("url `{}` is not https", self.url));
}
if !is_lowercase_hex_64(&self.sha256) {
return Err(format!(
"sha256 `{}` is not 64 lowercase hex chars",
self.sha256
));
}
if self.target.trim().is_empty() {
return Err("target must be non-empty".to_string());
}
if self.binary.trim().is_empty() {
return Err("binary must be non-empty".to_string());
}
if !is_safe_relative_binary_path(&self.binary) {
return Err(format!(
"binary `{}` must be a relative path inside the archive",
self.binary
));
}
Ok(())
}
}
fn is_safe_relative_binary_path(value: &str) -> bool {
let path = Path::new(value);
!path.is_absolute()
&& path
.components()
.all(|component| matches!(component, std::path::Component::Normal(_)))
}
fn is_lowercase_hex_64(value: &str) -> bool {
value.len() == 64
&& value
.chars()
.all(|ch| ch.is_ascii_digit() || ('a'..='f').contains(&ch))
}
#[must_use]
pub fn default_peers_lock_path() -> PathBuf {
if let Some(path) = std::env::var_os("CHIO_PEERS_LOCK") {
return PathBuf::from(path);
}
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
let candidate = PathBuf::from(xdg).join("chio").join(PEERS_LOCK_FILENAME);
if candidate.exists() {
return candidate;
}
}
if let Some(home) = std::env::var_os("HOME") {
let candidate = PathBuf::from(home)
.join(".config")
.join("chio")
.join(PEERS_LOCK_FILENAME);
if candidate.exists() {
return candidate;
}
}
let in_repo = in_repo_default_lock_path();
if in_repo.exists() {
return in_repo;
}
let cwd = PathBuf::from(PEERS_LOCK_FILENAME);
if cwd.exists() {
return cwd;
}
in_repo
}
fn in_repo_default_lock_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(PEERS_LOCK_FILENAME)
}
pub fn sha256_hex(bytes: &[u8]) -> String {
chio_core::sha256_hex(bytes)
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
const VALID: &str = r#"
schema = "chio.conformance.peers/v1"
[[peer]]
language = "python"
url = "https://example.com/py.tar.gz"
sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
target = "x86_64-unknown-linux-gnu"
binary = "chio-py-peer"
[[peer]]
language = "js"
url = "https://example.com/js.tar.gz"
sha256 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
target = "aarch64-apple-darwin"
binary = "chio-js-peer"
"#;
#[test]
fn parses_valid_lockfile() {
let lock = PeersLock::parse_str(VALID).expect("parse valid lockfile");
assert_eq!(lock.schema, PEERS_LOCK_SCHEMA);
assert_eq!(lock.peers.len(), 2);
lock.validate().expect("valid lockfile passes validation");
}
#[test]
fn rejects_wrong_schema() {
let bad = VALID.replace("chio.conformance.peers/v1", "chio.conformance.peers/v2");
let lock = PeersLock::parse_str(&bad).expect("still parses");
let err = lock.validate().expect_err("wrong schema rejected");
assert!(format!("{err}").contains("unexpected schema"));
}
#[test]
fn rejects_non_https_url() {
let bad = VALID.replace(
"https://example.com/py.tar.gz",
"http://example.com/py.tar.gz",
);
let lock = PeersLock::parse_str(&bad).expect("still parses");
let err = lock.validate().expect_err("http rejected");
assert!(format!("{err}").contains("not https"));
}
#[test]
fn rejects_short_sha256() {
let bad = VALID.replace(
"0000000000000000000000000000000000000000000000000000000000000000",
"deadbeef",
);
let lock = PeersLock::parse_str(&bad).expect("still parses");
let err = lock.validate().expect_err("short sha256 rejected");
assert!(format!("{err}").contains("64 lowercase hex"));
}
#[test]
fn rejects_uppercase_sha256() {
let bad = VALID.replace(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
);
let lock = PeersLock::parse_str(&bad).expect("still parses");
let err = lock.validate().expect_err("uppercase sha256 rejected");
assert!(format!("{err}").contains("64 lowercase hex"));
}
#[test]
fn rejects_unsupported_language() {
let bad = VALID.replace(r#"language = "python""#, r#"language = "ruby""#);
let lock = PeersLock::parse_str(&bad).expect("still parses");
let err = lock.validate().expect_err("ruby rejected");
assert!(format!("{err}").contains("unsupported language"));
}
#[test]
fn rejects_unsafe_binary_paths() {
for binary in ["../chio-py-peer", "/tmp/chio-py-peer"] {
let bad = VALID.replace(
r#"binary = "chio-py-peer""#,
&format!(r#"binary = "{binary}""#),
);
let lock = PeersLock::parse_str(&bad).expect("still parses");
let err = lock.validate().expect_err("unsafe binary path rejected");
assert!(format!("{err}").contains("relative path inside the archive"));
}
}
#[test]
fn rejects_empty_peer_list() {
let bad = "schema = \"chio.conformance.peers/v1\"\n";
let lock = PeersLock::parse_str(bad).expect("still parses");
let err = lock.validate().expect_err("empty list rejected");
assert!(format!("{err}").contains("no [[peer]] entries"));
}
#[test]
fn entries_for_filters_by_language_and_target() {
let lock = PeersLock::parse_str(VALID).expect("parse");
let py_only = lock.entries_for_language("python");
assert_eq!(py_only.len(), 1);
assert_eq!(py_only[0].language, "python");
let py_linux = lock.entries_for("python", "x86_64-unknown-linux-gnu");
assert_eq!(py_linux.len(), 1);
let py_darwin = lock.entries_for("python", "aarch64-apple-darwin");
assert!(py_darwin.is_empty());
let all = lock.entries_for("", "");
assert_eq!(all.len(), 2);
}
#[test]
fn shipped_lockfile_validates() {
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set during cargo test");
let path = Path::new(&manifest_dir).join("peers.lock.toml");
let lock = PeersLock::load(&path).expect("load shipped peers.lock.toml");
lock.validate().expect("shipped lockfile validates");
assert!(!lock.peers.is_empty());
}
#[test]
fn sha256_hex_matches_known_vector() {
assert_eq!(
sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
);
assert_eq!(
sha256_hex(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
);
}
#[test]
fn published_defaults_to_true() {
let lock = PeersLock::parse_str(VALID).expect("parse");
assert!(lock.peers[0].published);
assert!(lock.peers[1].published);
}
#[test]
fn partition_by_published_separates_skipped_entries() {
let raw = r#"
schema = "chio.conformance.peers/v1"
[[peer]]
language = "python"
url = "https://example.com/py.tar.gz"
sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
target = "x86_64-unknown-linux-gnu"
binary = "chio-py-peer"
published = false
[[peer]]
language = "js"
url = "https://example.com/js.tar.gz"
sha256 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
target = "aarch64-apple-darwin"
binary = "chio-js-peer"
published = true
"#;
let lock = PeersLock::parse_str(raw).expect("parse");
let all = lock.entries_for("", "");
let (published, skipped) = PeersLock::partition_by_published(&all);
assert_eq!(published.len(), 1);
assert_eq!(skipped.len(), 1);
assert_eq!(published[0].language, "js");
assert_eq!(skipped[0].language, "python");
}
#[test]
fn shipped_lockfile_marks_placeholders_as_unpublished() {
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set during cargo test");
let path = Path::new(&manifest_dir).join("peers.lock.toml");
let lock = PeersLock::load(&path).expect("load shipped peers.lock.toml");
for (idx, entry) in lock.peers.iter().enumerate() {
assert!(
!entry.published,
"shipped peer[{idx}] ({} / {}) is flagged published but the lockfile carries placeholder pins; flip published=true only when the release pipeline cuts a real binary",
entry.language,
entry.target,
);
}
}
}