onelf_sysroot/
platform.rs1use std::io;
18use std::path::Path;
19
20pub const PLATFORM_FILE: &str = "etc/onelf/platform.toml";
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Pin {
26 pub label: String,
27 pub url: String,
28 pub blake3: String,
30}
31
32fn invalid(root: &Path, what: &str) -> io::Error {
33 io::Error::new(
34 io::ErrorKind::InvalidData,
35 format!("{}: {PLATFORM_FILE}: {what}", root.display()),
36 )
37}
38
39pub fn read(root: &Path) -> io::Result<Option<Pin>> {
41 let path = root.join(PLATFORM_FILE);
42 let text = match std::fs::read_to_string(&path) {
43 Ok(t) => t,
44 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
45 Err(e) => return Err(io::Error::new(e.kind(), format!("{}: {e}", path.display()))),
46 };
47 parse(&text).map(Some).map_err(|what| invalid(root, &what))
48}
49
50pub fn parse(text: &str) -> Result<Pin, String> {
53 let value: toml::Value = toml::from_str(text).map_err(|e| e.to_string())?;
54 let label = value
55 .get("label")
56 .and_then(toml::Value::as_str)
57 .ok_or("no label")?
58 .trim()
59 .to_string();
60 check_label(&label)?;
61 let gl = value.get("gl").ok_or("no [gl] table")?;
62 let url = gl
63 .get("url")
64 .and_then(toml::Value::as_str)
65 .ok_or("no gl.url")?
66 .to_string();
67 let blake3 = gl
68 .get("blake3")
69 .and_then(toml::Value::as_str)
70 .ok_or("no gl.blake3")?
71 .to_ascii_lowercase();
72 check_hash(&blake3)?;
73 check_url(&url)?;
74 Ok(Pin { label, url, blake3 })
75}
76
77pub fn check_label(label: &str) -> Result<(), String> {
80 if label.is_empty() || label.contains('/') || label.starts_with('.') {
81 return Err(format!(
82 "{label:?}: a label is a non-empty name without slashes or a leading dot"
83 ));
84 }
85 Ok(())
86}
87
88pub fn check_hash(hash: &str) -> Result<(), String> {
90 if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
91 return Err(format!("{hash}: not a BLAKE3 hash (64 hex characters)"));
92 }
93 Ok(())
94}
95
96pub fn check_url(url: &str) -> Result<(), String> {
99 if url.starts_with("https://") || url.starts_with("file://") {
100 Ok(())
101 } else {
102 Err(format!(
103 "{url}: the GL build must be an https:// or file:// URL"
104 ))
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::db::fixture::temp_root;
112
113 const HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
114
115 #[test]
116 fn reads_a_complete_file() {
117 let root = temp_root("platform");
118 std::fs::create_dir_all(root.join("etc/onelf")).unwrap();
119 std::fs::write(
120 root.join(PLATFORM_FILE),
121 format!("label = \"platform-1\"\n\n[gl]\nurl = \"https://example.com/gl.onelf\"\nblake3 = \"{}\"\n", HASH.to_ascii_uppercase()),
122 )
123 .unwrap();
124 let pin = read(&root).unwrap().unwrap();
125 assert_eq!(pin.label, "platform-1");
126 assert_eq!(pin.url, "https://example.com/gl.onelf");
127 assert_eq!(pin.blake3, HASH, "hash is normalized to lowercase");
128 }
129
130 #[test]
131 fn an_absent_file_is_no_pin() {
132 let root = temp_root("noplatform");
133 assert_eq!(read(&root).unwrap(), None);
134 }
135
136 #[test]
137 fn a_truncated_file_names_the_sysroot() {
138 let root = temp_root("badplatform");
139 std::fs::create_dir_all(root.join("etc/onelf")).unwrap();
140 std::fs::write(
141 root.join(PLATFORM_FILE),
142 "label = \"x\"\n[gl]\nurl = \"https://e/gl\"\nblake3 = \"012",
143 )
144 .unwrap();
145 let err = read(&root).unwrap_err();
146 assert!(err.to_string().contains("etc/onelf/platform.toml"), "{err}");
147 }
148
149 #[test]
150 fn schemes_and_hashes_are_checked() {
151 assert!(check_url("http://example.com/gl.onelf").is_err());
152 assert!(check_url("file:///srv/gl.onelf").is_ok());
153 assert!(check_hash("abc").is_err());
154 assert!(check_hash(HASH).is_ok());
155 let text = format!("label = \"a/b\"\n[gl]\nurl = \"https://e/gl\"\nblake3 = \"{HASH}\"\n");
156 assert!(parse(&text).is_err(), "a label is a directory name");
157 assert!(check_label(".hidden").is_err());
158 assert!(check_label("platform-1.tar.zst").is_ok());
159 }
160}