Skip to content

Commit 048b925

Browse files
committed
fix: file::MutableMultiValue escapes input values and maintains key separator specific whitespace. (#331)
1 parent c9a2390 commit 048b925

File tree

4 files changed

+152
-113
lines changed

4 files changed

+152
-113
lines changed

git-config/src/file/mutable/mod.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,108 @@
1+
use crate::file::SectionBody;
2+
use crate::parse::Event;
3+
use bstr::{BStr, BString};
4+
use bstr::{ByteSlice, ByteVec};
5+
use std::borrow::Cow;
6+
7+
fn escape_value(value: &BStr) -> BString {
8+
let starts_with_whitespace = value.get(0).map_or(false, |b| b.is_ascii_whitespace());
9+
let ends_with_whitespace = value
10+
.get(value.len().saturating_sub(1))
11+
.map_or(false, |b| b.is_ascii_whitespace());
12+
let contains_comment_indicators = value.find_byteset(b";#").is_some();
13+
let quote = starts_with_whitespace || ends_with_whitespace || contains_comment_indicators;
14+
15+
let mut buf: BString = Vec::with_capacity(value.len()).into();
16+
if quote {
17+
buf.push(b'"');
18+
}
19+
20+
for b in value.iter().copied() {
21+
match b {
22+
b'\n' => buf.push_str("\\n"),
23+
b'\t' => buf.push_str("\\t"),
24+
b'"' => buf.push_str("\\\""),
25+
b'\\' => buf.push_str("\\\\"),
26+
_ => buf.push(b),
27+
}
28+
}
29+
30+
if quote {
31+
buf.push(b'"');
32+
}
33+
buf
34+
}
35+
36+
#[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
37+
struct Whitespace<'a> {
38+
pre_key: Option<Cow<'a, BStr>>,
39+
pre_sep: Option<Cow<'a, BStr>>,
40+
post_sep: Option<Cow<'a, BStr>>,
41+
}
42+
43+
impl Default for Whitespace<'_> {
44+
fn default() -> Self {
45+
Whitespace {
46+
pre_key: Some(b"\t".as_bstr().into()),
47+
pre_sep: Some(b" ".as_bstr().into()),
48+
post_sep: Some(b" ".as_bstr().into()),
49+
}
50+
}
51+
}
52+
53+
impl<'a> Whitespace<'a> {
54+
fn key_value_separators(&self) -> Vec<Event<'a>> {
55+
let mut out = Vec::with_capacity(3);
56+
if let Some(ws) = &self.pre_sep {
57+
out.push(Event::Whitespace(ws.clone()));
58+
}
59+
out.push(Event::KeyValueSeparator);
60+
if let Some(ws) = &self.post_sep {
61+
out.push(Event::Whitespace(ws.clone()));
62+
}
63+
out
64+
}
65+
}
66+
67+
impl<'a> From<&SectionBody<'a>> for Whitespace<'a> {
68+
fn from(s: &SectionBody<'a>) -> Self {
69+
let key_pos =
70+
s.0.iter()
71+
.enumerate()
72+
.find_map(|(idx, e)| matches!(e, Event::SectionKey(_)).then(|| idx));
73+
key_pos
74+
.map(|key_pos| {
75+
let pre_key = s.0[..key_pos].iter().rev().next().and_then(|e| match e {
76+
Event::Whitespace(s) => Some(s.clone()),
77+
_ => None,
78+
});
79+
let from_key = &s.0[key_pos..];
80+
let (pre_sep, post_sep) = from_key
81+
.iter()
82+
.enumerate()
83+
.find_map(|(idx, e)| matches!(e, Event::KeyValueSeparator).then(|| idx))
84+
.map(|sep_pos| {
85+
(
86+
from_key.get(sep_pos - 1).and_then(|e| match e {
87+
Event::Whitespace(ws) => Some(ws.clone()),
88+
_ => None,
89+
}),
90+
from_key.get(sep_pos + 1).and_then(|e| match e {
91+
Event::Whitespace(ws) => Some(ws.clone()),
92+
_ => None,
93+
}),
94+
)
95+
})
96+
.unwrap_or_default();
97+
Whitespace {
98+
pre_key,
99+
pre_sep,
100+
post_sep,
101+
}
102+
})
103+
.unwrap_or_default()
104+
}
105+
}
106+
1107
pub(crate) mod section;
2108
pub(crate) mod value;

