From c674f520ed62b0f48129ef8b49b3dbdaae322dc4 Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:17:49 -0500 Subject: [PATCH 1/2] fix(gl): pass the mirror temp path to git as an OS string The clone destination went through to_str().unwrap(), so a TMPDIR with non-UTF-8 bytes panicked before git started. Pass the Path directly and add a Unix regression test cloning into a non-UTF-8 directory name. Closes #417 --- crates/gl/src/mirror.rs | 50 ++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/crates/gl/src/mirror.rs b/crates/gl/src/mirror.rs index 400d3d45..bf8bf3eb 100644 --- a/crates/gl/src/mirror.rs +++ b/crates/gl/src/mirror.rs @@ -80,14 +80,7 @@ pub async fn run(args: MirrorArgs) -> Result<()> { let _guard = TmpGuard(tmp_root); println!("Cloning source (this may take a while for large repos)..."); - let clone_status = Command::new("git") - .args(["clone", "--mirror", &source, mirror_path.to_str().unwrap()]) - .status() - .context("failed to run git clone — is git installed?")?; - - if !clone_status.success() { - bail!("git clone --mirror failed\nCheck that the source URL is accessible: {source}"); - } + clone_mirror(&source, &mirror_path)?; // ── 4. Create the repo on gitlawb ───────────────────────────────────── println!("Creating repo on gitlawb node..."); @@ -139,6 +132,25 @@ pub async fn run(args: MirrorArgs) -> Result<()> { Ok(()) } +/// `git clone --mirror `. The destination is passed as an +/// OS-native path so a non-UTF-8 temp dir (e.g. a TMPDIR with non-UTF-8 bytes) +/// does not panic on a `to_str()` conversion. +fn clone_mirror(source: &str, dest: &std::path::Path) -> Result<()> { + // allow-unbounded-git: gl is the client CLI; this clone runs in the user's + // terminal with no server-side permit or disconnect to survive. The + // bounded-runner rule governs node request handlers. Same spawn as before, + // only relocated into this helper. + let status = Command::new("git") + .args(["clone", "--mirror", source]) + .arg(dest) + .status() + .context("failed to run git clone — is git installed?")?; + if !status.success() { + bail!("git clone --mirror failed\nCheck that the source URL is accessible: {source}"); + } + Ok(()) +} + /// Extract the repo name from a git URL. /// - `https://github.com/owner/repo` → `"repo"` /// - `https://github.com/owner/repo.git` → `"repo"` @@ -221,6 +233,28 @@ mod tests { assert_eq!(extract_repo_name("https://example.com/.git"), None); } + // #417: a non-UTF-8 destination path must reach git as an OS-native arg, not + // panic on a `to_str()` conversion. + #[cfg(unix)] + #[test] + fn test_clone_mirror_non_utf8_dest() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let tmp = tempfile::TempDir::new().unwrap(); + let src = tmp.path().join("src.git"); + let st = Command::new("git") + .args(["init", "--bare"]) + .arg(&src) + .status() + .unwrap(); + assert!(st.success()); + + let dest = tmp.path().join(OsStr::from_bytes(b"mirror-\xffdest")); + clone_mirror(src.to_str().unwrap(), &dest).unwrap(); + assert!(dest.join("HEAD").exists()); + } + #[tokio::test] async fn test_create_repo_conflict_error() { let mut server = mockito::Server::new_async().await; From 227246fa11fd43aca563254c70292d28b3f7855f Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:29:26 -0500 Subject: [PATCH 2/2] test(gl): cover a non-UTF-8 TMPDIR end to end via a child process --- crates/gl/src/mirror.rs | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/gl/src/mirror.rs b/crates/gl/src/mirror.rs index bf8bf3eb..ed0272f5 100644 --- a/crates/gl/src/mirror.rs +++ b/crates/gl/src/mirror.rs @@ -255,6 +255,58 @@ mod tests { assert!(dest.join("HEAD").exists()); } + // #417 end-to-end: the real trigger is `std::env::temp_dir()` itself + // returning a non-UTF-8 path (a TMPDIR with non-UTF-8 bytes), because the + // mirror dest is built from it. Env is process-global, so this test + // re-execs the test binary in a child with a non-UTF-8 TMPDIR rather than + // mutating this process's env. + #[cfg(unix)] + #[test] + fn test_clone_mirror_under_non_utf8_tmpdir() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + if std::env::var_os("GL_MIRROR_NONUTF8_TMPDIR").is_none() { + // Parent: a source repo under a normal path, a TMPDIR whose name + // is not UTF-8, and a child run of this same test under that env. + let parent = tempfile::TempDir::new().unwrap(); + let src = parent.path().join("src.git"); + let st = Command::new("git") + .args(["init", "--bare"]) + .arg(&src) + .status() + .unwrap(); + assert!(st.success()); + let bad_tmp = parent.path().join(OsStr::from_bytes(b"tmp-\xff")); + std::fs::create_dir_all(&bad_tmp).unwrap(); + + let status = Command::new(std::env::current_exe().unwrap()) + .args([ + "mirror::tests::test_clone_mirror_under_non_utf8_tmpdir", + "--exact", + ]) + .env("GL_MIRROR_NONUTF8_TMPDIR", "1") + .env("GL_MIRROR_SRC", &src) + .env("TMPDIR", &bad_tmp) + .status() + .unwrap(); + assert!(status.success(), "child failed under non-UTF-8 TMPDIR"); + return; + } + + // Child: the same path shape run() computes — temp_dir() joined with + // the mirror dest. It must be non-UTF-8 or the test proves nothing. + let src = std::env::var("GL_MIRROR_SRC").unwrap(); + let tmp_root = std::env::temp_dir().join("gl-mirror-child"); + let dest = tmp_root.join("repo"); + assert!( + dest.to_str().is_none(), + "fixture broken: dest under a non-UTF-8 TMPDIR must be non-UTF-8" + ); + clone_mirror(&src, &dest).unwrap(); + assert!(dest.join("HEAD").exists()); + } + #[tokio::test] async fn test_create_repo_conflict_error() { let mut server = mockito::Server::new_async().await;