node: Use `gix_packetline`

`gix-packetline` already is in the dependency closure of `radicle-node`.
Use the well-tested and -used reader instead of ours.
This commit is contained in:
Lorenz Leutgeb 2026-02-16 02:20:55 +01:00
parent c96aea0699
commit 5fa68ed814
No known key found for this signature in database
5 changed files with 94 additions and 111 deletions

1
Cargo.lock generated
View File

@ -3133,6 +3133,7 @@ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"cyphernet", "cyphernet",
"fastrand", "fastrand",
"gix-packetline",
"lexopt", "lexopt",
"log", "log",
"mio 1.0.4", "mio 1.0.4",

View File

@ -31,6 +31,7 @@ dunce = "1.0.5"
fastrand = { version = "2.0.0", default-features = false } fastrand = { version = "2.0.0", default-features = false }
git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] } git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] }
gix-hash = { version = "0.22.1", default-features = false, features = ["sha1"] } 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" human-panic = "2.0.6"
itertools = "0.14" itertools = "0.14"
lexopt = "0.3.0" lexopt = "0.3.0"

View File

@ -23,6 +23,7 @@ colored = { workspace = true }
crossbeam-channel = { workspace = true } crossbeam-channel = { workspace = true }
cyphernet = { workspace = true, features = ["tor", "dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] } cyphernet = { workspace = true, features = ["tor", "dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] }
fastrand = { workspace = true } fastrand = { workspace = true }
gix-packetline = { workspace = true, features = ["blocking-io"] }
lexopt = { workspace = true } lexopt = { workspace = true }
log = { workspace = true, features = ["kv", "std"] } log = { workspace = true, features = ["kv", "std"] }
mio = { version = "1", features = ["net", "os-poll"] } mio = { version = "1", features = ["net", "os-poll"] }

View File

@ -141,15 +141,51 @@ impl Worker {
let timeout = channels.timeout(); let timeout = channels.timeout();
let (mut stream_r, stream_w) = channels.split(); let (mut stream_r, stream_w) = channels.split();
let header = match upload_pack::pktline::git_request(&mut stream_r) {
Ok(header) => header, let mut iter = gix_packetline::blocking_io::StreamingPeekableIter::new(
Err(e) => { &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 { return FetchResult::Responder {
rid: None, rid: None,
result: Err(UploadError::PacketLine(e)), 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); log::debug!(target: "worker", "Spawning upload-pack process for {} on stream {stream}..", header.repo);
if let Err(e) = self.is_authorized(remote, header.repo) { if let Err(e) = self.is_authorized(remote, header.repo) {

View File

@ -25,7 +25,7 @@ pub fn upload_pack<R, W>(
remote: NodeId, remote: NodeId,
storage: &Storage, storage: &Storage,
emitter: &Emitter<Event>, emitter: &Emitter<Event>,
header: &pktline::GitRequest, header: &GitRequest,
mut recv: R, mut recv: R,
send: W, send: W,
timeout: Duration, timeout: Duration,
@ -239,119 +239,63 @@ where
} }
} }
pub(super) mod pktline { /// The Git request packet-line for a repository.
use std::io; ///
use std::io::Read; /// See <https://git-scm.com/docs/pack-protocol.html#_git_transport>.
use std::str; ///
/// 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>)>,
}
use radicle::prelude::RepoId; impl GitRequest {
pub(super) fn from_packetline(
pub const HEADER_LEN: usize = 4; packet_line: gix_packetline::PacketLineRef<'_>,
) -> Option<GitRequest> {
/// Read and parse the `GitRequest` data from the client side. packet_line.as_slice().and_then(Self::parse)
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> { /// Parse a Git command from a packet-line.
stream: &'a mut R, 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');
impl<'a, R: io::Read> Reader<'a, R> { let path = parts.next()?.to_owned();
/// Create a new packet-line reader. let repo = path.strip_prefix('/')?.parse().ok()?;
pub fn new(stream: &'a mut R) -> Self { let host = match parts.next() {
Self { stream } None | Some("") => None,
} Some(host) => {
let host = host.strip_prefix("host=")?;
/// Parse a Git request packet-line. match host.split_once(':') {
/// None => Some((host.to_owned(), None)),
/// Example: `0032git-upload-pack /project.git\0host=myserver.com\0` Some((host, port)) => {
/// let port = port.parse::<u16>().ok()?;
fn read_request_pktline(&mut self) -> io::Result<(GitRequest, Vec<u8>)> { Some((host.to_owned(), Some(port)))
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 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 {
/// 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)))
}
} }
} }
}; }
let extra = parts };
.skip_while(|part| part.is_empty()) let extra = parts
.map(|part| match part.split_once('=') { .skip_while(|part| part.is_empty())
None => (part.to_owned(), None), .map(|part| match part.split_once('=') {
Some((k, v)) => (k.to_owned(), Some(v.to_owned())), None => (part.to_owned(), None),
}) Some((k, v)) => (k.to_owned(), Some(v.to_owned())),
.collect();
Some(Self {
repo,
path,
host,
extra,
}) })
} .collect();
Some(Self {
repo,
path,
host,
extra,
})
} }
} }