use std::io;
use std::path::Path;
pub const PLATFORM_FILE: &str = "etc/onelf/platform.toml";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pin {
pub label: String,
pub url: String,
pub blake3: String,
}
fn invalid(root: &Path, what: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{}: {PLATFORM_FILE}: {what}", root.display()),
)
}
pub fn read(root: &Path) -> io::Result<Option<Pin>> {
let path = root.join(PLATFORM_FILE);
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(io::Error::new(e.kind(), format!("{}: {e}", path.display()))),
};
parse(&text).map(Some).map_err(|what| invalid(root, &what))
}
pub fn parse(text: &str) -> Result<Pin, String> {
let value: toml::Value = toml::from_str(text).map_err(|e| e.to_string())?;
let label = value
.get("label")
.and_then(toml::Value::as_str)
.ok_or("no label")?
.trim()
.to_string();
check_label(&label)?;
let gl = value.get("gl").ok_or("no [gl] table")?;
let url = gl
.get("url")
.and_then(toml::Value::as_str)
.ok_or("no gl.url")?
.to_string();
let blake3 = gl
.get("blake3")
.and_then(toml::Value::as_str)
.ok_or("no gl.blake3")?
.to_ascii_lowercase();
check_hash(&blake3)?;
check_url(&url)?;
Ok(Pin { label, url, blake3 })
}
pub fn check_label(label: &str) -> Result<(), String> {
if label.is_empty() || label.contains('/') || label.starts_with('.') {
return Err(format!(
"{label:?}: a label is a non-empty name without slashes or a leading dot"
));
}
Ok(())
}
pub fn check_hash(hash: &str) -> Result<(), String> {
if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(format!("{hash}: not a BLAKE3 hash (64 hex characters)"));
}
Ok(())
}
pub fn check_url(url: &str) -> Result<(), String> {
if url.starts_with("https://") || url.starts_with("file://") {
Ok(())
} else {
Err(format!(
"{url}: the GL build must be an https:// or file:// URL"
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::fixture::temp_root;
const HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
#[test]
fn reads_a_complete_file() {
let root = temp_root("platform");
std::fs::create_dir_all(root.join("etc/onelf")).unwrap();
std::fs::write(
root.join(PLATFORM_FILE),
format!("label = \"platform-1\"\n\n[gl]\nurl = \"https://example.com/gl.onelf\"\nblake3 = \"{}\"\n", HASH.to_ascii_uppercase()),
)
.unwrap();
let pin = read(&root).unwrap().unwrap();
assert_eq!(pin.label, "platform-1");
assert_eq!(pin.url, "https://example.com/gl.onelf");
assert_eq!(pin.blake3, HASH, "hash is normalized to lowercase");
}
#[test]
fn an_absent_file_is_no_pin() {
let root = temp_root("noplatform");
assert_eq!(read(&root).unwrap(), None);
}
#[test]
fn a_truncated_file_names_the_sysroot() {
let root = temp_root("badplatform");
std::fs::create_dir_all(root.join("etc/onelf")).unwrap();
std::fs::write(
root.join(PLATFORM_FILE),
"label = \"x\"\n[gl]\nurl = \"https://e/gl\"\nblake3 = \"012",
)
.unwrap();
let err = read(&root).unwrap_err();
assert!(err.to_string().contains("etc/onelf/platform.toml"), "{err}");
}
#[test]
fn schemes_and_hashes_are_checked() {
assert!(check_url("http://example.com/gl.onelf").is_err());
assert!(check_url("file:///srv/gl.onelf").is_ok());
assert!(check_hash("abc").is_err());
assert!(check_hash(HASH).is_ok());
let text = format!("label = \"a/b\"\n[gl]\nurl = \"https://e/gl\"\nblake3 = \"{HASH}\"\n");
assert!(parse(&text).is_err(), "a label is a directory name");
assert!(check_label(".hidden").is_err());
assert!(check_label("platform-1.tar.zst").is_ok());
}
}