unlab-gpu 0.1.0

Micro scripting language for neural networks that uses unmtx-gpu.
Documentation
//
// Copyright (c) 2025-2026 Ɓukasz Szpakowski
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
//! A filesytem module.
use std::collections::HashSet;
use std::fs::copy;
use std::fs::create_dir;
use std::fs::create_dir_all;
use std::fs::read_dir;
use std::fs::remove_dir;
use std::fs::remove_file;
use std::fs::symlink_metadata;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Result;
use std::path::Path;
use std::path::PathBuf;

fn recursively_copy_with_path_bufs(src_path_buf: &mut PathBuf, dst_path_buf: &mut PathBuf) -> Result<()>
{
    let metadata = symlink_metadata(src_path_buf.as_path())?;
    if metadata.is_dir() {
        match create_dir(dst_path_buf.as_path()) {
            Ok(()) => (),
            Err(err) if err.kind() == ErrorKind::AlreadyExists => (),
            Err(err) => return Err(err),
        }
        let canon_src_path_buf = src_path_buf.canonicalize()?;
        let canon_dst_path_buf = dst_path_buf.canonicalize()?;
        if canon_src_path_buf == canon_dst_path_buf {
            return Ok(());
        }
        if canon_dst_path_buf.starts_with(canon_src_path_buf) {
            remove_dir(dst_path_buf.as_path())?;
            return Err(Error::new(ErrorKind::Other, "destination directory can't be in source directory"));
        }
        let entries = read_dir(src_path_buf.as_path())?;
        for entry in entries {
            let tmp_entry = entry?;
            src_path_buf.push(tmp_entry.file_name());
            dst_path_buf.push(tmp_entry.file_name());
            recursively_copy_with_path_bufs(src_path_buf, dst_path_buf)?;
            dst_path_buf.pop();
            src_path_buf.pop();
        }
    } else if metadata.is_file() {
        let canon_src_path_buf = src_path_buf.canonicalize()?;
        match dst_path_buf.canonicalize() {
            Ok(canon_dst_path_buf) => {
                if canon_src_path_buf == canon_dst_path_buf {
                    return Ok(());
                }
            },
            Err(err) if err.kind() == ErrorKind::NotFound => (),
            Err(err) => return Err(err),
        }
        copy(src_path_buf.as_path(), dst_path_buf.as_path())?;
    } else if metadata.is_symlink() {
        return Err(Error::new(ErrorKind::Other, "can't copy symbolic link"));
    } else {
        return Err(Error::new(ErrorKind::Other, "can't copy device file"));
    }
    Ok(())
}

/// Recursively copies the files and/or the directories.
///
/// This function automatically creates a destition directory with parent directories. If the
/// canonical path of destination contains the canonical path of source, this function also
/// returns an error. The file or the directory isn't copied if the canonical path of source is
/// equal to the canonical path of destination. Symbolic links and device files aren't copied. If
/// the symbolic link or the device file is occurred, this function also returns an error.
pub fn recursively_copy<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()>
{
    let mut src_path_buf = PathBuf::from(src.as_ref());
    let mut dst_path_buf = PathBuf::from(dst.as_ref());
    let mut dst_parent_path_buf = dst_path_buf.clone();
    dst_parent_path_buf.pop();
    if dst_parent_path_buf != PathBuf::from("") {
        create_dir_all(dst_parent_path_buf)?;
    }
    recursively_copy_with_path_bufs(&mut src_path_buf, &mut dst_path_buf)
}

fn recursively_remove_with_path_buf(path_buf: &mut PathBuf, is_force: bool) -> Result<()>
{
    let metadata = match symlink_metadata(path_buf.as_path()) {
        Ok(tmp_metadata) => tmp_metadata,
        Err(err) if is_force && err.kind() == ErrorKind::NotFound => return Ok(()),
        Err(err) => return Err(err),
    };
    if metadata.is_dir() {
        let entries = match read_dir(path_buf.as_path()) {
            Ok(tmp_entries) => tmp_entries,
            Err(err) if is_force && err.kind() == ErrorKind::NotFound => return Ok(()),
            Err(err) => return Err(err),
        };
        for entry in entries {
            let tmp_entry = entry?;
            path_buf.push(tmp_entry.file_name());
            recursively_remove_with_path_buf(path_buf, is_force)?;
            path_buf.pop();
        }
        match remove_dir(path_buf.as_path()) {
            Ok(()) => (),
            Err(err) if is_force && err.kind() == ErrorKind::NotFound => (),
            Err(err) => return Err(err),
        }
    } else {
        match remove_file(path_buf.as_path()) {
            Ok(()) => (),
            Err(err) if is_force && err.kind() == ErrorKind::NotFound => (),
            Err(err) => return Err(err),
        }
    }
    Ok(())
}