git-config/src/file/mutable/section.rs

Lines changed: 5 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use std::{
44
ops::{Deref, Range},
55
};
66

7-
use bstr::{BStr, BString, ByteSlice, ByteVec};
7+
use bstr::{BStr, BString, ByteVec};
88

9+
use crate::file::mutable::{escape_value, Whitespace};
910
use crate::{
1011
file::{Index, Size},
1112
lookup, parse,
@@ -30,7 +31,7 @@ impl<'a, 'event> MutableSection<'a, 'event> {
3031
}
3132

3233
self.section.0.push(Event::SectionKey(key));
33-
self.section.0.extend(self.key_value_separators());
34+
self.section.0.extend(self.whitespace.key_value_separators());
3435
self.section.0.push(Event::Value(escape_value(value.as_ref()).into()));
3536
if self.implicit_newline {
3637
self.section.0.push(Event::Newline(BString::from("\n").into()));
@@ -150,7 +151,7 @@ impl<'a, 'event> MutableSection<'a, 'event> {
150151
// Internal methods that may require exact indices for faster operations.
151152
impl<'a, 'event> MutableSection<'a, 'event> {
152153
pub(crate) fn new(section: &'a mut SectionBody<'event>) -> Self {
153-
let whitespace = compute_whitespace(section);
154+
let whitespace = (&*section).into();
154155
Self {
155156
section,
156157
implicit_newline: true,
@@ -195,7 +196,7 @@ impl<'a, 'event> MutableSection<'a, 'event> {
195196
self.section.0.insert(index.0, Event::Value(escape_value(value).into()));
196197
size += 1;
197198

198-
let sep_events = self.key_value_separators();
199+
let sep_events = self.whitespace.key_value_separators();
199200
size += sep_events.len();
200201
self.section.0.insert_many(index.0, sep_events.into_iter().rev());
201202

@@ -217,102 +218,6 @@ impl<'a, 'event> MutableSection<'a, 'event> {
217218
acc
218219
})
219220
}
220-
221-
fn key_value_separators(&self) -> Vec<Event<'event>> {
222-
let mut out = Vec::with_capacity(3);
223-
if let Some(ws) = &self.whitespace.pre_sep {
224-
out.push(Event::Whitespace(ws.clone()));
225-
}
226-
out.push(Event::KeyValueSeparator);
227-
if let Some(ws) = &self.whitespace.post_sep {
228-
out.push(Event::Whitespace(ws.clone()));
229-
}
230-
out
231-
}
232-
}
233-
234-
#[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
235-
struct Whitespace<'a> {
236-
pre_key: Option<Cow<'a, BStr>>,
237-
pre_sep: Option<Cow<'a, BStr>>,
238-
post_sep: Option<Cow<'a, BStr>>,
239-
}
240-
241-
impl Default for Whitespace<'_> {
242-
fn default() -> Self {
243-
Whitespace {
244-
pre_key: Some(b"\t".as_bstr().into()),
245-
pre_sep: Some(b" ".as_bstr().into()),
246-
post_sep: Some(b" ".as_bstr().into()),
247-
}
248-
}
249-
}
250-
251-
fn compute_whitespace<'a>(s: &mut SectionBody<'a>) -> Whitespace<'a> {
252-
let key_pos =
253-
s.0.iter()
254-
.enumerate()
255-
.find_map(|(idx, e)| matches!(e, Event::SectionKey(_)).then(|| idx));
256-
key_pos
257-
.map(|key_pos| {
258-
let pre_key = s.0[..key_pos].iter().rev().next().and_then(|e| match e {
259-
Event::Whitespace(s) => Some(s.clone()),
260-
_ => None,
261-
});
262-
let from_key = &s.0[key_pos..];
263-
let (pre_sep, post_sep) = from_key
264-
.iter()
265-
.enumerate()
266-
.find_map(|(idx, e)| matches!(e, Event::KeyValueSeparator).then(|| idx))
267-
.map(|sep_pos| {
268-
(
269-
from_key.get(sep_pos - 1).and_then(|e| match e {
270-
Event::Whitespace(ws) => Some(ws.clone()),
271-
_ => None,
272-
}),
273-
from_key.get(sep_pos + 1).and_then(|e| match e {
274-
Event::Whitespace(ws) => Some(ws.clone()),
275-
_ => None,
276-
}),
277-
)
278-
})
279-
.unwrap_or_default();
280-
Whitespace {
281-
pre_key,
282-
pre_sep,
283-
post_sep,
284-
}
285-
})
286-
.unwrap_or_default()
287-
}
288-
289-
fn escape_value(value: &BStr) -> BString {
290-
let starts_with_whitespace = value.get(0).map_or(false, |b| b.is_ascii_whitespace());
291-
let ends_with_whitespace = value
292-
.get(value.len().saturating_sub(1))
293-
.map_or(false, |b| b.is_ascii_whitespace());
294-
let contains_comment_indicators = value.find_byteset(b";#").is_some();
295-
let quote = starts_with_whitespace || ends_with_whitespace || contains_comment_indicators;
296-
297-
let mut buf: BString = Vec::with_capacity(value.len()).into();
298-
if quote {
299-
buf.push(b'"');
300-
}
301-
302-
for b in value.iter().copied() {
303-
match b {
304-
b'\n' => buf.push_str("\\n"),
305-
b'\t' => buf.push_str("\\t"),
306-
b'"' => buf.push_str("\\\""),
307-
b'\\' => buf.push_str("\\\\"),
308-
_ => buf.push(b),
309-
}
310-
}
311-
312-
if quote {
313-
buf.push(b'"');
314-
}
315-
buf
316221
}
317222

