ggstd 0.1.0

Partial implementation of Go standard library
Documentation
// Copyright 2023 The rust-ggstd authors. All rights reserved.
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

use std::fs::{DirBuilder, File};
#[cfg(target_os = "linux")]
use std::os::unix::{fs::DirBuilderExt, prelude::OpenOptionsExt};
use std::path::{Path, PathBuf};

/// TempFile contains information about a created temporary file and
/// will remove the temporary file after the object goes out of scope.
/// This is a convenience structure over [create_temp] results.
pub struct TempFile {
    pub f: std::fs::File,
    pub path: PathBuf,
    removed: bool,
}

impl TempFile {
    /// new creates a new temporary file in the default directory for temporary files.
    /// The new file's name is generated by adding a random string to the end of pattern.
    /// If pattern includes a "*", the random string replaces the last "*" instead.
    /// The temporary file will be removed when the object goes out of scope.
    pub fn new(pattern: &str) -> std::io::Result<Self> {
        let (f, path) = create_temp(None, pattern)?;
        Ok(Self {
            f,
            path,
            removed: false,
        })
    }

    /// new_in_dir is the same as `new`, except that the directory `dir` is used instead
    /// of the system directory for temporary files.
    pub fn new_in_dir(dir: &Path, pattern: &str) -> std::io::Result<Self> {
        let (f, path) = create_temp(Some(dir), pattern)?;
        Ok(Self {
            f,
            path,
            removed: false,
        })
    }

    /// remove removes the file
    pub fn remove(&mut self) -> std::io::Result<()> {
        if !self.removed {
            std::fs::remove_file(&self.path)?;
            self.removed = true;
        }
        Ok(())
    }
}

impl Drop for TempFile {
    fn drop(&mut self) {
        _ = self.remove()
    }
}

/// TempDir contains information about a created temporary directory and
/// will remove the directory recursively after the object goes out of scope.
/// This is a convenience structure over [mkdir_temp] results.
pub struct TempDir {
    pub path: PathBuf,
    removed: bool,
}

impl TempDir {
    /// new creates a new temporary directory in the default directory for temporary files.
    /// The new directory's name is generated by adding a random string to the end of pattern.
    /// If pattern includes a "*", the random string replaces the last "*" instead.
    /// The temporary directory will be removed when the object goes out of scope.
    pub fn new(pattern: &str) -> std::io::Result<Self> {
        let path = mkdir_temp(None, pattern)?;
        Ok(Self {
            path,
            removed: false,
        })
    }

    /// new_in_dir is the same as `new`, except that the directory `dir` is used instead
    /// of the system directory for temporary files.
    pub fn new_in_dir(dir: &Path, pattern: &str) -> std::io::Result<Self> {
        let path = mkdir_temp(Some(dir), pattern)?;
        Ok(Self {
            path,
            removed: false,
        })
    }

    /// remove removes the directory recursively
    pub fn remove(&mut self) -> std::io::Result<()> {
        if !self.removed {
            std::fs::remove_dir_all(&self.path)?;
            self.removed = true;
        }
        Ok(())
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        _ = self.remove()
    }
}

/// create_temp creates a new temporary file in the directory dir,
/// opens the file for reading and writing, and returns [TempFile] structure
/// that contains the opened file and the path to it.
/// The filename is generated by taking pattern and adding a random string to the end.
/// If pattern includes a "*", the random string replaces the last "*".
/// If dir is the empty string, create_temp uses the default directory for temporary files, as returned by temp_dir.
/// Multiple programs or goroutines calling create_temp simultaneously will not choose the same file.
// The caller can use the file's Name method to find the pathname of the file.
/// It is the caller's responsibility to remove the file when it is no longer needed.
/// [TempFile] can be used as a more convenient interface for creating and automatically
/// removing a temporary file.
pub fn create_temp(
    dir: Option<&std::path::Path>,
    pattern: &str,
) -> std::io::Result<(File, PathBuf)> {
    let (prefix, suffix) = prefix_and_suffix(pattern)?;
    // 	if err != nil {
    // 		return nil, &PathError{Op: "createtemp", Path: pattern, Err: err}
    // 	}

    let dir = if let Some(dir) = dir {
        PathBuf::from(dir)
    } else {
        super::temp_dir()
    };

    let mut attempt = 0;
    loop {
        let random = crate::runtime::fastrand();
        let file_name = if let Some(suffix) = suffix {
            format!("{}{}{}", prefix, random, suffix)
        } else {
            format!("{}{}", prefix, random)
        };
        let path = dir.join(file_name);
        let mut options = std::fs::OpenOptions::new();
        options.read(true).write(true).create_new(true);

        #[cfg(target_os = "linux")]
        options.mode(0o600);

        let file = options.open(&path);
        match file {
            Ok(file) => return Ok((file, path)),
            Err(err) => {
                if err.kind() == std::io::ErrorKind::AlreadyExists {
                    attempt += 1;
                    if attempt < 10000 {
                        continue;
                    }
                }
                return Err(err);
            }
        }
    }
}

// var errPatternHasSeparator = errors.New("pattern contains path separator")
const ERR_PATTERN_HAS_SEPARATOR: &str = "pattern contains path separator";

pub fn is_err_pattern_has_separator(err: &std::io::Error) -> bool {
    err.to_string().contains(ERR_PATTERN_HAS_SEPARATOR)
}

/// prefix_and_suffix splits pattern by the last wildcard "*", if applicable,
/// returning prefix as the part before "*" and suffix as the part after "*".
fn prefix_and_suffix(pattern: &str) -> std::io::Result<(&str, Option<&str>)> {
    for c in pattern.chars() {
        if super::is_path_separator(c) {
            return Err(crate::errors::new_stdio_other_error(
                ERR_PATTERN_HAS_SEPARATOR.to_string(),
            ));
        }
    }
    let parts: Vec<&str> = pattern.rsplitn(2, '*').collect();
    if parts.len() == 2 {
        Ok((parts[1], Some(parts[0])))
    } else {
        Ok((pattern, None))
    }
}

/// mkdir_temp creates a new temporary directory in the directory dir
/// and returns the pathname of the new directory.
/// The new directory's name is generated by adding a random string to the end of pattern.
/// If pattern includes a "*", the random string replaces the last "*" instead.
/// If dir is the empty string, mkdir_temp uses the default directory for temporary files, as returned by temp_dir.
/// Multiple programs or goroutines calling mkdir_temp simultaneously will not choose the same directory.
/// It is the caller's responsibility to remove the directory when it is no longer needed.
/// [TempDir] can be used as a more convenient interface for creating and automatically
/// removing a temporary directory.
pub fn mkdir_temp(dir: Option<&std::path::Path>, pattern: &str) -> std::io::Result<PathBuf> {
    let (prefix, suffix) = prefix_and_suffix(pattern)?;
    let dir = if let Some(dir) = dir {
        PathBuf::from(dir)
    } else {
        super::temp_dir()
    };

    let mut attempt = 0;
    loop {
        let random = crate::runtime::fastrand();
        let file_name = if let Some(suffix) = suffix {
            format!("{}{}{}", prefix, random, suffix)
        } else {
            format!("{}{}", prefix, random)
        };
        let path = dir.join(file_name);

        #[cfg(target_os = "windows")]
        let err = DirBuilder::new().create(&path);

        #[cfg(target_os = "linux")]
        let err = DirBuilder::new().mode(0o700).create(&path);

        if let Err(err) = err {
            if err.kind() == std::io::ErrorKind::AlreadyExists {
                attempt += 1;
                if attempt < 10000 {
                    continue;
                }
            }
            return Err(err);
        }
        return Ok(path);
    }
}