Skip to main content

git_plumber/tui/widget/pack_idx_details/formatters/
objects.rs

1use crate::git::pack::PackIndex;
2use ratatui::style::{Color, Modifier, Style};
3use ratatui::text::Line;
4
5pub struct ObjectsFormatter<'a> {
6    pack_index: &'a PackIndex,
7}
8
9impl<'a> ObjectsFormatter<'a> {
10    pub fn new(pack_index: &'a PackIndex) -> Self {
11        Self { pack_index }
12    }
13
14    /// Calculate the 1-based byte position in the .idx file for a given object entry
15    ///
16    /// Pack index file structure:
17    /// - Header: 8 bytes (magic + version)
18    /// - Fan-out table: 256 * 4 = 1024 bytes
19    /// - Object names: N * 20 bytes (where N = object count)
20    /// - CRC32 checksums: N * 4 bytes
21    /// - Offsets: N * 4 bytes
22    /// - Large offsets: variable (if any)
23    /// - Pack checksum: 20 bytes
24    /// - Index checksum: 20 bytes
25    fn calculate_object_byte_position(&self, object_index: usize) -> u64 {
26        let header_size = 8u64; // magic (4) + version (4)
27        let fanout_size = 256 * 4u64; // 256 entries * 4 bytes each
28        let object_names_start = header_size + fanout_size;
29
30        // Each object name is 20 bytes
31        // Add 1 to convert from 0-based to 1-based indexing
32        object_names_start + (object_index as u64 * 20) + 1
33    }
34
35    /// Calculate the 1-based byte position in the .idx file for a CRC32 entry
36    fn calculate_crc32_byte_position(&self, object_index: usize) -> u64 {
37        let header_size = 8u64; // magic (4) + version (4)
38        let fanout_size = 256 * 4u64; // 256 entries * 4 bytes each
39        let object_names_size = self.pack_index.object_count() as u64 * 20; // N * 20 bytes
40        let crc32_start = header_size + fanout_size + object_names_size;
41
42        // Each CRC32 is 4 bytes
43        // Add 1 to convert from 0-based to 1-based indexing
44        crc32_start + (object_index as u64 * 4) + 1
45    }
46
47    /// Calculate the 1-based byte position in the .idx file for an offset entry
48    fn calculate_offset_byte_position(&self, object_index: usize) -> u64 {
49        let header_size = 8u64; // magic (4) + version (4)
50        let fanout_size = 256 * 4u64; // 256 entries * 4 bytes each
51        let object_names_size = self.pack_index.object_count() as u64 * 20; // N * 20 bytes
52        let crc32_size = self.pack_index.object_count() as u64 * 4; // N * 4 bytes
53        let offsets_start = header_size + fanout_size + object_names_size + crc32_size;
54
55        // Each offset is 4 bytes
56        // Add 1 to convert from 0-based to 1-based indexing
57        offsets_start + (object_index as u64 * 4) + 1
58    }
59
60    pub fn format_objects_overview(&self, lines: &mut Vec<Line<'static>>) {
61        self.format_object_names_table(lines);
62        self.format_crc32_table(lines);
63        self.format_offsets_table(lines);
64    }
65
66    fn format_object_names_table(&self, lines: &mut Vec<Line<'static>>) {
67        lines.push(Line::styled(
68            "OBJECT NAMES TABLE",
69            Style::default().add_modifier(Modifier::BOLD),
70        ));
71        lines.push(Line::from("─".repeat(25)));
72        lines.push(Line::from(""));
73
74        lines.push(Line::from(
75            "SHA-1 hashes sorted lexicographically (20 bytes each)",
76        ));
77        lines.push(Line::from(format!(
78            "Total objects: {}",
79            self.pack_index.object_count()
80        )));
81        lines.push(Line::from(""));
82
83        if self.pack_index.object_names.is_empty() {
84            lines.push(Line::from("No objects in this index."));
85            lines.push(Line::from(""));
86            return;
87        }
88
89        // Table header
90        lines.push(Line::from(
91            "┌──────┬──────────────────────────────────────────┐",
92        ));
93        lines.push(Line::styled(
94            "│ Byte │ SHA-1 Hash                               │",
95            Style::default().add_modifier(Modifier::BOLD),
96        ));
97        lines.push(Line::from(
98            "├──────┼──────────────────────────────────────────┤",
99        ));
100
101        let entries_to_show = self.determine_sample_indices();
102
103        for (i, show_entry) in entries_to_show.iter().enumerate() {
104            if *show_entry {
105                let hash = hex::encode(self.pack_index.object_names[i]);
106                let byte_pos = self.calculate_object_byte_position(i);
107
108                let line_parts = vec![
109                    ("│ ".to_string(), Style::default()),
110                    (format!("{:4}", byte_pos), Style::default().fg(Color::Cyan)),
111                    (" │ ".to_string(), Style::default()),
112                    (hash, Style::default().fg(Color::Yellow)),
113                    (" │".to_string(), Style::default()),
114                ];
115
116                let mut line = Line::default();
117                for (text, style) in line_parts {
118                    line.spans.push(ratatui::text::Span::styled(text, style));
119                }
120                lines.push(line);
121            } else if i > 0 && entries_to_show[i - 1] {
122                // Show ellipsis after last shown entry
123                let line_parts = vec![
124                    ("│  ".to_string(), Style::default()),
125                    ("...".to_string(), Style::default().fg(Color::Gray)),
126                    (" │ ".to_string(), Style::default()),
127                    (
128                        "...                                     ".to_string(),
129                        Style::default().fg(Color::Gray),
130                    ),
131                    (" │".to_string(), Style::default()),
132                ];
133
134                let mut line = Line::default();
135                for (text, style) in line_parts {
136                    line.spans.push(ratatui::text::Span::styled(text, style));
137                }
138                lines.push(line);
139            }
140        }
141
142        lines.push(Line::from(
143            "└──────┴──────────────────────────────────────────┘",
144        ));
145        lines.push(Line::from(""));
146    }
147
148    fn format_crc32_table(&self, lines: &mut Vec<Line<'static>>) {
149        lines.push(Line::styled(
150            "CRC32 CHECKSUMS TABLE",
151            Style::default().add_modifier(Modifier::BOLD),
152        ));
153        lines.push(Line::from("─".repeat(30)));
154        lines.push(Line::from(""));
155
156        lines.push(Line::from(
157            "CRC32 checksums for integrity verification (4 bytes each)",
158        ));
159        lines.push(Line::from(format!(
160            "Total checksums: {}",
161            self.pack_index.crc32_checksums.len()
162        )));
163        lines.push(Line::from(""));
164
165        if self.pack_index.crc32_checksums.is_empty() {
166            lines.push(Line::from("No CRC32 checksums available."));
167            lines.push(Line::from(""));
168            return;
169        }
170
171        // Table header
172        lines.push(Line::from("┌──────┬─────────────┐"));
173        lines.push(Line::styled(
174            "│ Byte │ Hex Bytes   │",
175            Style::default().add_modifier(Modifier::BOLD),
176        ));
177        lines.push(Line::from("├──────┼─────────────┤"));
178
179        let entries_to_show = self.determine_sample_indices();
180
181        for (i, show_entry) in entries_to_show.iter().enumerate() {
182            if *show_entry {
183                let crc32 = self.pack_index.crc32_checksums[i];
184                let byte_pos = self.calculate_crc32_byte_position(i);
185                let hex_bytes = format!(
186                    "{:02x} {:02x} {:02x} {:02x}",
187                    (crc32 >> 24) & 0xff,
188                    (crc32 >> 16) & 0xff,
189                    (crc32 >> 8) & 0xff,
190                    crc32 & 0xff
191                );
192
193                let line_parts = vec![
194                    ("│ ".to_string(), Style::default()),
195                    (format!("{:4}", byte_pos), Style::default().fg(Color::Cyan)),
196                    (" │ ".to_string(), Style::default()),
197                    (
198                        format!("{:11}", hex_bytes),
199                        Style::default().fg(Color::Blue),
200                    ),
201                    (" │".to_string(), Style::default()),
202                ];
203
204                let mut line = Line::default();
205                for (text, style) in line_parts {
206                    line.spans.push(ratatui::text::Span::styled(text, style));
207                }
208                lines.push(line);
209            } else if i > 0 && entries_to_show[i - 1] {
210                // Show ellipsis after last shown entry
211                let line_parts = vec![
212                    ("│  ".to_string(), Style::default()),
213                    ("...".to_string(), Style::default().fg(Color::Gray)),
214                    (" │       ".to_string(), Style::default()),
215                    ("...".to_string(), Style::default().fg(Color::Gray)),
216                    ("   │".to_string(), Style::default()),
217                ];
218
219                let mut line = Line::default();
220                for (text, style) in line_parts {
221                    line.spans.push(ratatui::text::Span::styled(text, style));
222                }
223                lines.push(line);
224            }
225        }
226
227        lines.push(Line::from("└──────┴─────────────┘"));
228        lines.push(Line::from(""));
229    }
230
231    fn format_offsets_table(&self, lines: &mut Vec<Line<'static>>) {
232        lines.push(Line::styled(
233            "PACK FILE OFFSETS TABLE",
234            Style::default().add_modifier(Modifier::BOLD),
235        ));
236        lines.push(Line::from("─".repeat(30)));
237        lines.push(Line::from(""));
238
239        lines.push(Line::from("Pack file byte positions (4 bytes each)"));
240        lines.push(Line::from(format!(
241            "Total offsets: {}",
242            self.pack_index.offsets.len()
243        )));
244
245        // Show large offset info if present
246        let large_offset_count = self
247            .pack_index
248            .offsets
249            .iter()
250            .filter(|&&offset| offset & 0x80000000 != 0)
251            .count();
252
253        if large_offset_count > 0 {
254            lines.push(Line::styled(
255                format!(
256                    "Large offsets: {} (MSB set, using 8-byte table)",
257                    large_offset_count
258                ),
259                Style::default().fg(Color::Yellow),
260            ));
261        }
262        lines.push(Line::from(""));
263
264        if self.pack_index.offsets.is_empty() {
265            lines.push(Line::from("No offsets available."));
266            lines.push(Line::from(""));
267            return;
268        }
269
270        // Table header
271        lines.push(Line::from("┌──────┬─────────────┬───────────┐"));
272        lines.push(Line::styled(
273            "│ Byte │ Hex Bytes   │ Offset    │",
274            Style::default().add_modifier(Modifier::BOLD),
275        ));
276        lines.push(Line::from("├──────┼─────────────┼───────────┤"));
277
278        let entries_to_show = self.determine_sample_indices();
279
280        for (i, show_entry) in entries_to_show.iter().enumerate() {
281            if *show_entry {
282                let raw_offset = self.pack_index.offsets[i];
283                let actual_offset = self.pack_index.get_object_offset(i);
284                let byte_pos = self.calculate_offset_byte_position(i);
285                let hex_bytes = format!(
286                    "{:02x} {:02x} {:02x} {:02x}",
287                    (raw_offset >> 24) & 0xff,
288                    (raw_offset >> 16) & 0xff,
289                    (raw_offset >> 8) & 0xff,
290                    raw_offset & 0xff
291                );
292
293                let offset_display = if raw_offset & 0x80000000 != 0 {
294                    format!("{:>8} L", actual_offset) // L for Large offset
295                } else {
296                    format!("{:>9}", actual_offset)
297                };
298
299                let line_parts = vec![
300                    ("│ ".to_string(), Style::default()),
301                    (format!("{:4}", byte_pos), Style::default().fg(Color::Cyan)),
302                    (" │ ".to_string(), Style::default()),
303                    (
304                        format!("{:10}", hex_bytes),
305                        Style::default().fg(Color::Blue),
306                    ),
307                    (" │ ".to_string(), Style::default()),
308                    (
309                        offset_display,
310                        if raw_offset & 0x80000000 != 0 {
311                            Style::default().fg(Color::Yellow)
312                        } else {
313                            Style::default().fg(Color::Magenta)
314                        },
315                    ),
316                    (" │".to_string(), Style::default()),
317                ];
318
319                let mut line = Line::default();
320                for (text, style) in line_parts {
321                    line.spans.push(ratatui::text::Span::styled(text, style));
322                }
323                lines.push(line);
324            } else if i > 0 && entries_to_show[i - 1] {
325                // Show ellipsis after last shown entry
326                let line_parts = vec![
327                    ("│  ".to_string(), Style::default()),
328                    ("...".to_string(), Style::default().fg(Color::Gray)),
329                    (" │     ".to_string(), Style::default()),
330                    ("...".to_string(), Style::default().fg(Color::Gray)),
331                    ("     │   ".to_string(), Style::default()),
332                    ("...".to_string(), Style::default().fg(Color::Gray)),
333                    ("     │".to_string(), Style::default()),
334                ];
335
336                let mut line = Line::default();
337                for (text, style) in line_parts {
338                    line.spans.push(ratatui::text::Span::styled(text, style));
339                }
340                lines.push(line);
341            }
342        }
343
344        lines.push(Line::from("└──────┴─────────────┴───────────┘"));
345        lines.push(Line::from(""));
346
347        // Add large offset table info if present
348        if let Some(ref large_offsets) = self.pack_index.large_offsets {
349            self.add_large_offsets_info(lines, large_offsets);
350        }
351    }
352
353    fn add_large_offsets_info(&self, lines: &mut Vec<Line<'static>>, large_offsets: &[u64]) {
354        lines.push(Line::styled(
355            "Large Offset Table (8-byte offsets for pack files > 4GB):",
356            Style::default().add_modifier(Modifier::BOLD),
357        ));
358        lines.push(Line::from(format!("Entries: {}", large_offsets.len())));
359
360        if !large_offsets.is_empty() {
361            let min_large = large_offsets.iter().min().unwrap_or(&0);
362            let max_large = large_offsets.iter().max().unwrap_or(&0);
363            lines.push(Line::from(format!(
364                "Range: {} - {} bytes",
365                min_large, max_large
366            )));
367        }
368        lines.push(Line::from(""));
369    }
370
371    /// Determines which object indices should be shown in tables
372    /// Shows first few, last few, and some from the middle if there are many objects
373    fn determine_sample_indices(&self) -> Vec<bool> {
374        let object_count = self.pack_index.object_count();
375        let mut show_entry = vec![false; object_count];
376
377        if object_count == 0 {
378            return show_entry;
379        }
380
381        // For small collections, show all entries
382        if object_count <= 10 {
383            show_entry.fill(true);
384            return show_entry;
385        }
386
387        // For larger collections, show strategic samples
388        let sample_size = 3; // Show first 3, last 3, and some from middle
389
390        // Always show first few entries
391        for item in show_entry.iter_mut().take(sample_size.min(object_count)) {
392            *item = true;
393        }
394
395        // Always show last few entries
396        let last_start = object_count.saturating_sub(sample_size);
397        for item in show_entry
398            .iter_mut()
399            .skip(last_start)
400            .take(object_count - last_start)
401        {
402            *item = true;
403        }
404
405        // Show some from the middle if we have enough objects
406        if object_count > 20 {
407            let mid_start = (object_count / 2).saturating_sub(1);
408            let mid_end = (mid_start + 2).min(object_count);
409
410            for item in show_entry
411                .iter_mut()
412                .skip(mid_start)
413                .take(mid_end - mid_start)
414            {
415                *item = true;
416            }
417        }
418
419        show_entry
420    }
421}