Now let's take a silly one
1use std::collections::HashMap;
2use std::io::{self, Write};
3
4use flate2::{Decompress, FlushDecompress, Status};
5use gix_pack::data::{Entry, entry::Header, header};
6
7use crate::error::{PackError, PackLimit};
8
9const MAX_OBJECTS: u32 = 2_000_000;
10const MAX_OBJECT_BYTES: u64 = 512 * 1024 * 1024;
11const MAX_TOTAL_BYTES: u64 = 4 * 1024 * 1024 * 1024;
12const MAX_DELTA_DEPTH: usize = 50;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct PackLimits {
16 pub max_objects: u32,
17 pub max_object_bytes: u64,
18 pub max_total_bytes: u64,
19 pub max_delta_depth: usize,
20}
21
22impl Default for PackLimits {
23 fn default() -> Self {
24 Self {
25 max_objects: MAX_OBJECTS,
26 max_object_bytes: MAX_OBJECT_BYTES,
27 max_total_bytes: MAX_TOTAL_BYTES,
28 max_delta_depth: MAX_DELTA_DEPTH,
29 }
30 }
31}
32
33pub(crate) fn malformed(message: &str) -> PackError {
34 PackError::Pack(message.to_string())
35}
36
37pub fn meter(pack: &[u8], limits: &PackLimits, kind: gix::hash::Kind) -> Result<(), PackError> {
38 let hash_len = kind.len_in_bytes();
39 if pack.len() < 12 + hash_len {
40 return Err(malformed("packfile is truncated"));
41 }
42 let trailer = pack.len() - hash_len;
43
44 let head: [u8; 12] = pack[..12]
45 .try_into()
46 .map_err(|_| malformed("packfile header is truncated"))?;
47 let (_version, num_objects) =
48 header::decode(&head).map_err(|error| PackError::Pack(error.to_string()))?;
49 if num_objects > limits.max_objects {
50 return Err(PackError::LimitExceeded(PackLimit::Objects));
51 }
52
53 let mut offset = 12u64;
54 let mut total = 0u64;
55 let mut base_of: HashMap<u64, u64> = HashMap::new();
56
57 (0..num_objects).try_for_each(|_| -> Result<(), PackError> {
58 let start = offset;
59 let mut reader: &[u8] = pack
60 .get(start as usize..trailer)
61 .ok_or_else(|| malformed("entry offset past pack end"))?;
62 let entry = Entry::from_read(&mut reader, offset, hash_len)
63 .map_err(|error| PackError::Pack(error.to_string()))?;
64
65 if entry.decompressed_size > limits.max_object_bytes {
66 return Err(PackError::LimitExceeded(PackLimit::ObjectBytes));
67 }
68 total = total
69 .checked_add(entry.decompressed_size)
70 .ok_or_else(|| malformed("decompressed size overflow"))?;
71 if total > limits.max_total_bytes {
72 return Err(PackError::LimitExceeded(PackLimit::TotalBytes));
73 }
74
75 if let Header::OfsDelta { base_distance } = entry.header {
76 let base = entry
77 .checked_base_pack_offset(base_distance)
78 .ok_or_else(|| malformed("ofs-delta base out of range"))?;
79 base_of.insert(start, base);
80 }
81
82 let data_start = entry.data_offset as usize;
83 let consumed = inflate_into(
84 pack.get(data_start..)
85 .ok_or_else(|| malformed("entry body past pack end"))?,
86 entry.decompressed_size,
87 &mut io::sink(),
88 )?;
89 offset = entry
90 .data_offset
91 .checked_add(consumed)
92 .ok_or_else(|| malformed("pack offset overflow"))?;
93 Ok(())
94 })?;
95
96 if offset as usize != trailer {
97 return Err(malformed("pack entries do not fill packfile"));
98 }
99
100 check_depth(&base_of, limits.max_delta_depth)
101}
102
103pub(crate) fn inflate_into(
104 input: &[u8],
105 expected: u64,
106 out: &mut dyn Write,
107) -> Result<u64, PackError> {
108 let mut decompress = Decompress::new(true);
109 let mut scratch = [0u8; 8192];
110 let mut produced = 0u64;
111 loop {
112 let consumed = decompress.total_in() as usize;
113 let out_before = decompress.total_out();
114 let status = decompress
115 .decompress(
116 input.get(consumed..).unwrap_or_default(),
117 &mut scratch,
118 FlushDecompress::None,
119 )
120 .map_err(|error| PackError::Pack(format!("inflate: {error}")))?;
121 let written = (decompress.total_out() - out_before) as usize;
122 produced += written as u64;
123 if produced > expected {
124 return Err(malformed("object inflates beyond its declared size"));
125 }
126 out.write_all(&scratch[..written])
127 .map_err(|error| PackError::Pack(format!("inflate sink: {error}")))?;
128 match status {
129 Status::StreamEnd => break,
130 Status::Ok | Status::BufError => {
131 if decompress.total_in() as usize == consumed
132 && decompress.total_out() == out_before
133 {
134 return Err(malformed("inflate stalled or pack truncated"));
135 }
136 }
137 }
138 }
139 if produced != expected {
140 return Err(malformed("object decompressed size mismatch"));
141 }
142 Ok(decompress.total_in())
143}
144
145pub(crate) fn check_depth(base_of: &HashMap<u64, u64>, max: usize) -> Result<(), PackError> {
146 base_of.keys().try_for_each(|start| {
147 let mut depth = 0usize;
148 let mut cursor = *start;
149 while let Some(&base) = base_of.get(&cursor) {
150 depth += 1;
151 if depth > max {
152 return Err(PackError::LimitExceeded(PackLimit::DeltaDepth));
153 }
154 cursor = base;
155 }
156 Ok(())
157 })
158}