/// Recursively removes the files and/or the directories.
///
/// This function doesn't returns an error for a directory or a file that doesn't exist if the
/// force flag is set.
pub fn recursively_remove<P: AsRef<Path>>(path: P, is_force: bool) -> Result<()>
{
    let mut path_buf = PathBuf::from(path.as_ref());
    recursively_remove_with_path_buf(&mut path_buf, is_force)
}

fn get_dir_paths_and_paths_in_dir(path: &Path, suffix_path_buf: &mut PathBuf, dir_paths: &mut HashSet<PathBuf>, paths: &mut HashSet<PathBuf>, path_vec: &mut Option<Vec<PathBuf>>, ignored_paths: &HashSet<PathBuf>, depth: Option<usize>) -> Result<()>
{
    let mut path_buf = PathBuf::from(path);
    if suffix_path_buf != &PathBuf::from("") {
        path_buf.push(suffix_path_buf.as_path());
    }
    let metadata = match symlink_metadata(path_buf.as_path()) {
        Ok(tmp_metadata) => tmp_metadata,
        Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
        Err(err) => return Err(err),
    };
    if metadata.is_dir() && depth.map(|d| d > 0).unwrap_or(true) {
        match read_dir(path_buf.as_path()) {
            Ok(entries) => {
                dir_paths.insert(suffix_path_buf.clone());
                for entry in entries {
                    let tmp_entry = entry?;
                    suffix_path_buf.push(tmp_entry.file_name());
                    get_dir_paths_and_paths_in_dir(path, suffix_path_buf, dir_paths, paths, path_vec, ignored_paths, depth.map(|d| d - 1))?;
                    suffix_path_buf.pop();
                }
            },
            Err(err) if err.kind() == ErrorKind::NotFound => (),
            Err(err) => return Err(err),
        }
    } else {
        if !ignored_paths.contains(suffix_path_buf) {
            paths.insert(suffix_path_buf.clone());
            match path_vec {
                Some(path_vec) => path_vec.push(suffix_path_buf.clone()),
                None => (),
            }
        }
    }
    Ok(())
}

/// Searches the path conflicts for the source directory and the destination directory.
///
/// This function returns the conflict paths and the paths of source directory for the optional
/// depth. The path of source directory are compared with the path of destination directory. If
/// these paths are same, there occurs the path conflict.
pub fn conflicts<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q, ignored_paths: &HashSet<PathBuf>, depth: Option<usize>) -> Result<(Vec<PathBuf>, Vec<PathBuf>)>
{
    let mut paths: Option<Vec<PathBuf>> = Some(Vec::new());
    let mut src_dir_paths: HashSet<PathBuf> = HashSet::new();
    let mut src_paths: HashSet<PathBuf> = HashSet::new();
    let mut src_suffix_path_buf = PathBuf::from("");
    get_dir_paths_and_paths_in_dir(src.as_ref(), &mut src_suffix_path_buf, &mut src_dir_paths, &mut src_paths, &mut paths, &HashSet::new(), depth)?;
    let mut dst_dir_paths: HashSet<PathBuf> = HashSet::new();
    let mut dst_paths: HashSet<PathBuf> = HashSet::new();
    let mut dst_suffix_path_buf = PathBuf::from("");
    get_dir_paths_and_paths_in_dir(dst.as_ref(), &mut dst_suffix_path_buf, &mut dst_dir_paths, &mut dst_paths, &mut None, ignored_paths, depth)?;
    let mut conflict_paths: Vec<PathBuf> = src_dir_paths.intersection(&dst_paths).map(|p| p.clone()).collect();
    let mut conflict_paths2: Vec<PathBuf> = src_paths.intersection(&dst_dir_paths).map(|p| p.clone()).collect();
    let mut conflict_paths3: Vec<PathBuf> = src_paths.intersection(&dst_paths).map(|p| p.clone()).collect();
    conflict_paths.append(&mut conflict_paths2);
    conflict_paths.append(&mut conflict_paths3);
    Ok((conflict_paths, paths.unwrap_or(Vec::new())))
}

