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