Skip to main content

sley_remote/
install.rs

1// sley#7: untrusted-input parsing crate — fallible ops propagate errors;
2// the only retained `expect`s would be documented compile-time invariants.
3#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
4
5use sley_core::ObjectFormat;
6use sley_core::Result;
7use sley_odb::{
8    FileObjectDatabase, PackStreamProgress, RawPackInstallOptions, RawPackInstallResult,
9    RawPackInstaller,
10};
11use std::cell::RefCell;
12
13use crate::{ProgressSink, TransferProgress};
14
15/// Wraps a [`RawPackInstaller`] so the streaming pack counters produced while
16/// indexing are forwarded to a [`ProgressSink`] as [`TransferProgress`]. Passed
17/// as the `destination` of the generic `install_upload_pack_*` helpers, it
18/// threads live byte/object progress without changing their signatures.
19///
20/// The [`RawPackInstaller`] method takes `&self`, so the `&mut dyn ProgressSink`
21/// is held behind a [`RefCell`]; the borrow is confined to one install call.
22pub(crate) struct ProgressInstaller<'a, I> {
23    inner: &'a I,
24    sink: RefCell<&'a mut dyn ProgressSink>,
25}
26
27impl<'a, I> ProgressInstaller<'a, I> {
28    pub(crate) fn new(inner: &'a I, sink: &'a mut dyn ProgressSink) -> Self {
29        Self {
30            inner,
31            sink: RefCell::new(sink),
32        }
33    }
34}
35
36impl<I> RawPackInstaller for ProgressInstaller<'_, I>
37where
38    I: RawPackInstaller,
39{
40    fn install_raw_pack_from_reader_with_options<R>(
41        &self,
42        reader: &mut R,
43        options: RawPackInstallOptions,
44    ) -> Result<RawPackInstallResult>
45    where
46        R: Read,
47    {
48        self.inner
49            .install_raw_pack_from_reader_with_progress(reader, options, |progress| {
50                self.sink.borrow_mut().transfer(transfer_from_pack(progress));
51            })
52    }
53}
54
55fn transfer_from_pack(progress: PackStreamProgress) -> TransferProgress {
56    TransferProgress {
57        received_bytes: progress.received_bytes,
58        received_objects: progress.received_objects,
59        total_objects: Some(progress.total_objects),
60        // Deltas are resolved in one batch after the streaming parse, so
61        // per-delta progress is not surfaced yet. TODO(sley#146 follow-up):
62        // thread indexed-deltas from `resolve_pack_entries`.
63        indexed_deltas: 0,
64    }
65}
66use sley_protocol::{
67    PktLineFrame, ProtocolV2FetchResponseHeader, ProtocolV2FetchResponseSection,
68    ProtocolV2FetchShallowInfo, SideBandChannel, demux_upload_pack_packfile_response,
69    parse_sideband_packet, read_pkt_line_frame, read_protocol_v2_fetch_response_header,
70    read_upload_pack_packfile_response, read_upload_pack_raw_packfile_response_header,
71    read_upload_pack_shallow_info_section,
72    read_upload_pack_shallow_info_and_raw_packfile_response_header,
73};
74use std::io::{Cursor, ErrorKind, Read};
75
76fn raw_pack_install_options(
77    promisor: bool,
78    max_input_size: Option<u64>,
79) -> RawPackInstallOptions {
80    RawPackInstallOptions {
81        promisor,
82        max_input_size,
83    }
84}
85
86pub fn install_upload_pack_raw_response_from_reader<I, R>(
87    format: ObjectFormat,
88    reader: &mut R,
89    destination: &I,
90    max_input_size: Option<u64>,
91) -> Result<RawPackInstallResult>
92where
93    I: RawPackInstaller,
94    R: Read,
95{
96    let header = read_upload_pack_raw_packfile_response_header(format, reader)?;
97    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
98    destination.install_raw_pack_from_reader_with_options(
99        &mut pack_reader,
100        raw_pack_install_options(false, max_input_size),
101    )
102}
103
104/// Install an upload-pack packfile response that may use side-band-64k.
105///
106/// Smart HTTP always delivers the pack inside sideband channel 1 after any
107/// leading `ACK`/`NAK` pkt-lines (see `read_upload_pack_packfile_response`).
108pub fn install_upload_pack_packfile_response_from_reader<I, R>(
109    format: ObjectFormat,
110    reader: &mut R,
111    destination: &I,
112    max_input_size: Option<u64>,
113) -> Result<RawPackInstallResult>
114where
115    I: RawPackInstaller,
116    R: Read,
117{
118    install_upload_pack_sideband_response_from_reader(format, reader, destination, false, max_input_size)
119}
120
121pub fn install_upload_pack_packfile_promisor_response_from_reader<R>(
122    format: ObjectFormat,
123    reader: &mut R,
124    destination: &FileObjectDatabase,
125    max_input_size: Option<u64>,
126) -> Result<RawPackInstallResult>
127where
128    R: Read,
129{
130    install_upload_pack_sideband_response_from_reader(format, reader, destination, true, max_input_size)
131}
132
133fn install_upload_pack_sideband_response_from_reader<I, R>(
134    format: ObjectFormat,
135    reader: &mut R,
136    destination: &I,
137    promisor: bool,
138    max_input_size: Option<u64>,
139) -> Result<RawPackInstallResult>
140where
141    I: RawPackInstaller,
142    R: Read,
143{
144    let response = read_upload_pack_packfile_response(format, reader)?;
145    let demuxed = demux_upload_pack_packfile_response(&response)?;
146    let mut pack_reader = demuxed.data.as_slice();
147    let result = destination.install_raw_pack_from_reader_with_options(
148        &mut pack_reader,
149        raw_pack_install_options(promisor, max_input_size),
150    )?;
151    Ok(if promisor {
152        RawPackInstallResult {
153            object_ids: result.object_ids,
154        }
155    } else {
156        result
157    })
158}
159
160pub fn install_upload_pack_raw_promisor_response_from_reader<R>(
161    format: ObjectFormat,
162    reader: &mut R,
163    destination: &FileObjectDatabase,
164    max_input_size: Option<u64>,
165) -> Result<RawPackInstallResult>
166where
167    R: Read,
168{
169    let header = read_upload_pack_raw_packfile_response_header(format, reader)?;
170    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
171    let result = destination.install_raw_pack_from_reader_with_options(
172        &mut pack_reader,
173        raw_pack_install_options(true, max_input_size),
174    )?;
175    Ok(RawPackInstallResult {
176        object_ids: result.object_ids,
177    })
178}
179
180pub fn install_upload_pack_shallow_raw_response_from_reader<I, R>(
181    format: ObjectFormat,
182    reader: &mut R,
183    destination: &I,
184    max_input_size: Option<u64>,
185) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
186where
187    I: RawPackInstaller,
188    R: Read,
189{
190    let (shallow, header) =
191        read_upload_pack_shallow_info_and_raw_packfile_response_header(format, reader)?;
192    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
193    let result = destination.install_raw_pack_from_reader_with_options(
194        &mut pack_reader,
195        raw_pack_install_options(false, max_input_size),
196    )?;
197    Ok((shallow, result))
198}
199
200/// Shallow deepen over smart HTTP: shallow-info section then a sideband pack.
201pub fn install_upload_pack_shallow_packfile_response_from_reader<I, R>(
202    format: ObjectFormat,
203    reader: &mut R,
204    destination: &I,
205    max_input_size: Option<u64>,
206) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
207where
208    I: RawPackInstaller,
209    R: Read,
210{
211    install_upload_pack_shallow_sideband_response_from_reader(
212        format,
213        reader,
214        destination,
215        false,
216        max_input_size,
217    )
218}
219
220fn install_upload_pack_shallow_sideband_response_from_reader<I, R>(
221    format: ObjectFormat,
222    reader: &mut R,
223    destination: &I,
224    promisor: bool,
225    max_input_size: Option<u64>,
226) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
227where
228    I: RawPackInstaller,
229    R: Read,
230{
231    let shallow = read_upload_pack_shallow_info_section(format, reader)?;
232    let result =
233        install_upload_pack_sideband_response_from_reader(format, reader, destination, promisor, max_input_size)?;
234    Ok((shallow, result))
235}
236
237pub fn install_upload_pack_shallow_raw_promisor_response_from_reader<R>(
238    format: ObjectFormat,
239    reader: &mut R,
240    destination: &FileObjectDatabase,
241    max_input_size: Option<u64>,
242) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
243where
244    R: Read,
245{
246    let (shallow, header) =
247        read_upload_pack_shallow_info_and_raw_packfile_response_header(format, reader)?;
248    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
249    let result = destination.install_raw_pack_from_reader_with_options(
250        &mut pack_reader,
251        raw_pack_install_options(true, max_input_size),
252    )?;
253    Ok((
254        shallow,
255        RawPackInstallResult {
256            object_ids: result.object_ids,
257        },
258    ))
259}
260
261pub fn install_upload_pack_shallow_packfile_promisor_response_from_reader<R>(
262    format: ObjectFormat,
263    reader: &mut R,
264    destination: &FileObjectDatabase,
265    max_input_size: Option<u64>,
266) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
267where
268    R: Read,
269{
270    install_upload_pack_shallow_sideband_response_from_reader(
271        format,
272        reader,
273        destination,
274        true,
275        max_input_size,
276    )
277}
278
279pub fn install_protocol_v2_fetch_response_from_reader<I, R>(
280    format: ObjectFormat,
281    reader: &mut R,
282    sideband_all: bool,
283    destination: &I,
284    max_input_size: Option<u64>,
285) -> Result<(ProtocolV2FetchResponseHeader, Option<RawPackInstallResult>)>
286where
287    I: RawPackInstaller,
288    R: Read,
289{
290    let header = read_protocol_v2_fetch_response_header(format, reader, sideband_all)?;
291    if !header.has_packfile {
292        return Ok((header, None));
293    }
294    let mut pack_reader = ProtocolV2PackfileReader::new(reader);
295    let result = destination.install_raw_pack_from_reader_with_options(
296        &mut pack_reader,
297        raw_pack_install_options(false, max_input_size),
298    )?;
299    Ok((header, Some(result)))
300}
301
302pub fn install_protocol_v2_fetch_promisor_response_from_reader<R>(
303    format: ObjectFormat,
304    reader: &mut R,
305    sideband_all: bool,
306    destination: &FileObjectDatabase,
307    max_input_size: Option<u64>,
308) -> Result<(ProtocolV2FetchResponseHeader, Option<RawPackInstallResult>)>
309where
310    R: Read,
311{
312    let header = read_protocol_v2_fetch_response_header(format, reader, sideband_all)?;
313    if !header.has_packfile {
314        return Ok((header, None));
315    }
316    let mut pack_reader = ProtocolV2PackfileReader::new(reader);
317    let result = destination.install_raw_pack_from_reader_with_options(
318        &mut pack_reader,
319        raw_pack_install_options(true, max_input_size),
320    )?;
321    Ok((
322        header,
323        Some(RawPackInstallResult {
324            object_ids: result.object_ids,
325        }),
326    ))
327}
328
329struct ProtocolV2PackfileReader<'a, R> {
330    reader: &'a mut R,
331    pending: Vec<u8>,
332    pending_offset: usize,
333    done: bool,
334}
335
336impl<'a, R> ProtocolV2PackfileReader<'a, R> {
337    fn new(reader: &'a mut R) -> Self {
338        Self {
339            reader,
340            pending: Vec::new(),
341            pending_offset: 0,
342            done: false,
343        }
344    }
345
346    fn drain_pending(&mut self, buf: &mut [u8]) -> usize {
347        let available = self.pending.len().saturating_sub(self.pending_offset);
348        let to_copy = available.min(buf.len());
349        if to_copy == 0 {
350            if self.pending_offset >= self.pending.len() {
351                self.pending.clear();
352                self.pending_offset = 0;
353            }
354            return 0;
355        }
356        let end = self.pending_offset + to_copy;
357        buf[..to_copy].copy_from_slice(&self.pending[self.pending_offset..end]);
358        self.pending_offset = end;
359        if self.pending_offset == self.pending.len() {
360            self.pending.clear();
361            self.pending_offset = 0;
362        }
363        to_copy
364    }
365}
366
367impl<R> Read for ProtocolV2PackfileReader<'_, R>
368where
369    R: Read,
370{
371    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
372        if buf.is_empty() {
373            return Ok(0);
374        }
375        let mut written = 0usize;
376        while written < buf.len() {
377            let copied = self.drain_pending(&mut buf[written..]);
378            if copied > 0 {
379                written += copied;
380                continue;
381            }
382            if self.done {
383                break;
384            }
385            let frame = read_pkt_line_frame(self.reader)
386                .map_err(git_error_to_io)?
387                .ok_or_else(|| {
388                    std::io::Error::new(
389                        ErrorKind::UnexpectedEof,
390                        "protocol v2 packfile ended before flush",
391                    )
392                })?;
393            match frame {
394                PktLineFrame::Data(payload) => {
395                    let packet = parse_sideband_packet(&payload).map_err(git_error_to_io)?;
396                    match packet.channel {
397                        SideBandChannel::Data => {
398                            self.pending = packet.data;
399                            self.pending_offset = 0;
400                        }
401                        SideBandChannel::Progress => {}
402                        SideBandChannel::Fatal => {
403                            let message = String::from_utf8_lossy(&packet.data).into_owned();
404                            return Err(std::io::Error::new(
405                                ErrorKind::InvalidData,
406                                format!("sideband fatal: {message}"),
407                            ));
408                        }
409                    }
410                }
411                PktLineFrame::Flush => {
412                    self.done = true;
413                    break;
414                }
415                PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
416                    return Err(std::io::Error::new(
417                        ErrorKind::InvalidData,
418                        "protocol v2 packfile section ended unexpectedly",
419                    ));
420                }
421            }
422        }
423        Ok(written)
424    }
425}
426
427fn git_error_to_io(err: sley_core::GitError) -> std::io::Error {
428    std::io::Error::new(ErrorKind::InvalidData, err.to_string())
429}
430
431pub fn shallow_info_from_protocol_v2_fetch_header(
432    header: &ProtocolV2FetchResponseHeader,
433) -> Vec<ProtocolV2FetchShallowInfo> {
434    let mut shallow_info = Vec::new();
435    for section in &header.sections {
436        if let ProtocolV2FetchResponseSection::ShallowInfo(entries) = section {
437            shallow_info.extend(entries.clone());
438        }
439    }
440    shallow_info
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use sley_core::ObjectFormat;
447    use sley_object::{EncodedObject, ObjectType};
448    use sley_odb::{FileObjectDatabase, ObjectDatabase, ObjectReader};
449    use sley_pack::PackFile;
450    use sley_protocol::{
451        SideBandChannel, SideBandPacket, UploadPackAcknowledgment, UploadPackRawPackfileResponse,
452        encode_sideband_packet, encode_upload_pack_raw_packfile_response, write_pkt_line_payload,
453        write_protocol_v2_fetch_response, write_upload_pack_raw_packfile_response,
454    };
455    use std::fs;
456    use std::path::Path;
457    use std::sync::atomic::{AtomicU64, Ordering};
458
459    static TEST_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
460
461    #[test]
462    fn raw_upload_pack_response_stream_installs_pack_without_buffering_response() {
463        let root = test_temp_root("sley-remote-install-upload-pack-raw-stream-install");
464        let format = ObjectFormat::Sha1;
465        let object = EncodedObject::new(ObjectType::Blob, b"raw streamed upload-pack\n".to_vec());
466        let oid = object
467            .object_id(format)
468            .expect("test operation should succeed");
469        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
470            .expect("test operation should succeed");
471        let response = UploadPackRawPackfileResponse {
472            acknowledgments: vec![UploadPackAcknowledgment::Nak],
473            packfile: pack.pack,
474        };
475        let encoded =
476            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
477        let destination = FileObjectDatabase::new(root.join("objects"), format);
478        let mut reader = encoded.as_slice();
479
480        let result =
481            install_upload_pack_raw_response_from_reader(format, &mut reader, &destination, None)
482                .expect("test operation should succeed");
483
484        assert_eq!(result.object_ids, vec![oid]);
485        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
486        let _ = fs::remove_dir_all(&root);
487    }
488
489    #[test]
490    fn shallow_raw_upload_pack_response_stream_installs_pack_without_buffering_response() {
491        let root = test_temp_root("sley-remote-install-upload-pack-shallow-raw-stream-install");
492        let format = ObjectFormat::Sha1;
493        let shallow_oid =
494            sley_core::ObjectId::from_hex(format, "1111111111111111111111111111111111111111")
495                .expect("test operation should succeed");
496        let object =
497            EncodedObject::new(ObjectType::Blob, b"shallow streamed upload-pack\n".to_vec());
498        let oid = object
499            .object_id(format)
500            .expect("test operation should succeed");
501        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
502            .expect("test operation should succeed");
503        let response = UploadPackRawPackfileResponse {
504            acknowledgments: vec![UploadPackAcknowledgment::Nak],
505            packfile: pack.pack,
506        };
507        let mut encoded = Vec::new();
508        write_pkt_line_payload(&mut encoded, format!("shallow {shallow_oid}\n").as_bytes())
509            .expect("test operation should succeed");
510        encoded.extend_from_slice(b"0000");
511        write_upload_pack_raw_packfile_response(&mut encoded, &response)
512            .expect("test operation should succeed");
513        let destination = FileObjectDatabase::new(root.join("objects"), format);
514        let mut reader = encoded.as_slice();
515
516        let (shallow, result) =
517            install_upload_pack_shallow_raw_response_from_reader(format, &mut reader, &destination, None)
518                .expect("test operation should succeed");
519
520        assert_eq!(
521            shallow,
522            vec![ProtocolV2FetchShallowInfo::Shallow(shallow_oid)]
523        );
524        assert_eq!(result.object_ids, vec![oid]);
525        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
526        let _ = fs::remove_dir_all(&root);
527    }
528
529    #[test]
530    fn raw_upload_pack_response_installs_promisor_pack_sidecar() {
531        let root = test_temp_root("sley-remote-install-upload-pack-promisor-install");
532        let format = ObjectFormat::Sha1;
533        let object = EncodedObject::new(ObjectType::Blob, b"promisor upload-pack\n".to_vec());
534        let oid = object
535            .object_id(format)
536            .expect("test operation should succeed");
537        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
538            .expect("test operation should succeed");
539        let response = UploadPackRawPackfileResponse {
540            acknowledgments: Vec::new(),
541            packfile: pack.pack,
542        };
543        let mut encoded = Vec::new();
544        write_upload_pack_raw_packfile_response(&mut encoded, &response)
545            .expect("test operation should succeed");
546        let destination = FileObjectDatabase::new(root.join("objects"), format);
547        let mut reader = encoded.as_slice();
548
549        let result = install_upload_pack_raw_promisor_response_from_reader(
550            format,
551            &mut reader,
552            &destination,
553            None,
554        )
555        .expect("test operation should succeed");
556
557        assert_eq!(result.object_ids, vec![oid]);
558        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
559        assert_promisor_sidecar(&root.join("objects"));
560        let _ = fs::remove_dir_all(&root);
561    }
562
563    #[test]
564    fn protocol_v2_fetch_response_packfile_demuxes_and_installs_pack() {
565        let root = test_temp_root("sley-remote-install-v2-response-install");
566        let format = ObjectFormat::Sha1;
567        let object = EncodedObject::new(ObjectType::Blob, b"v2 response packfile\n".to_vec());
568        let oid = object
569            .object_id(format)
570            .expect("test operation should succeed");
571        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
572            .expect("test operation should succeed");
573        let sections = vec![ProtocolV2FetchResponseSection::Packfile(vec![
574            encode_sideband_packet(&SideBandPacket {
575                channel: SideBandChannel::Progress,
576                data: b"counting objects\n".to_vec(),
577            })
578            .expect("test operation should succeed"),
579            encode_sideband_packet(&SideBandPacket {
580                channel: SideBandChannel::Data,
581                data: pack.pack,
582            })
583            .expect("test operation should succeed"),
584        ])];
585        let mut encoded = Vec::new();
586        write_protocol_v2_fetch_response(&mut encoded, &sections)
587            .expect("test operation should succeed");
588        let destination = FileObjectDatabase::new(root.join("objects"), format);
589        let mut reader = encoded.as_slice();
590
591        let (header, result) = install_protocol_v2_fetch_response_from_reader(
592            format,
593            &mut reader,
594            false,
595            &destination,
596            None,
597        )
598        .expect("test operation should succeed");
599        let result = result.expect("packfile should be installed");
600
601        assert!(header.has_packfile);
602        assert!(header.sections.is_empty());
603        assert_eq!(result.object_ids, vec![oid]);
604        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
605        let _ = fs::remove_dir_all(&root);
606    }
607
608    #[test]
609    fn protocol_v2_fetch_response_packfile_installs_promisor_sidecar() {
610        let root = test_temp_root("sley-remote-install-v2-response-promisor-install");
611        let format = ObjectFormat::Sha1;
612        let object = EncodedObject::new(ObjectType::Blob, b"v2 promisor packfile\n".to_vec());
613        let oid = object
614            .object_id(format)
615            .expect("test operation should succeed");
616        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
617            .expect("test operation should succeed");
618        let sections = vec![ProtocolV2FetchResponseSection::Packfile(vec![
619            encode_sideband_packet(&SideBandPacket {
620                channel: SideBandChannel::Data,
621                data: pack.pack,
622            })
623            .expect("test operation should succeed"),
624        ])];
625        let mut encoded = Vec::new();
626        write_protocol_v2_fetch_response(&mut encoded, &sections)
627            .expect("test operation should succeed");
628        let destination = FileObjectDatabase::new(root.join("objects"), format);
629        let mut reader = encoded.as_slice();
630
631        let (header, result) = install_protocol_v2_fetch_promisor_response_from_reader(
632            format,
633            &mut reader,
634            false,
635            &destination,
636            None,
637        )
638        .expect("test operation should succeed");
639        let result = result.expect("packfile should be installed");
640
641        assert!(header.has_packfile);
642        assert_eq!(result.object_ids, vec![oid]);
643        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
644        assert_promisor_sidecar(&root.join("objects"));
645        let _ = fs::remove_dir_all(&root);
646    }
647
648    #[test]
649    fn protocol_v2_fetch_response_without_packfile_installs_nothing() {
650        let root = test_temp_root("sley-remote-install-v2-response-empty");
651        let destination = FileObjectDatabase::new(root.join("objects"), ObjectFormat::Sha1);
652        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(Vec::new())];
653        let mut encoded = Vec::new();
654        write_protocol_v2_fetch_response(&mut encoded, &sections)
655            .expect("test operation should succeed");
656        let mut reader = encoded.as_slice();
657
658        let (header, result) = install_protocol_v2_fetch_response_from_reader(
659            ObjectFormat::Sha1,
660            &mut reader,
661            false,
662            &destination,
663            None,
664        )
665        .expect("test operation should succeed");
666
667        assert!(!header.has_packfile);
668        assert!(result.is_none());
669        assert!(!root.join("objects").join("pack").exists());
670        let _ = fs::remove_dir_all(&root);
671    }
672
673    #[test]
674    fn raw_upload_pack_response_rejects_pack_exceeding_max_input_size() {
675        let root = test_temp_root("sley-remote-install-upload-pack-max-size");
676        let format = ObjectFormat::Sha1;
677        let object = EncodedObject::new(ObjectType::Blob, b"oversized fetch pack\n".to_vec());
678        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
679            .expect("test operation should succeed");
680        let response = UploadPackRawPackfileResponse {
681            acknowledgments: Vec::new(),
682            packfile: pack.pack,
683        };
684        let encoded =
685            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
686        let destination = FileObjectDatabase::new(root.join("objects"), format);
687        let mut reader = encoded.as_slice();
688        let limit = 32u64;
689
690        let err = install_upload_pack_raw_response_from_reader(
691            format,
692            &mut reader,
693            &destination,
694            Some(limit),
695        )
696        .expect_err("oversized pack should be rejected");
697
698        assert!(
699            err.to_string().contains("pack exceeds maximum allowed size"),
700            "unexpected error: {err}"
701        );
702        let pack_dir = root.join("objects").join("pack");
703        let installed = fs::read_dir(&pack_dir)
704            .map(|entries| {
705                entries
706                    .filter_map(|entry| entry.ok())
707                    .filter(|entry| {
708                        entry
709                            .path()
710                            .extension()
711                            .and_then(|ext| ext.to_str())
712                            == Some("pack")
713                    })
714                    .count()
715            })
716            .unwrap_or_default();
717        assert_eq!(installed, 0);
718        let _ = fs::remove_dir_all(&root);
719    }
720
721    #[test]
722    fn install_helpers_accept_custom_raw_pack_installer() {
723        #[derive(Default)]
724        struct RecordingInstaller {
725            packs: std::cell::RefCell<Vec<Vec<u8>>>,
726        }
727
728        impl RawPackInstaller for RecordingInstaller {
729            fn install_raw_pack_from_reader_with_options<R>(
730                &self,
731                reader: &mut R,
732                _options: RawPackInstallOptions,
733            ) -> Result<RawPackInstallResult>
734            where
735                R: Read,
736            {
737                let mut pack_bytes = Vec::new();
738                reader.read_to_end(&mut pack_bytes)?;
739                self.packs.borrow_mut().push(pack_bytes.to_vec());
740                Ok(RawPackInstallResult {
741                    object_ids: Vec::new(),
742                })
743            }
744        }
745
746        let installer = RecordingInstaller::default();
747        let response = UploadPackRawPackfileResponse {
748            acknowledgments: Vec::new(),
749            packfile: b"PACKcustom".to_vec(),
750        };
751        let encoded =
752            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
753        let mut reader = encoded.as_slice();
754
755        let result = install_upload_pack_raw_response_from_reader(
756            ObjectFormat::Sha1,
757            &mut reader, &installer, None)
758        .expect("test operation should succeed");
759
760        assert!(result.object_ids.is_empty());
761        assert_eq!(installer.packs.into_inner(), vec![b"PACKcustom".to_vec()]);
762    }
763
764    #[test]
765    fn raw_upload_pack_response_installs_into_in_memory_database() {
766        let format = ObjectFormat::Sha1;
767        let object = EncodedObject::new(ObjectType::Blob, b"in memory fetch pack\n".to_vec());
768        let oid = object
769            .object_id(format)
770            .expect("test operation should succeed");
771        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
772            .expect("test operation should succeed");
773        let response = UploadPackRawPackfileResponse {
774            acknowledgments: Vec::new(),
775            packfile: pack.pack,
776        };
777        let encoded =
778            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
779        let destination = ObjectDatabase::new(format);
780        let mut reader = encoded.as_slice();
781
782        let result =
783            install_upload_pack_raw_response_from_reader(format, &mut reader, &destination, None)
784                .expect("test operation should succeed");
785
786        assert_eq!(result.object_ids, vec![oid]);
787        assert_eq!(
788            destination
789                .read_object(&oid)
790                .expect("test operation should succeed")
791                .as_ref(),
792            &object
793        );
794    }
795
796    #[derive(Default)]
797    struct RecordingProgress {
798        samples: Vec<TransferProgress>,
799    }
800
801    impl ProgressSink for RecordingProgress {
802        fn transfer(&mut self, progress: TransferProgress) {
803            self.samples.push(progress);
804        }
805    }
806
807    fn multi_object_raw_pack(format: ObjectFormat, count: usize) -> Vec<u8> {
808        let objects: Vec<EncodedObject> = (0..count)
809            .map(|i| {
810                EncodedObject::new(
811                    ObjectType::Blob,
812                    format!("sley#146 progress fixture object {i}\n").into_bytes(),
813                )
814            })
815            .collect();
816        let pack = PackFile::write_undeltified(&objects, format)
817            .expect("test operation should succeed");
818        let response = UploadPackRawPackfileResponse {
819            acknowledgments: Vec::new(),
820            packfile: pack.pack,
821        };
822        encode_upload_pack_raw_packfile_response(&response).expect("response should encode")
823    }
824
825    #[test]
826    fn progress_installer_reports_monotonic_transfer() {
827        let root = test_temp_root("sley-remote-install-progress");
828        let format = ObjectFormat::Sha1;
829        let object_count = 24usize;
830        let encoded = multi_object_raw_pack(format, object_count);
831        let destination = FileObjectDatabase::new(root.join("objects"), format);
832
833        let mut sink = RecordingProgress::default();
834        {
835            let installer = ProgressInstaller::new(&destination, &mut sink);
836            let mut reader = encoded.as_slice();
837            install_upload_pack_raw_response_from_reader(format, &mut reader, &installer, None)
838                .expect("test operation should succeed");
839        }
840
841        let samples = sink.samples;
842        assert!(
843            !samples.is_empty(),
844            "transfer() should be called at least once"
845        );
846        let total = object_count as u64;
847
848        // total_objects is announced from the pack header on every sample.
849        for sample in &samples {
850            assert_eq!(sample.total_objects, Some(total));
851        }
852
853        // Monotonically non-decreasing byte and object counters.
854        for window in samples.windows(2) {
855            assert!(
856                window[1].received_bytes >= window[0].received_bytes,
857                "received_bytes regressed: {} -> {}",
858                window[0].received_bytes,
859                window[1].received_bytes
860            );
861            assert!(
862                window[1].received_objects >= window[0].received_objects,
863                "received_objects regressed: {} -> {}",
864                window[0].received_objects,
865                window[1].received_objects
866            );
867        }
868
869        let first_nonzero = samples
870            .iter()
871            .find(|sample| sample.received_bytes > 0)
872            .expect("at least one sample should have received_bytes > 0");
873        let last = samples.last().expect("samples is non-empty");
874
875        // Bytes advanced incrementally, not in one final jump.
876        assert!(last.received_bytes > 0);
877        assert!(
878            last.received_bytes > first_nonzero.received_bytes,
879            "received_bytes did not advance: first {} vs last {}",
880            first_nonzero.received_bytes,
881            last.received_bytes
882        );
883
884        // Objects advanced incrementally to the announced total.
885        assert!(
886            samples.iter().any(|sample| sample.received_objects > 0
887                && sample.received_objects < total),
888            "expected at least one intermediate object sample"
889        );
890        assert_eq!(last.received_objects, total);
891        assert_eq!(last.total_objects, Some(total));
892        assert_eq!(last.total_objects, Some(last.received_objects));
893
894        let _ = fs::remove_dir_all(&root);
895    }
896
897    #[test]
898    fn silent_progress_install_uses_default_no_op_path() {
899        let root = test_temp_root("sley-remote-install-progress-silent");
900        let format = ObjectFormat::Sha1;
901        let encoded = multi_object_raw_pack(format, 8);
902        let destination = FileObjectDatabase::new(root.join("objects"), format);
903
904        // The default `ProgressSink::transfer` is a no-op; installing through a
905        // ProgressInstaller wrapping SilentProgress must still succeed.
906        let mut sink = crate::SilentProgress;
907        let result = {
908            let installer = ProgressInstaller::new(&destination, &mut sink);
909            let mut reader = encoded.as_slice();
910            install_upload_pack_raw_response_from_reader(format, &mut reader, &installer, None)
911                .expect("test operation should succeed")
912        };
913        assert_eq!(result.object_ids.len(), 8);
914
915        // And installing directly (no wrapper at all) via the default trait
916        // method is unaffected.
917        let plain_root = test_temp_root("sley-remote-install-progress-plain");
918        let plain_db = FileObjectDatabase::new(plain_root.join("objects"), format);
919        let mut reader = encoded.as_slice();
920        let plain = install_upload_pack_raw_response_from_reader(format, &mut reader, &plain_db, None)
921            .expect("test operation should succeed");
922        assert_eq!(plain.object_ids.len(), 8);
923
924        let _ = fs::remove_dir_all(&root);
925        let _ = fs::remove_dir_all(&plain_root);
926    }
927
928    fn test_temp_root(prefix: &str) -> std::path::PathBuf {
929        let root = std::env::temp_dir().join(format!(
930            "{prefix}-{}-{}",
931            std::process::id(),
932            TEST_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
933        ));
934        let _ = fs::remove_dir_all(&root);
935        root
936    }
937
938    fn assert_pack_install(
939        objects_dir: &Path,
940        db: &FileObjectDatabase,
941        oid: &sley_core::ObjectId,
942        object: &EncodedObject,
943    ) {
944        assert!(
945            !db.loose()
946                .object_path(oid)
947                .expect("test operation should succeed")
948                .exists()
949        );
950        let pack_dir = objects_dir.join("pack");
951        let packs = fs::read_dir(&pack_dir)
952            .expect("test operation should succeed")
953            .map(|entry| entry.expect("test operation should succeed").path())
954            .collect::<Vec<_>>();
955        assert!(
956            packs
957                .iter()
958                .any(|path| path.extension().and_then(|ext| ext.to_str()) == Some("pack"))
959        );
960        assert!(
961            packs
962                .iter()
963                .any(|path| path.extension().and_then(|ext| ext.to_str()) == Some("idx"))
964        );
965        assert!(db.contains(oid).expect("test operation should succeed"));
966        assert_eq!(
967            db.read_object(oid)
968                .expect("test operation should succeed")
969                .as_ref(),
970            object
971        );
972    }
973
974    fn assert_promisor_sidecar(objects_dir: &Path) {
975        let pack_dir = objects_dir.join("pack");
976        let promisors = fs::read_dir(&pack_dir)
977            .expect("test operation should succeed")
978            .map(|entry| entry.expect("test operation should succeed").path())
979            .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("promisor"))
980            .collect::<Vec<_>>();
981        assert_eq!(promisors.len(), 1);
982        assert_eq!(
983            fs::read(&promisors[0]).expect("test operation should succeed"),
984            b""
985        );
986    }
987}