dir-path-2-tree · Rust

One task in several forms: recursive, iterative, with an explicit stack and with a depth limit - measured, tested, and shown where recursion overflows the stack.

dir-path-2-tree

A Rust program that reads dot-separated paths from a file and builds a hierarchical tree structure, displaying it with indentation to visualize the hierarchy.

Project Structure

Building the Project

Ensure you have Rust installed. If not, install it from rustup.rs.

To build the project in release mode:

cargo build --release

The compiled binary will be available at target/release/dir-path-2-tree.exe (on Windows) or target/release/dir-path-2-tree (on Unix).

Running the Program

Place your input paths in a file named file.txt in the project directory. Each line should contain dot-separated path components.

Example file.txt:

var.text.bla.bla
usr.bin.bash
usr.bin.c
home.me.and.you
var.you.dont.know
home.me.can.dont

Run the program:

cargo run --release

Output:

home
 me
  and
   you
  can
   dont
usr
 bin
  bash
  c
var
 text
  bla
   bla
 you
  dont
   know

The tree is displayed with each element on its own line, indented by depth level to show the hierarchy. Note that entries are sorted alphabetically by BTreeMap.

Running Unit Tests

Run all unit tests:

cargo test --lib

Run tests with output displayed:

cargo test --lib -- --nocapture

Run a specific test:

cargo test --lib test_insert_single_path

Available Tests

The library includes 7 comprehensive unit tests:

All tests should show:

test result: ok. 7 passed; 0 failed; 0 ignored

Further Tests

Run them all with cargo test.

Code Coverage

Coverage is measured with cargo-llvm-cov, source-based through LLVM:

rustup component add llvm-tools-preview
cargo install cargo-llvm-cov --locked
cargo llvm-cov --html

The report is written to target/llvm-cov/html/index.html; cargo llvm-cov alone prints the summary. The project page shows the coverage of every push to main, file by file and line by line.

What the numbers leave out: src/main.rs runs only with cargo run; the print_ functions, thin wrappers that write to the console, run in doc tests, which cargo-llvm-cov does not count by default; and a child process of the stack overflow demonstration aborts before it can write its profile - the routines it crashes in are covered by the other tests.

Architecture

Tree Structure

The Tree struct wraps a BTreeMap to maintain sorted, hierarchical paths:

pub struct Tree(pub BTreeMap<String, Box<Tree>>);

Key Functions

The same task in several forms, on purpose:

Function Form Call stack per level of the tree
insert_path recursive one frame
insert_path_iterative a loop that walks down with a mutable reference none
insert_path_iterative_with_stack the recursion translated mechanically into an explicit stack; needs unsafe to satisfy the borrow checker none
write_tree / print_tree recursive, pre-order one frame
write_tree_iterative / print_tree_iterative an explicit stack of iterators, same order none
write_tree_safe / print_tree_safe recursive with a depth limit: an error instead of a crash one frame, at most max_depth
Drop for Tree takes the levels out one by one none

The write_ functions write to any std::io::Write - a Vec<u8> in the tests, io::sink() in the benchmarks; the print_ functions write to the console.

The implementation uses Rust's ownership and recursive patterns to elegantly handle nested tree structures without manual memory management.

Recursion, Iteration and the Stack

Recursion is the natural form for a tree, and the right one while the depth is small and known. When the depth comes from outside - a file, a message, a user - every level costs a stack frame, and a deep enough input overflows the stack. Rust cannot catch that: the process aborts. On a desktop the main thread has megabytes of stack; a thread on an embedded target often has a few kilobytes.

tests/stack_overflow.rs shows it with one path of depth 5 000 (tests/data/deep_path.txt, 29 KB) on a thread with 64 KiB of stack. Each crashing case runs in a child process - the test binary started again with only that test - and the test checks that the child died with Rust's message:

$ cargo test --test stack_overflow -- --nocapture --test-threads=1
running 5 tests
test iterative_insert_write_and_drop_need_no_call_stack ... ok
test recursive_drop_overflows_the_stack ... recursive_drop_overflows_the_stack: the child ended with signal: 6 (SIGABRT) (core dumped)
    thread '<unknown>' (438428) has overflowed its stack
    fatal runtime error: stack overflow, aborting
