This repository has no description
0

Configure Feed

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

core / knot2 / crates / knot-git / src / archive.rs
3.4 kB 119 lines
1use std::sync::atomic::AtomicBool; 2 3use gix::bstr::BString; 4use knot_types::{Oid, ParseError}; 5 6use crate::error::{GitError, backend}; 7use crate::repo::Repo; 8 9#[derive(Debug, Clone, Copy, PartialEq, Eq)] 10pub enum ArchiveFormat { 11 Tar, 12 TarGz, 13 Zip, 14} 15 16impl ArchiveFormat { 17 fn gix(self) -> gix_archive::Format { 18 match self { 19 ArchiveFormat::Tar => gix_archive::Format::Tar, 20 ArchiveFormat::TarGz => gix_archive::Format::TarGz { 21 compression_level: None, 22 }, 23 ArchiveFormat::Zip => gix_archive::Format::Zip { 24 compression_level: None, 25 }, 26 } 27 } 28} 29 30#[derive(Debug, Clone, PartialEq, Eq)] 31pub struct ArchivePrefix(String); 32 33impl ArchivePrefix { 34 pub fn new(value: impl Into<String>) -> Result<Self, ParseError> { 35 let value = value.into(); 36 let safe = !value.contains('\0') 37 && !value.starts_with(['/', '\\']) 38 && value.split(['/', '\\']).all(|component| component != ".."); 39 match safe { 40 true => Ok(Self(value)), 41 false => Err(ParseError::Invalid { 42 kind: "archive prefix", 43 value, 44 }), 45 } 46 } 47 48 pub fn as_str(&self) -> &str { 49 &self.0 50 } 51} 52 53impl Repo { 54 pub fn peel_to_tree(&self, oid: Oid) -> Result<Oid, GitError> { 55 self.git() 56 .find_object(oid.object_id()) 57 .map_err(backend)? 58 .peel_to_tree() 59 .map(|tree| Oid::from(tree.id)) 60 .map_err(backend) 61 } 62 63 pub fn write_archive( 64 &self, 65 tree: Oid, 66 format: ArchiveFormat, 67 prefix: Option<&ArchivePrefix>, 68 mut out: impl std::io::Write + std::io::Seek, 69 ) -> Result<(), GitError> { 70 let (stream, _index) = self 71 .git() 72 .worktree_stream(tree.object_id()) 73 .map_err(backend)?; 74 let interrupt = AtomicBool::new(false); 75 self.git() 76 .worktree_archive( 77 stream, 78 &mut out, 79 gix::progress::Discard, 80 &interrupt, 81 gix_archive::Options { 82 format: format.gix(), 83 tree_prefix: prefix.map(|prefix| BString::from(prefix.as_str())), 84 modification_time: 0, 85 }, 86 ) 87 .map_err(backend) 88 } 89} 90 91#[cfg(test)] 92mod tests { 93 use super::ArchivePrefix; 94 95 #[test] 96 fn a_plain_nested_prefix_is_accepted() { 97 assert!(ArchivePrefix::new("squid-main").is_ok()); 98 assert!(ArchivePrefix::new("nested/path").is_ok()); 99 } 100 101 #[test] 102 fn traversal_is_rejected_across_both_separators() { 103 assert!(ArchivePrefix::new("../escape").is_err()); 104 assert!(ArchivePrefix::new("nested/../escape").is_err()); 105 assert!(ArchivePrefix::new("..\\escape").is_err()); 106 assert!(ArchivePrefix::new("nested\\..\\escape").is_err()); 107 assert!( 108 ArchivePrefix::new("dotted-..-name/").is_ok(), 109 "a component that merely contains dot-dot is not a traversal" 110 ); 111 } 112 113 #[test] 114 fn absolute_and_null_bearing_prefixes_are_rejected() { 115 assert!(ArchivePrefix::new("/etc").is_err()); 116 assert!(ArchivePrefix::new("\\windows").is_err()); 117 assert!(ArchivePrefix::new("good\0bad").is_err()); 118 } 119}