Skip to content

Commit b463c4f

Browse files
committed
Limit tag namespace names to not have subdirectories
Now that `spfs clean` only discovers and walks tags in tag namespaces defined at the top level, it is unsafe to allow creating tag namespaces in subdirectories. It is assumed that this change will not affect any existing uses of tag namespaces and that a deprecation process is not necessary. Signed-off-by: J Robert Ray <[email protected]>
1 parent 2319ff3 commit b463c4f

File tree

5 files changed

+61
-17
lines changed

5 files changed

+61
-17
lines changed

crates/spfs/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ impl RemoteConfig {
266266
builder.when(tracking::TimeSpec::parse(v)?);
267267
}
268268
"tag_namespace" => {
269-
builder.tag_namespace(TagNamespaceBuf::new(RelativePath::new(&v)));
269+
builder.tag_namespace(TagNamespaceBuf::new(RelativePath::new(&v))?);
270270
}
271271
_ => (),
272272
}

crates/spfs/src/fixtures.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ impl TempRepo {
4242
let mut repo = spfs::storage::fs::FsRepository::open(tempdir.path().join("repo"))
4343
.await
4444
.unwrap();
45-
repo.set_tag_namespace(Some(spfs::storage::TagNamespaceBuf::new(
46-
namespace.as_ref(),
47-
)));
45+
repo.set_tag_namespace(Some(
46+
spfs::storage::TagNamespaceBuf::new(namespace.as_ref())
47+
.expect("tag namespaces used in tests must be valid"),
48+
));
4849
TempRepo::FS(Arc::new(repo.into()), Arc::clone(tempdir))
4950
}
5051
_ => panic!("only TempRepo::FS type supports setting tag namespaces"),

crates/spfs/src/server/tag.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ fn string_to_namespace(namespace: &String) -> Option<&TagNamespace> {
1919
if namespace.is_empty() {
2020
None
2121
} else {
22-
Some(TagNamespace::new(RelativePath::new(namespace)))
22+
Some(
23+
TagNamespace::new(RelativePath::new(namespace))
24+
.expect("namespace was valid before being passed over rpc as a string"),
25+
)
2326
}
2427
}
2528

crates/spfs/src/storage/tag_namespace.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use std::ops::Deref;
77
use relative_path::{RelativePath, RelativePathBuf};
88
use serde::{Deserialize, Serialize};
99

10+
use crate::{Error, Result};
11+
1012
/// A suffix on directory names that indicates that the directory is a tag namespace.
1113
pub const TAG_NAMESPACE_MARKER: &str = "#ns";
1214

@@ -16,7 +18,22 @@ pub const TAG_NAMESPACE_MARKER: &str = "#ns";
1618
pub struct TagNamespace(RelativePath);
1719

1820
impl TagNamespace {
19-
pub fn new(path: &RelativePath) -> &Self {
21+
/// Create a new TagNamespace instance.
22+
///
23+
/// The path provided must only contain a single path component.
24+
pub fn new(path: &RelativePath) -> Result<&Self> {
25+
TagNamespaceBuf::validate_path(path)?;
26+
27+
// Safety: TagNamespace is repr(transparent) over RelativePath
28+
Ok(unsafe { &*(path as *const RelativePath as *const TagNamespace) })
29+
}
30+
31+
/// Create a new TagNamespace instance without validation.
32+
///
33+
/// # Safety
34+
///
35+
/// The path provided must only contain a single path component.
36+
unsafe fn new_unchecked(path: &RelativePath) -> &Self {
2037
// Safety: TagNamespace is repr(transparent) over RelativePath
2138
unsafe { &*(path as *const RelativePath as *const TagNamespace) }
2239
}
@@ -52,8 +69,34 @@ impl TagNamespaceBuf {
5269
}
5370

5471
/// Create a new tag namespace from the given path.
55-
pub fn new<P: AsRef<RelativePath>>(path: P) -> Self {
56-
Self(path.as_ref().to_owned())
72+
pub fn new<P: AsRef<RelativePath>>(path: P) -> Result<Self> {
73+
let path = path.as_ref();
74+
TagNamespaceBuf::validate_path(path)?;
75+
Ok(Self(path.to_owned()))
76+
}
77+
78+
/// Validate that a RelativePath is a valid tag namespace name.
79+
///
80+
/// Tag namespaces may not have any directory structure meaning the
81+
/// directory created for the tag namespace will be created in the top level
82+
/// of the tags directory in the spfs repository.
83+
///
84+
/// When `spfs clean` walks tags to avoid data loss it must walk all tags is
85+
/// all namespaces and with the current implementation only tag namespaces
86+
/// defined in the top level of the tags directory will be discovered.
87+
#[inline]
88+
fn validate_path(path: &RelativePath) -> Result<()> {
89+
let mut it = path.components();
90+
if it.next().is_none() {
91+
return Err(Error::String("tag namespace must not be empty".to_string()));
92+
}
93+
if it.next().is_some() {
94+
return Err(Error::String(
95+
"tag namespace must not contain any directory structure".to_string(),
96+
));
97+
}
98+
99+
Ok(())
57100
}
58101
}
59102

@@ -73,7 +116,8 @@ impl std::ops::Deref for TagNamespaceBuf {
73116
type Target = TagNamespace;
74117

75118
fn deref(&self) -> &Self::Target {
76-
TagNamespace::new(self.0.deref())
119+
// Safety: A TagNamespaceBuf has already been checked for validity.
120+
unsafe { TagNamespace::new_unchecked(self.0.deref()) }
77121
}
78122
}
79123

crates/spfs/src/storage/tag_test.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,15 @@ async fn test_tag_in_namespace_name_collision(
383383
// | none | foo/bar/baz |
384384
// | foo | bar/baz |
385385
// | foo/bar | baz |
386+
//
387+
// However the last one is not currently checked because "foo/bar" has
388+
// become an invalid tag namespace name. Should that change in the future,
389+
// the code to check it was removed in the commit that added this message.
386390

387391
let repo_in_foo = tmprepo.with_tag_namespace("foo").await;
388-
let repo_in_foo_bar = tmprepo.with_tag_namespace("foo/bar").await;
389392

390393
let foo_bar_baz = random_digest();
391394
let bar_baz = random_digest();
392-
let baz = random_digest();
393395

394396
let spec_foo_bar_baz = tracking::TagSpec::parse("foo/bar/baz").unwrap();
395397
tmprepo
@@ -400,14 +402,8 @@ async fn test_tag_in_namespace_name_collision(
400402
let spec_bar_baz = tracking::TagSpec::parse("bar/baz").unwrap();
401403
repo_in_foo.push_tag(&spec_bar_baz, &bar_baz).await.unwrap();
402404

403-
let spec_baz = tracking::TagSpec::parse("baz").unwrap();
404-
repo_in_foo_bar.push_tag(&spec_baz, &baz).await.unwrap();
405-
406405
// Now confirm these can be read back.
407406

408-
let tag = repo_in_foo_bar.resolve_tag(&spec_baz).await.unwrap();
409-
assert_eq!(tag.target, baz);
410-
411407
let tag = repo_in_foo.resolve_tag(&spec_bar_baz).await.unwrap();
412408
assert_eq!(tag.target, bar_baz);
413409

0 commit comments

Comments
 (0)