Repository: metalbear-co/mirrord (language: rust)

Issue:
Can't run `sbt` installed with `cs setup` on macOS
### Bug Description

Running sbt fails.

### Steps to Reproduce

1. `brew uninstall sbt`.
2. `brew install coursier/formulas/coursier`
3. `cs setup`

```
mirrord exec sbt
New mirrord version available: 3.125.2. To update, run: `"brew upgrade metalbear-co/mirrord/mirrord"`.
To disable version checks, set env variable MIRRORD_CHECK_VERSION to 'false'.
  x mirrord exec
    ✓ update to 3.125.2 available
    x preparing to launch process
      ✓ layer extracted
      ✓ using operator
        ✓ operator version compatible
        ✓ operator license valid
        ✓ user credentials prepared
        ✓ session started
          ✓ connected to the target
      ✓ arm64 layer library extracted                                                                                                                                                                                                                                                                                      Error:   × SIP Error: `UnlikelyError(
  │     "Can't read script contents as utf8",
  │ )`
  ╰─▶ Unlikely error happened `Can't read script contents as utf8`
  help: This issue is related to SIP on macOS. Please create an issue or consult with us on Discord
        
        
        - If you're still stuck:
        
        >> Please open a new bug report at https://github.com/metalbear-co/mirrord/issues/new/choose
        
        >> Or join our Discord https://discord.gg/metalbear and request help in #mirrord-help
        
        >> Or email us at hi@metalbear.co
        
