1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub struct ZipEntryLocation {
6 pub file_name: String,
7 pub local_header_offset: u64,
8 pub compressed_size: u64,
9 pub uncompressed_size: u64,
10 #[serde(default)]
11 pub extra_field_len: u64,
12}
13
14impl ZipEntryLocation {
15 pub fn to_http_range_header(&self) -> (String, String) {
17 let end_byte = self.local_header_offset
19 + 30
20 + self.file_name.len() as u64
21 + self.extra_field_len.max(1024)
22 + self.compressed_size;
23 (
24 "Range".to_string(),
25 format!("bytes={}-{}", self.local_header_offset, end_byte),
26 )
27 }
28}
29
30#[derive(Debug, Clone)]
32pub struct ZipHeaderReader;
33
34impl ZipHeaderReader {
35 pub fn find_eocd(bytes: &[u8]) -> Option<usize> {
37 if bytes.len() < 22 {
38 return None;
39 }
40 let eocd_sig = b"PK\x05\x06";
41 let mut idx = bytes.len() - 22;
42 loop {
43 if &bytes[idx..idx + 4] == eocd_sig {
44 return Some(idx);
45 }
46 if idx == 0 {
47 break;
48 }
49 idx -= 1;
50 }
51 None
52 }
53
54 pub fn parse_central_directory(tail_bytes: &[u8]) -> Result<Vec<ZipEntryLocation>, String> {
56 let eocd_idx = Self::find_eocd(tail_bytes).ok_or_else(|| {
57 "EOCD record (PK\\x05\\x06) signature not found in tail bytes".to_string()
58 })?;
59
60 if tail_bytes.len() < eocd_idx + 22 {
61 return Err("Truncated EOCD header".to_string());
62 }
63
64 let entry_count =
65 u16::from_le_bytes([tail_bytes[eocd_idx + 10], tail_bytes[eocd_idx + 11]]) as usize;
66 let cd_size = u32::from_le_bytes([
67 tail_bytes[eocd_idx + 12],
68 tail_bytes[eocd_idx + 13],
69 tail_bytes[eocd_idx + 14],
70 tail_bytes[eocd_idx + 15],
71 ]) as usize;
72
73 let mut entries = Vec::with_capacity(entry_count);
74 let cd_start = eocd_idx.saturating_sub(cd_size);
75
76 let mut pos = cd_start;
77 let cd_sig = b"PK\x01\x02";
78
79 while pos + 46 <= eocd_idx {
80 if &tail_bytes[pos..pos + 4] != cd_sig {
81 pos += 1;
82 continue;
83 }
84
85 let comp_size = u32::from_le_bytes([
86 tail_bytes[pos + 20],
87 tail_bytes[pos + 21],
88 tail_bytes[pos + 22],
89 tail_bytes[pos + 23],
90 ]) as u64;
91
92 let uncomp_size = u32::from_le_bytes([
93 tail_bytes[pos + 24],
94 tail_bytes[pos + 25],
95 tail_bytes[pos + 26],
96 tail_bytes[pos + 27],
97 ]) as u64;
98
99 let name_len =
100 u16::from_le_bytes([tail_bytes[pos + 28], tail_bytes[pos + 29]]) as usize;
101 let extra_len =
102 u16::from_le_bytes([tail_bytes[pos + 30], tail_bytes[pos + 31]]) as usize;
103 let comment_len =
104 u16::from_le_bytes([tail_bytes[pos + 32], tail_bytes[pos + 33]]) as usize;
105
106 let offset = u32::from_le_bytes([
107 tail_bytes[pos + 42],
108 tail_bytes[pos + 43],
109 tail_bytes[pos + 44],
110 tail_bytes[pos + 45],
111 ]) as u64;
112
113 let mut final_comp_size = comp_size;
114 let mut final_uncomp_size = uncomp_size;
115 let mut final_offset = offset;
116
117 if extra_len >= 4 {
118 let extra_start = pos + 46 + name_len;
119 let extra_end = (extra_start + extra_len).min(tail_bytes.len());
120 let mut e_pos = extra_start;
121 while e_pos + 4 <= extra_end {
122 let header_id = u16::from_le_bytes([tail_bytes[e_pos], tail_bytes[e_pos + 1]]);
123 let data_size =
124 u16::from_le_bytes([tail_bytes[e_pos + 2], tail_bytes[e_pos + 3]]) as usize;
125 let data_start = e_pos + 4;
126 let data_end = (data_start + data_size).min(extra_end);
127 if header_id == 0x0001 {
128 let mut d_pos = data_start;
129 if final_uncomp_size == 0xFFFFFFFF && d_pos + 8 <= data_end {
130 final_uncomp_size = u64::from_le_bytes(
131 tail_bytes[d_pos..d_pos + 8].try_into().unwrap_or([0; 8]),
132 );
133 d_pos += 8;
134 }
135 if final_comp_size == 0xFFFFFFFF && d_pos + 8 <= data_end {
136 final_comp_size = u64::from_le_bytes(
137 tail_bytes[d_pos..d_pos + 8].try_into().unwrap_or([0; 8]),
138 );
139 d_pos += 8;
140 }
141 if final_offset == 0xFFFFFFFF && d_pos + 8 <= data_end {
142 final_offset = u64::from_le_bytes(
143 tail_bytes[d_pos..d_pos + 8].try_into().unwrap_or([0; 8]),
144 );
145 }
146 break;
147 }
148 e_pos += 4 + data_size;
149 }
150 }
151
152 let name_start = pos + 46;
153 if name_start + name_len <= tail_bytes.len() {
154 let file_name =
155 String::from_utf8_lossy(&tail_bytes[name_start..name_start + name_len])
156 .to_string();
157 entries.push(ZipEntryLocation {
158 file_name,
159 local_header_offset: final_offset,
160 compressed_size: final_comp_size,
161 uncompressed_size: final_uncomp_size,
162 extra_field_len: extra_len as u64,
163 });
164 }
165
166 pos += 46 + name_len + extra_len + comment_len;
167 }
168
169 Ok(entries)
170 }
171}