Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/cspell.dictionaries/jargon.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dequeue
dev
devs
discoverability
dotdot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please ignore it on top of the file
don't need to be global

duplicative
dsync
endianness
Expand Down
67 changes: 61 additions & 6 deletions src/uu/mkdir/src/mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ fn chmod(_path: &Path, _mode: u32) -> UResult<()> {
Ok(())
}

// Return true if the directory at `path` has been created by this call.
// Create a directory at the given path.
// Uses iterative approach instead of recursion to avoid stack overflow with deep nesting.
// `is_parent` argument is not used on windows
#[allow(unused_variables)]
Comment on lines 221 to 222
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment should be moved to create_single_dir as this function no longer contains any Windows-specific code.

Suggested change
// `is_parent` argument is not used on windows
#[allow(unused_variables)]

fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
Expand All @@ -231,15 +232,52 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
return Ok(());
}

// Iterative implementation: collect all directories to create, then create them
// This avoids stack overflow with deeply nested directories
if config.recursive {
match path.parent() {
Some(p) => create_dir(p, true, config)?,
None => {
USimpleError::new(1, translate!("mkdir-error-failed-to-create-tree"));
// Collect all parent directories that need to be created
let mut dirs_to_create = Vec::new();
let mut current = path;

// Walk up the tree collecting non-existent directories
while let Some(parent) = current.parent() {
if parent == Path::new("") {
break;
}
// Only check exists() for paths without ".." to avoid false positives
// ("test_dir/.." may return true even when the logical parent doesn't exist)
// Use as_os_str() to avoid string allocation which can use stack space
let has_dotdot = parent
.as_os_str()
.as_encoded_bytes()
.windows(2)
.any(|w| w == b"..");
Comment on lines +250 to +254
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be clearer to use components():

Suggested change
let has_dotdot = parent
.as_os_str()
.as_encoded_bytes()
.windows(2)
.any(|w| w == b"..");
let has_dotdot = parent.components().any(|c| c.as_os_str() == "..");


if !has_dotdot && parent.exists() {
break;
}
dirs_to_create.push(parent);
current = parent;
}

// Reverse to create from root to leaf
dirs_to_create.reverse();

// Create each parent directory
for dir in dirs_to_create {
create_single_dir(dir, true, config)?;
}
}

// Create the target directory
create_single_dir(path, is_parent, config)
}

// Helper function to create a single directory with appropriate permissions
#[allow(unused_variables)]
fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
let path_exists = path.exists();

match std::fs::create_dir(path) {
Ok(()) => {
if config.verbose {
Expand Down Expand Up @@ -287,7 +325,24 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
Ok(())
}

Err(_) if path.is_dir() => Ok(()),
Err(_) if path.is_dir() => {
// Directory already exists - still need to print verbose message if requested
// in recursive mode, but only for logical directory names, not parent references
// (i.e., paths ending in ".." like "test_dir/.." shouldn't print)
let ends_with_dotdot = path
.components()
.next_back()
.map(|c| matches!(c, std::path::Component::ParentDir))
.unwrap_or(false);
Comment on lines +332 to +336
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can re-arrange things a bit and get rid of the unwrap_or:

Suggested change
let ends_with_dotdot = path
.components()
.next_back()
.map(|c| matches!(c, std::path::Component::ParentDir))
.unwrap_or(false);
let ends_with_dotdot = matches!(
path.components().next_back(),
Some(std::path::Component::ParentDir)
);


if config.verbose && is_parent && config.recursive && !ends_with_dotdot {
println!(
"{}",
translate!("mkdir-verbose-created-directory", "util_name" => uucore::util_name(), "path" => path.quote())
);
}
Ok(())
}
Err(e) => Err(e.into()),
}
}
25 changes: 25 additions & 0 deletions tests/by-util/test_mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,28 @@ fn test_selinux_invalid() {
// invalid context, so, no directory
assert!(!at.dir_exists(dest));
}

#[test]
fn test_mkdir_deep_nesting() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test supposed to fail without the changes made to mkdir.rs?

// Regression test for stack overflow with deeply nested directories.
// The iterative implementation should handle arbitrary depth without stack overflow.
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

// Create a path with 350 levels of nesting
let depth = 350;
let dir_name = "d";
let mut path = String::new();
for i in 0..depth {
if i > 0 {
path.push('/');
}
path.push_str(dir_name);
}
Comment on lines +444 to +450
Copy link
Contributor

@cakebaker cakebaker Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can omit the condition if you use a PathBuf instead of a String:

Suggested change
let mut path = String::new();
for i in 0..depth {
if i > 0 {
path.push('/');
}
path.push_str(dir_name);
}
let mut path = PathBuf::new();
for _ in 0..depth {
path.push(dir_name);
}

Or you could use a functional approach:

    let path = Path::new(dir_name).join(std::iter::repeat(dir_name).take(depth).collect::<PathBuf>());


// This should succeed without stack overflow
scene.ucmd().arg("-p").arg(&path).succeeds();

// Verify the deepest directory exists
assert!(at.dir_exists(&path));
}
Loading