Skip to content

Commit 2980753

Browse files
committed
mkdir: implement all review feedback
- Fix critical "." component handling bug in recursive directory creation - Optimize path processing to avoid O(C²) complexity - Implement matches! macro pattern for idiomatic Rust - Convert test to use PathBuf instead of String - Move Windows-specific comment to correct function location - Add comprehensive test coverage for "." and ".." path components - Remove redundant test comments per code review feedback
1 parent 365711a commit 2980753

File tree

2 files changed

+113
-20
lines changed

2 files changed

+113
-20
lines changed

src/uu/mkdir/src/mkdir.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use clap::builder::ValueParser;
99
use clap::parser::ValuesRef;
1010
use clap::{Arg, ArgAction, ArgMatches, Command};
11+
use std::collections::VecDeque;
1112
use std::ffi::OsString;
1213
use std::path::{Path, PathBuf};
1314
#[cfg(not(windows))]
@@ -218,7 +219,6 @@ fn chmod(_path: &Path, _mode: u32) -> UResult<()> {
218219

219220
// Create a directory at the given path.
220221
// Uses iterative approach instead of recursion to avoid stack overflow with deep nesting.
221-
// `is_parent` argument is not used on windows
222222
#[allow(unused_variables)]
223223
fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
224224
let path_exists = path.exists();
@@ -235,24 +235,22 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
235235
// Iterative implementation: collect all directories to create, then create them
236236
// This avoids stack overflow with deeply nested directories
237237
if config.recursive {
238-
// Collect all parent directories that need to be created
239-
let mut dirs_to_create = Vec::new();
238+
// Collect all parent directories to process
239+
let mut dirs_to_process = VecDeque::new();
240240
let mut current = path;
241241

242-
// Walk up the tree collecting non-existent directories
242+
// Walk up the tree collecting all parent directories
243243
while let Some(parent) = current.parent() {
244-
if parent == Path::new("") || parent.exists() {
244+
if parent == Path::new("") {
245245
break;
246246
}
247-
dirs_to_create.push(parent);
247+
248+
dirs_to_process.push_front(parent);
248249
current = parent;
249250
}
250251

251-
// Reverse to create from root to leaf
252-
dirs_to_create.reverse();
253-
254-
// Create each parent directory
255-
for dir in dirs_to_create {
252+
// Process each parent directory (already in correct order)
253+
for dir in dirs_to_process {
256254
create_single_dir(dir, true, config)?;
257255
}
258256
}
@@ -262,6 +260,7 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
262260
}
263261

264262
// Helper function to create a single directory with appropriate permissions
263+
// `is_parent` argument is not used on windows
265264
#[allow(unused_variables)]
266265
fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
267266
let path_exists = path.exists();
@@ -313,7 +312,24 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<(
313312
Ok(())
314313
}
315314

316-
Err(_) if path.is_dir() => Ok(()),
315+
Err(_) if path.is_dir() => {
316+
// Directory already exists - check if this is a logical directory creation
317+
// (i.e., not just a parent reference like "test_dir/..")
318+
let ends_with_dotdot = matches!(
319+
path.components().next_back(),
320+
Some(std::path::Component::ParentDir)
321+
);
322+
323+
// Print verbose message for logical directories, even if they exist
324+
// This matches GNU behavior for paths like "test_dir/../test_dir_a"
325+
if config.verbose && is_parent && config.recursive && !ends_with_dotdot {
326+
println!(
327+
"{}",
328+
translate!("mkdir-verbose-created-directory", "util_name" => uucore::util_name(), "path" => path.quote())
329+
);
330+
}
331+
Ok(())
332+
}
317333
Err(e) => Err(e.into()),
318334
}
319335
}

tests/by-util/test_mkdir.rs

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -441,17 +441,94 @@ fn test_mkdir_deep_nesting() {
441441
// Create a path with 350 levels of nesting
442442
let depth = 350;
443443
let dir_name = "d";
444-
let mut path = String::new();
445-
for i in 0..depth {
446-
if i > 0 {
447-
path.push('/');
448-
}
449-
path.push_str(dir_name);
444+
let mut path = std::path::PathBuf::new();
445+
for _ in 0..depth {
446+
path.push(dir_name);
450447
}
451448

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

455-
// Verify the deepest directory exists
456451
assert!(at.dir_exists(&path));
457452
}
453+
454+
#[test]
455+
fn test_mkdir_dot_components() {
456+
// Test handling of "." (current directory) components
457+
// This addresses the critical bug identified in asder8215's review
458+
let scene = TestScenario::new(util_name!());
459+
let at = &scene.fixtures;
460+
461+
// Test case from review comment: test/././test2/././test3/././test4
462+
// GNU mkdir normalizes this path and creates test/test2/test3/test4
463+
let path = "test/././test2/././test3/././test4";
464+
scene.ucmd().arg("-p").arg(path).succeeds();
465+
466+
// Verify expected structure exists (GNU compatibility)
467+
assert!(at.dir_exists("test/test2/test3/test4"));
468+
469+
// Test leading "." - should create test_dot/test_dot2
470+
scene
471+
.ucmd()
472+
.arg("-p")
473+
.arg("./test_dot/test_dot2")
474+
.succeeds();
475+
assert!(at.dir_exists("test_dot/test_dot2"));
476+
477+
// Test mixed "." and normal components
478+
scene
479+
.ucmd()
480+
.arg("-p")
481+
.arg("mixed/./normal/./path")
482+
.succeeds();
483+
assert!(at.dir_exists("mixed/normal/path"));
484+
485+
// Test that the command works without creating redundant directories
486+
// The key test is that it doesn't fail or create incorrect structure
487+
// The actual filesystem behavior (whether literal "." dirs exist)
488+
// may vary, but the logical result should be correct
489+
}
490+
491+
#[test]
492+
fn test_mkdir_dotdot_components() {
493+
// Test handling of ".." (parent directory) components
494+
let scene = TestScenario::new(util_name!());
495+
let at = &scene.fixtures;
496+
497+
at.mkdir("base");
498+
at.mkdir("base/child");
499+
500+
scene
501+
.ucmd()
502+
.arg("-p")
503+
.arg("base/child/../sibling")
504+
.succeeds();
505+
assert!(at.dir_exists("base/sibling"));
506+
507+
scene
508+
.ucmd()
509+
.arg("-p")
510+
.arg("base/child/../../other")
511+
.succeeds();
512+
assert!(at.dir_exists("other"));
513+
514+
scene
515+
.ucmd()
516+
.arg("-p")
517+
.arg("newbase/newchild/../newsibling")
518+
.succeeds();
519+
assert!(at.dir_exists("newbase/newsibling"));
520+
}
521+
522+
#[test]
523+
fn test_mkdir_mixed_special_components() {
524+
// Test complex paths with both "." and ".." components
525+
let scene = TestScenario::new(util_name!());
526+
let at = &scene.fixtures;
527+
528+
scene
529+
.ucmd()
530+
.arg("-p")
531+
.arg("./start/./middle/../end/./final")
532+
.succeeds();
533+
assert!(at.dir_exists("start/end/final"));
534+
}

0 commit comments

Comments
 (0)