Skip to main content

ebook_rs/
stream_zip.rs

1use serde::{Deserialize, Serialize};
2
3/// Location index of a file entry inside a ZIP archive.
4#[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    /// Generate HTTP Range header for fetching this entry over HTTP.
16    pub fn to_http_range_header(&self) -> (String, String) {
17        // Range header spanning local header offset + entry payload
18        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/// Remote ZIP Central Directory Header Streamer.
31#[derive(Debug, Clone)]
32pub struct ZipHeaderReader;
33
34impl ZipHeaderReader {
35    /// Scan tail byte slice (last ~64KB of ZIP archive) for End of Central Directory (EOCD) signature `PK\x05\x06`.
36    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    /// Parse End of Central Directory record and Central Directory entries from tail bytes.
55    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}