normalizefs 0.0.11

Normalization of file system paths
Documentation
/*
Normalizing of file system pathes.
Written by Radim Kolar <hsn@sendmail.cz> 2025
https://gitlab.com/hsn10/normalizefs

This is free and unencumbered software released into the public domain.
For more information, please refer to <https://unlicense.org/>

CC0: This work has been marked as dedicated to the public domain.
For more information, please refer to <https://creativecommons.org/public-domain/cc0/>

SPDX-License-Identifier: Unlicense OR CC0-1.0
*/

#![allow(non_upper_case_globals)]
#![allow(clippy::bool_comparison)]

use std::path::{Path, PathBuf};
use std::fs::create_dir_all as create_dirs;
use std::fs::remove_dir_all as remove_dirs;
use std::sync::{Mutex, LazyLock};

use minstd::MINSTD;

/** filename used for detection in unit tests */
const TEST_FNAME: &str = "normalizefs.txt";

/** shared global random number generator */
static shared_rng: LazyLock<Mutex<MINSTD>> = LazyLock::new(||
{
   let g = minstd::MINSTD::seed(minstd::clamp_seed(stdrandom::fast_i32()));
   Mutex::new(g)
});

/**
    create specified file.

    panics on error.
*/
fn create_file(file: impl AsRef<Path>) {
   use std::fs::OpenOptions;
   OpenOptions::new()
      .read(false)
      .write(true)
      .create(true)
      .truncate(true)
   .open(file).expect("file created");
}

/** check if specified file (not directory) exists by
    opening it.
*/
fn exists(file: impl AsRef<Path>) -> bool {
   use std::fs::OpenOptions;
   OpenOptions::new()
      .read(true)
      .write(false)
      .create(false)
   .open(file).is_ok()
}

/**
   check if `TEST_FNAME` exists in specified directory.
*/
fn exists_in(dir: impl AsRef<Path>) -> bool {
   exists( dir.as_ref().join( TEST_FNAME ) )
}

/** Prepare directory structure before test.

This function creates a nested directory structure in temporary directory.
Creates directories up to the specified `levels` and optionally creates
a file at a specific level (`inlevel`).

# Arguments

* `levels` - The total number of directory levels to create.
* `inlevel` - The specific level at which to create a test file. Levels
  are counted from top. 0 = highest level. To not create any file specify
  value lower than zero.

# Returns

A tuple:
* `PathBuf` - The path to the root directory.
* `PathBuf` - The path to the deepest directory created.

*/
fn prepare_dirs(levels: isize, inlevel: isize) -> (PathBuf,PathBuf) {
   let tmp = std::env::temp_dir();
   let mut g = shared_rng.lock().expect("aquired valid rng mutex");
   /* root directory of test tree */
   let root = tmp.join(format!("walkup{}", g.next()));
   let mut current = { let mut c = PathBuf::new(); c.clone_from(&root); c };

   create_dirs(&root).expect("root created");

   for level in 0..=levels {
      if level > 0 {
         // go 1 level deeper
         current = current.join(format!("dir{}", g.next()));
      }

      if level == inlevel {
         create_dirs(&current).expect("subdir created");
         create_file(current.join(TEST_FNAME));
      }
   }
   create_dirs(&current).expect("lowest lvl subdir created");
   (root, current)
}

//    P R E P A R E   D I R S   -   V A L I D A T I O N

/**
  zero depth empty tree
*/
#[test]
fn prepare_dirs_validation_lvl0_empty() {
   let (top, low) = prepare_dirs(0, -1);
   assert_eq! ( top, low );
   assert! ( exists_in(&top) == false );
   let _ = remove_dirs(top);
}

/**
  zero depth tree with test file
*/
#[test]
fn prepare_dirs_validation_lvl0_with_file() {
   let (top, low) = prepare_dirs(0, 0);
   assert_eq! ( top, low );
   assert! ( exists_in(&top) );
   let _ = remove_dirs(top);
}

/**
   one level deep empty
*/
#[test]
fn prepare_dirs_validation_lvl1_empty() {
   let (top, low) = prepare_dirs(1, -1);
   let _ = remove_dirs(&top);
   assert! ( top != low );
}

/**
   one level deep with file in low
*/
#[test]
fn prepare_dirs_validation_lvl1_with_file_low() {
   let (top, low) = prepare_dirs(1, 1);
   assert! ( top != low );
   assert! ( exists_in(&top) == false );
   assert! ( exists_in(&low) == true );
   let _ = remove_dirs(top);
}

/**
   one level deep with file in high
*/
#[test]
fn prepare_dirs_validation_lvl1_with_file_high() {
   let (top, low) = prepare_dirs(1, 0);
   assert! ( top != low );
   assert! ( exists_in(&top) == true );
   assert! ( exists_in(&low) == false );
   let _ = remove_dirs(top);
}


//    T E S T S    -    M A K E  A B S O L U T E


use super::canonicalize;

#[test]
fn make_absolute_curdir() {
   let (top, low) = prepare_dirs(0, -1);
   let _guard = shared_rng.lock().expect("Using rng as CD lock");
   let saved = std::env::current_dir().unwrap();
   std::env::set_current_dir(&low).unwrap();
   let rc = canonicalize( "." );
   std::env::set_current_dir(&saved).expect("Failed to change curdir back");
   drop(_guard);
   let _ = remove_dirs(top);
   assert! ( rc.is_ok(), "make_absolute failed to read current_dir");
   let rc = rc.unwrap();
   assert_eq! ( low, rc );
   assert! ( rc.is_absolute() );
}

#[test]
fn make_absolute_updir() {
   let (top, low) = prepare_dirs(1, -1);
   let _guard = shared_rng.lock().expect("Using rng as CD lock");
   let saved = std::env::current_dir().unwrap();
   std::env::set_current_dir(&low).unwrap();
   let rc = canonicalize( ".." );
   std::env::set_current_dir(&saved).expect("Failed to change curdir back");
   drop(_guard);
   let _ = remove_dirs(&top);
   assert! ( rc.is_ok(), "make_absolute failed to read current_dir");
   let rc = rc.unwrap();
   assert! ( rc.is_absolute() );
   // Output is not normalized so paths do not have to be equal
   // assert_eq! ( top, rc );
}