fix(format): resolve each hard link once
A hard link's target may go through other hard links, and each was
resolved again every time a path went through it. With each link's
target naming the previous link twice (g/s{i} -> /g/s{i-1}/s{i-1}) the
work doubled per link: finish() took 46 s for 26 links in a debug build,
and 60 would never finish. Resolved links are now remembered, so the work
is linear in the links, and a hard link met again while it is being
resolved is reported as a cycle by name. The depth limit (64) still bounds
the recursion through links not yet resolved.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -17,8 +17,8 @@ use std::collections::BTreeMap;
|
||||
use crate::error::FormatError;
|
||||
use crate::type_builders::{AttrValue, DatasetBuilder, GroupBuilder, GroupItem};
|
||||
|
||||
/// Hard links followed while resolving one hard-link target path. Guards
|
||||
/// against hard links whose targets name each other.
|
||||
/// Depth of the chain of unresolved hard links followed while resolving one
|
||||
/// hard-link target path (a bound on recursion; cycles are found exactly).
|
||||
const MAX_LINK_DEPTH: usize = 64;
|
||||
|
||||
fn err(msg: String) -> FormatError {
|
||||
@@ -256,10 +256,22 @@ impl Builder {
|
||||
}
|
||||
|
||||
/// The object a hard link's `target` path names, from group `from`.
|
||||
fn resolve(&self, from: usize, target: &str, depth: usize) -> Result<Obj, FormatError> {
|
||||
///
|
||||
/// Hard links met on the way are resolved once and remembered in
|
||||
/// `memo` (by group and link index), so a target that goes through
|
||||
/// other hard links costs time linear in the links, not exponential; a
|
||||
/// hard link met again while it is being resolved is a cycle.
|
||||
fn resolve(
|
||||
&self,
|
||||
memo: &mut [Vec<Resolution>],
|
||||
from: usize,
|
||||
target: &str,
|
||||
depth: usize,
|
||||
) -> Result<Obj, FormatError> {
|
||||
if depth > MAX_LINK_DEPTH {
|
||||
return Err(err(format!(
|
||||
"hard link target {target:?}: too many hard links to follow (a cycle?)"
|
||||
"hard link target {target:?}: more than {MAX_LINK_DEPTH} hard links \
|
||||
to follow"
|
||||
)));
|
||||
}
|
||||
let (mut cur, rest) = match target.strip_prefix('/') {
|
||||
@@ -291,7 +303,22 @@ impl Builder {
|
||||
obj = match &grp.links[li].1 {
|
||||
Target::Group(child) => Obj::Group(*child),
|
||||
Target::Dataset(d) => Obj::Dataset(*d),
|
||||
Target::Hard(p) => self.resolve(cur, p, depth + 1)?,
|
||||
Target::Hard(p) => match memo[cur][li] {
|
||||
Resolution::Done(o) => o,
|
||||
Resolution::InProgress => {
|
||||
return Err(err(format!(
|
||||
"hard link target {target:?}: the hard link {:?} leads \
|
||||
back to itself (a cycle)",
|
||||
join(&grp.path, c)
|
||||
)));
|
||||
}
|
||||
Resolution::Todo => {
|
||||
memo[cur][li] = Resolution::InProgress;
|
||||
let o = self.resolve(memo, cur, p, depth + 1)?;
|
||||
memo[cur][li] = Resolution::Done(o);
|
||||
o
|
||||
}
|
||||
},
|
||||
Target::Soft(_) | Target::External { .. } => {
|
||||
return Err(err(format!(
|
||||
"hard link target {target:?} goes through a soft or external \
|
||||
@@ -305,6 +332,14 @@ impl Builder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where resolving one hard link has got to.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Resolution {
|
||||
Todo,
|
||||
InProgress,
|
||||
Done(Obj),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Obj {
|
||||
Group(usize),
|
||||
@@ -325,14 +360,27 @@ pub(crate) fn build(root: GroupBuilder, default_track_order: bool) -> Result<Tre
|
||||
let mut group_refs = vec![0u32; b.groups.len()];
|
||||
let mut ds_refs = vec![0u32; b.datasets.len()];
|
||||
group_refs[0] = 1; // the superblock's reference to the root
|
||||
let mut memo: Vec<Vec<Resolution>> = b
|
||||
.groups
|
||||
.iter()
|
||||
.map(|g| vec![Resolution::Todo; g.links.len()])
|
||||
.collect();
|
||||
let mut resolved: Vec<Vec<Option<Obj>>> = Vec::with_capacity(b.groups.len());
|
||||
for (gi, g) in b.groups.iter().enumerate() {
|
||||
let mut row = Vec::with_capacity(g.links.len());
|
||||
for (_, t) in &g.links {
|
||||
for (li, (_, t)) in g.links.iter().enumerate() {
|
||||
let obj = match t {
|
||||
Target::Group(i) => Some(Obj::Group(*i)),
|
||||
Target::Dataset(d) => Some(Obj::Dataset(*d)),
|
||||
Target::Hard(p) => Some(b.resolve(gi, p, 0)?),
|
||||
Target::Hard(p) => Some(match memo[gi][li] {
|
||||
Resolution::Done(o) => o,
|
||||
_ => {
|
||||
memo[gi][li] = Resolution::InProgress;
|
||||
let o = b.resolve(&mut memo, gi, p, 0)?;
|
||||
memo[gi][li] = Resolution::Done(o);
|
||||
o
|
||||
}
|
||||
}),
|
||||
Target::Soft(_) | Target::External { .. } => None,
|
||||
};
|
||||
match obj {
|
||||
|
||||
Reference in New Issue
Block a user