fn get_paths_in_dir(path: &Path, suffix_path_buf: &mut PathBuf, paths: &mut Vec<PathBuf>, depth: Option<usize>) -> Result<()>
{
    let mut path_buf = PathBuf::from(path);
    if suffix_path_buf != &PathBuf::from("") {
        path_buf.push(suffix_path_buf.as_path());
    }
    let metadata = match symlink_metadata(path_buf.as_path()) {
        Ok(tmp_metadata) => tmp_metadata,
        Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
        Err(err) => return Err(err),
    };
    if metadata.is_dir() && depth.map(|d| d > 0).unwrap_or(true) {
        match read_dir(path_buf.as_path()) {
            Ok(entries) => {
                for entry in entries {
                    let tmp_entry = entry?;
                    suffix_path_buf.push(tmp_entry.file_name());
                    get_paths_in_dir(path, suffix_path_buf, paths, depth.map(|d| d - 1))?;
                    suffix_path_buf.pop();
                }
            },
            Err(err) if err.kind() == ErrorKind::NotFound => (),
            Err(err) => return Err(err),
        }
    } else {
        paths.push(suffix_path_buf.clone());
    }
    Ok(())
}

/// Returns the paths in the directory for the optional depth.
pub fn paths_in_dir<P: AsRef<Path>>(path: P, depth: Option<usize>) -> Result<Vec<PathBuf>>
{
    let mut paths: Vec<PathBuf> = Vec::new();
    let mut suffix_path_buf = PathBuf::from("");
    get_paths_in_dir(path.as_ref(), &mut suffix_path_buf, &mut paths, depth)?;
    Ok(paths)
}

/// Recursively copies the files and/or the directories which are in the directories of paths.
///
/// See [`recursively_copy`].
pub fn recursively_copy_paths_in_dir<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q, paths: &[PathBuf]) -> Result<()>
{
    for suffix_path_buf in paths {
        let mut src_path_buf = PathBuf::from(src.as_ref());
        src_path_buf.push(suffix_path_buf.as_path());
        let mut dst_path_buf = PathBuf::from(dst.as_ref());
        dst_path_buf.push(suffix_path_buf.as_path());
        recursively_copy(src_path_buf.as_path(), dst_path_buf.as_path())?;
    }
    Ok(())
}

/// Recursively removes the files and/or the directories which are in the directories of paths.
///
/// See [`recursively_remove`].
pub fn recursively_remove_paths_in_dir<P: AsRef<Path>>(path: P, paths: &[PathBuf], is_force: bool) -> Result<()>
{
    for suffix_path_buf in paths {
        let mut path_buf = PathBuf::from(path.as_ref());
        path_buf.push(suffix_path_buf.as_path());
        recursively_remove(path_buf.as_path(), is_force)?;
        let mut tmp_suffix_path_buf = suffix_path_buf.clone();
        if tmp_suffix_path_buf != PathBuf::from("") {
            tmp_suffix_path_buf.pop();
            while tmp_suffix_path_buf != PathBuf::from("") {
                let mut dir_path_buf = PathBuf::from(path.as_ref());
                dir_path_buf.push(tmp_suffix_path_buf.as_path());
                match remove_dir(dir_path_buf.as_path()) {
                    Ok(()) => (),
                    Err(_) => break,
                }
                tmp_suffix_path_buf.pop();
            }
        }
    }
    Ok(())
}

/// Returns the path to one directory in the directory if the directory has only one entry that is
/// the directory, otherwise returns the path to the directory.
pub fn only_one_dir_in_dir<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>>
{
    let entries = read_dir(path)?;
    let mut is_first = true;
    let mut path_buf: Option<PathBuf> = None;
    for entry in entries {
        let tmp_entry = entry?;
        if is_first && tmp_entry.metadata()?.is_dir() {
            path_buf = Some(tmp_entry.path());
        } else {
            path_buf = None;
        }
        is_first = false;
    }
    Ok(path_buf)
}

#[cfg(test)]
mod tests;