This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-pack / src / archive.rs
4.7 kB 146 lines
1use std::io::{self, Read, Seek, SeekFrom}; 2 3use knot_git::{ArchiveFormat, ArchiveLimit, ArchivePrefix, Repo}; 4use knot_types::Oid; 5 6use crate::error::PackError; 7use crate::pkt; 8 9struct Request { 10 treeish: String, 11 format: ArchiveFormat, 12 prefix: Option<ArchivePrefix>, 13} 14 15pub fn stream( 16 repo: &Repo, 17 request: &[u8], 18 limit: ArchiveLimit, 19 sink: &mut dyn FnMut(&[u8]) -> io::Result<()>, 20) -> Result<(), PackError> { 21 let args = parse_arguments(request)?; 22 match build(repo, &args, limit) { 23 Ok(mut spool) => { 24 let mut head = Vec::new(); 25 pkt::write_data(&mut head, b"ACK\n")?; 26 pkt::write_flush(&mut head)?; 27 emit(sink, &head)?; 28 std::iter::from_fn(|| { 29 let mut chunk = vec![0u8; pkt::MAX_BAND]; 30 match spool.read(&mut chunk) { 31 Ok(0) => None, 32 Ok(read) => { 33 chunk.truncate(read); 34 Some(Ok(chunk)) 35 } 36 Err(error) => Some(Err(error)), 37 } 38 }) 39 .try_for_each(|chunk| -> Result<(), PackError> { 40 let chunk = chunk.map_err(|error| PackError::Pack(error.to_string()))?; 41 let mut framed = Vec::new(); 42 pkt::write_band(&mut framed, &chunk)?; 43 emit(sink, &framed) 44 })?; 45 let mut tail = Vec::new(); 46 pkt::write_flush(&mut tail)?; 47 emit(sink, &tail) 48 } 49 Err(error) => { 50 let mut buf = Vec::new(); 51 pkt::write_data( 52 &mut buf, 53 format!("NACK {}\n", error.to_string().replace('\n', " ")).as_bytes(), 54 )?; 55 pkt::write_flush(&mut buf)?; 56 emit(sink, &buf) 57 } 58 } 59} 60 61fn emit(sink: &mut dyn FnMut(&[u8]) -> io::Result<()>, bytes: &[u8]) -> Result<(), PackError> { 62 sink(bytes).map_err(|error| PackError::Pack(error.to_string())) 63} 64 65fn parse_arguments(request: &[u8]) -> Result<Vec<String>, PackError> { 66 Ok(pkt::data_payloads(request)? 67 .iter() 68 .filter_map(|line| { 69 std::str::from_utf8(line) 70 .ok()? 71 .trim_end_matches('\n') 72 .strip_prefix("argument ") 73 .map(str::to_string) 74 }) 75 .collect()) 76} 77 78fn interpret(args: &[String]) -> Result<Request, PackError> { 79 let format = args 80 .iter() 81 .find_map(|arg| arg.strip_prefix("--format=")) 82 .map(format_from) 83 .unwrap_or(ArchiveFormat::Tar); 84 let prefix = args 85 .iter() 86 .find_map(|arg| arg.strip_prefix("--prefix=")) 87 .map(|raw| { 88 ArchivePrefix::new(raw).map_err(|_| { 89 PackError::Protocol("archive prefix must not escape archive root".to_string()) 90 }) 91 }) 92 .transpose()?; 93 let treeish = args 94 .iter() 95 .find(|arg| !arg.starts_with('-')) 96 .cloned() 97 .ok_or_else(|| PackError::Protocol("archive request has no tree-ish".to_string()))?; 98 Ok(Request { 99 treeish, 100 format, 101 prefix, 102 }) 103} 104 105fn format_from(value: &str) -> ArchiveFormat { 106 match value { 107 "zip" => ArchiveFormat::Zip, 108 "tar.gz" | "tgz" => ArchiveFormat::TarGz, 109 _ => ArchiveFormat::Tar, 110 } 111} 112 113fn build(repo: &Repo, args: &[String], limit: ArchiveLimit) -> Result<std::fs::File, PackError> { 114 let request = interpret(args)?; 115 let id = repo 116 .resolve_revision(&request.treeish) 117 .ok_or_else(|| PackError::Protocol(format!("cannot resolve {}", request.treeish)))?; 118 let commit = ensure_public_commit(repo, id)?; 119 let tree = repo 120 .peel_to_tree(commit) 121 .map_err(|error| PackError::Pack(error.to_string()))?; 122 let mut spool = tempfile::tempfile().map_err(|error| PackError::Pack(error.to_string()))?; 123 repo.write_archive( 124 tree, 125 request.format, 126 request.prefix.as_ref(), 127 limit, 128 &mut spool, 129 ) 130 .map_err(|error| PackError::Pack(error.to_string()))?; 131 spool 132 .seek(SeekFrom::Start(0)) 133 .map_err(|error| PackError::Pack(error.to_string()))?; 134 Ok(spool) 135} 136 137fn ensure_public_commit(repo: &Repo, id: Oid) -> Result<Oid, PackError> { 138 let unreachable = 139 || PackError::Protocol("tree-ish is not reachable from public ref".to_string()); 140 let commit = repo.peel_to_commit(id).map_err(|_| unreachable())?; 141 match repo.reachable_from_public(commit) { 142 Ok(true) => Ok(commit), 143 Ok(false) => Err(unreachable()), 144 Err(error) => Err(PackError::Pack(error.to_string())), 145 } 146}