Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
node: Use `gix_packetline`
Merged lorenz opened 2 months ago

gix-packetline already is in the dependency closure of radicle-node. Use the well-tested and -used reader instead of ours.

5 files changed +95 -112 c96aea06 5fa68ed8
modified Cargo.lock
@@ -3133,6 +3133,7 @@ dependencies = [
 "crossbeam-channel",
 "cyphernet",
 "fastrand",
+
 "gix-packetline",
 "lexopt",
 "log",
 "mio 1.0.4",
modified Cargo.toml
@@ -31,6 +31,7 @@ dunce = "1.0.5"
fastrand = { version = "2.0.0", default-features = false }
git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] }
gix-hash = { version = "0.22.1", default-features = false, features = ["sha1"] }
+
gix-packetline = { version = "0.21.1", default-features = false }
human-panic = "2.0.6"
itertools = "0.14"
lexopt = "0.3.0"
modified crates/radicle-node/Cargo.toml
@@ -23,6 +23,7 @@ colored = { workspace = true }
crossbeam-channel = { workspace = true }
cyphernet = { workspace = true, features = ["tor", "dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] }
fastrand = { workspace = true }
+
gix-packetline = { workspace = true, features = ["blocking-io"] }
lexopt = { workspace = true }
log = { workspace = true, features = ["kv", "std"] }
mio = { version = "1", features = ["net", "os-poll"] }
modified crates/radicle-node/src/worker.rs
@@ -141,15 +141,51 @@ impl Worker {

                let timeout = channels.timeout();
                let (mut stream_r, stream_w) = channels.split();
-
                let header = match upload_pack::pktline::git_request(&mut stream_r) {
-
                    Ok(header) => header,
-
                    Err(e) => {
+

+
                let mut iter = gix_packetline::blocking_io::StreamingPeekableIter::new(
+
                    &mut stream_r,
+
                    &[gix_packetline::PacketLineRef::Flush],
+
                    false, /* packet tracing */
+
                );
+

+
                let header = match iter.read_line() {
+
                    None => {
+
                        return FetchResult::Responder {
+
                            rid: None,
+
                            result: Err(UploadError::PacketLine(std::io::Error::new(
+
                                std::io::ErrorKind::UnexpectedEof,
+
                                "unexpected end of stream while reading upload-pack header",
+
                            ))),
+
                        }
+
                    }
+
                    Some(Err(e)) => {
                        return FetchResult::Responder {
                            rid: None,
                            result: Err(UploadError::PacketLine(e)),
                        }
                    }
+
                    Some(Ok(Err(e))) => {
+
                        return FetchResult::Responder {
+
                            rid: None,
+
                            result: Err(UploadError::PacketLine(std::io::Error::new(
+
                                std::io::ErrorKind::InvalidData,
+
                                format!("invalid upload-pack header: {e}"),
+
                            ))),
+
                        }
+
                    }
+
                    Some(Ok(Ok(header))) => header,
                };
+

+
                let Some(header) = upload_pack::GitRequest::from_packetline(header) else {
+
                    return FetchResult::Responder {
+
                        rid: None,
+
                        result: Err(UploadError::PacketLine(std::io::Error::new(
+
                            std::io::ErrorKind::InvalidData,
+
                            "failed to parse upload-pack header",
+
                        ))),
+
                    };
+
                };
+

                log::debug!(target: "worker", "Spawning upload-pack process for {} on stream {stream}..", header.repo);

                if let Err(e) = self.is_authorized(remote, header.repo) {
modified crates/radicle-node/src/worker/upload_pack.rs
@@ -25,7 +25,7 @@ pub fn upload_pack<R, W>(
    remote: NodeId,
    storage: &Storage,
    emitter: &Emitter<Event>,
-
    header: &pktline::GitRequest,
+
    header: &GitRequest,
    mut recv: R,
    send: W,
    timeout: Duration,
@@ -239,119 +239,63 @@ where
    }
}

-
pub(super) mod pktline {
-
    use std::io;
-
    use std::io::Read;
-
    use std::str;
-

-
    use radicle::prelude::RepoId;
-

-
    pub const HEADER_LEN: usize = 4;
-

-
    /// Read and parse the `GitRequest` data from the client side.
-
    pub fn git_request<R>(reader: &mut R) -> io::Result<GitRequest>
-
    where
-
        R: io::Read,
-
    {
-
        let mut reader = Reader::new(reader);
-
        let (header, _) = reader.read_request_pktline()?;
-
        Ok(header)
-
    }
-

-
    struct Reader<'a, R> {
-
        stream: &'a mut R,
-
    }
-

-
    impl<'a, R: io::Read> Reader<'a, R> {
-
        /// Create a new packet-line reader.
-
        pub fn new(stream: &'a mut R) -> Self {
-
            Self { stream }
-
        }
-

-
        /// Parse a Git request packet-line.
-
        ///
-
        /// Example: `0032git-upload-pack /project.git\0host=myserver.com\0`
-
        ///
-
        fn read_request_pktline(&mut self) -> io::Result<(GitRequest, Vec<u8>)> {
-
            let mut pktline = [0u8; 1024];
-
            let length = self.read_pktline(&mut pktline)?;
-
            let Some(cmd) = GitRequest::parse(&pktline[4..length]) else {
-
                return Err(io::ErrorKind::InvalidInput.into());
-
            };
-
            Ok((cmd, Vec::from(&pktline[..length])))
-
        }
-

-
        /// Parse a Git packet-line.
-
        fn read_pktline(&mut self, buf: &mut [u8]) -> io::Result<usize> {
-
            self.read_exact(&mut buf[..HEADER_LEN])?;
-

-
            let length = str::from_utf8(&buf[..HEADER_LEN])
-
                .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
-
            let length = usize::from_str_radix(length, 16)
-
                .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
-

-
            self.read_exact(&mut buf[HEADER_LEN..length])?;
-

-
            Ok(length)
-
        }
-
    }
-

-
    impl<R: io::Read> io::Read for Reader<'_, R> {
-
        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
-
            self.stream.read(buf)
-
        }
-
    }
+
/// The Git request packet-line for a repository.
+
///
+
/// See <https://git-scm.com/docs/pack-protocol.html#_git_transport>.
+
///
+
/// Example: `0032git-upload-pack /rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git\0host=myserver.com\0`
+
#[derive(Debug)]
+
pub struct GitRequest {
+
    pub repo: RepoId,
+
    #[allow(dead_code)]
+
    pub path: String,
+
    #[allow(dead_code)]
+
    pub host: Option<(String, Option<u16>)>,
+
    pub extra: Vec<(String, Option<String>)>,
+
}

-
    /// The Git request packet-line for a Heartwood repository.
-
    ///
-
    /// Example: `0032git-upload-pack /rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git\0host=myserver.com\0`
-
    #[derive(Debug)]
-
    pub struct GitRequest {
-
        pub repo: RepoId,
-
        #[allow(dead_code)]
-
        pub path: String,
-
        #[allow(dead_code)]
-
        pub host: Option<(String, Option<u16>)>,
-
        pub extra: Vec<(String, Option<String>)>,
+
impl GitRequest {
+
    pub(super) fn from_packetline(
+
        packet_line: gix_packetline::PacketLineRef<'_>,
+
    ) -> Option<GitRequest> {
+
        packet_line.as_slice().and_then(Self::parse)
    }

-
    impl GitRequest {
-
        /// Parse a Git command from a packet-line.
-
        fn parse(input: &[u8]) -> Option<Self> {
-
            let input = str::from_utf8(input).ok()?;
-
            let mut parts = input
-
                .strip_prefix("git-upload-pack ")?
-
                .split_terminator('\0');
-

-
            let path = parts.next()?.to_owned();
-
            let repo = path.strip_prefix('/')?.parse().ok()?;
-
            let host = match parts.next() {
-
                None | Some("") => None,
-
                Some(host) => {
-
                    let host = host.strip_prefix("host=")?;
-
                    match host.split_once(':') {
-
                        None => Some((host.to_owned(), None)),
-
                        Some((host, port)) => {
-
                            let port = port.parse::<u16>().ok()?;
-
                            Some((host.to_owned(), Some(port)))
-
                        }
+
    /// Parse a Git command from a packet-line.
+
    fn parse(input: &[u8]) -> Option<Self> {
+
        let input = std::str::from_utf8(input).ok()?;
+
        let mut parts = input
+
            .strip_prefix("git-upload-pack ")?
+
            .split_terminator('\0');
+

+
        let path = parts.next()?.to_owned();
+
        let repo = path.strip_prefix('/')?.parse().ok()?;
+
        let host = match parts.next() {
+
            None | Some("") => None,
+
            Some(host) => {
+
                let host = host.strip_prefix("host=")?;
+
                match host.split_once(':') {
+
                    None => Some((host.to_owned(), None)),
+
                    Some((host, port)) => {
+
                        let port = port.parse::<u16>().ok()?;
+
                        Some((host.to_owned(), Some(port)))
                    }
                }
-
            };
-
            let extra = parts
-
                .skip_while(|part| part.is_empty())
-
                .map(|part| match part.split_once('=') {
-
                    None => (part.to_owned(), None),
-
                    Some((k, v)) => (k.to_owned(), Some(v.to_owned())),
-
                })
-
                .collect();
-

-
            Some(Self {
-
                repo,
-
                path,
-
                host,
-
                extra,
+
            }
+
        };
+
        let extra = parts
+
            .skip_while(|part| part.is_empty())
+
            .map(|part| match part.split_once('=') {
+
                None => (part.to_owned(), None),
+
                Some((k, v)) => (k.to_owned(), Some(v.to_owned())),
            })
-
        }
+
            .collect();
+

+
        Some(Self {
+
            repo,
+
            path,
+
            host,
+
            extra,
+
        })
    }
}