1use super::{json::Value, Provider, FORMAT, RELEASES, SONAMES};
13use crate::{elf, glibc::Snapshot, libgcc, sha256, stub, version::Version};
14use std::{
15 collections::BTreeSet,
16 env, fmt, fs,
17 path::{Path, PathBuf},
18};
19
20pub const TARGET: &str = "x86_64-unknown-linux-gnu";
22pub const MANIFEST_FORMAT: &str = "libc-abi-provider-manifest-1";
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
27pub enum Mode {
28 Source,
30 Prebuilt,
32 Container,
34}
35
36impl Mode {
37 pub const ALL: [Mode; 3] = [Mode::Source, Mode::Prebuilt, Mode::Container];
39
40 pub fn name(self) -> &'static str {
43 match self {
44 Mode::Source => "source",
45 Mode::Prebuilt => "prebuilt",
46 Mode::Container => "container",
47 }
48 }
49}
50
51impl fmt::Display for Mode {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str(self.name())
54 }
55}
56
57impl std::str::FromStr for Mode {
58 type Err = String;
59 fn from_str(text: &str) -> Result<Self, String> {
60 Mode::ALL
61 .into_iter()
62 .find(|m| m.name() == text)
63 .ok_or_else(|| {
64 format!("unknown provider mode {text:?}; use source, prebuilt or container")
65 })
66 }
67}
68
69pub fn default_root() -> Result<PathBuf, String> {
71 let root = if let Some(dir) = env::var_os("CARGO_LIBC_PROVIDER_DIR") {
72 PathBuf::from(dir)
73 } else if let Some(home) = env::var_os("CARGO_HOME") {
74 PathBuf::from(home).join("libc-providers")
75 } else if let Some(home) = env::var_os("HOME") {
76 PathBuf::from(home).join(".cargo/libc-providers")
77 } else {
78 return Err(
79 "set CARGO_LIBC_PROVIDER_DIR, CARGO_HOME or HOME for the provider cache".into(),
80 );
81 };
82 if !root.is_absolute() {
83 return Err(format!(
84 "the provider cache {} must be an absolute path",
85 root.display()
86 ));
87 }
88 Ok(root)
89}
90
91pub fn release(name: &str) -> Result<&'static str, String> {
93 RELEASES
94 .iter()
95 .copied()
96 .find(|r| *r == name)
97 .ok_or_else(|| {
98 format!(
99 "glibc {name} has no provider; supported: {}",
100 RELEASES.join(", ")
101 )
102 })
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct Entry {
108 pub release: String,
110 pub mode: Mode,
112 pub hash: String,
114 pub dir: PathBuf,
116}
117
118impl Entry {
119 pub fn descriptor(&self) -> PathBuf {
122 self.dir.join("provider.txt")
123 }
124
125 pub fn manifest(&self) -> Result<Value, String> {
131 let path = self.dir.join("manifest.json");
132 let text = fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
133 super::json::parse(&text).map_err(|e| format!("{}: {e}", path.display()))
134 }
135}
136
137pub fn release_dir(root: &Path, release: &str) -> PathBuf {
139 root.join(TARGET).join(format!("glibc-{release}"))
140}
141
142pub fn entry(root: &Path, release: &str, mode: Mode, hash: &str) -> Entry {
144 Entry {
145 release: release.into(),
146 mode,
147 hash: hash.into(),
148 dir: release_dir(root, release).join(format!("{mode}-{hash}")),
149 }
150}
151
152pub fn list(root: &Path) -> Result<Vec<Entry>, String> {
154 let mut entries = Vec::new();
155 let target = root.join(TARGET);
156 if !target.is_dir() {
157 return Ok(entries);
158 }
159 for &release in RELEASES {
160 let dir = release_dir(root, release);
161 let Ok(read) = fs::read_dir(&dir) else {
162 continue;
163 };
164 for item in read {
165 let item = item.map_err(|e| e.to_string())?;
166 let name = item.file_name().to_string_lossy().into_owned();
167 let Some((mode, hash)) = name.split_once('-') else {
168 continue;
169 };
170 let (Ok(mode), true) = (mode.parse::<Mode>(), hex(hash)) else {
171 continue;
172 };
173 if item.path().join("manifest.json").is_file() {
174 entries.push(Entry {
175 release: release.into(),
176 mode,
177 hash: hash.into(),
178 dir: item.path(),
179 });
180 }
181 }
182 }
183 entries.sort_by(|a, b| {
184 let key = |e: &Entry| (e.release.parse::<Version>().ok(), e.mode, e.hash.clone());
185 key(a).cmp(&key(b))
186 });
187 Ok(entries)
188}
189
190fn hex(text: &str) -> bool {
191 !text.is_empty() && text.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
192}
193
194pub fn content_hash(inputs: &[(&str, &[u8])]) -> String {
196 let mut text = Vec::new();
197 for (name, bytes) in inputs {
198 text.extend(format!("{name}\t{}\n", bytes.len()).as_bytes());
199 text.extend(*bytes);
200 text.push(b'\n');
201 }
202 sha256::hex(&sha256::sha256(&text))[..16].to_string()
203}
204
205pub fn inventory(dir: &Path) -> Result<Value, String> {
208 let mut items = Vec::new();
209 let mut stack = vec![PathBuf::new()];
210 let mut seen = BTreeSet::new();
211 while let Some(relative) = stack.pop() {
212 let mut names: Vec<_> = fs::read_dir(dir.join(&relative))
213 .map_err(|e| format!("{}: {e}", dir.join(&relative).display()))?
214 .map(|e| e.map(|e| e.file_name()))
215 .collect::<Result<_, _>>()
216 .map_err(|e| e.to_string())?;
217 names.sort();
218 for name in names {
219 let path = relative.join(&name);
220 let full = dir.join(&path);
221 let text = path
222 .to_str()
223 .ok_or("provider paths must be UTF-8")?
224 .replace('\\', "/");
225 if relative.as_os_str().is_empty()
226 && matches!(text.as_str(), "provider.txt" | "manifest.json")
227 {
228 continue;
229 }
230 let meta = fs::symlink_metadata(&full).map_err(|e| e.to_string())?;
231 if meta.file_type().is_symlink() {
232 let link = fs::read_link(&full).map_err(|e| e.to_string())?;
233 let link = link.to_str().ok_or("link targets must be UTF-8")?;
234 if Path::new(link).is_absolute() || link.contains("..") {
235 return Err(format!("{text}: links must stay in their directory"));
236 }
237 seen.insert(text.clone());
238 items.push(Value::Object(vec![
239 ("path".into(), Value::String(text)),
240 ("link".into(), Value::String(link.into())),
241 ]));
242 } else if meta.is_dir() {
243 stack.push(path);
244 } else if meta.is_file() {
245 let bytes = fs::read(&full).map_err(|e| format!("{}: {e}", full.display()))?;
246 seen.insert(text.clone());
247 items.push(Value::Object(vec![
248 ("path".into(), Value::String(text)),
249 (
250 "sha256".into(),
251 Value::String(sha256::hex(&sha256::sha256(&bytes))),
252 ),
253 ("size".into(), Value::Integer(bytes.len() as u64)),
254 ]));
255 } else {
256 return Err(format!("{text}: not a file, directory or link"));
257 }
258 }
259 }
260 items.sort_by(|a, b| a.str("path").ok().cmp(&b.str("path").ok()));
261 Ok(Value::Array(items))
262}
263
264pub fn link_stub(
267 tools: &super::build::Tools,
268 dir: &Path,
269 base: &str,
270 soname: &str,
271 sources: &stub::Sources,
272) -> Result<(), String> {
273 let (assembly, map, object) = (
274 dir.join(format!("{base}.S")),
275 dir.join(format!("{base}.map")),
276 dir.join(format!("{base}.o")),
277 );
278 fs::write(&assembly, &sources.assembly).map_err(|e| e.to_string())?;
279 fs::write(&map, &sources.versions).map_err(|e| e.to_string())?;
280 let text = |p: &Path| p.to_string_lossy().into_owned();
281 super::build::run(
282 &tools.assembler,
283 &["--64".into(), text(&assembly), "-o".into(), text(&object)],
284 dir,
285 )?;
286 super::build::run(
287 &tools.linker,
288 &[
289 "-shared".into(),
290 "--build-id=none".into(),
291 "-soname".into(),
292 soname.into(),
293 format!("--version-script={}", text(&map)),
294 text(&object),
295 "-o".into(),
296 text(&dir.join(soname)),
297 ],
298 dir,
299 )?;
300 fs::remove_file(&object).map_err(|e| e.to_string())
301}
302
303pub fn symlink(dir: &Path, target: &str, name: &str) -> Result<(), String> {
305 #[cfg(unix)]
306 {
307 std::os::unix::fs::symlink(target, dir.join(name))
308 .map_err(|e| format!("{}: {e}", dir.join(name).display()))
309 }
310 #[cfg(not(unix))]
311 {
312 let _ = (dir, target, name);
313 Err("providers are prepared on Linux only".into())
314 }
315}
316
317pub fn write_glibc_stubs(
319 tools: &super::build::Tools,
320 dir: &Path,
321 stubs: &[(&str, stub::Sources)],
322) -> Result<(), String> {
323 fs::create_dir_all(dir).map_err(|e| e.to_string())?;
324 for (library, sources) in stubs {
325 let soname = stub::glibc_soname(library).ok_or("unknown library")?;
326 link_stub(tools, dir, library, soname, sources)?;
327 symlink(dir, soname, &format!("lib{library}.so"))?;
328 }
329 Ok(())
330}
331
332pub fn write_libgcc_stub(tools: &super::build::Tools, dir: &Path) -> Result<(), String> {
334 fs::create_dir_all(dir).map_err(|e| e.to_string())?;
335 link_stub(tools, dir, "libgcc_s", libgcc::SONAME, &libgcc::stub()?)?;
336 symlink(dir, libgcc::SONAME, "libgcc_s.so")
337}
338
339pub fn verify(entry: &Entry, snapshot: &Snapshot) -> Result<Provider, String> {
343 let manifest = entry.manifest()?;
344 if manifest.str("format")? != MANIFEST_FORMAT
345 || manifest.str("glibc")? != entry.release
346 || manifest.str("mode")? != entry.mode.name()
347 || manifest.str("hash")? != entry.hash
348 {
349 return Err(format!(
350 "{}: manifest does not describe this entry",
351 entry.dir.display()
352 ));
353 }
354 let recorded = manifest.get("files").ok_or("manifest lacks files")?;
355 let actual = inventory(&entry.dir)?;
356 if *recorded != actual {
357 let names = |v: &Value| -> BTreeSet<String> {
358 match v {
359 Value::Array(items) => items.iter().map(|i| format!("{i:?}")).collect(),
360 _ => BTreeSet::new(),
361 }
362 };
363 let changed: Vec<String> = names(recorded)
364 .symmetric_difference(&names(&actual))
365 .take(3)
366 .cloned()
367 .collect();
368 return Err(format!(
369 "{}: files differ from manifest.json: {changed:?}",
370 entry.dir.display()
371 ));
372 }
373 let provider = Provider::load(&entry.descriptor())?;
374 provider.validate_selection(entry.release.parse()?)?;
375 provider.validate_files()?;
376 for path in [
377 &provider.sysroot,
378 &provider.stubs,
379 &provider.compiler_runtime,
380 ] {
381 if !path.starts_with(&entry.dir) {
382 return Err(format!(
383 "{} must point inside {}",
384 path.display(),
385 entry.dir.display()
386 ));
387 }
388 }
389 if snapshot.release.to_string() != entry.release {
390 return Err("the ABI snapshot is for another release".into());
391 }
392 for (library, soname) in crate::glibc::LIBRARIES
393 .iter()
394 .map(|l| (*l, stub::glibc_soname(l).expect("known")))
395 {
396 debug_assert!(SONAMES.contains(&soname));
397 let metadata = elf::Metadata::load(&provider.stubs.join(soname))?;
398 stub::render_glibc(snapshot, library, &metadata)
399 .map_err(|e| format!("stub {soname}: {e}"))?;
400 }
401 let runtime = elf::Metadata::load(&provider.compiler_runtime.join(libgcc::SONAME))?;
402 let key = |s: &elf::Symbol| {
403 (
404 s.name.clone(),
405 s.version.clone(),
406 s.default_version,
407 s.symbol_type,
408 s.size * u64::from(s.symbol_type != 2),
409 )
410 };
411 let exported: BTreeSet<_> = runtime
412 .symbols
413 .iter()
414 .filter(|s| s.section != 0 && s.version.as_deref().is_some_and(|v| v.starts_with("GCC_")))
415 .map(key)
416 .collect();
417 let expected: BTreeSet<_> = libgcc::stub()?.selected.symbols.iter().map(key).collect();
418 if exported != expected || runtime.soname != libgcc::SONAME {
419 return Err(format!(
420 "{}: the libgcc_s stub does not have GCC {}'s interface",
421 provider.compiler_runtime.display(),
422 libgcc::GCC_RELEASE
423 ));
424 }
425 Ok(provider)
426}
427
428pub fn copy_tree(dir: &Path, target: &Path) -> Result<(), String> {
430 fs::create_dir(target).map_err(|e| format!("cannot create new {}: {e}", target.display()))?;
431 for item in fs::read_dir(dir).map_err(|e| e.to_string())? {
432 let item = item.map_err(|e| e.to_string())?;
433 let (from, to) = (item.path(), target.join(item.file_name()));
434 let kind = fs::symlink_metadata(&from)
435 .map_err(|e| e.to_string())?
436 .file_type();
437 if kind.is_symlink() {
438 let link = fs::read_link(&from).map_err(|e| e.to_string())?;
439 let parent = to.parent().expect("inside target");
440 let name = to
441 .file_name()
442 .expect("named")
443 .to_string_lossy()
444 .into_owned();
445 symlink(
446 parent,
447 link.to_str().ok_or("link targets must be UTF-8")?,
448 &name,
449 )?;
450 } else if kind.is_dir() {
451 copy_tree(&from, &to)?;
452 } else {
453 fs::copy(&from, &to).map_err(|e| format!("{}: {e}", from.display()))?;
454 }
455 }
456 Ok(())
457}
458
459pub fn remove_tree(dir: &Path) -> Result<(), String> {
461 if !dir.exists() {
462 return Ok(());
463 }
464 if fs::remove_dir_all(dir).is_ok() {
465 return Ok(());
466 }
467 let mut stack = vec![dir.to_path_buf()];
468 while let Some(path) = stack.pop() {
469 if let Ok(meta) = fs::symlink_metadata(&path) {
470 if meta.is_dir() {
471 let mut permissions = meta.permissions();
472 #[allow(clippy::permissions_set_readonly_false)]
473 permissions.set_readonly(false);
474 let _ = fs::set_permissions(&path, permissions);
475 if let Ok(read) = fs::read_dir(&path) {
476 stack.extend(read.flatten().map(|e| e.path()));
477 }
478 }
479 }
480 }
481 fs::remove_dir_all(dir).map_err(|e| format!("cannot remove {}: {e}", dir.display()))
482}
483
484pub fn export(entry: &Entry, to: &Path) -> Result<PathBuf, String> {
486 let target = to
487 .join(format!("glibc-{}", entry.release))
488 .join(entry.dir.file_name().expect("named"));
489 fs::create_dir_all(target.parent().expect("has parent")).map_err(|e| e.to_string())?;
490 copy_tree(&entry.dir, &target)?;
491 fs::remove_file(target.join("provider.txt")).map_err(|e| e.to_string())?;
493 Ok(target)
494}
495
496pub fn import(from: &Path, root: &Path, gcc_startup: &Path) -> Result<Vec<Entry>, String> {
499 let mut imported = Vec::new();
500 for release_item in fs::read_dir(from).map_err(|e| format!("{}: {e}", from.display()))? {
501 let release_path = release_item.map_err(|e| e.to_string())?.path();
502 let Some(release_name) = release_path
503 .file_name()
504 .and_then(|n| n.to_str())
505 .and_then(|n| n.strip_prefix("glibc-"))
506 .map(String::from)
507 else {
508 continue;
509 };
510 let release_name = release(&release_name)?;
511 for item in fs::read_dir(&release_path).map_err(|e| e.to_string())? {
512 let source = item.map_err(|e| e.to_string())?.path();
513 let name = source
514 .file_name()
515 .and_then(|n| n.to_str())
516 .ok_or("entry names must be UTF-8")?
517 .to_string();
518 let Some((mode, hash)) = name.split_once('-') else {
519 continue;
520 };
521 let mode: Mode = mode.parse()?;
522 if !hex(hash) {
523 return Err(format!("{}: not a provider entry", source.display()));
524 }
525 let staged = Entry {
526 release: release_name.into(),
527 mode,
528 hash: hash.into(),
529 dir: source.clone(),
530 };
531 let manifest = staged.manifest()?;
532 if manifest.get("files") != Some(&inventory(&source)?) {
533 return Err(format!(
534 "{}: files differ from manifest.json",
535 source.display()
536 ));
537 }
538 let target = entry(root, release_name, mode, hash);
539 if target.dir.exists() {
540 imported.push(target);
541 continue;
542 }
543 let parent = target.dir.parent().expect("has parent");
544 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
545 let temporary = parent.join(format!(".tmp-import-{}-{name}", std::process::id()));
546 remove_tree(&temporary)?;
547 copy_tree(&source, &temporary)?;
548 let text = descriptor_text(&target.dir, release_name, gcc_startup)?;
550 fs::write(temporary.join("provider.txt"), text).map_err(|e| e.to_string())?;
551 fs::rename(&temporary, &target.dir).map_err(|e| e.to_string())?;
552 imported.push(target);
553 }
554 }
555 Ok(imported)
556}
557
558pub fn descriptor_text(dir: &Path, release: &str, gcc_startup: &Path) -> Result<String, String> {
560 let mut text = format!("{FORMAT}\ntarget\t{TARGET}\nglibc\t{release}\n");
561 for (key, path) in [
562 ("sysroot", dir.join("sysroot")),
563 ("stubs", dir.join("stubs")),
564 ("gcc-startup", gcc_startup.to_path_buf()),
565 ("compiler-runtime", dir.join("compiler-runtime")),
566 ] {
567 let path = path.to_str().ok_or("provider paths must be UTF-8")?;
568 text.push_str(&format!("{key}\t{path}\n"));
569 }
570 Provider::parse(&text)?;
571 Ok(text)
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 #[test]
579 fn names_entries_by_release_mode_and_hash() {
580 let root = Path::new("/cache");
581 let e = entry(root, "2.28", Mode::Source, "0123456789abcdef");
582 assert_eq!(
583 e.dir,
584 Path::new("/cache/x86_64-unknown-linux-gnu/glibc-2.28/source-0123456789abcdef")
585 );
586 assert_eq!(e.descriptor(), e.dir.join("provider.txt"));
587 assert_eq!("container".parse::<Mode>().unwrap(), Mode::Container);
588 assert!("binary".parse::<Mode>().is_err());
589 assert!(release("2.16").is_err());
590 assert_eq!(release("2.44").unwrap(), "2.44");
591 }
592
593 #[test]
594 fn hashes_every_input() {
595 let a = content_hash(&[("x", b"1"), ("y", b"2")]);
596 assert_eq!(a.len(), 16);
597 assert_ne!(a, content_hash(&[("x", b"1"), ("y", b"3")]));
598 assert_ne!(
600 content_hash(&[("x", b"12"), ("y", b"")]),
601 content_hash(&[("x", b"1"), ("y", b"2")])
602 );
603 }
604
605 #[test]
606 fn inventories_files_and_rejects_changes() {
607 let dir = env::temp_dir().join(format!("lat-cache-test-{}", std::process::id()));
608 let _ = remove_tree(&dir);
609 fs::create_dir_all(dir.join("stubs")).unwrap();
610 fs::write(dir.join("stubs/a"), b"abc").unwrap();
611 fs::write(dir.join("provider.txt"), b"machine specific").unwrap();
612 let first = inventory(&dir).unwrap();
613 let text = first.to_text();
614 assert!(text.contains("\"stubs/a\""), "{text}");
615 assert!(text.contains(&sha256::hex(&sha256::sha256(b"abc"))));
616 assert!(!text.contains("provider.txt"));
617 fs::write(dir.join("provider.txt"), b"another machine").unwrap();
618 assert_eq!(inventory(&dir).unwrap(), first);
619 fs::write(dir.join("stubs/a"), b"abd").unwrap();
620 assert_ne!(inventory(&dir).unwrap(), first);
621 remove_tree(&dir).unwrap();
622 }
623}