318223
impl<'event> Deref for MutableSection<'_, 'event> {

git-config/src/file/mutable/value.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{borrow::Cow, collections::HashMap, ops::DerefMut};
33
use bstr::{BStr, BString, ByteVec};
44

55
use crate::file::mutable::section::{MutableSection, SectionBody};
6+
use crate::file::mutable::{escape_value, Whitespace};
67
use crate::{
78
file::{Index, SectionBodyId, Size},
89
lookup,
@@ -249,12 +250,14 @@ impl<'borrow, 'lookup, 'event> MutableMultiValue<'borrow, 'lookup, 'event> {
249250
value: Cow<'a, BStr>,
250251
) {
251252
let (offset, size) = MutableMultiValue::index_and_size(offsets, section_id, offset_index);
253+
let whitespace: Whitespace<'_> = (&*section).into();
252254
let section = section.as_mut();
253255
section.drain(offset..offset + size);
254256

255-
MutableMultiValue::set_offset(offsets, section_id, offset_index, 3);
256-
section.insert(offset, Event::Value(value));
257-
section.insert(offset, Event::KeyValueSeparator);
257+
let key_sep_events = whitespace.key_value_separators();
258+
MutableMultiValue::set_offset(offsets, section_id, offset_index, 2 + key_sep_events.len());
259+
section.insert(offset, Event::Value(escape_value(value.as_ref()).into()));
260+
section.insert_many(offset, key_sep_events.into_iter().rev());
258261
section.insert(offset, Event::SectionKey(key.to_owned()));
259262
}
260263

git-config/tests/file/mutable/multi_value.rs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,35 @@ mod access {
4646
}
4747

4848
mod set {
49+
use crate::file::cow_str;
4950
use crate::file::mutable::multi_value::init_config;
5051

52+
#[test]
53+
fn values_are_escaped() -> crate::Result {
54+
for value in ["a b", " a b", "a b\t", ";c", "#c", "a\nb\n\tc"] {
55+
let mut config = init_config();
56+
let mut values = config.raw_values_mut("core", None, "a")?;
57+
values.set_values_all(value.into());
58+
59+
let config_str = config.to_string();
60+
let config: git_config::File = config_str.parse()?;
61+
assert_eq!(
62+
config.raw_values("core", None, "a")?,
63+
vec![cow_str(value), cow_str(value), cow_str(value)],
64+
"{config_str:?}"
65+
);
66+
}
67+
Ok(())
68+
}
69+
5170
#[test]
5271
fn single_at_start() -> crate::Result {
5372
let mut config = init_config();
5473
let mut values = config.raw_values_mut("core", None, "a")?;
5574
values.set_string(0, "Hello".into());
5675
assert_eq!(
5776
config.to_string(),
58-
"[core]\n a=Hello\n [core]\n a=d\n a=f"
77+
"[core]\n a = Hello\n [core]\n a =d\n a= f"
5978
);
6079
Ok(())
6180
}
@@ -67,7 +86,7 @@ mod set {
6786
values.set_string(2, "Hello".into());
6887
assert_eq!(
6988
config.to_string(),
70-
"[core]\n a=b\"100\"\n [core]\n a=d\n a=Hello"
89+
"[core]\n a = b\"100\"\n [core]\n a =d\n a= Hello"
7190
);
7291
Ok(())
7392
}
@@ -79,7 +98,7 @@ mod set {
7998
values.set_owned_values_all("Hello");
8099
assert_eq!(
81100
config.to_string(),
82-
"[core]\n a=Hello\n [core]\n a=Hello\n a=Hello"
101+
"[core]\n a = Hello\n [core]\n a= Hello\n a =Hello"
83102
);
84103
Ok(())
85104
}
@@ -89,7 +108,10 @@ mod set {
89108
let mut config = init_config();
90109
let mut values = config.raw_values_mut("core", None, "a")?;
91110
values.set_owned_values_all("");
92-
assert_eq!(config.to_string(), "[core]\n a=\n [core]\n a=\n a=");
111+
assert_eq!(
112+
config.to_string(),
113+
"[core]\n a = \n [core]\n a= \n a ="
114+
);
93115
Ok(())
94116
}
95117
}
@@ -103,12 +125,15 @@ mod delete {
103125
{
104126
let mut values = config.raw_values_mut("core", None, "a")?;
105127
values.delete(0);
106-
assert_eq!(config.to_string(), "[core]\n \n [core]\n a=d\n a=f",);
128+
assert_eq!(
129+
config.to_string(),
130+
"[core]\n \n [core]\n a =d\n a= f",
131+
);
107132
}
108133

109134
let mut values = config.raw_values_mut("core", None, "a")?;
110135
values.delete(1);
111-
assert_eq!(config.to_string(), "[core]\n \n [core]\n a=d\n ",);
136+
assert_eq!(config.to_string(), "[core]\n \n [core]\n a =d\n ");
112137
Ok(())
113138
}
114139

@@ -118,17 +143,17 @@ mod delete {
118143
let mut values = config.raw_values_mut("core", None, "a")?;
119144
values.delete_all();
120145
assert!(values.get().is_err());
121-
assert_eq!(config.to_string(), "[core]\n \n [core]\n \n ",);
146+
assert_eq!(config.to_string(), "[core]\n \n [core]\n \n ");
122147
Ok(())
123148
}
124149
}
125150

126151
fn init_config() -> git_config::File<'static> {
127152
r#"[core]
128-
a=b"100"
153+
a = b"100"
129154
[core]
130-
a=d
131-
a=f"#
155+
a =d
156+
a= f"#
132157
.parse()
133158
.unwrap()
134159
}

0 commit comments

Comments
 (0)