use std::{
io,
path::{Path, PathBuf},
};
pub const MAX_SOCKET_PATH_BYTES: Option<usize> = if cfg!(unix) {
Some(if cfg!(any(target_os = "linux", target_os = "android")) {
107
} else {
103
})
} else {
None
};
pub const MAX_NAME_BYTES: usize = 48;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SocketName(&'static str);
impl SocketName {
pub const fn new(name: &'static str) -> Self {
let bytes = name.as_bytes();
assert!(!bytes.is_empty(), "socket name is empty");
assert!(
bytes.len() <= MAX_NAME_BYTES,
"socket name is longer than MAX_NAME_BYTES"
);
assert!(!matches!(bytes, b"." | b".."), "socket name is `.` or `..`");
let mut index = 0;
while index < bytes.len() {
let byte = bytes[index];
assert!(
byte != b'/' && byte != b'\\' && byte != 0,
"socket name contains a path separator or NUL"
);
index += 1;
}
Self(name)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
pub fn check_socket_path(socket: &Path) -> io::Result<()> {
let Some(limit) = MAX_SOCKET_PATH_BYTES else {
return Ok(());
};
let bytes = path_bytes(socket);
if bytes > limit {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"socket path is {bytes} bytes, but a Unix socket path can be at most \
{limit} on this platform: {}",
socket.display()
),
));
}
Ok(())
}
pub fn max_dir_bytes(names: &[SocketName]) -> Option<usize> {
let limit = MAX_SOCKET_PATH_BYTES?;
let longest = names.iter().map(|name| name.0.len()).max().unwrap_or(0);
Some(limit.saturating_sub(longest + 1))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SocketDir {
dir: PathBuf,
}
impl SocketDir {
pub fn new(dir: impl Into<PathBuf>, names: &[SocketName]) -> io::Result<Self> {
let dir = dir.into();
if let (Some(limit), Some(longest)) = (
MAX_SOCKET_PATH_BYTES,
names.iter().max_by_key(|name| name.0.len()),
) {
let bytes = path_bytes(&dir.join(longest.0));
if bytes > limit {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"socket `{}` in this directory is {bytes} bytes, but a Unix socket path \
can be at most {limit} on this platform, so the directory can be at \
most {} bytes: {}",
longest.0,
limit.saturating_sub(longest.0.len() + 1),
dir.display()
),
));
}
}
Ok(Self { dir })
}
pub fn dir(&self) -> &Path {
&self.dir
}
pub fn path(&self, name: SocketName) -> io::Result<PathBuf> {
let path = self.dir.join(name.0);
check_socket_path(&path)?;
Ok(path)
}
}
#[cfg(unix)]
fn path_bytes(path: &Path) -> usize {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().len()
}
#[cfg(not(unix))]
fn path_bytes(path: &Path) -> usize {
path.as_os_str().len()
}
#[cfg(test)]
mod tests {
use super::*;
const DAEMON: SocketName = SocketName::new("daemon.sock");
#[cfg(unix)]
const CONTROL: SocketName = SocketName::new("daemon.control.v2.sock");
#[test]
fn a_name_is_checked_where_it_is_declared() {
assert_eq!(DAEMON.as_str(), "daemon.sock");
for bad in ["", ".", "..", "a/b", "a\\b", "a\0b"] {
let name = bad.to_string().leak();
assert!(
std::panic::catch_unwind(|| SocketName::new(name)).is_err(),
"{bad:?} was accepted"
);
}
let long = "a".repeat(MAX_NAME_BYTES + 1).leak();
assert!(std::panic::catch_unwind(|| SocketName::new(long)).is_err());
let limit = "a".repeat(MAX_NAME_BYTES).leak();
assert_eq!(SocketName::new(limit).as_str().len(), MAX_NAME_BYTES);
}
#[cfg(unix)]
#[test]
fn a_directory_is_checked_against_its_longest_socket() {
let dir = "/Users/zondax-ci/actions-runner/runner-3/_work/_temp/kache-runtime-36081599948-1-build-sign";
let daemon_only = SocketDir::new(dir, &[DAEMON]);
let both = SocketDir::new(dir, &[DAEMON, CONTROL]);
if MAX_SOCKET_PATH_BYTES == Some(103) {
assert!(daemon_only.is_ok());
let error = both.unwrap_err().to_string();
assert!(error.contains("daemon.control.v2.sock"), "{error}");
assert!(error.contains("at most 80 bytes"), "{error}");
}
}
#[cfg(unix)]
#[test]
fn a_directory_at_the_budget_fits_and_one_byte_more_does_not() {
let limit = MAX_SOCKET_PATH_BYTES.unwrap();
let budget = max_dir_bytes(&[DAEMON, CONTROL]).unwrap();
let dir = format!("/{}", "d".repeat(budget - 1));
assert_eq!(dir.len(), budget);
let fits = SocketDir::new(&dir, &[DAEMON, CONTROL]).unwrap();
assert_eq!(path_bytes(&fits.path(CONTROL).unwrap()), limit);
assert!(SocketDir::new(format!("{dir}d"), &[DAEMON, CONTROL]).is_err());
}
#[cfg(unix)]
#[test]
fn an_undeclared_name_is_still_checked() {
let budget = max_dir_bytes(&[DAEMON]).unwrap();
let dir = SocketDir::new(format!("/{}", "d".repeat(budget - 1)), &[DAEMON]).unwrap();
assert!(dir.path(DAEMON).is_ok());
assert!(dir.path(CONTROL).is_err());
}
#[test]
fn a_path_names_its_length_and_the_limit() {
if let Some(limit) = MAX_SOCKET_PATH_BYTES {
let path = format!("/{}", "a".repeat(limit));
let error = check_socket_path(Path::new(&path)).unwrap_err().to_string();
assert!(error.contains(&format!("{} bytes", limit + 1)), "{error}");
assert!(error.contains(&format!("at most {limit}")), "{error}");
} else {
assert!(check_socket_path(Path::new(&"a".repeat(4096))).is_ok());
assert_eq!(max_dir_bytes(&[DAEMON]), None);
}
}
}