Skip to content

Commit aa2d064

Browse files
committed
fix!: parse 'handshake' of file:// based protocol version 0.
This special protocol kicks in when `git` serves `file://` directly and no version number is specified. Then it doesn't advertise capabilities at all, but shows 0000 right away. Make sure we can parse it, and show it by adding `Version::V0` as well.
1 parent 8c72a23 commit aa2d064

File tree

6 files changed

+120
-77
lines changed

6 files changed

+120
-77
lines changed

gix-transport/src/client/async_io/bufread_ext.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ impl<'a, T: AsyncRead + Unpin> ExtendedBufRead for gix_packetline::read::WithSid
125125
}
126126
fn reset(&mut self, version: Protocol) {
127127
match version {
128-
Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]),
128+
Protocol::V0 | Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]),
129129
Protocol::V2 => self.reset_with(&[
130130
gix_packetline::PacketLineRef::Delimiter,
131131
gix_packetline::PacketLineRef::Flush,

gix-transport/src/client/blocking_io/bufread_ext.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ impl<'a, T: io::Read> ExtendedBufRead for gix_packetline::read::WithSidebands<'a
113113
}
114114
fn reset(&mut self, version: Protocol) {
115115
match version {
116-
Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]),
116+
Protocol::V0 | Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]),
117117
Protocol::V2 => self.reset_with(&[
118118
gix_packetline::PacketLineRef::Delimiter,
119119
gix_packetline::PacketLineRef::Flush,

gix-transport/src/client/capabilities.rs

Lines changed: 101 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,29 @@ pub enum Error {
2323
}
2424

2525
/// A structure to represent multiple [capabilities][Capability] or features supported by the server.
26-
#[derive(Debug, Clone, Default)]
26+
///
27+
/// ### Deviation
28+
///
29+
/// As a *shortcoming*, we are unable to parse `V1` as emitted from `git-upload-pack` without a `git-daemon` or server,
30+
/// as it will not emit any capabilities for some reason. Only `V2` and `V0` work in that context.
31+
#[derive(Debug, Clone)]
2732
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2833
pub struct Capabilities {
2934
data: BString,
3035
value_sep: u8,
3136
}
3237

38+
/// This implementation yields exactly those minimal capabilities that are required for `gix` to work, nothing more and nothing less.
39+
///
40+
/// This is a bit of a hack just get tests with Protocol V0 to work, which is a good way to enforce stateful transports.
41+
/// Of course, V1 would also do that but when calling `git-upload-pack` directly, it advertises so badly that this is easier to implement.
42+
impl Default for Capabilities {
43+
fn default() -> Self {
44+
Capabilities::from_lines("version 2\nmulti_ack_detailed\nside-band-64k\n".into())
45+
.expect("valid format, known at compile time")
46+
}
47+
}
48+
3349
/// The name of a single capability.
3450
pub struct Capability<'a>(&'a BStr);
3551

@@ -185,44 +201,50 @@ pub mod recv {
185201
// format looks like, thus there is no binary blob that could ever look like an ERR line by accident.
186202
rd.fail_on_err_lines(true);
187203

188-
let line = rd
189-
.peek_line()
190-
.ok_or(client::Error::ExpectedLine("capabilities or version"))???;
191-
let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?;
192-
193-
let version = Capabilities::extract_protocol(line)?;
194-
match version {
195-
Protocol::V1 => {
196-
let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
197-
rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
198-
Ok(Outcome {
199-
capabilities,
200-
refs: Some(Box::new(rd.as_read())),
201-
protocol: Protocol::V1,
202-
})
203-
}
204-
Protocol::V2 => Ok(Outcome {
205-
capabilities: {
206-
let mut rd = rd.as_read();
207-
let mut buf = Vec::new();
208-
while let Some(line) = rd.read_data_line() {
209-
let line = line??;
210-
match line.as_bstr() {
211-
Some(line) => {
212-
buf.push_str(line);
213-
if buf.last() != Some(&b'\n') {
214-
buf.push(b'\n');
215-
}
216-
}
217-
None => break,
204+
Ok(match rd.peek_line() {
205+
Some(line) => {
206+
let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?;
207+
let version = Capabilities::extract_protocol(line)?;
208+
match version {
209+
Protocol::V0 => unreachable!("already handled in `None` case"),
210+
Protocol::V1 => {
211+
let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
212+
rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
213+
Outcome {
214+
capabilities,
215+
refs: Some(Box::new(rd.as_read())),
216+
protocol: Protocol::V1,
218217
}
219218
}
220-
Capabilities::from_lines(buf.into())?
221-
},
222-
refs: None,
223-
protocol: Protocol::V2,
224-
}),
225-
}
219+
Protocol::V2 => Outcome {
220+
capabilities: {
221+
let mut rd = rd.as_read();
222+
let mut buf = Vec::new();
223+
while let Some(line) = rd.read_data_line() {
224+
let line = line??;
225+
match line.as_bstr() {
226+
Some(line) => {
227+
buf.push_str(line);
228+
if buf.last() != Some(&b'\n') {
229+
buf.push(b'\n');
230+
}
231+
}
232+
None => break,
233+
}
234+
}
235+
Capabilities::from_lines(buf.into())?
236+
},
237+
refs: None,
238+
protocol: Protocol::V2,
239+
},
240+
}
241+
}
242+
None => Outcome {
243+
capabilities: Capabilities::default(),
244+
refs: Some(Box::new(rd.as_read())),
245+
protocol: Protocol::V0,
246+
},
247+
})
226248
}
227249
}
228250
}
@@ -263,45 +285,50 @@ pub mod recv {
263285
// format looks like, thus there is no binary blob that could ever look like an ERR line by accident.
264286
rd.fail_on_err_lines(true);
265287

266-
let line = rd
267-
.peek_line()
268-
.await
269-
.ok_or(client::Error::ExpectedLine("capabilities or version"))???;
270-
let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?;
271-
272-
let version = Capabilities::extract_protocol(line)?;
273-
match version {
274-
Protocol::V1 => {
275-
let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
276-
rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
277-
Ok(Outcome {
278-
capabilities,
279-
refs: Some(Box::new(rd.as_read())),
280-
protocol: Protocol::V1,
281-
})
282-
}
283-
Protocol::V2 => Ok(Outcome {
284-
capabilities: {
285-
let mut rd = rd.as_read();
286-
let mut buf = Vec::new();
287-
while let Some(line) = rd.read_data_line().await {
288-
let line = line??;
289-
match line.as_bstr() {
290-
Some(line) => {
291-
buf.push_str(line);
292-
if buf.last() != Some(&b'\n') {
293-
buf.push(b'\n');
294-
}
295-
}
296-
None => break,
288+
Ok(match rd.peek_line().await {
289+
Some(line) => {
290+
let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?;
291+
let version = Capabilities::extract_protocol(line)?;
292+
match version {
293+
Protocol::V0 => unreachable!("already handled in `None` case"),
294+
Protocol::V1 => {
295+
let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
296+
rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
297+
Outcome {
298+
capabilities,
299+
refs: Some(Box::new(rd.as_read())),
300+
protocol: Protocol::V1,
297301
}
298302
}
299-
Capabilities::from_lines(buf.into())?
300-
},
301-
refs: None,
302-
protocol: Protocol::V2,
303-
}),
304-
}
303+
Protocol::V2 => Outcome {
304+
capabilities: {
305+
let mut rd = rd.as_read();
306+
let mut buf = Vec::new();
307+
while let Some(line) = rd.read_data_line().await {
308+
let line = line??;
309+
match line.as_bstr() {
310+
Some(line) => {
311+
buf.push_str(line);
312+
if buf.last() != Some(&b'\n') {
313+
buf.push(b'\n');
314+
}
315+
}
316+
None => break,
317+
}
318+
}
319+
Capabilities::from_lines(buf.into())?
320+
},
321+
refs: None,
322+
protocol: Protocol::V2,
323+
},
324+
}
325+
}
326+
None => Outcome {
327+
capabilities: Capabilities::default(),
328+
refs: Some(Box::new(rd.as_read())),
329+
protocol: Protocol::V0,
330+
},
331+
})
305332
}
306333
}
307334
}

gix-transport/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub use gix_packetline as packetline;
2323
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2424
#[allow(missing_docs)]
2525
pub enum Protocol {
26+
/// Version 0 is like V1, but doesn't show capabilities at all, at least when hosted without `git-daemon`.
27+
V0 = 0,
2628
/// Version 1 was the first one conceived, is stateful, and our implementation was seen to cause deadlocks. Prefer V2
2729
V1 = 1,
2830
/// A command-based and stateless protocol with clear semantics, and the one to use assuming the server isn't very old.

gix-transport/tests/client/capabilities.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,18 @@ fn from_bytes() -> crate::Result {
4949
);
5050
Ok(())
5151
}
52+
53+
#[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
54+
async fn from_lines_with_version_detection_v0() -> crate::Result {
55+
let mut buf = Vec::<u8>::new();
56+
gix_packetline::encode::flush_to_write(&mut buf).await?;
57+
let mut stream =
58+
gix_packetline::StreamingPeekableIter::new(buf.as_slice(), &[gix_packetline::PacketLineRef::Flush]);
59+
let caps = Capabilities::from_lines_with_version_detection(&mut stream)
60+
.await
61+
.expect("we can parse V0 as very special case, useful for testing stateful connections in other crates")
62+
.capabilities;
63+
assert!(caps.contains("multi_ack_detailed"));
64+
assert!(caps.contains("side-band-64k"));
65+
Ok(())
66+
}

gix-transport/tests/client/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#[cfg(feature = "blocking-client")]
22
mod blocking_io;
3-
#[cfg(not(feature = "http-client-curl"))]
43
mod capabilities;
54
mod git;

0 commit comments

Comments
 (0)