Files
ghostty-rs/crates/ghostty-os/src/temp_dir.rs
T
ddidderr f8c5ce06d6 feat(os): port Ghostty's Linux OS helpers
Ports ZIG/src/os for Linux into ghostty-os: XDG dirs, home dir, hostname,
env/PATH helpers, passwd lookup (via nix), shell detection, resources
dir discovery, string encodings (printf-q, URL percent), systemd
sd_notify, i18n via gettextrs, xdg-open, desktop/cgroup/kernel info,
temp dirs, URI parsing (port of std.Uri incl. Ghostty's extra options),
and a zbus-based Flatpak HostCommand implementation.

The crate stays free of unsafe. Consequences, all tracked as
TODO(integration) in docs/rewrite/notes/ghostty-os.md:
- locale::ensure_locale is a no-op (setlocale/setenv are unsafe; GTK
  calls setlocale itself); setenv/unsetenv are not exposed.
- passwd's Flatpak branch needs the termio pty and falls back to the
  sandbox entry for now.
- FlatpakHostCommand's D-Bus path is untested (no portal here).
Intentional fix: a leading '@' in NOTIFY_SOCKET is treated as an
abstract socket.

macOS/Windows/WASM modules are out of scope.

Test Plan:
- cargo test -p ghostty-os (88 tests)
- cargo clippy -p ghostty-os --all-targets -- -D warnings
2026-10-11 21:54:11 +02:00

132 lines
4.0 KiB
Rust

//! Creates a temporary directory at runtime that can be safely used to
//! store temporary data and is destroyed on drop.
use std::io;
use std::os::unix::fs::DirBuilderExt;
use std::path::{Path, PathBuf};
use crate::file::alloc_tmp_dir;
// 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,
}
impl TempDir {
/// Create the temporary directory.
pub fn init() -> io::Result<TempDir> {
let parent = match alloc_tmp_dir() {
Some(p) => PathBuf::from(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];
rustix::rand::getrandom(&mut rand_buf, rustix::rand::GetRandomFlags::empty())?;
let name = b64_encode(&rand_buf);
match std::fs::DirBuilder::new()
.mode(0o755)
.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);
}
}