#[derive(Clone, Debug)] pub struct RefUpdate { pub name: String, pub old_oid: String, pub new_oid: String, } impl RefUpdate { pub fn parse_ref_updates(data: &[u8]) -> Result, String> { let mut refs = Vec::new(); for payload in parse_pkt_line_payloads(data)? { let line = String::from_utf8_lossy(payload); let line = line.trim_end_matches(['\r', '\n']); if line.is_empty() { continue; } let mut parts = line.splitn(3, ' '); let old_oid = parts.next().unwrap_or_default(); let new_oid = parts.next().unwrap_or_default(); let raw_name = parts.next().unwrap_or_default(); let name = raw_name .split_once('\0') .map(|(name, _)| name) .unwrap_or(raw_name) .trim(); if old_oid.len() != 40 || new_oid.len() != 40 || name.is_empty() { continue; } refs.push(RefUpdate { old_oid: old_oid.to_string(), new_oid: new_oid.to_string(), name: name.to_string(), }); } Ok(refs) } } fn parse_pkt_line_payloads(data: &[u8]) -> Result, String> { let mut payloads = Vec::new(); let mut offset = 0; while offset + 4 <= data.len() { let header = std::str::from_utf8(&data[offset..offset + 4]) .map_err(|_| "invalid pkt-line header encoding".to_string())?; let len = usize::from_str_radix(header, 16) .map_err(|_| format!("invalid pkt-line length: {header}"))?; offset += 4; match len { 0 => break, 1..=3 => return Err(format!("invalid pkt-line length: {len}")), _ => { let payload_len = len - 4; if offset + payload_len > data.len() { return Err("truncated pkt-line payload".to_string()); } payloads.push(&data[offset..offset + payload_len]); offset += payload_len; } } } Ok(payloads) } #[cfg(test)] mod tests { use super::RefUpdate; fn pkt(payload: &str) -> Vec { let len = payload.len() + 4; let mut out = format!("{len:04x}").into_bytes(); out.extend_from_slice(payload.as_bytes()); out } #[test] fn parses_receive_pack_ref_with_capabilities() { let mut data = pkt( "0000000000000000000000000000000000000000 1111111111111111111111111111111111111111 refs/heads/feature\0 report-status\n", ); data.extend_from_slice(b"0000"); let refs = RefUpdate::parse_ref_updates(&data).unwrap(); assert_eq!(refs.len(), 1); assert_eq!(refs[0].old_oid, "0000000000000000000000000000000000000000"); assert_eq!(refs[0].new_oid, "1111111111111111111111111111111111111111"); assert_eq!(refs[0].name, "refs/heads/feature"); } #[test] fn parses_receive_pack_ref_without_pack_payload() { let mut data = pkt( "2222222222222222222222222222222222222222 0000000000000000000000000000000000000000 refs/heads/old\n", ); data.extend_from_slice(b"0000"); let refs = RefUpdate::parse_ref_updates(&data).unwrap(); assert_eq!(refs.len(), 1); assert_eq!(refs[0].name, "refs/heads/old"); assert_eq!(refs[0].new_oid, "0000000000000000000000000000000000000000"); } }