Files
ghostty-rs/crates/ghostty-os/src/temp_dir.rs
T
ddidderr 8519f1ba1b fix: preserve non-UTF-8 bytes in OS, config and CLI layers
Zig treats paths, arguments and environment values as byte slices; the
port converted them lossily in many places. Now (final-Z1.md):
- argv, env (incl. present non-UTF-8 values), HOME, PATH lookup,
  passwd, resources dir, TMPDIR, cgroup, config path canonicalization,
  +validate-config, +list-themes and Zig-string escapes (\xFF) keep raw
  bytes. String-based config values carry bytes via ghostty_os::osbytes,
  a bijective escape (noncharacter U+FDD0 + byte) that leaves all valid
  UTF-8 - including Nerd Font private-use icons - unchanged.
- os/file rlimit fallbacks exist on all targets.
- i18n locale canonicalization is an exact port of libintl's tables
  (e.g. pa-Guru -> pa_IN).
- New ghostty-build-info crate is the single source of version, channel,
  commit and build mode for all consumers.

Test Plan:
- cargo test -p ghostty-os -p ghostty-config -p ghostty-cli
  (incl. randomized osbytes round-trip property tests)
2026-10-12 08:14:02 +02:00

178 lines
5.3 KiB
Rust

//! Creates a temporary directory at runtime that can be safely used to
//! store temporary data and is destroyed on drop.
#[cfg(unix)]
use std::os::unix::fs::DirBuilderExt;
use std::{
io,
path::{Path, PathBuf},
};
use crate::file::alloc_tmp_dir_os;
// The amount of random bytes to get to determine our filename.
const RANDOM_BYTES: usize = 16;
// Base64 alphabet, replacing the standard `+/` with `-_` so that it can
// be used in a file name on any filesystem.
const B64_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
/// Unpadded base64 using [`B64_ALPHABET`].
fn b64_encode(data: &[u8]) -> String {
let mut out = String::with_capacity((data.len() * 4).div_ceil(3));
for chunk in data.chunks(3) {
let n = match chunk.len() {
3 => u32::from(chunk[0]) << 16 | u32::from(chunk[1]) << 8 | u32::from(chunk[2]),
2 => u32::from(chunk[0]) << 16 | u32::from(chunk[1]) << 8,
_ => u32::from(chunk[0]) << 16,
};
let chars = chunk.len() + 1;
for i in 0..chars {
let idx = (n >> (18 - 6 * i)) & 0x3f;
out.push(B64_ALPHABET[idx as usize] as char);
}
}
out
}
#[derive(Debug)]
pub struct TempDir {
/// Parent directory
parent: PathBuf,
/// Name of the directory (basename only).
name: String,
}
/// Fill `buf` with random bytes (Zig: `std.crypto.random.bytes`).
#[cfg(any(target_os = "linux", target_os = "android"))]
fn fill_random(buf: &mut [u8]) -> io::Result<()> {
rustix::rand::getrandom(buf, rustix::rand::GetRandomFlags::empty())?;
Ok(())
}
/// Darwin has no `getrandom` syscall (Zig uses `getentropy`); read the
/// kernel CSPRNG device instead.
#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
fn fill_random(buf: &mut [u8]) -> io::Result<()> {
use std::io::Read;
std::fs::File::open("/dev/urandom")?.read_exact(buf)
}
/// Windows: the system preferred RNG (`BCryptGenRandom`).
#[cfg(windows)]
#[allow(unsafe_code)]
fn fill_random(buf: &mut [u8]) -> io::Result<()> {
use windows_sys::Win32::Security::Cryptography::{
BCRYPT_USE_SYSTEM_PREFERRED_RNG,
BCryptGenRandom,
};
// SAFETY: `buf` is valid for `len` bytes; a null algorithm handle is
// allowed together with BCRYPT_USE_SYSTEM_PREFERRED_RNG.
let status = unsafe {
BCryptGenRandom(
std::ptr::null_mut(),
buf.as_mut_ptr(),
buf.len() as u32,
BCRYPT_USE_SYSTEM_PREFERRED_RNG,
)
};
if status < 0 {
return Err(io::Error::other(format!(
"BCryptGenRandom failed: {status:#x}"
)));
}
Ok(())
}
impl TempDir {
/// Create the temporary directory.
pub fn init() -> io::Result<TempDir> {
let parent = match alloc_tmp_dir_os() {
Some(p) => p,
None => std::env::current_dir()?,
};
// Verify the parent can be opened (Zig opens it as a Dir).
std::fs::read_dir(&parent)?;
// We now loop forever until we can find a directory that we can create.
loop {
let mut rand_buf = [0u8; RANDOM_BYTES];
fill_random(&mut rand_buf)?;
let name = b64_encode(&rand_buf);
#[allow(unused_mut)]
let mut builder = std::fs::DirBuilder::new();
#[cfg(unix)]
builder.mode(0o755);
match builder.create(parent.join(&name)) {
Ok(()) => return Ok(TempDir { parent, name }),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(e),
}
}
}
/// Name returns the name of the directory. This is just the basename
/// and is not the full absolute path.
pub fn name(&self) -> &str {
&self.name
}
/// The parent directory containing the temporary directory.
pub fn parent(&self) -> &Path {
&self.parent
}
/// Full path to the temporary directory.
pub fn path(&self) -> PathBuf {
self.parent.join(&self.name)
}
/// Finish with the temporary directory. This deletes all contents in the
/// directory.
pub fn deinit(self) {}
}
impl Drop for TempDir {
fn drop(&mut self) {
if let Err(err) = std::fs::remove_dir_all(self.path()) {
log::error!("error deleting temp dir err={err}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn temp_dir_lifecycle() {
let td = TempDir::init().unwrap();
let name = td.name().to_owned();
assert!(!name.is_empty());
assert_eq!(name.len(), 22);
// Can open a new handle to it proves it exists.
let parent = td.parent().to_owned();
assert!(std::fs::read_dir(parent.join(&name)).is_ok());
std::fs::write(td.path().join("file"), b"x").unwrap();
// Should be deleted after we deinit
td.deinit();
let err = std::fs::read_dir(parent.join(&name)).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn b64_encoder() {
assert_eq!(b64_encode(b""), "");
assert_eq!(b64_encode(b"f"), "Zg");
assert_eq!(b64_encode(b"fo"), "Zm8");
assert_eq!(b64_encode(b"foo"), "Zm9v");
assert_eq!(b64_encode(&[0xfb, 0xff]), "-_8");
assert_eq!(b64_encode(&[0u8; 16]).len(), 22);
}
}