1#![cfg(unix)]
19
20mod elf;
21mod macho;
22
23use std::collections::HashSet;
24use std::os::unix::fs::MetadataExt;
25use std::path::{Path, PathBuf};
26
27use eyre::{WrapErr, bail};
28
29use eyre::Result;
30
31struct Replacement {
32 pub placeholder: &'static [u8],
33 pub value: Vec<u8>,
34}
35
36fn standard_replacements(prefix: &Path, repository: &Path) -> Vec<Replacement> {
37 let prefix = prefix.to_string_lossy();
38 let repository = repository.to_string_lossy();
39 let macos = cfg!(target_os = "macos");
40 vec![
41 Replacement {
42 placeholder: b"@@HOMEBREW_PREFIX@@",
43 value: prefix.as_bytes().to_vec(),
44 },
45 Replacement {
46 placeholder: b"@@HOMEBREW_CELLAR@@",
47 value: format!("{prefix}/Cellar").into_bytes(),
48 },
49 Replacement {
50 placeholder: b"@@HOMEBREW_REPOSITORY@@",
51 value: repository.as_bytes().to_vec(),
52 },
53 Replacement {
54 placeholder: b"@@HOMEBREW_LIBRARY@@",
55 value: format!("{repository}/Library").into_bytes(),
56 },
57 Replacement {
58 placeholder: b"@@HOMEBREW_PERL@@",
59 value: if macos {
61 b"/usr/bin/perl".to_vec()
62 } else {
63 format!("{prefix}/opt/perl/bin/perl").into_bytes()
64 },
65 },
66 Replacement {
67 placeholder: b"@@HOMEBREW_JAVA@@",
68 value: if macos {
69 format!("{prefix}/opt/openjdk/libexec/openjdk.jdk/Contents/Home").into_bytes()
70 } else {
71 format!("{prefix}/opt/openjdk/libexec").into_bytes()
72 },
73 },
74 ]
75}
76
77#[derive(Debug, Default)]
78pub struct RelocationReport {
79 pub changed_files: Vec<PathBuf>,
81 pub changed_machos: Vec<PathBuf>,
83}
84
85fn is_macho(content: &[u8]) -> bool {
86 if content.len() < 4 {
87 return false;
88 }
89 matches!(
90 u32::from_be_bytes([content[0], content[1], content[2], content[3]]),
91 0xfeedface | 0xcefaedfe | 0xfeedfacf | 0xcffaedfe | 0xcafebabe | 0xbebafeca
92 )
93}
94
95fn text_executable_shebang_end(content: &[u8]) -> Option<usize> {
98 let prefix = content.get(..1024.min(content.len()))?;
99 let rest = prefix.strip_prefix(b"#!")?;
100 let line_end = rest
101 .iter()
102 .position(|&b| b == b'\n' || b == b'\r')
103 .unwrap_or(rest.len());
104 let interpreter = &rest[..line_end];
105 (!interpreter.contains(&0) && interpreter.iter().any(|b| !b.is_ascii_whitespace()))
106 .then_some(2 + line_end)
107}
108
109fn contains_any_placeholder(content: &[u8], replacements: &[Replacement]) -> bool {
110 replacements
111 .iter()
112 .any(|r| memmem(content, r.placeholder).is_some())
113}
114
115fn memmem(haystack: &[u8], needle: &[u8]) -> Option<usize> {
116 haystack
117 .windows(needle.len())
118 .position(|window| window == needle)
119}
120
121fn replace_text(content: &[u8], replacements: &[Replacement]) -> Vec<u8> {
123 let mut out = content.to_vec();
124 for r in replacements {
125 let mut result = Vec::with_capacity(out.len());
126 let mut rest: &[u8] = &out;
127 while let Some(pos) = memmem(rest, r.placeholder) {
128 result.extend_from_slice(&rest[..pos]);
129 result.extend_from_slice(&r.value);
130 rest = &rest[pos + r.placeholder.len()..];
131 }
132 result.extend_from_slice(rest);
133 out = result;
134 }
135 out
136}
137
138fn replace_shebang(content: &[u8], shebang_end: usize, replacements: &[Replacement]) -> Vec<u8> {
142 let shebang = replace_text(&content[..shebang_end], replacements);
143 let mut out = Vec::with_capacity(shebang.len() + content.len() - shebang_end);
144 out.extend_from_slice(&shebang);
145 out.extend_from_slice(&content[shebang_end..]);
146 out
147}
148
149fn replace_in_binary(
152 content: &mut [u8],
153 replacements: &[Replacement],
154 path: &Path,
155) -> Result<bool> {
156 let mut changed = false;
157 for r in replacements {
158 let mut search_from = 0;
159 while let Some(rel_pos) = memmem(&content[search_from..], r.placeholder) {
160 let start = search_from + rel_pos;
161 let str_end = content[start..]
165 .iter()
166 .position(|&b| b == 0)
167 .map(|p| start + p)
168 .unwrap_or(content.len());
169 let slot_end = content[str_end..]
172 .iter()
173 .position(|&b| b != 0)
174 .map(|p| str_end + p)
175 .unwrap_or(content.len());
176 let old = content[start..str_end].to_vec();
177 let mut new = r.value.clone();
178 new.extend_from_slice(&old[r.placeholder.len()..]);
179 let slot = slot_end.saturating_sub(start);
180 if new.len() + 1 > slot {
181 bail!(
182 "cannot relocate {}: replacement for {} does not fit ({} > {} bytes)",
183 path.display(),
184 String::from_utf8_lossy(r.placeholder),
185 new.len() + 1,
186 slot,
187 );
188 }
189 content[start..start + new.len()].copy_from_slice(&new);
190 for b in &mut content[start + new.len()..slot_end] {
191 *b = 0;
192 }
193 changed = true;
194 search_from = start + new.len();
195 }
196 }
197 Ok(changed)
198}
199
200pub fn relocate_keg(
204 keg: &Path,
205 formula_name: &str,
206 skip_linkage: bool,
207 prefix: &Path,
208 repository: &Path,
209) -> Result<RelocationReport> {
210 relocate_keg_with_replacements(
211 keg,
212 formula_name,
213 skip_linkage,
214 prefix,
215 &standard_replacements(prefix, repository),
216 )
217}
218
219fn relocate_keg_with_replacements(
220 keg: &Path,
221 formula_name: &str,
222 skip_linkage: bool,
223 prefix: &Path,
224 replacements: &[Replacement],
225) -> Result<RelocationReport> {
226 let elf_opts = elf::LinkageOpts::for_formula(formula_name, prefix);
227 let patch_elf = formula_name != "glibc" && !formula_name.starts_with("glibc@");
230 let mut report = RelocationReport::default();
231 let mut changed_macho_inodes = HashSet::new();
232 for entry in walkdir::WalkDir::new(keg).follow_links(false) {
233 let entry = entry?;
234 if !entry.file_type().is_file() {
235 continue;
236 }
237 let path = entry.path();
238 let metadata = path.metadata()?;
239 let inode = (metadata.dev(), metadata.ino());
240 if changed_macho_inodes.contains(&inode) {
242 report.changed_machos.push(path.to_path_buf());
243 report.changed_files.push(path.to_path_buf());
244 continue;
245 }
246 let content =
247 std::fs::read(path).wrap_err_with(|| format!("failed read: {}", path.display()))?;
248 if !contains_any_placeholder(&content, replacements) {
249 continue;
250 }
251 let macho = is_macho(&content);
252 let elf = cfg!(target_os = "linux") && elf::is_elf(&content);
253 let shebang_end = text_executable_shebang_end(&content);
254 if skip_linkage && (macho || elf || (content.contains(&0) && shebang_end.is_none())) {
255 continue;
256 }
257 let perms = metadata.permissions();
258 let mut writable = perms.clone();
260 std::os::unix::fs::PermissionsExt::set_mode(
261 &mut writable,
262 std::os::unix::fs::PermissionsExt::mode(&perms) | 0o200,
263 );
264 std::fs::set_permissions(path, writable)?;
265 if macho || (!elf && content.contains(&0) && shebang_end.is_none()) {
266 let mut content = content;
273 let mut changed = macho && macho::patch(&mut content, replacements, path)?;
274 changed |= replace_in_binary(&mut content, replacements, path)?;
275 if changed {
276 std::fs::write(path, &content)
277 .wrap_err_with(|| format!("failed write: {}", path.display()))?;
278 if macho {
279 changed_macho_inodes.insert(inode);
280 report.changed_machos.push(path.to_path_buf());
281 }
282 report.changed_files.push(path.to_path_buf());
283 }
284 } else if elf {
285 if patch_elf {
290 let mut content = content;
291 if elf::patch(&mut content, &elf_opts, path)? {
292 std::fs::write(path, &content)
293 .wrap_err_with(|| format!("failed write: {}", path.display()))?;
294 report.changed_files.push(path.to_path_buf());
295 }
296 }
297 } else {
298 let new_content = if content.contains(&0) {
299 replace_shebang(&content, shebang_end.unwrap(), replacements)
302 } else {
303 replace_text(&content, replacements)
304 };
305 if new_content != content {
306 std::fs::write(path, &new_content)
307 .wrap_err_with(|| format!("failed write: {}", path.display()))?;
308 report.changed_files.push(path.to_path_buf());
309 }
310 }
311 std::fs::set_permissions(path, perms)?;
312 }
313 Ok(report)
314}
315
316#[cfg(test)]
317mod tests;