Skip to content

Commit ac2e155

Browse files
committed
add precompose_unicode related tests
* reading precomposes unicode * writing precomposes unicode of input names for good measure
1 parent 7dda00a commit ac2e155

File tree

10 files changed

+232
-9
lines changed

10 files changed

+232
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-ref/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ gix-path = { version = "^0.10.3", path = "../gix-path" }
2525
gix-hash = { version = "^0.14.1", path = "../gix-hash" }
2626
gix-date = { version = "^0.8.3", path = "../gix-date" }
2727
gix-object = { version = "^0.40.1", path = "../gix-object" }
28+
gix-utils = { version = "^0.1.8", path = "../gix-utils" }
2829
gix-validate = { version = "^0.8.3", path = "../gix-validate" }
2930
gix-actor = { version = "^0.29.1", path = "../gix-actor" }
3031
gix-lock = { version = "^12.0.0", path = "../gix-lock" }

gix-ref/src/store/file/find.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub use error::Error;
1010
use crate::{
1111
file,
1212
store_impl::{file::loose, packed},
13-
BStr, BString, FullNameRef, PartialNameRef, Reference,
13+
BStr, BString, FullNameRef, PartialName, PartialNameRef, Reference,
1414
};
1515

1616
impl file::Store {
@@ -66,27 +66,58 @@ impl file::Store {
6666
partial_name: &PartialNameRef,
6767
packed: Option<&packed::Buffer>,
6868
) -> Result<Option<Reference>, Error> {
69+
fn decompose_if(mut r: Reference, input_changed_to_precomposed: bool) -> Reference {
70+
if input_changed_to_precomposed {
71+
use gix_object::bstr::ByteSlice;
72+
let decomposed = r
73+
.name
74+
.0
75+
.to_str()
76+
.ok()
77+
.map(|name| gix_utils::str::decompose(name.into()));
78+
if let Some(Cow::Owned(decomposed)) = decomposed {
79+
r.name.0 = decomposed.into();
80+
}
81+
}
82+
r
83+
}
6984
let mut buf = BString::default();
85+
let mut precomposed_partial_name_storage = packed.filter(|_| self.precompose_unicode).and_then(|_| {
86+
use gix_object::bstr::ByteSlice;
87+
let precomposed = partial_name.0.to_str().ok()?;
88+
let precomposed = gix_utils::str::precompose(precomposed.into());
89+
match precomposed {
90+
Cow::Owned(precomposed) => Some(PartialName(precomposed.into())),
91+
Cow::Borrowed(_) => None,
92+
}
93+
});
94+
let precomposed_partial_name = precomposed_partial_name_storage.as_ref().map(|n| n.as_ref());
7095
for inbetween in &["", "tags", "heads", "remotes"] {
71-
match self.find_inner(inbetween, partial_name, packed, &mut buf) {
72-
Ok(Some(r)) => return Ok(Some(r)),
96+
match self.find_inner(inbetween, partial_name, precomposed_partial_name, packed, &mut buf) {
97+
Ok(Some(r)) => return Ok(Some(decompose_if(r, precomposed_partial_name.is_some()))),
7398
Ok(None) => {
7499
continue;
75100
}
76101
Err(err) => return Err(err),
77102
}
78103
}
79104
if partial_name.as_bstr() != "HEAD" {
105+
if let Some(mut precomposed) = precomposed_partial_name_storage {
106+
precomposed = precomposed.join("HEAD".into()).expect("HEAD is valid name");
107+
precomposed_partial_name_storage = Some(precomposed);
108+
}
80109
self.find_inner(
81110
"remotes",
82111
partial_name
83112
.to_owned()
84113
.join("HEAD".into())
85114
.expect("HEAD is valid name")
86115
.as_ref(),
116+
precomposed_partial_name_storage.as_ref().map(|n| n.as_ref()),
87117
None,
88118
&mut buf,
89119
)
120+
.map(|res| res.map(|r| decompose_if(r, precomposed_partial_name_storage.is_some())))
90121
} else {
91122
Ok(None)
92123
}
@@ -96,10 +127,13 @@ impl file::Store {
96127
&self,
97128
inbetween: &str,
98129
partial_name: &PartialNameRef,
130+
precomposed_partial_name: Option<&PartialNameRef>,
99131
packed: Option<&packed::Buffer>,
100132
path_buf: &mut BString,
101133
) -> Result<Option<Reference>, Error> {
102-
let full_name = partial_name.construct_full_name_ref(inbetween, path_buf);
134+
let full_name = precomposed_partial_name
135+
.unwrap_or(partial_name)
136+
.construct_full_name_ref(inbetween, path_buf);
103137
let content_buf = self.ref_contents(full_name).map_err(|err| Error::ReadFileContents {
104138
source: err,
105139
path: self.reference_path(full_name),

gix-ref/src/store/file/packed.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ impl file::Store {
1616
Ok(packed::Transaction::new_from_pack_and_lock(
1717
self.assure_packed_refs_uptodate()?,
1818
lock,
19+
self.precompose_unicode,
1920
))
2021
}
2122

gix-ref/src/store/file/transaction/mod.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,21 @@ use crate::{
1111
/// How to handle packed refs during a transaction
1212
#[derive(Default)]
1313
pub enum PackedRefs<'a> {
14-
/// Only propagate deletions of references. This is the default
14+
/// Only propagate deletions of references. This is the default.
15+
/// This means deleted references are removed from disk if they are loose and from the packed-refs file if they are present.
1516
#[default]
1617
DeletionsOnly,
17-
/// Propagate deletions as well as updates to references which are peeled, that is contain an object id
18+
/// Propagate deletions as well as updates to references which are peeled and contain an object id.
19+
///
20+
/// This means deleted references are removed from disk if they are loose and from the packed-refs file if they are present,
21+
/// while updates are also written into the loose file as well as into packed-refs, potentially creating an entry.
1822
DeletionsAndNonSymbolicUpdates(Box<dyn gix_object::Find + 'a>),
19-
/// Propagate deletions as well as updates to references which are peeled, that is contain an object id. Furthermore delete the
23+
/// Propagate deletions as well as updates to references which are peeled and contain an object id. Furthermore delete the
2024
/// reference which is originally updated if it exists. If it doesn't, the new value will be written into the packed ref right away.
2125
/// Note that this doesn't affect symbolic references at all, which can't be placed into packed refs.
26+
///
27+
/// Thus, this is similar to `DeletionsAndNonSymbolicUpdates`, but removes the loose reference after the update, leaving only their copy
28+
/// in `packed-refs`.
2229
DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(Box<dyn gix_object::Find + 'a>),
2330
}
2431

gix-ref/src/store/file/transaction/prepare.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ impl<'s, 'p> Transaction<'s, 'p> {
329329
self.store
330330
.assure_packed_refs_uptodate()?
331331
.map(|p| {
332-
buffer_into_transaction(p, packed_refs_lock_fail_mode)
332+
buffer_into_transaction(p, packed_refs_lock_fail_mode, self.store.precompose_unicode)
333333
.map_err(Error::PackedTransactionAcquire)
334334
})
335335
.transpose()?

gix-ref/src/store/packed/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub(crate) struct Transaction {
3838
lock: Option<gix_lock::File>,
3939
#[allow(dead_code)] // It just has to be kept alive, hence no reads
4040
closed_lock: Option<gix_lock::Marker>,
41+
precompose_unicode: bool,
4142
}
4243

4344
/// A reference as parsed from the `packed-refs` file

gix-ref/src/store/packed/transaction.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::borrow::Cow;
12
use std::{fmt::Formatter, io::Write};
23

34
use crate::{
@@ -14,12 +15,14 @@ impl packed::Transaction {
1415
pub(crate) fn new_from_pack_and_lock(
1516
buffer: Option<file::packed::SharedBufferSnapshot>,
1617
lock: gix_lock::File,
18+
precompose_unicode: bool,
1719
) -> Self {
1820
packed::Transaction {
1921
buffer,
2022
edits: None,
2123
lock: Some(lock),
2224
closed_lock: None,
25+
precompose_unicode,
2326
}
2427
}
2528
}
@@ -55,6 +58,26 @@ impl packed::Transaction {
5558
// Remove all edits which are deletions that aren't here in the first place
5659
let mut edits: Vec<Edit> = edits
5760
.into_iter()
61+
.map(|mut edit| {
62+
use gix_object::bstr::ByteSlice;
63+
if self.precompose_unicode {
64+
let precomposed = edit
65+
.name
66+
.0
67+
.to_str()
68+
.ok()
69+
.map(|name| gix_utils::str::precompose(name.into()));
70+
match precomposed {
71+
None | Some(Cow::Borrowed(_)) => edit,
72+
Some(Cow::Owned(precomposed)) => {
73+
edit.name.0 = precomposed.into();
74+
edit
75+
}
76+
}
77+
} else {
78+
edit
79+
}
80+
})
5881
.filter(|edit| {
5982
if let Change::Delete { .. } = edit.change {
6083
buffer.as_ref().map_or(true, |b| b.find(edit.name.as_ref()).is_ok())
@@ -227,13 +250,15 @@ fn write_edit(out: &mut dyn std::io::Write, edit: &Edit, lines_written: &mut i32
227250
pub(crate) fn buffer_into_transaction(
228251
buffer: file::packed::SharedBufferSnapshot,
229252
lock_mode: gix_lock::acquire::Fail,
253+
precompose_unicode: bool,
230254
) -> Result<packed::Transaction, gix_lock::acquire::Error> {
231255
let lock = gix_lock::File::acquire_to_update_resource(&buffer.path, lock_mode, None)?;
232256
Ok(packed::Transaction {
233257
buffer: Some(buffer),
234258
lock: Some(lock),
235259
closed_lock: None,
236260
edits: None,
261+
precompose_unicode,
237262
})
238263
}
239264

gix-ref/tests/file/store/mod.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,157 @@
1+
use crate::file::transaction::prepare_and_commit::{committer, create_at};
2+
use crate::file::EmptyCommit;
3+
use gix_lock::acquire::Fail;
4+
use gix_ref::file::transaction::PackedRefs;
5+
use gix_ref::store::WriteReflog;
6+
use gix_ref::transaction::{Change, LogChange, PreviousValue, RefEdit};
7+
18
mod access;
29
mod find;
310
mod iter;
411
mod reflog;
12+
13+
#[test]
14+
fn precompose_unicode_journey() -> crate::Result {
15+
let tmp = gix_testtools::tempfile::TempDir::new()?;
16+
let precomposed_a = "ä";
17+
let decomposed_a = "a\u{308}";
18+
let root = tmp.path().join(decomposed_a);
19+
std::fs::create_dir(&root)?;
20+
21+
let store_decomposed = gix_ref::file::Store::at(
22+
root,
23+
WriteReflog::Always,
24+
gix_hash::Kind::Sha1,
25+
false, /* precompose_unicode */
26+
);
27+
assert!(!store_decomposed.precompose_unicode);
28+
29+
let decomposed_ref = format!("refs/heads/{decomposed_a}");
30+
store_decomposed
31+
.transaction()
32+
.prepare(Some(create_at(&decomposed_ref)), Fail::Immediately, Fail::Immediately)?
33+
.commit(committer().to_ref())?;
34+
35+
let r = store_decomposed.iter()?.all()?.next().expect("created one ref")?;
36+
assert_eq!(r.name.as_bstr(), decomposed_ref, "no transformation happens by default");
37+
38+
let fs_folds_decomposed = gix_fs::Capabilities::probe(tmp.path()).precompose_unicode;
39+
if !fs_folds_decomposed {
40+
// For file-access to work, we need a filesystem for which precomposed == decomposed.
41+
return Ok(());
42+
}
43+
44+
let store_precomposed = gix_ref::file::Store::at(
45+
tmp.path().join(precomposed_a), // it's important that root paths are also precomposed then.
46+
WriteReflog::Always,
47+
gix_hash::Kind::Sha1,
48+
true, /* precompose_unicode */
49+
);
50+
51+
let precomposed_ref = format!("refs/heads/{precomposed_a}");
52+
let r = store_precomposed.iter()?.all()?.next().expect("created one ref")?;
53+
assert_eq!(
54+
r.name.as_bstr(),
55+
precomposed_ref,
56+
"it transforms all refs it sees to precomposed format, important when sending them over the wire"
57+
);
58+
59+
assert_eq!(
60+
store_precomposed.find(precomposed_a)?.name.as_bstr(),
61+
precomposed_ref,
62+
"can find as precomposed, even though on disk is decomposed it is decomposed"
63+
);
64+
assert_eq!(
65+
store_precomposed.find(decomposed_a)?.name.as_bstr(),
66+
decomposed_ref,
67+
"can find as decomposed, and it keeps it as is to not violate expectations of the returned name being equal to the input (when comparing as bytes)"
68+
);
69+
70+
let decomposed_u = "u\u{308}";
71+
let decomposed_ref = format!("refs/heads/{decomposed_u}");
72+
let edits = store_precomposed
73+
.transaction()
74+
.prepare(Some(create_at(&decomposed_ref)), Fail::Immediately, Fail::Immediately)?
75+
.commit(committer().to_ref())?;
76+
assert_eq!(
77+
edits[0].name.as_bstr(),
78+
decomposed_ref,
79+
"it doesn't alter the composition style to allow input and output to remain unchanged"
80+
);
81+
82+
assert_eq!(
83+
store_decomposed.iter()?.all()?.nth(1).expect("two refs")?.name.shorten(),
84+
decomposed_u,
85+
"the ref name isn't transformed in any way and left decomposed on disk as well, making sure internal loose/packed-ref interactions work reliably"
86+
);
87+
88+
assert!(
89+
store_precomposed.cached_packed_buffer()?.is_none(),
90+
"no packed-refs yet"
91+
);
92+
let edits = store_precomposed
93+
.transaction()
94+
.packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(
95+
Box::new(EmptyCommit),
96+
))
97+
.prepare(
98+
// Intentionally use the decomposed versions of their names
99+
store_decomposed
100+
.loose_iter()?
101+
.filter_map(|r| r.ok().filter(|r| r.kind() == gix_ref::Kind::Peeled))
102+
.map(|r| RefEdit {
103+
change: Change::Update {
104+
log: LogChange::default(),
105+
expected: PreviousValue::MustExistAndMatch(r.target.clone()),
106+
new: r.target,
107+
},
108+
name: r.name,
109+
deref: false,
110+
}),
111+
Fail::Immediately,
112+
Fail::Immediately,
113+
)?
114+
.commit(committer().to_ref())?;
115+
assert!(
116+
store_precomposed.cached_packed_buffer()?.is_some(),
117+
"refs were written into the packed-refs file"
118+
);
119+
120+
assert_eq!(store_precomposed.loose_iter()?.count(), 0, "all loose refs are gone");
121+
assert_eq!(edits.len(), 2);
122+
assert_eq!(
123+
edits[0].name.shorten(),
124+
decomposed_a,
125+
"composition stays the same for consistency"
126+
);
127+
assert_eq!(
128+
edits[1].name.shorten(),
129+
decomposed_u,
130+
"composition stays the same for consistency"
131+
);
132+
133+
assert_eq!(
134+
store_decomposed.find(precomposed_a)?.name.shorten(),
135+
precomposed_a,
136+
"the decomposed store can only find what's in packed-refs verbatim"
137+
);
138+
assert!(
139+
store_decomposed.try_find(decomposed_a)?.is_none(),
140+
"decomposed inputs don't match in packed-refs"
141+
);
142+
assert_eq!(
143+
store_precomposed.find(precomposed_a)?.name.shorten(),
144+
precomposed_a,
145+
"we find what's in the packed-refs file, which is native to packed-refs"
146+
);
147+
assert_eq!(
148+
store_precomposed.find(decomposed_a)?.name.shorten(),
149+
decomposed_a,
150+
"despite the input being decomposed, we find the ref (in packed-refs) as precomposed, but return it just like we inserted it"
151+
);
152+
153+
// TODO: symrefs
154+
// TODO: namespace
155+
156+
Ok(())
157+
}

gix-ref/tests/file/transaction/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub(crate) mod prepare_and_commit {
5454
}
5555
}
5656

57-
fn create_at(name: &str) -> RefEdit {
57+
pub(crate) fn create_at(name: &str) -> RefEdit {
5858
RefEdit {
5959
change: Change::Update {
6060
log: LogChange {

0 commit comments

Comments
 (0)