Skip to content

Commit

Permalink
Move Unix-specific features to Unix module (#3)
Browse files Browse the repository at this point in the history
- Extract POSIX features to POSIX module
- Encapsulate Unix CString and PathBuf conversions
- Improve error propagation of TemporaryDirectory
  • Loading branch information
LibertyNJ authored Mar 16, 2024
1 parent 15bdaf3 commit 2d0544f
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 31 deletions.
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
//!
//! A robot built on Raspberry Pi.
pub mod temporary_directory;
#[cfg(unix)]
pub mod unix;
5 changes: 5 additions & 0 deletions src/unix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Features available on Unix-like operating systems.
mod convert;
mod posix;
pub mod temporary_directory;
57 changes: 57 additions & 0 deletions src/unix/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Conversions that are only supported on Unix-like operating systems.
use std::ffi::{CString, NulError, OsString};
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf;

/// Converts a [`CString`] into a [`PathBuf`].
pub fn c_string_to_path_buf(c_string: CString) -> PathBuf {
PathBuf::from(OsString::from_vec(c_string.into_bytes()))
}

/// Converts a [`PathBuf`] into a [`CString`].
///
/// # Errors
///
/// This function will return an error if `path_buf` contains a nul byte.
pub fn path_buf_to_c_string(path_buf: PathBuf) -> Result<CString, NulError> {
CString::new(path_buf.into_os_string().into_vec())
}

#[cfg(test)]
mod tests {
use super::*;

mod c_string_to_path_buf {
use super::*;

#[test]
fn it_should_return_a_path_buf_representation_of_c_string() {
let c_string = CString::new("/foo").expect("should not contain any nul bytes");
assert!(c_string_to_path_buf(c_string) == PathBuf::from("/foo"));
}
}

mod path_buf_to_c_string {
use super::*;

#[test]
fn it_should_return_ok_when_path_buf_does_not_contain_a_null_byte() {
let path_buf = PathBuf::from("/foo");
assert!(path_buf_to_c_string(path_buf).is_ok());
}

#[test]
fn it_should_return_err_when_path_buf_contains_a_null_byte() {
let path_buf = PathBuf::from("/f\0o");
assert!(path_buf_to_c_string(path_buf).is_err());
}

#[test]
fn it_should_return_a_c_string_representation_of_path_buf() {
let path_buf = PathBuf::from("/foo");
assert!(path_buf_to_c_string(path_buf).is_ok_and(|c_string| c_string
== CString::new("/foo").expect("should not contain any nul bytes")));
}
}
}
91 changes: 91 additions & 0 deletions src/unix/posix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Features that are dependent on system conformance to POSIX standards.
use std::ffi::{c_char, CString, NulError};
use std::path::PathBuf;
use std::{env, io};

use super::convert;

extern "C" {
/// Securely creates a directory with a unique name derived from `template`.
///
/// `template` must be a string representing the desired path ending with at
/// least six trailing "X" characters. Six or more of the trailing "X"
/// characters will be modified in place to create a unique name for the
/// temporary directory. The directory is created with read, write, and execute
/// permissions for the user only.
///
/// Returns a pointer to `template` on success, or a null pointer on failure and
/// sets `errno` to indicate the error.
fn mkdtemp(template: *mut c_char) -> *mut c_char;
}

/// Securely creates a uniquely-named temporary directory.
///
/// The path to the underlying temporary directory is based on the system’s
/// temporary directory path composed with a random string.
///
/// # Errors
///
/// This function will return an error if it fails to create a temporary
/// directory.
pub fn create_temp_dir() -> Result<PathBuf, io::Error> {
let template = get_temp_dir_template()?.into_raw();
let result = unsafe { mkdtemp(template) };
let error = io::Error::last_os_error();
let path = unsafe { CString::from_raw(template) };

if result.is_null() {
Err(error)
} else {
Ok(convert::c_string_to_path_buf(path))
}
}

/// Returns a template for use with `mkdtemp`.
///
/// # Errors
///
/// This function will return an error if the system’s temporary directory path
/// contains a nul byte.
fn get_temp_dir_template() -> Result<CString, NulError> {
let mut template = env::temp_dir();
template.push("XXXXXX");
convert::path_buf_to_c_string(template)
}

#[cfg(test)]
mod tests {
use super::*;

mod create_temp_dir {
use std::fs;

use super::*;

#[test]
fn it_should_return_a_path_that_begins_with_the_system_temporary_directory() {
let temp_dir = create_temp_dir().expect("`create_temp_dir()` should succeed");
assert!(temp_dir.starts_with(env::temp_dir()));
let _ = fs::remove_dir_all(temp_dir);
}

#[test]
fn it_should_return_a_path_to_an_accessible_directory() {
let temp_dir = create_temp_dir().expect("`create_temp_dir()` should succeed");
assert!(temp_dir.is_dir());
let _ = fs::remove_dir_all(temp_dir);
}

#[test]
fn it_should_return_a_unique_path_for_each_call() {
let temp_dir_a = create_temp_dir().expect("`create_temp_dir()` should succeed");
let temp_dir_b = create_temp_dir().expect("`create_temp_dir()` should succeed");
assert_ne!(temp_dir_a, temp_dir_b);

for temp_dir in &[temp_dir_a, temp_dir_b] {
let _ = fs::remove_dir_all(temp_dir);
}
}
}
}
45 changes: 15 additions & 30 deletions src/temporary_directory.rs → src/unix/temporary_directory.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
//! Abstractions to make managing temporary directories easier.
use std::ffi::{c_char, CString, OsString};
#[cfg(unix)]
use std::os::unix::ffi::OsStringExt;
use std::fs;
use std::io::Error;
use std::path::{Path, PathBuf};
use std::{env, error, fs, io};

extern "C" {
fn mkdtemp(template: *mut c_char) -> *mut c_char;
}
use super::posix;

/// A secure, uniquely-named temporary directory.
///
Expand All @@ -18,7 +14,7 @@ extern "C" {
/// # Examples
///
/// ```
/// use otter_pi::temporary_directory::TemporaryDirectory;
/// use otter_pi::unix::temporary_directory::TemporaryDirectory;
///
/// let path = {
/// let temp_dir = TemporaryDirectory::new().unwrap();
Expand All @@ -36,40 +32,27 @@ pub struct TemporaryDirectory {
impl TemporaryDirectory {
/// Securely creates a uniquely-named temporary directory.
///
/// The path to the underlying temporary directory is based on the system's
/// The path to the underlying temporary directory is based on the systems
/// temporary directory path composed with a random string.
///
/// # Examples
///
/// ```
/// use std::env;
///
/// use otter_pi::temporary_directory::TemporaryDirectory;
/// use otter_pi::unix::temporary_directory::TemporaryDirectory;
///
/// let temp_dir = TemporaryDirectory::new().unwrap();
/// assert!(temp_dir.get_path().starts_with(env::temp_dir()));
/// ```
///
/// # Errors
///
/// This function will return an error if the internal path template derived
/// from the system temporary directory path contains a nul byte, or if it fails
/// to create the underlying temporary directory for any reason.
#[cfg(unix)]
pub fn new() -> Result<Self, Box<dyn error::Error>> {
let mut template = env::temp_dir();
template.push("XXXXXX");
let template = CString::new(template.into_os_string().into_vec())?;
let template = template.into_raw();
let ptr = unsafe { mkdtemp(template) };
let error = io::Error::last_os_error();
let path = unsafe { CString::from_raw(template) };

if ptr.is_null() {
Err(error)?
} else {
let path = PathBuf::from(OsString::from_vec(path.into_bytes()));
Ok(Self { path })
}
/// This function will return an error if it fails to create a temporary
/// directory.
pub fn new() -> Result<Self, Error> {
let path = posix::create_temp_dir()?;
Ok(Self { path })
}

/// Returns the path to the underlying temporary directory.
Expand All @@ -82,7 +65,7 @@ impl TemporaryDirectory {
/// ```
/// use std::fs;
///
/// use otter_pi::temporary_directory::TemporaryDirectory;
/// use otter_pi::unix::temporary_directory::TemporaryDirectory;
///
/// let temp_dir = TemporaryDirectory::new().unwrap();
/// let file_path = temp_dir.get_path().join("foo");
Expand All @@ -103,6 +86,8 @@ impl Drop for TemporaryDirectory {

#[cfg(test)]
mod tests {
use std::env;

use super::*;

#[test]
Expand Down

0 comments on commit 2d0544f

Please sign in to comment.