use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::{Error, Result, RustLibFile, RustLibPath};
pub(crate) fn validate_rust_lib_name(name: &str) -> Result<()> {
let mut characters = name.chars();
let valid_first = characters
.next()
.is_some_and(|character| character.is_ascii_alphanumeric());
let valid_rest = characters
.all(|character| character.is_ascii_alphanumeric() || character == '-' || character == '_');
if !valid_first || !valid_rest {
return Err(Error::InvalidRustLibName(name.to_owned()));
}
Ok(())
}
pub(crate) fn load_files(root: &Path) -> Result<Vec<RustLibFile>> {
let mut files = Vec::new();
load_directory(root, root, &mut files)?;
files.sort_by(|left, right| left.path.cmp(&right.path));
Ok(files)
}
fn load_directory(root: &Path, directory: &Path, files: &mut Vec<RustLibFile>) -> Result<()> {
let entries =
fs::read_dir(directory).map_err(|source| Error::io("read directory", directory, source))?;
for entry in entries {
let entry = entry.map_err(|source| Error::io("read directory entry", directory, source))?;
let path = entry.path();
let file_type = entry
.file_type()
.map_err(|source| Error::io("inspect directory entry", &path, source))?;
if file_type.is_symlink() {
return Err(Error::SymlinkNotAllowed(path));
}
if file_type.is_dir() {
load_directory(root, &path, files)?;
continue;
}
if !file_type.is_file() {
return Err(Error::UnsupportedFileType(path));
}
let relative = to_rust_lib_path(root, &path)?;
let bytes = fs::read(&path).map_err(|source| Error::io("read file", &path, source))?;
let contents = String::from_utf8(bytes).map_err(|_| Error::NonUtf8File(path))?;
files.push(RustLibFile::new(relative, contents));
}
Ok(())
}
fn to_rust_lib_path(root: &Path, path: &Path) -> Result<RustLibPath> {
let relative = path
.strip_prefix(root)
.expect("a recursively loaded path must remain beneath its root");
let mut text = String::new();
for component in relative.components() {
let component = component
.as_os_str()
.to_str()
.ok_or_else(|| Error::NonUtf8Path(path.to_path_buf()))?;
if !text.is_empty() {
text.push('/');
}
text.push_str(component);
}
RustLibPath::new(text)
}
pub(crate) fn validate_write_target(root: &Path, path: &RustLibPath) -> Result<()> {
let components: Vec<_> = path.as_str().split('/').collect();
let mut current = root.to_path_buf();
for (index, component) in components.iter().enumerate() {
current.push(component);
match fs::symlink_metadata(¤t) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return Err(Error::SymlinkNotAllowed(current));
}
let is_destination = index + 1 == components.len();
if (!is_destination && !metadata.is_dir())
|| (is_destination && !metadata.is_file())
{
return Err(Error::UnsupportedFileType(current));
}
}
Err(source) if source.kind() == io::ErrorKind::NotFound => break,
Err(source) => return Err(Error::io("inspect write path", current, source)),
}
}
Ok(())
}
pub(crate) fn write_file(root: &Path, file: &RustLibFile) -> Result<()> {
let components: Vec<_> = file.path.as_str().split('/').collect();
let mut current = root.to_path_buf();
for component in &components[..components.len() - 1] {
current.push(component);
match fs::symlink_metadata(¤t) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return Err(Error::SymlinkNotAllowed(current));
}
if !metadata.is_dir() {
return Err(Error::UnsupportedFileType(current));
}
}
Err(source) if source.kind() == io::ErrorKind::NotFound => fs::create_dir(¤t)
.map_err(|source| Error::io("create directory", ¤t, source))?,
Err(source) => return Err(Error::io("inspect directory", current, source)),
}
}
let destination = root.join(file.path.as_str());
match fs::symlink_metadata(&destination) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return Err(Error::SymlinkNotAllowed(destination));
}
if !metadata.is_file() {
return Err(Error::UnsupportedFileType(destination));
}
}
Err(source) if source.kind() == io::ErrorKind::NotFound => {}
Err(source) => return Err(Error::io("inspect destination file", destination, source)),
}
fs::write(&destination, file.contents.as_bytes())
.map_err(|source| Error::io("write file", destination, source))
}
pub(crate) fn copy_tree(source: &Path, destination: &Path) -> Result<()> {
fs::create_dir_all(destination)
.map_err(|error| Error::io("create copied Rust library", destination, error))?;
copy_directory(source, destination)
}
fn copy_directory(source: &Path, destination: &Path) -> Result<()> {
let entries =
fs::read_dir(source).map_err(|error| Error::io("read Rust library", source, error))?;
for entry in entries {
let entry = entry.map_err(|error| Error::io("read Rust library entry", source, error))?;
let source_path = entry.path();
let destination_path = destination.join(entry.file_name());
let file_type = entry
.file_type()
.map_err(|error| Error::io("inspect Rust library entry", &source_path, error))?;
if file_type.is_symlink() {
return Err(Error::SymlinkNotAllowed(source_path));
}
if file_type.is_dir() {
fs::create_dir(&destination_path).map_err(|error| {
Error::io(
"create copied Rust library directory",
&destination_path,
error,
)
})?;
copy_directory(&source_path, &destination_path)?;
} else if file_type.is_file() {
fs::copy(&source_path, &destination_path)
.map_err(|error| Error::io("copy Rust library file", &source_path, error))?;
} else {
return Err(Error::UnsupportedFileType(source_path));
}
}
Ok(())
}
pub(crate) fn absolute_lexical(path: &Path) -> Result<PathBuf> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|source| Error::io("read current directory", ".", source))?
.join(path)
};
let mut normalized = PathBuf::new();
for component in absolute.components() {
use std::path::Component;
match component {
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
_ => normalized.push(component.as_os_str()),
}
}
Ok(normalized)
}
pub(crate) fn paths_overlap(left: &Path, right: &Path) -> bool {
left == right || left.starts_with(right) || right.starts_with(left)
}