use std::cmp::Ordering;
use std::convert::Infallible;
use std::error;
use std::fmt;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::process;
use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
#[derive(Clone)]
pub struct Token(Repr);
#[derive(Clone)]
enum Repr {
Static(&'static str),
Shared(Arc<str>), }
impl Token {
pub const fn new(text: &'static str) -> Self {
Token(Repr::Static(text))
}
pub fn as_str(&self) -> &str {
match &self.0 {
Repr::Static(s) => s,
Repr::Shared(s) => s,
}
}
}
impl Default for Token {
fn default() -> Self {
Token(Repr::Static(""))
}
}
impl Deref for Token {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl AsRef<str> for Token {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&str> for Token {
fn from(text: &str) -> Self {
Token(Repr::Shared(Arc::from(text)))
}
}
impl From<String> for Token {
fn from(text: String) -> Self {
Token(Repr::Shared(Arc::from(text)))
}
}
impl From<&String> for Token {
fn from(text: &String) -> Self {
Token(Repr::Shared(Arc::from(text.as_str())))
}
}
impl From<&Token> for Token {
fn from(token: &Token) -> Self {
token.clone()
}
}
impl From<Token> for String {
fn from(token: Token) -> Self {
token.as_str().to_owned()
}
}
impl FromStr for Token {
type Err = Infallible;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Ok(Token::from(text))
}
}
impl PartialEq for Token {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl Eq for Token {}
impl PartialEq<str> for Token {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for Token {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<Token> for str {
fn eq(&self, other: &Token) -> bool {
self == other.as_str()
}
}
impl PartialEq<Token> for &str {
fn eq(&self, other: &Token) -> bool {
*self == other.as_str()
}
}
impl Hash for Token {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
impl PartialOrd for Token {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Token {
fn cmp(&self, other: &Self) -> Ordering {
self.as_str().cmp(other.as_str())
}
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.as_str())
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Token {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
pub(crate) struct SafeOutputFile {
destination: PathBuf,
path: PathBuf,
file: Option<fs::File>,
persisted: bool,
}
impl SafeOutputFile {
pub(crate) fn create(destination: &Path) -> io::Result<Self> {
let destination = real_path(destination);
let name = destination
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no file name"))?;
let seq = crate::next_id();
let path = destination.with_file_name(format!(".{}.{}-{seq}.tmp", name.to_string_lossy(), process::id()));
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
if destination.exists() {
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let file = options.open(&path)?;
let temp = SafeOutputFile {
destination,
path,
file: Some(file),
persisted: false,
};
#[cfg(windows)]
if temp.destination.exists() {
copy_access_control(&temp.destination, &temp.path)?;
}
Ok(temp)
}
pub(crate) fn file(&mut self) -> &mut fs::File {
self.file.as_mut().expect("the file is open until persisted")
}
pub(crate) fn persist(mut self) -> io::Result<()> {
#[cfg(unix)]
if let Ok(existing) = fs::metadata(&self.destination) {
self.file().set_permissions(existing.permissions())?;
}
self.file().sync_all()?;
self.file = None;
fs::rename(&self.path, &self.destination)?;
self.persisted = true;
Ok(())
}
}
impl Drop for SafeOutputFile {
fn drop(&mut self) {
if !self.persisted {
self.file = None;
let _ = fs::remove_file(&self.path);
}
}
}
#[cfg(windows)]
#[allow(unsafe_code)]
fn copy_access_control(from: &Path, to: &Path) -> io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use std::ptr;
use windows_sys::Win32::Foundation::{ERROR_SUCCESS, LocalFree};
use windows_sys::Win32::Security::Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT, SetNamedSecurityInfoW};
use windows_sys::Win32::Security::{
ACL, DACL_SECURITY_INFORMATION, GetSecurityDescriptorControl, PROTECTED_DACL_SECURITY_INFORMATION,
SE_DACL_PROTECTED, UNPROTECTED_DACL_SECURITY_INFORMATION,
};
let wide = |path: &Path| path.as_os_str().encode_wide().chain([0]).collect::<Vec<u16>>();
let (from, to) = (wide(from), wide(to));
let mut dacl: *mut ACL = ptr::null_mut();
let mut descriptor = ptr::null_mut();
let status = unsafe {
GetNamedSecurityInfoW(
from.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
ptr::null_mut(),
ptr::null_mut(),
&mut dacl,
ptr::null_mut(),
&mut descriptor,
)
};
if status != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(status as i32));
}
let mut control = 0;
let mut revision = 0;
let known = unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) } != 0;
let applied = if known {
let inheritance = if (control & SE_DACL_PROTECTED) != 0 {
PROTECTED_DACL_SECURITY_INFORMATION
} else {
UNPROTECTED_DACL_SECURITY_INFORMATION
};
let status = unsafe {
SetNamedSecurityInfoW(
to.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | inheritance,
ptr::null_mut(),
ptr::null_mut(),
dacl,
ptr::null_mut(),
)
};
if status == ERROR_SUCCESS {
Ok(())
} else {
Err(io::Error::from_raw_os_error(status as i32))
}
} else {
Err(io::Error::last_os_error())
};
unsafe { LocalFree(descriptor) };
applied
}
fn real_path(path: &Path) -> PathBuf {
if let Ok(real) = fs::canonicalize(path) {
return real;
}
let mut current = path.to_owned();
for _ in 0..32 {
let Ok(target) = fs::read_link(¤t) else {
break;
};
current = current.parent().map_or(target.clone(), |dir| dir.join(target));
}
current
}
pub fn is_valid_identifier(name: &str) -> bool {
let mut bytes = name.bytes();
bytes
.next()
.is_some_and(|first| first == b'_' || first.is_ascii_alphabetic())
&& bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric())
}
pub(crate) fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn error_chain(error: &dyn error::Error) -> String {
let mut text = error.to_string();
let mut source = error.source();
while let Some(cause) = source {
let rendered = cause.to_string();
if !rendered.is_empty() && !text.contains(&rendered) {
text.push_str(": ");
text.push_str(&rendered);
}
source = cause.source();
}
text
}
#[cfg(test)]
mod tests {
use super::*;
const KIND: Token = Token::new("Xform");
#[test]
fn const_static_equals_runtime() {
let runtime = Token::from("Xform".to_string());
assert_eq!(KIND, runtime);
assert_eq!(KIND.as_str(), "Xform");
let mut set = std::collections::HashSet::new();
set.insert(runtime);
assert!(set.contains(&KIND));
}
#[test]
fn identifier_rule() {
for name in ["a", "_", "_9", "chargroup", "Kind_2"] {
assert!(is_valid_identifier(name), "{name}");
}
for name in ["", "9lives", "my:kind", "my-kind", "my kind", "caf\u{e9}"] {
assert!(!is_valid_identifier(name), "{name}");
}
}
#[test]
fn compares_with_str() {
assert_eq!(KIND, "Xform");
assert_eq!("Xform", KIND);
assert_ne!(KIND, "Mesh");
let name: &str = "Xform";
assert!(KIND == name);
assert!(name == KIND);
}
}