Skip to main content

git_internal/internal/pack/
decode.rs

1//! Streaming pack decoder that validates headers, inflates entries, rebuilds deltas (including zstd),
2//! and populates caches/metadata for downstream consumers.
3
4use std::{
5    io::{self, BufRead, Cursor, ErrorKind, Read},
6    path::PathBuf,
7    sync::{
8        Arc,
9        atomic::{AtomicUsize, Ordering},
10    },
11    thread::{self, JoinHandle},
12    time::Instant,
13};
14
15use axum::Error;
16use bytes::Bytes;
17use flate2::bufread::ZlibDecoder;
18use futures_util::{Stream, StreamExt};
19use threadpool::ThreadPool;
20use tokio::sync::mpsc::UnboundedSender;
21use uuid::Uuid;
22
23use crate::{
24    errors::GitError,
25    hash::{ObjectHash, get_hash_kind, set_hash_kind},
26    internal::{
27        metadata::{EntryMeta, MetaAttached},
28        object::types::ObjectType,
29        pack::{
30            DEFAULT_TMP_DIR, Pack,
31            cache::{_Cache, Caches},
32            cache_object::{CacheObject, CacheObjectInfo, MemSizeRecorder},
33            channel_reader::StreamBufReader,
34            entry::Entry,
35            utils,
36            waitlist::Waitlist,
37            wrapper::Wrapper,
38        },
39    },
40    utils::CountingReader,
41    zstdelta,
42};
43
44/// A reader that counts bytes read and computes CRC32 checksum.
45/// which is used to verify the integrity of decompressed data.
46struct CrcCountingReader<'a, R> {
47    inner: R,
48    bytes_read: u64,
49    crc: &'a mut crc32fast::Hasher,
50}
51impl<R: Read> Read for CrcCountingReader<'_, R> {
52    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
53        let n = self.inner.read(buf)?;
54        self.bytes_read += n as u64;
55        self.crc.update(&buf[..n]);
56        Ok(n)
57    }
58}
59impl<R: BufRead> BufRead for CrcCountingReader<'_, R> {
60    fn fill_buf(&mut self) -> io::Result<&[u8]> {
61        self.inner.fill_buf()
62    }
63    fn consume(&mut self, amt: usize) {
64        let buf = self.inner.fill_buf().unwrap_or(&[]);
65        self.crc.update(&buf[..amt.min(buf.len())]);
66        self.bytes_read += amt as u64;
67        self.inner.consume(amt);
68    }
69}
70
71/// For the convenience of passing parameters
72struct SharedParams {
73    pub pool: Arc<ThreadPool>,
74    pub waitlist: Arc<Waitlist>,
75    pub caches: Arc<Caches>,
76    pub cache_objs_mem_size: Arc<AtomicUsize>,
77    pub callback: Arc<dyn Fn(MetaAttached<Entry, EntryMeta>) + Sync + Send>,
78}
79
80impl Drop for Pack {
81    fn drop(&mut self) {
82        if self.clean_tmp {
83            self.abort_decode();
84            if let Err(e) = self.caches.remove_tmp_dir() {
85                tracing::warn!(error = %e, "failed to remove pack decode temp directory");
86            }
87        }
88    }
89}
90
91impl Pack {
92    fn abort_decode(&self) {
93        self.pool.join();
94        self.caches.shutdown();
95    }
96
97    /// # Parameters
98    /// - `thread_num`: The number of threads to use for decoding and cache, `None` mean use the number of logical CPUs.
99    ///   It can't be zero, or panic <br>
100    /// - `mem_limit`: The maximum size of the memory cache in bytes, or None for unlimited.
101    ///   The 80% of it will be used for [Caches]  <br>
102    ///   ​**Not very accurate, because of memory alignment and other reasons, overuse about 15%** <br>
103    /// - `temp_path`: The path to a directory for temporary files, default is "./.cache_temp" <br>
104    ///   For example, thread_num = 4 will use up to 8 threads (4 for decoding and 4 for cache) <br>
105    /// - `clean_tmp`: whether to remove temp directory when Pack is dropped
106    pub fn new(
107        thread_num: Option<usize>,
108        mem_limit: Option<usize>,
109        temp_path: Option<PathBuf>,
110        clean_tmp: bool,
111    ) -> Self {
112        let mut temp_path = temp_path.unwrap_or(PathBuf::from(DEFAULT_TMP_DIR));
113        // add 8 random characters as subdirectory, check if the directory exists
114        loop {
115            let sub_dir = Uuid::new_v4().to_string()[..8].to_string();
116            temp_path.push(sub_dir);
117            if !temp_path.exists() {
118                break;
119            }
120            temp_path.pop();
121        }
122        let thread_num = thread_num.unwrap_or_else(num_cpus::get);
123        let cache_mem_size = mem_limit.map(|mem_limit| {
124            // Use wider math to avoid 32-bit overflow when computing 80%.
125            ((mem_limit as u128) * 4 / 5) as usize
126        });
127        Pack {
128            number: 0,
129            signature: ObjectHash::default(),
130            objects: Vec::new(),
131            pool: Arc::new(ThreadPool::new(thread_num)),
132            waitlist: Arc::new(Waitlist::new()),
133            caches: Arc::new(Caches::new(cache_mem_size, temp_path, thread_num)),
134            mem_limit,
135            cache_objs_mem: Arc::new(AtomicUsize::default()),
136            clean_tmp,
137        }
138    }
139
140    /// Checks and reads the header of a Git pack file.
141    ///
142    /// This function reads the first 12 bytes of a pack file, which include the b"PACK" magic identifier,
143    /// the version number, and the number of objects in the pack. It verifies that the magic identifier
144    /// is correct and that the version number is 2 (which is the version currently supported by Git).
145    /// It also collects these header bytes for later use, such as for hashing the entire pack file.
146    ///
147    /// # Parameters
148    /// * `pack` - A mutable reference to an object implementing the `Read` trait,
149    ///   representing the source of the pack file data (e.g., file, memory stream).
150    ///
151    /// # Returns
152    /// A `Result` which is:
153    /// * `Ok((u32, Vec<u8>))`: On successful reading and validation of the header, returns a tuple where:
154    ///     - The first element is the number of objects in the pack file (`u32`).
155    ///     - The second element is a vector containing the bytes of the pack file header (`Vec<u8>`).
156    /// * `Err(GitError)`: On failure, returns a [`GitError`] with a description of the issue.
157    ///
158    /// # Errors
159    /// This function can return an error in the following situations:
160    /// * If the pack file does not start with the "PACK" magic identifier.
161    /// * If the pack file's version number is not 2.
162    /// * If there are any issues reading from the provided `pack` source.
163    pub fn check_header(pack: &mut impl BufRead) -> Result<(u32, Vec<u8>), GitError> {
164        // A vector to store the header data for hashing later
165        let mut header_data = Vec::new();
166
167        // Read the first 4 bytes which should be "PACK"
168        let mut magic = [0; 4];
169        // Read the magic "PACK" identifier
170        let result = pack.read_exact(&mut magic);
171        match result {
172            Ok(_) => {
173                // Store these bytes for later
174                header_data.extend_from_slice(&magic);
175
176                // Check if the magic bytes match "PACK"
177                if magic != *b"PACK" {
178                    // If not, return an error indicating invalid pack header
179                    return Err(GitError::InvalidPackHeader(format!(
180                        "{},{},{},{}",
181                        magic[0], magic[1], magic[2], magic[3]
182                    )));
183                }
184            }
185            Err(e) => {
186                // If there is an error in reading, return a GitError
187                return Err(GitError::InvalidPackFile(format!(
188                    "Error reading magic identifier: {e}"
189                )));
190            }
191        }
192
193        // Read the next 4 bytes for the version number
194        let mut version_bytes = [0; 4];
195        let result = pack.read_exact(&mut version_bytes); // Read the version number
196        match result {
197            Ok(_) => {
198                // Store these bytes
199                header_data.extend_from_slice(&version_bytes);
200
201                // Convert the version bytes to an u32 integer
202                let version = u32::from_be_bytes(version_bytes);
203                if version != 2 {
204                    // Git currently supports version 2, so error if not version 2
205                    return Err(GitError::InvalidPackFile(format!(
206                        "Version Number is {version}, not 2"
207                    )));
208                }
209            }
210            Err(e) => {
211                // If there is an error in reading, return a GitError
212                return Err(GitError::InvalidPackFile(format!(
213                    "Error reading version number: {e}"
214                )));
215            }
216        }
217
218        // Read the next 4 bytes for the number of objects in the pack
219        let mut object_num_bytes = [0; 4];
220        // Read the number of objects
221        let result = pack.read_exact(&mut object_num_bytes);
222        match result {
223            Ok(_) => {
224                // Store these bytes
225                header_data.extend_from_slice(&object_num_bytes);
226                // Convert the object number bytes to an u32 integer
227                let object_num = u32::from_be_bytes(object_num_bytes);
228                // Return the number of objects and the header data for further processing
229                Ok((object_num, header_data))
230            }
231            Err(e) => {
232                // If there is an error in reading, return a GitError
233                Err(GitError::InvalidPackFile(format!(
234                    "Error reading object number: {e}"
235                )))
236            }
237        }
238    }
239
240    /// Decompresses data from a given Read and BufRead source using Zlib decompression.
241    ///
242    /// # Parameters
243    /// * `pack`: A source that implements both Read and BufRead traits (e.g., file, network stream).
244    /// * `expected_size`: The expected decompressed size of the data.
245    ///
246    /// # Returns
247    /// Returns a `Result` containing either:
248    /// * A tuple with a `Vec<u8>` of the decompressed data and the total number of input bytes processed,
249    /// * Or a `GitError` in case of a mismatch in expected size or any other reading error.
250    ///
251    pub fn decompress_data(
252        pack: &mut (impl BufRead + Send),
253        expected_size: usize,
254    ) -> Result<(Vec<u8>, usize), GitError> {
255        // Create a buffer with the expected size for the decompressed data
256        let mut buf = Vec::with_capacity(expected_size);
257
258        let mut counting_reader = CountingReader::new(pack);
259        // Create a new Zlib decoder with the original data
260        //let mut deflate = ZlibDecoder::new(pack);
261        let mut deflate = ZlibDecoder::new(&mut counting_reader);
262        // Attempt to read data to the end of the buffer
263        match deflate.read_to_end(&mut buf) {
264            Ok(_) => {
265                // Check if the length of the buffer matches the expected size
266                if buf.len() != expected_size {
267                    Err(GitError::InvalidPackFile(format!(
268                        "The object size {} does not match the expected size {}",
269                        buf.len(),
270                        expected_size
271                    )))
272                } else {
273                    // If everything is as expected, return the buffer, the original data, and the total number of input bytes processed
274                    let actual_input_bytes = counting_reader.bytes_read as usize;
275                    Ok((buf, actual_input_bytes))
276                }
277            }
278            Err(e) => {
279                // If there is an error in reading, return a GitError
280                Err(GitError::InvalidPackFile(format!(
281                    "Decompression error: {e}"
282                )))
283            }
284        }
285    }
286
287    /// Decodes a pack object from a given Read and BufRead source and returns the object as a [`CacheObject`].
288    ///
289    /// # Parameters
290    /// * `pack`: A source that implements both Read and BufRead traits.
291    /// * `offset`: A mutable reference to the current offset within the pack.
292    ///
293    /// # Returns
294    /// Returns a `Result` containing either:
295    /// * A tuple of the next offset in the pack and the original compressed data as `Vec<u8>`,
296    /// * Or a `GitError` in case of any reading or decompression error.
297    ///
298    pub fn decode_pack_object(
299        pack: &mut (impl BufRead + Send),
300        offset: &mut usize,
301    ) -> Result<Option<CacheObject>, GitError> {
302        let init_offset = *offset;
303        let mut hasher = crc32fast::Hasher::new();
304        let mut reader = CrcCountingReader {
305            inner: pack,
306            bytes_read: 0,
307            crc: &mut hasher,
308        };
309
310        // Attempt to read the type and size, handle potential errors
311        // Note: read_type_and_varint_size updates the offset manually, but we can rely on reader.bytes_read
312        let (type_bits, size) = match utils::read_type_and_varint_size(&mut reader, offset) {
313            Ok(result) => result,
314            Err(e) => {
315                // Handle the error e.g., by logging it or converting it to GitError
316                // and then return from the function
317                return Err(GitError::InvalidPackFile(format!("Read error: {e}")));
318            }
319        };
320
321        // Check if the object type is valid
322        let t = ObjectType::from_pack_type_u8(type_bits)?;
323
324        match t {
325            ObjectType::Commit | ObjectType::Tree | ObjectType::Blob | ObjectType::Tag => {
326                let (data, raw_size) = Pack::decompress_data(&mut reader, size)?;
327                *offset += raw_size;
328                let crc32 = hasher.finalize();
329                Ok(Some(CacheObject::new_for_undeltified(
330                    t,
331                    data,
332                    init_offset,
333                    crc32,
334                )))
335            }
336            ObjectType::OffsetDelta | ObjectType::OffsetZstdelta => {
337                let (delta_offset, bytes) =
338                    utils::read_offset_encoding(&mut reader).map_err(|e| {
339                        GitError::InvalidPackFile(format!("Read offset-delta base error: {e}"))
340                    })?;
341                *offset += bytes;
342
343                let (data, raw_size) = Pack::decompress_data(&mut reader, size)?;
344                *offset += raw_size;
345
346                let delta_offset = usize::try_from(delta_offset).map_err(|_| {
347                    GitError::InvalidObjectInfo("Invalid OffsetDelta offset".to_string())
348                })?;
349                let base_offset = init_offset.checked_sub(delta_offset).ok_or_else(|| {
350                    GitError::InvalidObjectInfo("Invalid OffsetDelta offset".to_string())
351                })?;
352
353                let mut reader = Cursor::new(&data);
354                let (_, final_size) = utils::read_delta_object_size(&mut reader)?;
355
356                let obj_info = match t {
357                    ObjectType::OffsetDelta => {
358                        CacheObjectInfo::OffsetDelta(base_offset, final_size)
359                    }
360                    ObjectType::OffsetZstdelta => {
361                        CacheObjectInfo::OffsetZstdelta(base_offset, final_size)
362                    }
363                    _ => unreachable!(),
364                };
365                let crc32 = hasher.finalize();
366                Ok(Some(CacheObject {
367                    info: obj_info,
368                    offset: init_offset,
369                    crc32,
370                    data_decompressed: data,
371                    mem_recorder: None,
372                    is_delta_in_pack: true,
373                }))
374            }
375            ObjectType::HashDelta => {
376                // Read hash bytes to get the reference object hash(size depends on hash kind,e.g.,20 for SHA1,32 for SHA256)
377                let ref_sha = ObjectHash::from_stream(&mut reader).map_err(|e| {
378                    GitError::InvalidPackFile(format!("Read hash-delta base hash error: {e}"))
379                })?;
380                // Offset is incremented by 20/32 bytes
381                *offset += get_hash_kind().size();
382
383                let (data, raw_size) = Pack::decompress_data(&mut reader, size)?;
384                *offset += raw_size;
385
386                let mut reader = Cursor::new(&data);
387                let (_, final_size) = utils::read_delta_object_size(&mut reader)?;
388
389                let crc32 = hasher.finalize();
390
391                Ok(Some(CacheObject {
392                    info: CacheObjectInfo::HashDelta(ref_sha, final_size),
393                    offset: init_offset,
394                    crc32,
395                    data_decompressed: data,
396                    mem_recorder: None,
397                    is_delta_in_pack: true,
398                }))
399            }
400            // AI object types (ContextSnapshot, Decision, etc.) use u8 IDs >= 8
401            // and cannot appear in a pack file (3-bit type field only holds 1-7).
402            // `from_pack_type_u8` already rejects them, but guard explicitly here.
403            other => Err(GitError::InvalidPackFile(format!(
404                "AI object type `{other}` cannot appear in a pack file"
405            ))),
406        }
407    }
408
409    /// Decodes a pack file from a given Read and BufRead source, for each object in the pack,
410    /// it decodes the object and processes it using the provided callback function.
411    ///
412    /// # Parameters
413    /// * pack_id_callback: A callback that seed pack_file sha1 for updating database
414    ///
415    pub fn decode<F, C>(
416        &mut self,
417        pack: &mut (impl BufRead + Send),
418        callback: F,
419        pack_id_callback: Option<C>,
420    ) -> Result<(), GitError>
421    where
422        F: Fn(MetaAttached<Entry, EntryMeta>) + Sync + Send + 'static,
423        C: FnOnce(ObjectHash) + Send + 'static,
424    {
425        let time = Instant::now();
426        let mut last_update_time = time.elapsed().as_millis();
427        let log_info = |_i: usize, pack: &Pack| {
428            tracing::info!(
429                "time {:.2} s \t decode: {:?} \t dec-num: {} \t cah-num: {} \t Objs: {} MB \t CacheUsed: {} MB",
430                time.elapsed().as_millis() as f64 / 1000.0,
431                _i,
432                pack.pool.queued_count(),
433                pack.caches.queued_tasks(),
434                pack.cache_objs_mem_used() / 1024 / 1024,
435                pack.caches.memory_used() / 1024 / 1024
436            );
437        };
438        let callback = Arc::new(callback);
439
440        let caches = self.caches.clone();
441        let mut reader = Wrapper::new(io::BufReader::new(pack));
442
443        let result = Pack::check_header(&mut reader);
444        match result {
445            Ok((object_num, _)) => {
446                self.number = object_num as usize;
447            }
448            Err(e) => {
449                return Err(e);
450            }
451        }
452        tracing::info!("The pack file has {} objects", self.number);
453        let mut offset: usize = 12;
454        let mut i = 0;
455        while i < self.number {
456            // log per 1000 objects and 1 second
457            if i % 1000 == 0 {
458                let time_now = time.elapsed().as_millis();
459                if time_now - last_update_time > 1000 {
460                    log_info(i, self);
461                    last_update_time = time_now;
462                }
463            }
464            // 3 parts: Waitlist + TheadPool + Caches
465            // hardcode the limit of the tasks of threads_pool queue, to limit memory
466            while self.pool.queued_count() > 2000
467                || self
468                    .mem_limit
469                    .map(|limit| self.memory_used() > limit)
470                    .unwrap_or(false)
471            {
472                thread::yield_now();
473            }
474            let r: Result<Option<CacheObject>, GitError> =
475                Pack::decode_pack_object(&mut reader, &mut offset);
476            match r {
477                Ok(Some(mut obj)) => {
478                    obj.set_mem_recorder(self.cache_objs_mem.clone());
479                    obj.record_mem_size();
480
481                    // Wrapper of Arc Params, for convenience to pass
482                    let params = Arc::new(SharedParams {
483                        pool: self.pool.clone(),
484                        waitlist: self.waitlist.clone(),
485                        caches: self.caches.clone(),
486                        cache_objs_mem_size: self.cache_objs_mem.clone(),
487                        callback: callback.clone(),
488                    });
489
490                    let caches = caches.clone();
491                    let waitlist = self.waitlist.clone();
492                    let kind = get_hash_kind();
493                    self.pool.execute(move || {
494                        set_hash_kind(kind);
495                        match obj.info {
496                            CacheObjectInfo::BaseObject(_, _) => {
497                                Self::cache_obj_and_process_waitlist(params, obj);
498                            }
499                            CacheObjectInfo::OffsetDelta(base_offset, _)
500                            | CacheObjectInfo::OffsetZstdelta(base_offset, _) => {
501                                if let Some(base_obj) = caches.get_by_offset(base_offset) {
502                                    Self::process_delta(params, obj, base_obj);
503                                } else {
504                                    // You can delete this 'if' block ↑, because there are Second check in 'else'
505                                    // It will be more readable, but the performance will be slightly reduced
506                                    waitlist.insert_offset(base_offset, obj);
507                                    // Second check: prevent that the base_obj thread has finished before the waitlist insert
508                                    if let Some(base_obj) = caches.get_by_offset(base_offset) {
509                                        Self::process_waitlist(params, base_obj);
510                                    }
511                                }
512                            }
513                            CacheObjectInfo::HashDelta(base_ref, _) => {
514                                if let Some(base_obj) = caches.get_by_hash(base_ref) {
515                                    Self::process_delta(params, obj, base_obj);
516                                } else {
517                                    waitlist.insert_ref(base_ref, obj);
518                                    if let Some(base_obj) = caches.get_by_hash(base_ref) {
519                                        Self::process_waitlist(params, base_obj);
520                                    }
521                                }
522                            }
523                        }
524                    });
525                }
526                Ok(None) => {}
527                Err(e) => {
528                    self.abort_decode();
529                    return Err(e);
530                }
531            }
532            i += 1;
533        }
534        log_info(i, self);
535        let render_hash = reader.final_hash();
536        self.signature = match ObjectHash::from_stream(&mut reader) {
537            Ok(signature) => signature,
538            Err(e) => {
539                self.abort_decode();
540                return Err(GitError::InvalidPackFile(format!(
541                    "Error reading pack trailer hash: {e}"
542                )));
543            }
544        };
545
546        if render_hash != self.signature {
547            self.abort_decode();
548            return Err(GitError::InvalidPackFile(format!(
549                "The pack file hash {} does not match the trailer hash {}",
550                render_hash, self.signature
551            )));
552        }
553
554        let end = utils::is_eof(&mut reader);
555        if !end {
556            self.abort_decode();
557            return Err(GitError::InvalidPackFile(
558                "The pack file is not at the end".to_string(),
559            ));
560        }
561
562        self.pool.join(); // wait for all threads to finish
563
564        // send pack id for metadata
565        if let Some(pack_callback) = pack_id_callback {
566            pack_callback(self.signature);
567        }
568        // !Attention: Caches threadpool may not stop, but it's not a problem (garbage file data)
569        // So that files != self.number
570        assert_eq!(self.waitlist.map_offset.len(), 0);
571        assert_eq!(self.waitlist.map_ref.len(), 0);
572        // Because we may skip some objects (e.g. AI objects), we use >= instead of ==
573        assert!(self.number >= caches.total_inserted());
574        tracing::info!(
575            "The pack file has been decoded successfully, takes: [ {:?} ]",
576            time.elapsed()
577        );
578        self.caches.clear(); // clear cached objects & stop threads
579        assert_eq!(self.cache_objs_mem_used(), 0); // all the objs should be dropped until here
580
581        // impl in Drop Trait
582        // if self.clean_tmp {
583        //     self.caches.remove_tmp_dir();
584        // }
585
586        Ok(())
587    }
588
589    /// Decode a Pack in a new thread and send the CacheObjects while decoding.
590    /// <br> Attention: It will consume the `pack` and return in a JoinHandle.
591    pub fn decode_async(
592        mut self,
593        mut pack: impl BufRead + Send + 'static,
594        sender: UnboundedSender<Entry>,
595    ) -> JoinHandle<Pack> {
596        let kind = get_hash_kind();
597        thread::spawn(move || {
598            set_hash_kind(kind);
599            self.decode(
600                &mut pack,
601                move |entry| {
602                    if let Err(e) = sender.send(entry.inner) {
603                        eprintln!("Channel full, failed to send entry: {e:?}");
604                    }
605                },
606                None::<fn(ObjectHash)>,
607            )
608            .unwrap();
609            self
610        })
611    }
612
613    /// Decodes a `Pack` from a `Stream` of `Bytes`, and sends the `Entry` while decoding.
614    pub async fn decode_stream(
615        mut self,
616        mut stream: impl Stream<Item = Result<Bytes, Error>> + Unpin + Send + 'static,
617        sender: UnboundedSender<MetaAttached<Entry, EntryMeta>>,
618        pack_hash_send: Option<UnboundedSender<ObjectHash>>,
619    ) -> Self {
620        let kind = get_hash_kind();
621        let (tx, rx) = std::sync::mpsc::channel();
622        let mut reader = StreamBufReader::new(rx);
623        tokio::spawn(async move {
624            while let Some(chunk) = stream.next().await {
625                let data = chunk.unwrap().to_vec();
626                if let Err(e) = tx.send(data) {
627                    eprintln!("Sending Error: {e:?}");
628                    break;
629                }
630            }
631        });
632        // CPU-bound task, so use spawn_blocking
633        // DO NOT use thread::spawn, because it will block tokio runtime (if single-threaded runtime, like in tests)
634        tokio::task::spawn_blocking(move || {
635            set_hash_kind(kind);
636            self.decode(
637                &mut reader,
638                move |entry: MetaAttached<Entry, EntryMeta>| {
639                    // as we used unbound channel here, it will never full so can be send with synchronous
640                    if let Err(e) = sender.send(entry) {
641                        eprintln!("unbound channel Sending Error: {e:?}");
642                    }
643                },
644                Some(move |pack_id: ObjectHash| {
645                    if let Some(pack_id_send) = pack_hash_send
646                        && let Err(e) = pack_id_send.send(pack_id)
647                    {
648                        eprintln!("unbound channel Sending Error: {e:?}");
649                    }
650                }),
651            )
652            .unwrap();
653            self
654        })
655        .await
656        .unwrap()
657    }
658
659    /// CacheObjects + Index size of Caches
660    fn memory_used(&self) -> usize {
661        self.cache_objs_mem_used() + self.caches.memory_used_index()
662    }
663
664    /// The total memory used by the CacheObjects of this Pack
665    fn cache_objs_mem_used(&self) -> usize {
666        self.cache_objs_mem.load(Ordering::Acquire)
667    }
668
669    /// Rebuild the Delta Object in a new thread & process the objects waiting for it recursively.
670    /// <br> This function must be *static*, because [&self] can't be moved into a new thread.
671    fn process_delta(
672        shared_params: Arc<SharedParams>,
673        delta_obj: CacheObject,
674        base_obj: Arc<CacheObject>,
675    ) {
676        shared_params.pool.clone().execute(move || {
677            let mut new_obj = match delta_obj.info {
678                CacheObjectInfo::OffsetDelta(_, _) | CacheObjectInfo::HashDelta(_, _) => {
679                    Pack::rebuild_delta(delta_obj, base_obj)
680                }
681                CacheObjectInfo::OffsetZstdelta(_, _) => {
682                    Pack::rebuild_zstdelta(delta_obj, base_obj)
683                }
684                _ => unreachable!(),
685            };
686
687            new_obj.set_mem_recorder(shared_params.cache_objs_mem_size.clone());
688            new_obj.record_mem_size();
689            Self::cache_obj_and_process_waitlist(shared_params, new_obj); //Indirect Recursion
690        });
691    }
692
693    /// Cache the new object & process the objects waiting for it (in multi-threading).
694    fn cache_obj_and_process_waitlist(shared_params: Arc<SharedParams>, new_obj: CacheObject) {
695        (shared_params.callback)(new_obj.to_entry_metadata());
696        let new_obj = shared_params.caches.insert(
697            new_obj.offset,
698            new_obj.base_object_hash().unwrap(),
699            new_obj,
700        );
701        Self::process_waitlist(shared_params, new_obj);
702    }
703
704    fn process_waitlist(shared_params: Arc<SharedParams>, base_obj: Arc<CacheObject>) {
705        let wait_objs = shared_params
706            .waitlist
707            .take(base_obj.offset, base_obj.base_object_hash().unwrap());
708        for obj in wait_objs {
709            // Process the objects waiting for the new object(base_obj = new_obj)
710            Self::process_delta(shared_params.clone(), obj, base_obj.clone());
711        }
712    }
713
714    /// Reconstruct the Delta Object based on the "base object"
715    /// and return the new object.
716    pub fn rebuild_delta(delta_obj: CacheObject, base_obj: Arc<CacheObject>) -> CacheObject {
717        const COPY_INSTRUCTION_FLAG: u8 = 1 << 7;
718        const COPY_OFFSET_BYTES: u8 = 4;
719        const COPY_SIZE_BYTES: u8 = 3;
720        const COPY_ZERO_SIZE: usize = 0x10000;
721
722        let mut stream = Cursor::new(&delta_obj.data_decompressed);
723
724        // Read the base object size
725        // (Size Encoding)
726        let (base_size, result_size) = utils::read_delta_object_size(&mut stream).unwrap();
727
728        // Get the base object data
729        let base_info = &base_obj.data_decompressed;
730        assert_eq!(base_info.len(), base_size, "Base object size mismatch");
731
732        let mut result = Vec::with_capacity(result_size);
733
734        loop {
735            // Check if the stream has ended, meaning the new object is done
736            let instruction = match utils::read_bytes(&mut stream) {
737                Ok([instruction]) => instruction,
738                Err(err) if err.kind() == ErrorKind::UnexpectedEof => break,
739                Err(err) => {
740                    panic!(
741                        "{}",
742                        GitError::DeltaObjectError(format!("Wrong instruction in delta :{err}"))
743                    );
744                }
745            };
746
747            if instruction & COPY_INSTRUCTION_FLAG == 0 {
748                // Data instruction; the instruction byte specifies the number of data bytes
749                if instruction == 0 {
750                    // Appending 0 bytes doesn't make sense, so git disallows it
751                    panic!(
752                        "{}",
753                        GitError::DeltaObjectError(String::from("Invalid data instruction"))
754                    );
755                }
756
757                // Append the provided bytes
758                let mut data = vec![0; instruction as usize];
759                stream.read_exact(&mut data).unwrap();
760                result.extend_from_slice(&data);
761            } else {
762                // Copy instruction
763                // +----------+---------+---------+---------+---------+-------+-------+-------+
764                // | 1xxxxxxx | offset1 | offset2 | offset3 | offset4 | size1 | size2 | size3 |
765                // +----------+---------+---------+---------+---------+-------+-------+-------+
766                let mut nonzero_bytes = instruction;
767                let offset =
768                    utils::read_partial_int(&mut stream, COPY_OFFSET_BYTES, &mut nonzero_bytes)
769                        .unwrap();
770                let mut size =
771                    utils::read_partial_int(&mut stream, COPY_SIZE_BYTES, &mut nonzero_bytes)
772                        .unwrap();
773                if size == 0 {
774                    // Copying 0 bytes doesn't make sense, so git assumes a different size
775                    size = COPY_ZERO_SIZE;
776                }
777                // Copy bytes from the base object
778                let base_data = base_info.get(offset..(offset + size)).ok_or_else(|| {
779                    GitError::DeltaObjectError("Invalid copy instruction".to_string())
780                });
781
782                match base_data {
783                    Ok(data) => result.extend_from_slice(data),
784                    Err(e) => panic!("{}", e),
785                }
786            }
787        }
788        assert_eq!(result_size, result.len(), "Result size mismatch");
789
790        let hash = utils::calculate_object_hash(base_obj.object_type(), &result);
791        // create new obj from `delta_obj` & `result` instead of modifying `delta_obj` for heap-size recording
792        CacheObject {
793            info: CacheObjectInfo::BaseObject(base_obj.object_type(), hash),
794            offset: delta_obj.offset,
795            crc32: delta_obj.crc32,
796            data_decompressed: result,
797            mem_recorder: None,
798            is_delta_in_pack: delta_obj.is_delta_in_pack,
799        } // Canonical form (Complete Object)
800        // Memory recording will happen after this function returns. See `process_delta`
801    }
802    pub fn rebuild_zstdelta(delta_obj: CacheObject, base_obj: Arc<CacheObject>) -> CacheObject {
803        let result = zstdelta::apply(&base_obj.data_decompressed, &delta_obj.data_decompressed)
804            .expect("Failed to apply zstdelta");
805        let hash = utils::calculate_object_hash(base_obj.object_type(), &result);
806        CacheObject {
807            info: CacheObjectInfo::BaseObject(base_obj.object_type(), hash),
808            offset: delta_obj.offset,
809            crc32: delta_obj.crc32,
810            data_decompressed: result,
811            mem_recorder: None,
812            is_delta_in_pack: delta_obj.is_delta_in_pack,
813        } // Canonical form (Complete Object)
814        // Memory recording will happen after this function returns. See `process_delta`
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use std::{
821        fs,
822        io::{BufReader, Cursor, prelude::*},
823        path::PathBuf,
824        sync::{
825            Arc,
826            atomic::{AtomicUsize, Ordering},
827        },
828    };
829
830    use flate2::{Compression, write::ZlibEncoder};
831    use futures_util::TryStreamExt;
832    use tokio_util::io::ReaderStream;
833
834    use crate::{
835        hash::{HashKind, ObjectHash, set_hash_kind_for_test},
836        internal::pack::{Pack, test_pack_download::download_pack_file, tests::init_logger},
837    };
838
839    #[tokio::test]
840    async fn test_pack_check_header() {
841        let (source, _guard) = download_pack_file("medium-sha1.pack");
842
843        let f = fs::File::open(source).unwrap();
844        let mut buf_reader = BufReader::new(f);
845        let (object_num, _) = Pack::check_header(&mut buf_reader).unwrap();
846
847        assert_eq!(object_num, 35031);
848    }
849
850    #[test]
851    fn test_decompress_data() {
852        let data = b"Hello, world!"; // Sample data to compress and then decompress
853        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
854        encoder.write_all(data).unwrap();
855        let compressed_data = encoder.finish().unwrap();
856        let compressed_size = compressed_data.len();
857
858        // Create a cursor for the compressed data to simulate a BufRead source
859        let mut cursor: Cursor<Vec<u8>> = Cursor::new(compressed_data);
860        let expected_size = data.len();
861
862        // Decompress the data and assert correctness
863        let result = Pack::decompress_data(&mut cursor, expected_size);
864        match result {
865            Ok((decompressed_data, bytes_read)) => {
866                assert_eq!(bytes_read, compressed_size);
867                assert_eq!(decompressed_data, data);
868            }
869            Err(e) => panic!("Decompression failed: {e:?}"),
870        }
871    }
872
873    #[test]
874    fn test_pack_decode_truncated_pack_returns_err_without_panic() {
875        let _guard = set_hash_kind_for_test(HashKind::Sha1);
876        let (source, _dl_guard) = download_pack_file("small-sha1.pack");
877        let mut bytes = fs::read(source).unwrap();
878        bytes.truncate(bytes.len() - 1);
879
880        let tmp_dir = tempfile::tempdir().unwrap();
881        let tmp_path = tmp_dir.path().to_path_buf();
882        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
883            let mut buffered = BufReader::new(Cursor::new(bytes));
884            let mut pack = Pack::new(Some(2), Some(1024 * 1024), Some(tmp_path), true);
885            pack.decode(&mut buffered, |_| {}, None::<fn(ObjectHash)>)
886        }));
887
888        assert!(result.is_ok(), "truncated pack decode should not panic");
889        assert!(
890            matches!(
891                result.unwrap(),
892                Err(crate::errors::GitError::InvalidPackFile(_))
893                    | Err(crate::errors::GitError::IOError(_))
894            ),
895            "truncated pack decode should return a pack error"
896        );
897    }
898
899    #[test]
900    #[cfg(target_pointer_width = "32")]
901    fn test_pack_new_mem_limit_no_overflow_32bit() {
902        // In the old code, 1.2B * 4 produced an intermediate 4.8B value, which exceeds
903        // 32-bit usize::MAX (~4.29B) and overflowed before a later division; this test
904        // covers that former panic path.
905        let mem_limit = 1_200_000_000usize;
906        let tmp = PathBuf::from("/tmp/.cache_temp");
907        let result = std::panic::catch_unwind(|| {
908            let _p = Pack::new(Some(1), Some(mem_limit), Some(tmp), true);
909        });
910        assert!(result.is_ok(), "Pack::new should not panic on 32-bit");
911    }
912
913    /// Helper function to run decode tests without delta objects
914    fn run_decode_no_delta(filename: &str, kind: HashKind) {
915        let _guard = set_hash_kind_for_test(kind);
916        let (source, _dl_guard) = download_pack_file(filename);
917
918        let tmp = PathBuf::from("/tmp/.cache_temp");
919
920        let f = fs::File::open(source).unwrap();
921        let mut buffered = BufReader::new(f);
922        let mut p = Pack::new(None, Some(1024 * 1024 * 20), Some(tmp), true);
923        p.decode(&mut buffered, |_| {}, None::<fn(ObjectHash)>)
924            .unwrap();
925    }
926    #[test]
927    fn test_pack_decode_without_delta() {
928        run_decode_no_delta("small-sha1.pack", HashKind::Sha1);
929        run_decode_no_delta("small-sha256.pack", HashKind::Sha256);
930    }
931
932    /// Helper function to run decode tests with delta objects
933    fn run_decode_with_ref_delta(filename: &str, kind: HashKind) {
934        let _guard = set_hash_kind_for_test(kind);
935        init_logger();
936
937        let (source, _dl_guard) = download_pack_file(filename);
938
939        let tmp = PathBuf::from("/tmp/.cache_temp");
940
941        let f = fs::File::open(source).unwrap();
942        let mut buffered = BufReader::new(f);
943        let mut p = Pack::new(None, Some(1024 * 1024 * 20), Some(tmp), true);
944        p.decode(&mut buffered, |_| {}, None::<fn(ObjectHash)>)
945            .unwrap();
946    }
947    #[test]
948    fn test_pack_decode_with_ref_delta() {
949        run_decode_with_ref_delta("ref-delta-sha1.pack", HashKind::Sha1);
950        run_decode_with_ref_delta("ref-delta-sha256.pack", HashKind::Sha256);
951    }
952
953    /// Helper function to run decode tests without memory limit
954    fn run_decode_no_mem_limit(filename: &str, kind: HashKind) {
955        let _guard = set_hash_kind_for_test(kind);
956        let (source, _dl_guard) = download_pack_file(filename);
957
958        let tmp = PathBuf::from("/tmp/.cache_temp");
959
960        let f = fs::File::open(source).unwrap();
961        let mut buffered = BufReader::new(f);
962        let mut p = Pack::new(None, None, Some(tmp), true);
963        p.decode(&mut buffered, |_| {}, None::<fn(ObjectHash)>)
964            .unwrap();
965    }
966    #[test]
967    fn test_pack_decode_no_mem_limit() {
968        run_decode_no_mem_limit("small-sha1.pack", HashKind::Sha1);
969        run_decode_no_mem_limit("small-sha256.pack", HashKind::Sha256);
970    }
971
972    /// Helper function to run decode tests with delta objects
973    async fn run_decode_large_with_delta(filename: &str, kind: HashKind) {
974        let _guard = set_hash_kind_for_test(kind);
975        init_logger();
976        let (source, _dl_guard) = download_pack_file(filename);
977
978        let tmp = PathBuf::from("/tmp/.cache_temp");
979
980        let f = fs::File::open(source).unwrap();
981        let mut buffered = BufReader::new(f);
982        let mut p = Pack::new(
983            Some(4),
984            Some(1024 * 1024 * 100), //try to avoid dead lock on CI servers with low memory
985            Some(tmp.clone()),
986            true,
987        );
988        let rt = p.decode(
989            &mut buffered,
990            |_obj| {
991                // println!("{:?} {}", obj.hash.to_string(), offset);
992            },
993            None::<fn(ObjectHash)>,
994        );
995        if let Err(e) = rt {
996            fs::remove_dir_all(tmp).unwrap();
997            panic!("Error: {e:?}");
998        }
999    }
1000    #[tokio::test]
1001    async fn test_pack_decode_with_large_file_with_delta_without_ref() {
1002        run_decode_large_with_delta("medium-sha1.pack", HashKind::Sha1).await;
1003        run_decode_large_with_delta("medium-sha256.pack", HashKind::Sha256).await;
1004    } // it will be stuck on dropping `Pack` on Windows if `mem_size` is None, so we need `mimalloc`
1005
1006    /// Helper function to run decode tests with large file stream
1007    async fn run_decode_large_stream(filename: &str, kind: HashKind) {
1008        let _guard = set_hash_kind_for_test(kind);
1009        init_logger();
1010        let (source, _dl_guard) = download_pack_file(filename);
1011
1012        let tmp = PathBuf::from("/tmp/.cache_temp");
1013        let f = tokio::fs::File::open(source).await.unwrap();
1014        let stream = ReaderStream::new(f).map_err(axum::Error::new);
1015        let p = Pack::new(Some(4), Some(1024 * 1024 * 100), Some(tmp.clone()), true);
1016
1017        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1018        let handle = tokio::spawn(async move { p.decode_stream(stream, tx, None).await });
1019        let count = Arc::new(AtomicUsize::new(0));
1020        let count_c = count.clone();
1021        // in tests, RUNTIME is single-threaded, so `sync code` will block the tokio runtime
1022        let consume = tokio::spawn(async move {
1023            let mut cnt = 0;
1024            while let Some(_entry) = rx.recv().await {
1025                cnt += 1;
1026            }
1027            tracing::info!("Received: {}", cnt);
1028            count_c.store(cnt, Ordering::Release);
1029        });
1030        let p = handle.await.unwrap();
1031        consume.await.unwrap();
1032        assert_eq!(count.load(Ordering::Acquire), p.number);
1033        assert_eq!(p.number, 35031);
1034    }
1035    #[tokio::test]
1036    async fn test_decode_large_file_stream() {
1037        run_decode_large_stream("medium-sha1.pack", HashKind::Sha1).await;
1038        run_decode_large_stream("medium-sha256.pack", HashKind::Sha256).await;
1039    }
1040
1041    /// Helper function to run decode tests with large file async
1042    async fn run_decode_large_file_async(filename: &str, kind: HashKind) {
1043        let _guard = set_hash_kind_for_test(kind);
1044        let (source, _dl_guard) = download_pack_file(filename);
1045
1046        let tmp = PathBuf::from("/tmp/.cache_temp");
1047        let f = fs::File::open(source).unwrap();
1048        let buffered = BufReader::new(f);
1049        let p = Pack::new(Some(4), Some(1024 * 1024 * 100), Some(tmp.clone()), true);
1050
1051        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1052        let handle = p.decode_async(buffered, tx); // new thread
1053        let mut cnt = 0;
1054        while let Some(_entry) = rx.recv().await {
1055            cnt += 1; //use entry here
1056        }
1057        let p = handle.join().unwrap();
1058        assert_eq!(cnt, p.number);
1059    }
1060    #[tokio::test]
1061    async fn test_decode_large_file_async() {
1062        run_decode_large_file_async("medium-sha1.pack", HashKind::Sha1).await;
1063        run_decode_large_file_async("medium-sha256.pack", HashKind::Sha256).await;
1064    }
1065
1066    /// Helper function to run decode tests with delta objects without reference
1067    fn run_decode_with_delta_no_ref(filename: &str, kind: HashKind) {
1068        let _guard = set_hash_kind_for_test(kind);
1069        let (source, _dl_guard) = download_pack_file(filename);
1070
1071        let tmp = PathBuf::from("/tmp/.cache_temp");
1072
1073        let f = fs::File::open(source).unwrap();
1074        let mut buffered = BufReader::new(f);
1075        let mut p = Pack::new(None, Some(1024 * 1024 * 20), Some(tmp), true);
1076        p.decode(&mut buffered, |_| {}, None::<fn(ObjectHash)>)
1077            .unwrap();
1078    }
1079    #[test]
1080    fn test_pack_decode_with_delta_without_ref() {
1081        run_decode_with_delta_no_ref("medium-sha1.pack", HashKind::Sha1);
1082        run_decode_with_delta_no_ref("medium-sha256.pack", HashKind::Sha256);
1083    }
1084
1085    #[test] // Take too long time
1086    fn test_pack_decode_multi_task_with_large_file_with_delta_without_ref() {
1087        let rt = tokio::runtime::Builder::new_current_thread()
1088            .enable_all()
1089            .build()
1090            .unwrap();
1091        rt.block_on(async move {
1092            // For each hash kind, run two decode tasks concurrently to simulate multi-task pressure.
1093            for (kind, filename) in [
1094                (HashKind::Sha1, "medium-sha1.pack"),
1095                (HashKind::Sha256, "medium-sha256.pack"),
1096            ] {
1097                let f1 = run_decode_large_with_delta(filename, kind);
1098                let f2 = run_decode_large_with_delta(filename, kind);
1099                let _ = futures::future::join(f1, f2).await;
1100            }
1101        });
1102    }
1103}