use aes_gcm::aead::generic_array::GenericArray;
use clap::Parser;
use env_logger::Env;
use indicatif::HumanBytes;
use log::{debug, error, trace, info};
use regex::{RegexSet, SetMatches};
use serde::{Deserialize, Serialize, Serializer, Deserializer, de::Error};
use std::io::{ErrorKind, Read};
use std::path;
use std::{
fmt::{self, Display},
io::{Write},
path::{Path, PathBuf},
time::{Instant, SystemTime}, net::{TcpListener},
};
use crate::*;
use crate::encrypted_comms::AsyncEncryptedComms;
use crate::memory_bound_channel::{Sender, Receiver};
use crate::parallel_walk_dir::parallel_walk_dir;
#[derive(clap::Parser)]
struct DoerCliArgs {
#[arg(long)]
doer: bool,
#[arg(long)]
port: Option<u16>,
#[arg(long, default_value="info")]
log_filter: String,
#[arg(long)]
dump_memory_usage: bool,
}
fn normalize_path(p: &Path) -> Result<RootRelativePath, String> {
if p.is_absolute() {
return Err("Must be relative".to_string());
}
let mut result = String::new();
for c in p.iter() {
let cs = match c.to_str() {
Some(x) => x,
None => return Err("Can't convert path component".to_string()),
};
if cs.contains('/') || cs.contains('\\') {
return Err("Illegal characters in path".to_string());
}
if !result.is_empty() {
result += "/";
}
result += cs;
}
Ok(RootRelativePath { inner: result })
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct RootRelativePath {
inner: String,
}
impl RootRelativePath {
pub fn root() -> RootRelativePath {
RootRelativePath { inner: "".to_string() }
}
pub fn is_root(&self) -> bool {
self.inner.is_empty()
}
pub fn get_full_path(&self, root: &Path) -> PathBuf {
if self.is_root() { root.to_path_buf() } else { root.join(&self.inner) }
}
pub fn regex_set_matches(&self, r: &RegexSet) -> SetMatches {
r.matches(&self.inner)
}
pub fn to_platform_path(&self, dir_separator: char) -> String {
self.inner.replace('/', &dir_separator.to_string())
}
}
impl Display for RootRelativePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_root() {
write!(f, "<ROOT>")
} else {
write!(f, "{}", self.inner)
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Filters {
#[serde(serialize_with = "serialize_regex_set_as_strings", deserialize_with="deserialize_regex_set_from_strings")]
pub regex_set: RegexSet,
pub kinds: Vec<FilterKind>,
}
fn serialize_regex_set_as_strings<S: Serializer>(r: &RegexSet, s: S) -> Result<S::Ok, S::Error> {
r.patterns().serialize(s)
}
fn deserialize_regex_set_from_strings<'de, D: Deserializer<'de>>(d: D) -> Result<RegexSet, D::Error> {
let patterns = <Vec<String>>::deserialize(d)?;
RegexSet::new(patterns).map_err(|e| D::Error::custom(e))
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum FilterKind {
Include,
Exclude,
}
#[derive(Serialize, Deserialize)]
pub enum Command {
SetRoot {
root: String, },
GetEntries {
filters: Filters,
},
CreateRootAncestors,
GetFileContent {
path: RootRelativePath,
},
CreateOrUpdateFile {
path: RootRelativePath,
#[serde(with = "serde_bytes")] data: Vec<u8>,
set_modified_time: Option<SystemTime>,
more_to_follow: bool,
},
CreateSymlink {
path: RootRelativePath,
kind: SymlinkKind,
target: SymlinkTarget,
},
CreateFolder {
path: RootRelativePath,
},
DeleteFile {
path: RootRelativePath,
},
DeleteFolder {
path: RootRelativePath,
},
DeleteSymlink {
path: RootRelativePath,
kind: SymlinkKind,
},
ProfilingTimeSync,
Marker(u64),
Shutdown,
}
impl encrypted_comms::IsFinalMessage for Command {
fn is_final_message(&self) -> bool {
match self {
Self::Shutdown => true,
_ => false
}
}
}
impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SetRoot { root } => f.debug_struct("SetRoot").field("root", root).finish(),
Self::GetEntries { filters } => f.debug_struct("GetEntries").field("filters", filters).finish(),
Self::CreateRootAncestors => write!(f, "CreateRootAncestors"),
Self::GetFileContent { path } => f.debug_struct("GetFileContent").field("path", path).finish(),
Self::CreateOrUpdateFile { path, data, set_modified_time, more_to_follow } => f.debug_struct("CreateOrUpdateFile").field("path", path).field("data", &format!("... ({})", HumanBytes(data.len() as u64))).field("set_modified_time", set_modified_time).field("more_to_follow", more_to_follow).finish(),
Self::CreateSymlink { path, kind, target } => f.debug_struct("CreateSymlink").field("path", path).field("kind", kind).field("target", target).finish(),
Self::CreateFolder { path } => f.debug_struct("CreateFolder").field("path", path).finish(),
Self::DeleteFile { path } => f.debug_struct("DeleteFile").field("path", path).finish(),
Self::DeleteFolder { path } => f.debug_struct("DeleteFolder").field("path", path).finish(),
Self::DeleteSymlink { path, kind } => f.debug_struct("DeleteSymlink").field("path", path).field("kind", kind).finish(),
Self::ProfilingTimeSync => write!(f, "ProfilingTimeSync"),
Self::Marker(arg0) => f.debug_tuple("Marker").field(arg0).finish(),
Self::Shutdown => write!(f, "Shutdown"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SymlinkKind {
File, Folder, Unknown, }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SymlinkTarget {
Normalized(String),
NotNormalized(String)
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum EntryDetails {
File {
modified_time: SystemTime,
size: u64
},
Folder,
Symlink {
kind: SymlinkKind,
target: SymlinkTarget,
},
}
fn entry_details_from_metadata(m: std::fs::Metadata, path: &Path) -> Result<EntryDetails, String> {
if m.is_dir() {
Ok(EntryDetails::Folder)
} else if m.is_file() {
let modified_time = match m.modified() {
Ok(m) => m,
Err(err) => return Err(format!("Unknown modified time for '{}': {err}", path.display())),
};
Ok(EntryDetails::File {
modified_time,
size: m.len(),
})
} else if m.is_symlink() {
let target = match std::fs::read_link(path) {
Ok(t) => t,
Err(err) => return Err(format!("Unable to read symlink target for '{}': {err}", path.display())),
};
let target = match normalize_path(&target) {
Ok(r) => SymlinkTarget::Normalized(r.inner),
Err(_) => SymlinkTarget::NotNormalized(target.to_string_lossy().to_string()),
};
#[cfg(windows)]
let kind = {
if std::os::windows::fs::FileTypeExt::is_symlink_file(&m.file_type()) {
SymlinkKind::File
} else if std::os::windows::fs::FileTypeExt::is_symlink_dir(&m.file_type()) {
SymlinkKind::Folder
} else {
return Err(format!("Unknown symlink type time for '{}'", path.display()));
}
};
#[cfg(not(windows))]
let kind = {
match std::fs::metadata(path) {
Ok(m) if m.is_file() => SymlinkKind::File,
Ok(m) if m.is_dir() => SymlinkKind::Folder,
_ => SymlinkKind::Unknown
}
};
Ok(EntryDetails::Symlink { kind, target })
} else {
return Err(format!("Unknown file type for '{}': {:?}", path.display(), m));
}
}
#[derive(Serialize, Deserialize)]
pub enum Response {
RootDetails {
root_details: Option<EntryDetails>, platform_differentiates_symlinks: bool,
platform_dir_separator: char,
},
Entry((RootRelativePath, EntryDetails)),
EndOfEntries,
FileContent {
#[serde(with = "serde_bytes")] data: Vec<u8>,
more_to_follow: bool,
},
ProfilingTimeSync(std::time::Duration),
ProfilingData(ProcessProfilingData),
Marker(u64),
Error(String),
}
impl encrypted_comms::IsFinalMessage for Response {
fn is_final_message(&self) -> bool {
match self {
Self::ProfilingData{..} => true,
_ => false
}
}
}
impl std::fmt::Debug for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RootDetails { root_details, platform_differentiates_symlinks, platform_dir_separator } => f.debug_struct("RootDetails").field("root_details", root_details).field("platform_differentiates_symlinks", platform_differentiates_symlinks).field("platform_dir_separator", platform_dir_separator).finish(),
Self::Entry(arg0) => f.debug_tuple("Entry").field(arg0).finish(),
Self::EndOfEntries => write!(f, "EndOfEntries"),
Self::FileContent { data, more_to_follow } => f.debug_struct("FileContent").field("data", &format!("... ({})", HumanBytes(data.len() as u64))).field("more_to_follow", more_to_follow).finish(),
Self::ProfilingTimeSync(arg0) => f.debug_tuple("ProfilingTimeSync").field(arg0).finish(),
Self::ProfilingData(_) => f.debug_tuple("ProfilingData").finish(),
Self::Marker(arg0) => f.debug_tuple("Marker").field(arg0).finish(),
Self::Error(arg0) => f.debug_tuple("Error").field(arg0).finish(),
}
}
}
#[allow(clippy::large_enum_variant)]
enum Comms {
Local {
sender: Sender<Response>,
receiver: Receiver<Command>,
},
Remote {
encrypted_comms: AsyncEncryptedComms<Response, Command>,
},
}
impl Comms {
pub fn send_response(&mut self, r: Response) -> Result<(), String> {
trace!("Sending response {:?} to {}", r, &self);
let sender = match self {
Comms::Local { sender, .. } => sender,
Comms::Remote { encrypted_comms, .. } => &mut encrypted_comms.sender,
};
sender.send(r).map_err(|_| format!("Lost communication with {}", &self))
}
pub fn receive_command(&mut self) -> Result<Command, String> {
trace!("Waiting for command from {}", &self);
let receiver = match self {
Comms::Local { receiver, .. } => receiver,
Comms::Remote { encrypted_comms, .. } => &mut encrypted_comms.receiver,
};
receiver.recv().map_err(|_| format!("Lost communication with {}", &self))
}
}
impl Display for Comms {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Comms::Local { .. } => write!(f, "Local boss"),
Comms::Remote { .. } => write!(f, "Remote boss"),
}
}
}
pub fn doer_main() -> ExitCode {
let main_timer = start_timer(function_name!());
let msg = format!("{}{}", HANDSHAKE_STARTED_MSG, VERSION);
println!("{}", msg);
eprintln!("{}", msg);
let args = DoerCliArgs::parse();
{
profile_this!("Configuring logging");
let mut builder = env_logger::Builder::from_env(Env::default().default_filter_or(args.log_filter));
builder.target(env_logger::Target::Stderr);
builder.format(|buf, record| {
writeln!(
buf,
"{} {} {} {}",
buf.timestamp_nanos(),
record.level(),
record.target(),
record.args()
)
});
builder.init();
}
let timer = start_timer("Handshaking");
let mut secret = String::new();
if let Err(e) = std::io::stdin().read_line(&mut secret) {
error!("Failed to receive secret: {}", e);
return ExitCode::from(22);
}
secret.pop();
let secret_bytes = match base64::decode(secret) {
Ok(b) => b,
Err(e) => {
error!("Failed to decode secret: {}", e);
return ExitCode::from(23);
}
};
let secret_key = GenericArray::from_slice(&secret_bytes);
let addr = ("0.0.0.0", args.port.unwrap_or(0));
let listener = match TcpListener::bind(addr) {
Ok(l) => {
debug!("Listening on {:?}", l.local_addr()); l
}
Err(e) => {
error!("Failed to bind to {:?}: {}", addr, e);
return ExitCode::from(24);
}
};
let msg = format!("{}{}", HANDSHAKE_COMPLETED_MSG, listener.local_addr().unwrap().port());
println!("{}", msg);
eprintln!("{}", msg);
stop_timer(timer);
let timer = start_timer("Waiting for connection");
let tcp_connection = match listener.accept() {
Ok((socket, addr)) => {
debug!("Client connected: {socket:?} {addr:?}");
socket
}
Err(e) => {
error!("Failed to accept: {}", e);
return ExitCode::from(25);
}
};
stop_timer(timer);
let mut comms = Comms::Remote {
encrypted_comms: AsyncEncryptedComms::new(
tcp_connection,
*secret_key,
1, 0,
("doer", "remote boss"),
)};
if let Err(e) = message_loop(&mut comms) {
debug!("doer process finished with error: {:?}", e);
return ExitCode::from(20)
}
stop_timer(main_timer);
if let Comms::Remote{ encrypted_comms } = comms { encrypted_comms.shutdown_with_final_message_sent_after_threads_joined(|| Response::ProfilingData(get_local_process_profiling()));
}
if args.dump_memory_usage {
info!("Doer peak memory usage: {}", profiling::get_peak_memory_usage());
}
debug!("doer process finished successfully!");
ExitCode::SUCCESS
}
pub fn doer_thread_running_on_boss(receiver: Receiver<Command>, sender: Sender<Response>) -> Result<(), String> {
debug!("doer thread running");
profile_this!();
match message_loop(&mut Comms::Local { sender, receiver }) {
Ok(_) => {
debug!("doer thread finished successfully!");
Ok(())
}
Err(e) => {
error!("doer thread finished with error: {:?}", e);
Err(format!("doer thread finished with error: {:?}", e))
}
}
}
struct DoerContext {
root: PathBuf,
in_progress_file_receive: Option<(RootRelativePath, std::fs::File)>,
}
fn message_loop(comms: &mut Comms) -> Result<(), ()> {
profile_this!();
let mut context : Option<DoerContext> = None;
loop {
match comms.receive_command() {
Ok(c) => {
match exec_command(c, comms, &mut context) {
Ok(false) => {
debug!("Shutdown command received - finishing message_loop");
return Ok(());
}
Ok(true) => (), Err(e) => {
error!("Error processing command: {}", e);
return Err(());
}
}
}
Err(_) => {
debug!("Boss disconnected - finishing message loop");
return Ok(());
}
}
}
}
fn exec_command(command: Command, comms: &mut Comms, context: &mut Option<DoerContext>) -> Result<bool, String> {
match command {
Command::SetRoot { root } => {
if let Err(e) = handle_set_root(comms, context, root) {
comms.send_response(Response::Error(e))?;
}
}
Command::GetEntries { filters } => {
profile_this!("GetEntries");
if let Err(e) = handle_get_entries(comms, context.as_mut().unwrap(), filters) {
comms.send_response(Response::Error(e))?;
}
}
Command::CreateRootAncestors => {
let path_to_create = context.as_ref().unwrap().root.parent();
trace!("Creating {:?} and all its ancestors", path_to_create);
if let Some(p) = path_to_create {
profile_this!(format!("CreateRootAncestors {}", p.to_str().unwrap().to_string()));
if let Err(e) = std::fs::create_dir_all(p) {
comms.send_response(Response::Error(format!("Error creating folder and ancestors for '{}': {e}", p.display())))?;
}
}
}
Command::GetFileContent { path } => {
let full_path = path.get_full_path(&context.as_ref().unwrap().root);
profile_this!(format!("GetFileContent {}", path.to_string()));
if let Err(e) = handle_get_file_contents(comms, &full_path) {
comms.send_response(Response::Error(e))?;
}
}
Command::CreateOrUpdateFile {
path,
data,
set_modified_time,
more_to_follow
} => {
let full_path = path.get_full_path(&context.as_ref().unwrap().root);
trace!("Creating/updating content of '{}'", full_path.display());
profile_this!(format!("CreateOrUpdateFile {}", path.to_string()));
let mut f = match context.as_mut().unwrap().in_progress_file_receive.take() {
Some((in_progress_path, f)) => {
if in_progress_path == path {
f
} else {
comms.send_response(Response::Error(format!("Unexpected continued file transfer!")))?;
return Ok(true);
}
},
None => match std::fs::File::create(&full_path) {
Ok(f) => f,
Err(e) => {
comms.send_response(Response::Error(format!("Error writing file contents to '{}': {e}", full_path.display())))?;
return Ok(true);
}
}
};
let r = f.write_all(&data);
if let Err(e) = r {
comms.send_response(Response::Error(format!("Error writing file contents to '{}': {e}", full_path.display())))?;
return Ok(true);
}
context.as_mut().unwrap().in_progress_file_receive = if more_to_follow {
Some((path, f))
} else {
None
};
if let Some(t) = set_modified_time {
trace!("Setting modifited time of '{}'", full_path.display());
let r =
filetime::set_file_mtime(&full_path, filetime::FileTime::from_system_time(t));
if let Err(e) = r {
comms.send_response(Response::Error(format!("Error setting modified time of '{}': {e}", full_path.display())))?;
return Ok(true);
}
}
}
Command::CreateFolder { path } => {
let full_path = path.get_full_path(&context.as_ref().unwrap().root);
trace!("Creating folder '{}'", full_path.display());
profile_this!(format!("CreateFolder {}", full_path.to_str().unwrap().to_string()));
if let Err(e) = std::fs::create_dir(&full_path) {
comms.send_response(Response::Error(format!("Error creating folder '{}': {e}", full_path.display())))?;
}
}
Command::CreateSymlink { path, kind, target } => {
if let Err(e) = handle_create_symlink(path, context.as_mut().unwrap(), kind, target) {
comms.send_response(Response::Error(e))?;
}
},
Command::DeleteFile { path } => {
let full_path = path.get_full_path(&context.as_ref().unwrap().root);
trace!("Deleting file '{}'", full_path.display());
profile_this!(format!("DeleteFile {}", path.to_string()));
if let Err(e) = std::fs::remove_file(&full_path) {
comms.send_response(Response::Error(format!("Error deleting file '{}': {e}", full_path.display())))?;
}
}
Command::DeleteFolder { path } => {
let full_path = path.get_full_path(&context.as_ref().unwrap().root);
trace!("Deleting folder '{}'", full_path.display());
profile_this!(format!("DeleteFolder {}", path.to_string()));
if let Err(e) = std::fs::remove_dir(&full_path) {
comms.send_response(Response::Error(format!("Error deleting folder '{}': {e}", full_path.display())))?;
}
}
Command::DeleteSymlink { path, kind } => {
let full_path = path.get_full_path(&context.as_ref().unwrap().root);
trace!("Deleting symlink '{}'", full_path.display());
let res = if cfg!(windows) {
match kind {
SymlinkKind::File => std::fs::remove_file(&full_path),
SymlinkKind::Folder => std::fs::remove_dir(&full_path),
SymlinkKind::Unknown => {
comms.send_response(Response::Error(format!("Can't delete symlink of unknown type '{}'", full_path.display())))?;
return Ok(true);
}
}
} else {
std::fs::remove_file(&full_path)
};
if let Err(e) = res {
comms.send_response(Response::Error(format!("Error deleting symlink '{}': {e}", full_path.display())))?;
}
},
Command::ProfilingTimeSync => {
comms.send_response(Response::ProfilingTimeSync(PROFILING_START.elapsed()))?;
},
Command::Marker(x) => {
comms.send_response(Response::Marker(x))?;
}
Command::Shutdown => {
return Ok(false);
},
}
Ok(true)
}
fn handle_set_root(comms: &mut Comms, context: &mut Option<DoerContext>, root: String) -> Result<(), String> {
*context = Some(DoerContext {
root: PathBuf::from(root),
in_progress_file_receive: None,
});
let context = context.as_ref().unwrap();
let platform_differentiates_symlinks = cfg!(windows);
let platform_dir_separator = std::path::MAIN_SEPARATOR;
let metadata = std::fs::symlink_metadata(&context.root);
match metadata {
Ok(m) => {
let entry_details = entry_details_from_metadata(m, &context.root)?;
comms.send_response(Response::RootDetails { root_details: Some(entry_details), platform_differentiates_symlinks, platform_dir_separator })?;
},
Err(e) if e.kind() == ErrorKind::NotFound => {
comms.send_response(Response::RootDetails { root_details: None, platform_differentiates_symlinks, platform_dir_separator })?;
}
Err(e) => return Err(format!(
"root '{}' can't be read: {}", context.root.display(), e)),
}
Ok(())
}
#[derive(PartialEq, Debug)]
enum FilterResult {
Include,
Exclude
}
fn apply_filters(path: &RootRelativePath, filters: &Filters) -> FilterResult {
if path.is_root() {
return FilterResult::Include;
}
let mut result = match filters.kinds.get(0) {
Some(FilterKind::Include) => FilterResult::Exclude,
Some(FilterKind::Exclude) => FilterResult::Include,
None => FilterResult::Include
};
let matches = path.regex_set_matches(&filters.regex_set);
for matched_filter_idx in matches {
let filter_kind = filters.kinds[matched_filter_idx];
match filter_kind {
FilterKind::Include => result = FilterResult::Include,
FilterKind::Exclude => result = FilterResult::Exclude,
}
}
result
}
fn filter_func(entry: &std::fs::DirEntry, root: &Path, filters: &Filters) -> Result<parallel_walk_dir::FilterResult<RootRelativePath>, String> {
let path = entry.path().strip_prefix(root).expect("Strip prefix failed").to_path_buf();
let path = match normalize_path(&path) {
Ok(p) => p,
Err(e) => return Err(format!("normalize_path failed on '{}': {e}", path.display())),
};
let skip = apply_filters(&path, &filters) == FilterResult::Exclude;
if skip {
trace!("Skipping '{}' due to filter", path);
}
Ok(parallel_walk_dir::FilterResult::<RootRelativePath> {
skip,
additional_data: path,
})
}
fn handle_get_entries(comms: &mut Comms, context: &mut DoerContext, filters: Filters) -> Result<(), String> {
let start = Instant::now();
let root = context.root.clone();
let entry_receiver = parallel_walk_dir(&context.root, move |e| filter_func(e, &root, &filters));
let mut count = 0;
while let Ok(entry) = entry_receiver.recv() {
count += 1;
match entry {
Err(e) => return Err(format!("Error fetching entries of root '{}': {e}", context.root.display())),
Ok(e) => {
trace!("Processing entry {:?}", e);
profile_this!("Processing entry");
let path = e.additional_data;
let metadata = match e.dir_entry.metadata() {
Ok(m) => m,
Err(err) => return Err(format!("Unable to get metadata for '{}': {err}", path)),
};
let d = entry_details_from_metadata(metadata, &e.dir_entry.path())?;
comms.send_response(Response::Entry((path, d)))?;
}
}
}
let elapsed = start.elapsed().as_millis();
comms.send_response(Response::EndOfEntries)?;
debug!(
"Walked {} in {}ms ({}/s)",
count,
elapsed,
1000.0 * count as f32 / elapsed as f32
);
Ok(())
}
fn handle_get_file_contents(comms: &mut Comms, full_path: &Path) -> Result<(), String> {
trace!("Getting content of '{}'", full_path.display());
let mut f = match std::fs::File::open(&full_path) {
Ok(f) => f,
Err(e) => return Err(format!("Error opening file '{}': {e}", full_path.display())),
};
let mut chunk_size = 4 * 1024;
let mut prev_buf = vec![0; 0];
let mut prev_buf_valid = 0;
let mut next_buf = vec![0; chunk_size];
loop {
profile_this!("Read iteration");
match f.read(&mut next_buf) {
Ok(n) if n == 0 => {
prev_buf.truncate(prev_buf_valid);
comms.send_response(Response::FileContent { data: prev_buf, more_to_follow: false })?;
return Ok(());
},
Ok(n) => {
if prev_buf_valid > 0 {
prev_buf.truncate(prev_buf_valid);
comms.send_response(Response::FileContent { data: prev_buf, more_to_follow: true })?;
}
prev_buf = next_buf;
prev_buf_valid = n;
if n < prev_buf.len() {
next_buf = vec![0; 32];
} else {
chunk_size = std::cmp::min(chunk_size * 2, 1024*1024*4);
next_buf = vec![0; chunk_size];
}
}
Err(e) => return Err(format!("Error getting file content of '{}': {e}", full_path.display())),
}
}
}
fn handle_create_symlink(path: RootRelativePath, context: &mut DoerContext, #[allow(unused)] kind: SymlinkKind, target: SymlinkTarget) -> Result<(), String> {
let full_path = path.get_full_path(&context.root);
trace!("Creating symlink at '{}'", full_path.display());
let target = match target {
SymlinkTarget::Normalized(s) => s.replace("/", &path::MAIN_SEPARATOR.to_string()),
SymlinkTarget::NotNormalized(s) => s, };
#[cfg(windows)]
let res = match kind {
SymlinkKind::File => std::os::windows::fs::symlink_file(target, &full_path),
SymlinkKind::Folder => std::os::windows::fs::symlink_dir(target, &full_path),
SymlinkKind::Unknown => {
return Err(format!("Can't create symlink of unknown kind on this platform '{}'", full_path.display()));
},
};
#[cfg(not(windows))]
let res = std::os::unix::fs::symlink(target, &full_path);
if let Err(e) = res {
return Err(format!("Failed to create symlink '{}': {e}", full_path.display()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path_is_root() {
let x = normalize_path(Path::new(""));
assert_eq!(x, Ok(RootRelativePath::root()));
assert_eq!(x.unwrap().is_root(), true);
}
#[test]
fn test_normalize_path_absolute() {
let x = if cfg!(windows) {
"C:\\Windows"
} else {
"/etc/hello"
};
assert_eq!(normalize_path(Path::new(x)), Err("Must be relative".to_string()));
}
#[cfg(unix)] #[test]
fn test_normalize_path_slashes_in_component() {
assert_eq!(normalize_path(Path::new("a path with\\backslashes/adsa")), Err("Illegal characters in path".to_string()));
}
#[test]
fn test_normalize_path_multiple_components() {
assert_eq!(normalize_path(Path::new("one/two/three")), Ok(RootRelativePath { inner: "one/two/three".to_string() }));
}
#[test]
fn test_apply_filters_root() {
let filters = Filters {
regex_set: RegexSet::new(&["^.*$"]).unwrap(),
kinds: vec![FilterKind::Exclude]
};
assert_eq!(apply_filters(&RootRelativePath { inner: "will be excluded".to_string() }, &filters), FilterResult::Exclude);
assert_eq!(apply_filters(&RootRelativePath::root(), &filters), FilterResult::Include);
}
#[test]
fn test_apply_filters_no_filters() {
let filters = Filters {
regex_set: RegexSet::empty(),
kinds: vec![]
};
assert_eq!(apply_filters(&RootRelativePath { inner: "yes".to_string() }, &filters), FilterResult::Include);
assert_eq!(apply_filters(&RootRelativePath { inner: "no".to_string() }, &filters), FilterResult::Include);
}
#[test]
fn test_apply_filters_single_include() {
let filters = Filters {
regex_set: RegexSet::new(&["^yes$"]).unwrap(),
kinds: vec![FilterKind::Include]
};
assert_eq!(apply_filters(&RootRelativePath { inner: "yes".to_string() }, &filters), FilterResult::Include);
assert_eq!(apply_filters(&RootRelativePath { inner: "no".to_string() }, &filters), FilterResult::Exclude);
}
#[test]
fn test_apply_filters_single_exclude() {
let filters = Filters {
regex_set: RegexSet::new(&["^no$"]).unwrap(),
kinds: vec![FilterKind::Exclude]
};
assert_eq!(apply_filters(&RootRelativePath { inner: "yes".to_string() }, &filters), FilterResult::Include);
assert_eq!(apply_filters(&RootRelativePath { inner: "no".to_string() }, &filters), FilterResult::Exclude);
}
#[test]
fn test_apply_filters_complex() {
let filters = Filters {
regex_set: RegexSet::new(&[
"^.*$",
"^build/.*$",
"^git/.*$",
"^build/output.exe$",
"^src/build/.*$",
]).unwrap(),
kinds: vec![
FilterKind::Include,
FilterKind::Exclude,
FilterKind::Exclude,
FilterKind::Include,
FilterKind::Exclude,
]
};
assert_eq!(apply_filters(&RootRelativePath { inner: "README".to_string() }, &filters), FilterResult::Include);
assert_eq!(apply_filters(&RootRelativePath { inner: "build/file.o".to_string() }, &filters), FilterResult::Exclude);
assert_eq!(apply_filters(&RootRelativePath { inner: "git/hash".to_string() }, &filters), FilterResult::Exclude);
assert_eq!(apply_filters(&RootRelativePath { inner: "build/rob".to_string() }, &filters), FilterResult::Exclude);
assert_eq!(apply_filters(&RootRelativePath { inner: "build/output.exe".to_string() }, &filters), FilterResult::Include);
assert_eq!(apply_filters(&RootRelativePath { inner: "src/build/file.o".to_string() }, &filters), FilterResult::Exclude);
assert_eq!(apply_filters(&RootRelativePath { inner: "src/source.cpp".to_string() }, &filters), FilterResult::Include);
}
}