Skip to main content

cargo_libc/
provider.rs

1//! Versioned local provider descriptor. This is a trusted build configuration,
2//! not a signed or redistributable SDK.
3use 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";
12/// glibc releases with a qualified provider; selection is exact, without fallback.
13pub 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    /// Static archives linked into every executable, relative to the sysroot.
108    /// glibc 2.28 merged libpthread_nonshared.a into libc_nonshared.a.
109    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}