ok
test recursive_insert_overflows_the_stack ... recursive_insert_overflows_the_stack: the child ended with signal: 6 (SIGABRT) (core dumped)
    thread '<unknown>' (438432) has overflowed its stack
    fatal runtime error: stack overflow, aborting
ok
test recursive_write_overflows_the_stack ... recursive_write_overflows_the_stack: the child ended with signal: 6 (SIGABRT) (core dumped)
    thread '<unknown>' (438436) has overflowed its stack
    fatal runtime error: stack overflow, aborting
ok
test the_depth_limit_turns_the_crash_into_an_error ... the_depth_limit_turns_the_crash_into_an_error: Maximum tree depth 30 exceeded (current depth: 31)
ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s
Case Result on 64 KiB What it shows
insert_path, recursive stack overflow one frame per level of the input
write_tree, recursive stack overflow the same when printing
a copy of the tree with the derived Drop stack overflow even a tree built and printed iteratively crashes when it goes out of scope: the derived drop recurses
insert_path_iterative, write_tree_iterative, Drop for Tree passes the depth costs heap, not stack
write_tree_safe with a limit of 30 error Maximum tree depth 30 exceeded the crash becomes an error the caller can handle

The limit has to fit the stack: 30 levels of write_tree_safe fit into 64 KiB in a debug build, 100 levels do not, because unoptimised frames are larger.

Where each form fits:

Benchmarks

benches/variants.rs measures the variants with Criterion on a wide tree (10 000 paths of depth 6) and a deep one (one path of depth 1 000), inserting and writing. Writing goes to io::sink(), so that the traversal is measured and not the console; each insert builds one tree, dropped outside the timing.

cargo bench --bench variants

The report is written to target/criterion/report/index.html.

Benchmark Variant Median of the means Means of the runs Against recursive
insert wide recursive 8.22 ms 8.22 ms, 8.27 ms, 7.95 ms —
insert wide iterative 5.62 ms 6.25 ms, 5.54 ms, 5.62 ms -32 %
insert wide explicit stack 5.85 ms 5.78 ms, 5.85 ms, 5.86 ms -29 %
insert deep recursive 62 µs 60 µs, 62 µs, 62 µs —
insert deep iterative 54 µs 54 µs, 56 µs, 54 µs -12 %
insert deep explicit stack 58 µs 59 µs, 57 µs, 58 µs -6 %
write wide recursive 3.21 ms 3.41 ms, 3.21 ms, 2.88 ms —
write wide iterative 3.30 ms 3.29 ms, 3.30 ms, 3.31 ms +3 %
write wide safe 2.97 ms 3.06 ms, 2.97 ms, 2.85 ms -7 %
write deep recursive 56 µs 56 µs, 57 µs, 55 µs —
write deep iterative 60 µs 60 µs, 61 µs, 59 µs +7 %
write deep safe 58 µs 56 µs, 58 µs, 65 µs +4 %

Measured on an Intel Core i7-7700K under WSL2 (Ubuntu 24.04), rustc 1.98.1, Criterion 0.5: each run pinned to one core (taskset -c 3), 10 s per benchmark, three runs. The table gives the median of the three means, the mean of each run, and the median against the recursive variant.

What the numbers say:

Project Page

The project page: https://ernstgross.github.io/dir-path-2-tree/

.github/workflows/pages.yml builds it on every push to main (tools/build_pages.py): this README, the test results with the stack overflow demonstration of that run, the code coverage with its report, the benchmarks with the Criterion report of that run, and the API documentation (cargo doc). The benchmark numbers above come from a dedicated machine; those of the page come from a shared GitHub runner and are indicative only.

Credits

Written by Ernst Gross. The tests of the stack overflow, the benchmarks, the project page and parts of this README were written with the help of Claude (Anthropic); the commits concerned carry the trailer Assisted-by: Claude (Anthropic).