This repository has no description
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}