1use crate::version::Version;
4use std::{
5 collections::BTreeMap,
6 fs,
7 io::Read,
8 path::{Path, PathBuf},
9};
10
11pub const FORMAT: &str = "libc-abi-provider-v1";
12pub const RELEASES: &[&str] = &[
14 "2.17", "2.18", "2.19", "2.20", "2.21", "2.22", "2.23", "2.24", "2.25", "2.26", "2.27", "2.28",
15 "2.29", "2.30", "2.31", "2.32", "2.33", "2.34", "2.35", "2.36", "2.37", "2.38", "2.39", "2.40",
16 "2.41", "2.42", "2.43", "2.44",
17];
18pub const SONAMES: [&str; 8] = [
19 "libc.so.6",
20 "libm.so.6",
21 "ld-linux-x86-64.so.2",
22 "libresolv.so.2",
23 "libpthread.so.0",
24 "libdl.so.2",
25 "librt.so.1",
26 "libutil.so.1",
27];
28
29#[derive(Clone, Debug)]
30pub struct Provider {
31 pub release: Version,
32 pub sysroot: PathBuf,
33 pub stubs: PathBuf,
34 pub gcc_startup: PathBuf,
35 pub compiler_runtime: PathBuf,
36 pub descriptor: String,
37}
38
39impl Provider {
40 pub fn load(path: &Path) -> Result<Self, String> {
41 let mut text = String::new();
42 fs::File::open(path)
43 .and_then(|f| f.take(16385).read_to_string(&mut text))
44 .map_err(|e| format!("cannot read provider {}: {e}", path.display()))?;
45 Self::parse(&text)
46 }
47
48 pub fn parse(text: &str) -> Result<Self, String> {
49 if text.len() > 16384 || text.contains('\r') || !text.ends_with('\n') {
50 return Err("provider must be LF-terminated UTF-8, at most 16 KiB".into());
51 }
52 let mut lines = text.lines();
53 if lines.next() != Some(FORMAT) {
54 return Err("unsupported provider format".into());
55 }
56 let mut fields = BTreeMap::new();
57 for line in lines {
58 let (key, value) = line.split_once('\t').ok_or("invalid provider field")?;
59 if ![
60 "target",
61 "glibc",
62 "sysroot",
63 "stubs",
64 "gcc-startup",
65 "compiler-runtime",
66 ]
67 .contains(&key)
68 || value.is_empty()
69 || value.chars().any(char::is_control)
70 || fields.insert(key, value).is_some()
71 {
72 return Err("unknown, duplicate, empty, or invalid provider field".into());
73 }
74 }
75 if fields.len() != 6
76 || fields.get("target") != Some(&"x86_64-unknown-linux-gnu")
77 || !fields.get("glibc").is_some_and(|v| RELEASES.contains(v))
78 {
79 return Err(format!(
80 "provider v1 requires exactly six fields, x86_64 Linux and glibc {}",
81 RELEASES.join(", ")
82 ));
83 }
84 let path = |key| {
85 let path = PathBuf::from(fields[key]);
86 if !path.is_absolute()
87 || path
88 .components()
89 .any(|c| matches!(c, std::path::Component::ParentDir))
90 {
91 return Err(format!(
92 "provider {key} must be an absolute path without parent components"
93 ));
94 }
95 Ok(path)
96 };
97 Ok(Self {
98 release: fields["glibc"].parse()?,
99 sysroot: path("sysroot")?,
100 stubs: path("stubs")?,
101 gcc_startup: path("gcc-startup")?,
102 compiler_runtime: path("compiler-runtime")?,
103 descriptor: text.into(),
104 })
105 }
106
107 pub fn nonshared_archives(&self) -> &'static [&'static str] {
110 let merged = Version {
111 major: 2,
112 minor: 28,
113 patch: 0,
114 };
115 if self.release < merged {
116 &[
117 "usr/lib64/libc_nonshared.a",
118 "usr/lib64/libpthread_nonshared.a",
119 ]
120 } else {
121 &["usr/lib64/libc_nonshared.a"]
122 }
123 }
124
125 pub fn validate_selection(&self, requested: Version) -> Result<(), String> {
126 if requested != self.release {
127 return Err(format!(
128 "provider targets glibc {}, but --glibc requested {requested}",
129 self.release
130 ));
131 }
132 Ok(())
133 }
134
135 pub fn validate_files(&self) -> Result<(), String> {
136 for root in [
137 &self.sysroot,
138 &self.stubs,
139 &self.gcc_startup,
140 &self.compiler_runtime,
141 ] {
142 if !root.is_dir() {
143 return Err(format!("provider directory missing: {}", root.display()));
144 }
145 }
146 for (root, names) in [
147 (
148 &self.sysroot,
149 &[
150 "usr/include/features.h",
151 "usr/lib64/crt1.o",
152 "usr/lib64/crti.o",
153 "usr/lib64/crtn.o",
154 "lib64/ld-linux-x86-64.so.2",
155 ][..],
156 ),
157 (&self.sysroot, self.nonshared_archives()),
158 (&self.gcc_startup, &["crtbegin.o", "crtend.o"][..]),
159 (
160 &self.compiler_runtime,
161 &["libgcc_s.so", "libgcc_s.so.1"][..],
162 ),
163 (&self.stubs, &SONAMES[..]),
164 (
165 &self.stubs,
166 &[
167 "libc.so",
168 "libm.so",
169 "libld.so",
170 "libresolv.so",
171 "libpthread.so",
172 "libdl.so",
173 "librt.so",
174 "libutil.so",
175 ][..],
176 ),
177 ] {
178 let canonical = fs::canonicalize(root).map_err(|e| e.to_string())?;
179 for name in names {
180 let file = fs::canonicalize(root.join(name))
181 .map_err(|e| format!("missing provider input {name}: {e}"))?;
182 if !file.starts_with(&canonical) || !file.is_file() {
183 return Err(format!(
184 "provider input {name} must resolve to a file inside {}",
185 root.display()
186 ));
187 }
188 }
189 }
190 for (linker_name, soname) in [
191 "libc.so",
192 "libm.so",
193 "libld.so",
194 "libresolv.so",
195 "libpthread.so",
196 "libdl.so",
197 "librt.so",
198 "libutil.so",
199 ]
200 .iter()
201 .zip(SONAMES)
202 {
203 if fs::canonicalize(self.stubs.join(linker_name)).map_err(|e| e.to_string())?
204 != fs::canonicalize(self.stubs.join(soname)).map_err(|e| e.to_string())?
205 {
206 return Err(format!(
207 "provider linker name {linker_name} must resolve to {soname}"
208 ));
209 }
210 }
211 Ok(())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 fn valid() -> String {
219 let root = if cfg!(windows) { "C:/sdk" } else { "/sdk" };
220 format!("{FORMAT}\ntarget\tx86_64-unknown-linux-gnu\nglibc\t2.17\nsysroot\t{root}\nstubs\t{root}/stubs\ngcc-startup\t{root}/gcc\ncompiler-runtime\t{root}/runtime\n")
221 }
222 #[test]
223 fn rejects_wrong_versions_and_ambiguous_descriptors() {
224 let text = valid();
225 assert!(Provider::parse(&text).is_ok());
226 for bad in [
227 text.replace("2.17", "2.16"),
228 text.replace(FORMAT, "libc-abi-provider-v2"),
229 format!("{text}glibc\t2.17\n"),
230 text.replace("stubs\t", "unknown\t"),
231 text.replace("/stubs", "/../stubs"),
232 text.replace('\n', "\r\n"),
233 text.trim_end().into(),
234 "x".repeat(16385),
235 ] {
236 assert!(Provider::parse(&bad).is_err());
237 }
238 assert!(Provider::parse(&text).unwrap().validate_files().is_err());
239 }
240
241 #[test]
242 fn nonshared_archives_follow_the_release() {
243 for (release, count) in [("2.17", 2), ("2.27", 2), ("2.28", 1), ("2.44", 1)] {
244 let provider = Provider::parse(&valid().replace("2.17", release)).unwrap();
245 let archives = provider.nonshared_archives();
246 assert_eq!(archives.len(), count);
247 assert_eq!(archives[0], "usr/lib64/libc_nonshared.a");
248 }
249 }
250
251 #[test]
252 fn selects_each_supported_release_without_fallback() {
253 for &release in RELEASES {
254 let provider = Provider::parse(&valid().replace("2.17", release)).unwrap();
255 assert_eq!(provider.release, release.parse().unwrap());
256 assert!(provider
257 .validate_selection(release.parse().unwrap())
258 .is_ok());
259 for other in RELEASES.iter().copied().chain(["2.16", "2.22.1", "2.45"]) {
260 if other != release {
261 assert!(provider.validate_selection(other.parse().unwrap()).is_err());
262 }
263 }
264 }
265 assert!(Provider::parse(&valid())
266 .unwrap()
267 .validate_selection("2.22".parse().unwrap())
268 .is_err());
269 assert!(Provider::parse(&valid().replace("2.17", "2.22"))
270 .unwrap()
271 .validate_selection("2.17".parse().unwrap())
272 .is_err());
273 assert!(Provider::parse(&valid().replace("2.17", "2.22.0")).is_err());
274 }
275}