gix_protocol/fetch/response/
blocking_io.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use std::io;

use gix_transport::{client, Protocol};

use crate::fetch::{
    response,
    response::{Acknowledgement, ShallowUpdate, WantedRef},
    Response,
};

fn parse_v2_section<'a, T>(
    line: &mut String,
    reader: &mut impl client::ExtendedBufRead<'a>,
    res: &mut Vec<T>,
    parse: impl Fn(&str) -> Result<T, response::Error>,
) -> Result<bool, response::Error> {
    line.clear();
    while reader.readline_str(line)? != 0 {
        res.push(parse(line)?);
        line.clear();
    }
    // End of message, or end of section?
    Ok(if reader.stopped_at() == Some(client::MessageKind::Delimiter) {
        // try reading more sections
        reader.reset(Protocol::V2);
        false
    } else {
        // we are done, there is no pack
        true
    })
}

impl Response {
    /// Parse a response of the given `version` of the protocol from `reader`.
    ///
    /// `client_expects_pack` is only relevant for V1 stateful connections, and if `false`, causes us to stop parsing when seeing `NAK`,
    /// and if `true` we will keep parsing until we get a pack as the client already signalled to the server that it's done.
    /// This way of doing things allows us to exploit knowledge about more recent versions of the protocol, which keeps code easier
    /// and more localized without having to support all the cruft that there is.
    ///
    /// `wants_to_negotiate` should be `false` for clones which is when we don't have sent any haves. The reason for this flag to exist
    /// is to predict how to parse V1 output only, and neither `client_expects_pack` nor `wants_to_negotiate` are relevant for V2.
    /// This ugliness is in place to avoid having to resort to an [an even more complex ugliness](https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/fetch-pack.c#L583-L594)
    /// that `git` has to use to predict how many acks are supposed to be read. We also genuinely hope that this covers it all….
    pub fn from_line_reader<'a>(
        version: Protocol,
        reader: &mut impl client::ExtendedBufRead<'a>,
        client_expects_pack: bool,
        wants_to_negotiate: bool,
    ) -> Result<Response, response::Error> {
        match version {
            Protocol::V0 | Protocol::V1 => {
                let mut line = String::new();
                let mut acks = Vec::<Acknowledgement>::new();
                let mut shallows = Vec::<ShallowUpdate>::new();
                let mut saw_ready = false;
                let has_pack = 'lines: loop {
                    line.clear();
                    let peeked_line = match reader.peek_data_line() {
                        Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
                        // This special case (hang/block forever) deals with a single NAK being a legitimate EOF sometimes
                        // Note that this might block forever in stateful connections as there it's not really clear
                        // if something will be following or not by just looking at the response. Instead you have to know
                        // [a lot](https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/fetch-pack.c#L583-L594)
                        // to deal with this correctly.
                        // For now this is acceptable, as V2 can be used as a workaround, which also is the default.
                        Some(Err(err)) if err.kind() == io::ErrorKind::UnexpectedEof => break 'lines false,
                        Some(Err(err)) => return Err(err.into()),
                        Some(Ok(Err(err))) => return Err(err.into()),
                        None => {
                            // maybe we saw a shallow flush packet, let's reset and retry
                            debug_assert_eq!(
                                reader.stopped_at(),
                                Some(client::MessageKind::Flush),
                                "If this isn't a flush packet, we don't know what's going on"
                            );
                            reader.readline_str(&mut line)?;
                            reader.reset(Protocol::V1);
                            match reader.peek_data_line() {
                                Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
                                Some(Err(err)) => return Err(err.into()),
                                Some(Ok(Err(err))) => return Err(err.into()),
                                None => break 'lines false, // EOF
                            }
                        }
                    };

                    if Response::parse_v1_ack_or_shallow_or_assume_pack(&mut acks, &mut shallows, &peeked_line) {
                        break 'lines true;
                    }
                    assert_ne!(reader.readline_str(&mut line)?, 0, "consuming a peeked line works");
                    // When the server sends ready, we know there is going to be a pack so no need to stop early.
                    saw_ready |= matches!(acks.last(), Some(Acknowledgement::Ready));
                    if let Some(Acknowledgement::Nak) = acks.last().filter(|_| !client_expects_pack || !saw_ready) {
                        if !wants_to_negotiate {
                            continue;
                        }
                        break 'lines false;
                    }
                };
                Ok(Response {
                    acks,
                    shallows,
                    wanted_refs: vec![],
                    has_pack,
                })
            }
            Protocol::V2 => {
                // NOTE: We only read acknowledgements and scrub to the pack file, until we have use for the other features
                let mut line = String::new();
                reader.reset(Protocol::V2);
                let mut acks = Vec::<Acknowledgement>::new();
                let mut shallows = Vec::<ShallowUpdate>::new();
                let mut wanted_refs = Vec::<WantedRef>::new();
                let has_pack = 'section: loop {
                    line.clear();
                    if reader.readline_str(&mut line)? == 0 {
                        return Err(response::Error::Io(io::Error::new(
                            io::ErrorKind::UnexpectedEof,
                            "Could not read message headline",
                        )));
                    };

                    match line.trim_end() {
                        "acknowledgments" => {
                            if parse_v2_section(&mut line, reader, &mut acks, Acknowledgement::from_line)? {
                                break 'section false;
                            }
                        }
                        "shallow-info" => {
                            if parse_v2_section(&mut line, reader, &mut shallows, ShallowUpdate::from_line)? {
                                break 'section false;
                            }
                        }
                        "wanted-refs" => {
                            if parse_v2_section(&mut line, reader, &mut wanted_refs, WantedRef::from_line)? {
                                break 'section false;
                            }
                        }
                        "packfile" => {
                            // what follows is the packfile itself, which can be read with a sideband enabled reader
                            break 'section true;
                        }
                        _ => return Err(response::Error::UnknownSectionHeader { header: line }),
                    }
                };
                Ok(Response {
                    acks,
                    shallows,
                    wanted_refs,
                    has_pack,
                })
            }
        }
    }
}