Skip to main content

elfpak_core/resolver/
tokens.rs

1//! Expansion of the dynamic string tokens glibc understands in
2//! `DT_RPATH`/`DT_RUNPATH`: `$ORIGIN`, `$LIB` and `$PLATFORM`.
3
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
7pub struct TokenContext {
8    /// Directory of the object that owns the search path, as a logical path.
9    pub origin: PathBuf,
10    /// Value of `$LIB` (`lib` or `lib64`).
11    pub lib: String,
12    /// Value of `$PLATFORM` (`x86_64`, `aarch64`, ...).
13    pub platform: Option<String>,
14}
15
16/// Expand dynamic string tokens. Unknown tokens are left verbatim, matching the
17/// loader's behaviour of simply not substituting what it does not know.
18pub fn expand(input: &str, ctx: &TokenContext) -> String {
19    assert!(ctx.origin.is_absolute());
20
21    let mut out = String::with_capacity(input.len());
22    let bytes = input.as_bytes();
23    let mut i = 0;
24    while i < bytes.len() {
25        // Every branch below advances `i` by at least one byte, so the walk is
26        // bounded by the length of the input.
27        if bytes[i] != b'$' {
28            // Copy verbatim up to the next `$`. Copying byte by byte would
29            // re-encode every non-ASCII character in the path.
30            let next = input[i..].find('$').map_or(input.len(), |at| i + at);
31            out.push_str(&input[i..next]);
32            i = next;
33            continue;
34        }
35        let (name, consumed) = read_token(&input[i + 1..]);
36        match substitute(&name, ctx) {
37            Some(value) => {
38                out.push_str(&value);
39                i += 1 + consumed;
40            }
41            None => {
42                // Not a token the loader knows, so copy the `$` and move on.
43                out.push('$');
44                i += 1;
45            }
46        }
47    }
48    out
49}
50
51/// Returns the token name and how many bytes of the input it occupies.
52///
53/// The count includes the braces of a `${NAME}` spelling, so a caller that
54/// advances by it lands just past the token either way.
55fn read_token(rest: &str) -> (String, usize) {
56    if let Some(stripped) = rest.strip_prefix('{') {
57        match stripped.find('}') {
58            Some(end) => (stripped[..end].to_string(), end + 2),
59            // Unterminated: not a token, and nothing is consumed.
60            None => (String::new(), 0),
61        }
62    } else {
63        let end = rest
64            .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
65            .unwrap_or(rest.len());
66        (rest[..end].to_string(), end)
67    }
68}
69
70fn substitute(name: &str, ctx: &TokenContext) -> Option<String> {
71    match name {
72        "ORIGIN" => Some(ctx.origin.to_string_lossy().into_owned()),
73        "LIB" => Some(ctx.lib.clone()),
74        "PLATFORM" => ctx.platform.clone(),
75        _ => None,
76    }
77}
78
79/// Expand a validated search path entry and normalize it to a logical absolute
80/// path. The ELF parser rejects entries that would remain relative because
81/// glibc resolves those against its runtime current working directory.
82pub fn expand_search_path(entry: &str, ctx: &TokenContext) -> PathBuf {
83    let expanded = expand(entry, ctx);
84    let path = Path::new(&expanded);
85    if path.is_absolute() {
86        crate::paths::normalize_absolute(path)
87    } else {
88        // The parser permits only `$ORIGIN`-prefixed relative spelling, which
89        // expands to an absolute path. Keep this fallback defensive for direct
90        // callers, without pretending ordinary relative paths are `$ORIGIN`.
91        crate::paths::normalize_absolute(path)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn ctx() -> TokenContext {
100        TokenContext {
101            origin: PathBuf::from("/opt/app/bin"),
102            lib: "lib64".to_string(),
103            platform: Some("x86_64".to_string()),
104        }
105    }
106
107    #[test]
108    fn expands_origin_in_both_spellings() {
109        assert_eq!(expand("$ORIGIN/../lib", &ctx()), "/opt/app/bin/../lib");
110        assert_eq!(expand("${ORIGIN}/../lib", &ctx()), "/opt/app/bin/../lib");
111    }
112
113    #[test]
114    fn expands_lib_and_platform() {
115        assert_eq!(expand("/usr/$LIB", &ctx()), "/usr/lib64");
116        assert_eq!(expand("/usr/lib/$PLATFORM", &ctx()), "/usr/lib/x86_64");
117    }
118
119    #[test]
120    fn unknown_tokens_stay_literal() {
121        assert_eq!(expand("/usr/$NOPE/lib", &ctx()), "/usr/$NOPE/lib");
122        assert_eq!(expand("/usr/$", &ctx()), "/usr/$");
123        assert_eq!(expand("/usr/${unterminated", &ctx()), "/usr/${unterminated");
124    }
125
126    #[test]
127    fn non_ascii_path_components_survive_expansion() {
128        let ctx = TokenContext {
129            origin: PathBuf::from("/opt/café/bin"),
130            ..ctx()
131        };
132        assert_eq!(expand("/opt/café/lib", &ctx), "/opt/café/lib");
133        assert_eq!(expand("$ORIGIN/../lib", &ctx), "/opt/café/bin/../lib");
134        assert_eq!(
135            expand_search_path("$ORIGIN/../lib", &ctx),
136            PathBuf::from("/opt/café/lib")
137        );
138    }
139
140    #[test]
141    fn search_paths_are_normalized_and_absolute() {
142        assert_eq!(
143            expand_search_path("$ORIGIN/../lib", &ctx()),
144            PathBuf::from("/opt/app/lib")
145        );
146        assert_eq!(expand_search_path("../lib", &ctx()), PathBuf::from("/lib"));
147        assert_eq!(
148            expand_search_path("/usr/lib", &ctx()),
149            PathBuf::from("/usr/lib")
150        );
151    }
152}