elfpak_core/resolver/
tokens.rs1use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
7pub struct TokenContext {
8 pub origin: PathBuf,
10 pub lib: String,
12 pub platform: Option<String>,
14}
15
16pub 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 if bytes[i] != b'$' {
28 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 out.push('$');
44 i += 1;
45 }
46 }
47 }
48 out
49}
50
51fn 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 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
79pub 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 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}