```

### Backtrace

_No response_

### mirrord layer logs

_No response_

### mirrord intproxy logs

_No response_

### mirrord agent logs

_No response_

### mirrord config

_No response_

### mirrord CLI version

_No response_

### mirrord-agent version

_No response_

### mirrord-operator version (if relevant)

_No response_

### plugin kind and version (if relevant)

_No response_

### Your operating system and version

MacOS 15.0.1

### Local process

sbt

### Local process version

_No response_

### Additional Info

_No response_


Discussion / hints:
<p><a href="https://linear.app/metalbear/issue/MBE-543/cant-run-sbt-installed-with-cs-setup-on-macos">MBE-543 Can't run `sbt` installed with `cs setup` on macOS</a></p>

Fix patch:
diff --git a/changelog.d/2947.fixed.md b/changelog.d/2947.fixed.md
new file mode 100644
index 00000000000..fff6ed5cab3
--- /dev/null
+++ b/changelog.d/2947.fixed.md
@@ -0,0 +1,1 @@
+Can now run cs-installed sbt. We now only need to be able to parse the first line of a script, so we now support scripts like that sbt, which starts with a normal shebang but then has text in a weird encoding, or maybe non-textual data.
diff --git a/mirrord/sip/Cargo.toml b/mirrord/sip/Cargo.toml
index 3bc8a079d0a..2814914cd8f 100644
--- a/mirrord/sip/Cargo.toml
+++ b/mirrord/sip/Cargo.toml
@@ -18,7 +18,7 @@ workspace = true
 
 [dependencies]
 # we don't like upstream apple-platform-rs because it depends on RSA which has an open CVE.
-apple-codesign = { git = "https://github.com/metalbear-co/apple-platform-rs-mini", version = "0.27", default-features = false}
+apple-codesign = { git = "https://github.com/metalbear-co/apple-platform-rs-mini", version = "0.27", default-features = false }
 object = "0.36"
 tempfile = "3"
 
diff --git a/mirrord/sip/src/lib.rs b/mirrord/sip/src/lib.rs
index 67b31fc159f..fd3b92611dc 100644
--- a/mirrord/sip/src/lib.rs
+++ b/mirrord/sip/src/lib.rs
@@ -10,7 +10,7 @@ mod main {
     use std::{
         env,
         ffi::OsStr,
-        io::{self, ErrorKind::AlreadyExists, Read},
+        io::{self, BufRead, ErrorKind::AlreadyExists},
         os::{macos::fs::MetadataExt, unix::fs::PermissionsExt},
         path::{Path, PathBuf},
         str::from_utf8,
@@ -343,12 +343,10 @@ mod main {
             // trailing newline is needed for scripts without an original shebang
             new_contents.push('\n');
         }
+        let mut bytes = new_contents.into_bytes();
+        bytes.extend_from_slice(contents);
 
-        new_contents.push_str(
-            from_utf8(contents)
-                .map_err(|_utf| UnlikelyError("Can't read script contents as utf8".to_string()))?,
-        );
-        std::fs::write(&patched_path, new_contents)?;
+        std::fs::write(&patched_path, bytes)?;
 
         // We set the permissions of the patched script to be like those of the original
         // script, but allowing the user to write, so that in the next run, when we are here
@@ -363,9 +361,9 @@ mod main {
 
     const SF_RESTRICTED: u32 = 0x00080000; // entitlement required for writing, from stat.h (macos)
 
-    /// Extract shebang from file contents.
-    fn get_shebang_from_string(file_contents: &str) -> Option<ScriptShebang> {
-        let rest = file_contents.strip_prefix("#!")?;
+    /// Extract shebang from first line of file.
+    fn get_shebang_from_string(first_line: &str) -> Option<ScriptShebang> {
+        let rest = first_line.strip_prefix("#!")?;
 
         let mut char_iter = rest.char_indices().skip_while(|(_, c)| c.is_whitespace()); // any whitespace directly after #!
         let (start_of_path, _first_char_of_path) = char_iter.next()?;
@@ -374,11 +372,11 @@ mod main {
         let (interpreter, len_with_whitespace) =
             if let Some((path_len, _next_char)) = path_char_iter.next() {
                 let total_len = path_len + 2; // +2 for #! because the index is in `rest`
-                (file_contents.get(start_of_path + 2..total_len)?, total_len)
+                (first_line.get(start_of_path + 2..total_len)?, total_len)
             } else {
                 // There is no next character after the interpreter, so the whole file is just
                 // magic, whitespace and path.
-                (file_contents.get(start_of_path + 2..)?, file_contents.len())
+                (first_line.get(start_of_path + 2..)?, first_line.len())
             };
         Some(ScriptShebang {
             interpreter_path: PathBuf::from(interpreter),
@@ -389,15 +387,22 @@ mod main {
     /// Including '#!', just until whitespace, no arguments.
     /// will not be called on object files
     fn read_shebang_from_file<P: AsRef<Path>>(path: P) -> Result<Option<ScriptShebang>> {
-        let mut f = std::fs::File::open(path)?;
-        let mut buffer = String::new();
-        match f.read_to_string(&mut buffer) {
-            Ok(_) => {}
-            Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(None),
-            Err(e) => return Err(SipError::IO(e)),
+        let f = std::fs::File::open(path)?;
+        let reader = io::BufReader::new(f);
+        let mut lines = reader.lines();
+        let Some(first_line_res) = lines.next() else {
+            trace!("No lines in script.");
+            return Ok(None);
+        };
+
+        match first_line_res {
+            Err(e) if e.kind() == io::ErrorKind::InvalidData => {
+                trace!("First line of file is not utf-8. Can't read a shebang.");
+                Ok(None)
+            }
+            Err(e) => Err(SipError::IO(e)),
+            Ok(line_string) => Ok(get_shebang_from_string(&line_string)),
         }
-
-        Ok(get_shebang_from_string(&buffer))
     }
 
     #[derive(Debug)]
@@ -496,13 +501,15 @@ mod main {
                 }
             })
         } else {
-            let shebang =
-                read_shebang_from_file(&complete_path)?.unwrap_or_else(|| ScriptShebang {
+            let shebang = read_shebang_from_file(&complete_path)?.unwrap_or_else(|| {
+                trace!("Did not find a shebang, defaulting to $SHELL.");
+                ScriptShebang {
                     interpreter_path: PathBuf::from(
                         env::var("SHELL").expect("$SHELL should be present"),
                     ),
                     start_of_rest_of_file: 0,
-                });
+                }
+            });
 
             let interpreter_complete_path = get_complete_path(&shebang.interpreter_path)?;
             if is_in_mirrord_tmp_dir(&interpreter_complete_path)? {


Test patch: