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::{FileObjectDatabase, RawPackInstallOptions, RawPackInstallResult, RawPackInstaller};
8use sley_protocol::{
9    PktLineFrame, ProtocolV2FetchResponseHeader, ProtocolV2FetchResponseSection,
10    ProtocolV2FetchShallowInfo, SideBandChannel, demux_upload_pack_packfile_response,
11    parse_sideband_packet, read_pkt_line_frame, read_protocol_v2_fetch_response_header,
12    read_upload_pack_packfile_response, read_upload_pack_raw_packfile_response_header,
13    read_upload_pack_shallow_info_section,
14    read_upload_pack_shallow_info_and_raw_packfile_response_header,
15};
16use std::io::{Cursor, ErrorKind, Read};
17
18fn raw_pack_install_options(
19    promisor: bool,
20    max_input_size: Option<u64>,
21) -> RawPackInstallOptions {
22    RawPackInstallOptions {
23        promisor,
24        max_input_size,
25    }
26}
27
28pub fn install_upload_pack_raw_response_from_reader<I, R>(
29    format: ObjectFormat,
30    reader: &mut R,
31    destination: &I,
32    max_input_size: Option<u64>,
33) -> Result<RawPackInstallResult>
34where
35    I: RawPackInstaller,
36    R: Read,
37{
38    let header = read_upload_pack_raw_packfile_response_header(format, reader)?;
39    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
40    destination.install_raw_pack_from_reader_with_options(
41        &mut pack_reader,
42        raw_pack_install_options(false, max_input_size),
43    )
44}
45
46/// Install an upload-pack packfile response that may use side-band-64k.
47///
48/// Smart HTTP always delivers the pack inside sideband channel 1 after any
49/// leading `ACK`/`NAK` pkt-lines (see `read_upload_pack_packfile_response`).
50pub fn install_upload_pack_packfile_response_from_reader<I, R>(
51    format: ObjectFormat,
52    reader: &mut R,
53    destination: &I,
54    max_input_size: Option<u64>,
55) -> Result<RawPackInstallResult>
56where
57    I: RawPackInstaller,
58    R: Read,
59{
60    install_upload_pack_sideband_response_from_reader(format, reader, destination, false, max_input_size)
61}
62
63pub fn install_upload_pack_packfile_promisor_response_from_reader<R>(
64    format: ObjectFormat,
65    reader: &mut R,
66    destination: &FileObjectDatabase,
67    max_input_size: Option<u64>,
68) -> Result<RawPackInstallResult>
69where
70    R: Read,
71{
72    install_upload_pack_sideband_response_from_reader(format, reader, destination, true, max_input_size)
73}
74
75fn install_upload_pack_sideband_response_from_reader<I, R>(
76    format: ObjectFormat,
77    reader: &mut R,
78    destination: &I,
79    promisor: bool,
80    max_input_size: Option<u64>,
81) -> Result<RawPackInstallResult>
82where
83    I: RawPackInstaller,
84    R: Read,
85{
86    let response = read_upload_pack_packfile_response(format, reader)?;
87    let demuxed = demux_upload_pack_packfile_response(&response)?;
88    let mut pack_reader = demuxed.data.as_slice();
89    let result = destination.install_raw_pack_from_reader_with_options(
90        &mut pack_reader,
91        raw_pack_install_options(promisor, max_input_size),
92    )?;
93    Ok(if promisor {
94        RawPackInstallResult {
95            object_ids: result.object_ids,
96        }
97    } else {
98        result
99    })
100}
101
102pub fn install_upload_pack_raw_promisor_response_from_reader<R>(
103    format: ObjectFormat,
104    reader: &mut R,
105    destination: &FileObjectDatabase,
106    max_input_size: Option<u64>,
107) -> Result<RawPackInstallResult>
108where
109    R: Read,
110{
111    let header = read_upload_pack_raw_packfile_response_header(format, reader)?;
112    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
113    let result = destination.install_raw_pack_from_reader_with_options(
114        &mut pack_reader,
115        raw_pack_install_options(true, max_input_size),
116    )?;
117    Ok(RawPackInstallResult {
118        object_ids: result.object_ids,
119    })
120}
121
122pub fn install_upload_pack_shallow_raw_response_from_reader<I, R>(
123    format: ObjectFormat,
124    reader: &mut R,
125    destination: &I,
126    max_input_size: Option<u64>,
127) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
128where
129    I: RawPackInstaller,
130    R: Read,
131{
132    let (shallow, header) =
133        read_upload_pack_shallow_info_and_raw_packfile_response_header(format, reader)?;
134    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
135    let result = destination.install_raw_pack_from_reader_with_options(
136        &mut pack_reader,
137        raw_pack_install_options(false, max_input_size),
138    )?;
139    Ok((shallow, result))
140}
141
142/// Shallow deepen over smart HTTP: shallow-info section then a sideband pack.
143pub fn install_upload_pack_shallow_packfile_response_from_reader<I, R>(
144    format: ObjectFormat,
145    reader: &mut R,
146    destination: &I,
147    max_input_size: Option<u64>,
148) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
149where
150    I: RawPackInstaller,
151    R: Read,
152{
153    install_upload_pack_shallow_sideband_response_from_reader(
154        format,
155        reader,
156        destination,
157        false,
158        max_input_size,
159    )
160}
161
162fn install_upload_pack_shallow_sideband_response_from_reader<I, R>(
163    format: ObjectFormat,
164    reader: &mut R,
165    destination: &I,
166    promisor: bool,
167    max_input_size: Option<u64>,
168) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
169where
170    I: RawPackInstaller,
171    R: Read,
172{
173    let shallow = read_upload_pack_shallow_info_section(format, reader)?;
174    let result =
175        install_upload_pack_sideband_response_from_reader(format, reader, destination, promisor, max_input_size)?;
176    Ok((shallow, result))
177}
178
179pub fn install_upload_pack_shallow_raw_promisor_response_from_reader<R>(
180    format: ObjectFormat,
181    reader: &mut R,
182    destination: &FileObjectDatabase,
183    max_input_size: Option<u64>,
184) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
185where
186    R: Read,
187{
188    let (shallow, header) =
189        read_upload_pack_shallow_info_and_raw_packfile_response_header(format, reader)?;
190    let mut pack_reader = Cursor::new(header.pack_prefix).chain(reader);
191    let result = destination.install_raw_pack_from_reader_with_options(
192        &mut pack_reader,
193        raw_pack_install_options(true, max_input_size),
194    )?;
195    Ok((
196        shallow,
197        RawPackInstallResult {
198            object_ids: result.object_ids,
199        },
200    ))
201}
202
203pub fn install_upload_pack_shallow_packfile_promisor_response_from_reader<R>(
204    format: ObjectFormat,
205    reader: &mut R,
206    destination: &FileObjectDatabase,
207    max_input_size: Option<u64>,
208) -> Result<(Vec<ProtocolV2FetchShallowInfo>, RawPackInstallResult)>
209where
210    R: Read,
211{
212    install_upload_pack_shallow_sideband_response_from_reader(
213        format,
214        reader,
215        destination,
216        true,
217        max_input_size,
218    )
219}
220
221pub fn install_protocol_v2_fetch_response_from_reader<I, R>(
222    format: ObjectFormat,
223    reader: &mut R,
224    sideband_all: bool,
225    destination: &I,
226    max_input_size: Option<u64>,
227) -> Result<(ProtocolV2FetchResponseHeader, Option<RawPackInstallResult>)>
228where
229    I: RawPackInstaller,
230    R: Read,
231{
232    let header = read_protocol_v2_fetch_response_header(format, reader, sideband_all)?;
233    if !header.has_packfile {
234        return Ok((header, None));
235    }
236    let mut pack_reader = ProtocolV2PackfileReader::new(reader);
237    let result = destination.install_raw_pack_from_reader_with_options(
238        &mut pack_reader,
239        raw_pack_install_options(false, max_input_size),
240    )?;
241    Ok((header, Some(result)))
242}
243
244pub fn install_protocol_v2_fetch_promisor_response_from_reader<R>(
245    format: ObjectFormat,
246    reader: &mut R,
247    sideband_all: bool,
248    destination: &FileObjectDatabase,
249    max_input_size: Option<u64>,
250) -> Result<(ProtocolV2FetchResponseHeader, Option<RawPackInstallResult>)>
251where
252    R: Read,
253{
254    let header = read_protocol_v2_fetch_response_header(format, reader, sideband_all)?;
255    if !header.has_packfile {
256        return Ok((header, None));
257    }
258    let mut pack_reader = ProtocolV2PackfileReader::new(reader);
259    let result = destination.install_raw_pack_from_reader_with_options(
260        &mut pack_reader,
261        raw_pack_install_options(true, max_input_size),
262    )?;
263    Ok((
264        header,
265        Some(RawPackInstallResult {
266            object_ids: result.object_ids,
267        }),
268    ))
269}
270
271struct ProtocolV2PackfileReader<'a, R> {
272    reader: &'a mut R,
273    pending: Vec<u8>,
274    pending_offset: usize,
275    done: bool,
276}
277
278impl<'a, R> ProtocolV2PackfileReader<'a, R> {
279    fn new(reader: &'a mut R) -> Self {
280        Self {
281            reader,
282            pending: Vec::new(),
283            pending_offset: 0,
284            done: false,
285        }
286    }
287
288    fn drain_pending(&mut self, buf: &mut [u8]) -> usize {
289        let available = self.pending.len().saturating_sub(self.pending_offset);
290        let to_copy = available.min(buf.len());
291        if to_copy == 0 {
292            if self.pending_offset >= self.pending.len() {
293                self.pending.clear();
294                self.pending_offset = 0;
295            }
296            return 0;
297        }
298        let end = self.pending_offset + to_copy;
299        buf[..to_copy].copy_from_slice(&self.pending[self.pending_offset..end]);
300        self.pending_offset = end;
301        if self.pending_offset == self.pending.len() {
302            self.pending.clear();
303            self.pending_offset = 0;
304        }
305        to_copy
306    }
307}
308
309impl<R> Read for ProtocolV2PackfileReader<'_, R>
310where
311    R: Read,
312{
313    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
314        if buf.is_empty() {
315            return Ok(0);
316        }
317        let mut written = 0usize;
318        while written < buf.len() {
319            let copied = self.drain_pending(&mut buf[written..]);
320            if copied > 0 {
321                written += copied;
322                continue;
323            }
324            if self.done {
325                break;
326            }
327            let frame = read_pkt_line_frame(self.reader)
328                .map_err(git_error_to_io)?
329                .ok_or_else(|| {
330                    std::io::Error::new(
331                        ErrorKind::UnexpectedEof,
332                        "protocol v2 packfile ended before flush",
333                    )
334                })?;
335            match frame {
336                PktLineFrame::Data(payload) => {
337                    let packet = parse_sideband_packet(&payload).map_err(git_error_to_io)?;
338                    match packet.channel {
339                        SideBandChannel::Data => {
340                            self.pending = packet.data;
341                            self.pending_offset = 0;
342                        }
343                        SideBandChannel::Progress => {}
344                        SideBandChannel::Fatal => {
345                            let message = String::from_utf8_lossy(&packet.data).into_owned();
346                            return Err(std::io::Error::new(
347                                ErrorKind::InvalidData,
348                                format!("sideband fatal: {message}"),
349                            ));
350                        }
351                    }
352                }
353                PktLineFrame::Flush => {
354                    self.done = true;
355                    break;
356                }
357                PktLineFrame::Delimiter | PktLineFrame::ResponseEnd => {
358                    return Err(std::io::Error::new(
359                        ErrorKind::InvalidData,
360                        "protocol v2 packfile section ended unexpectedly",
361                    ));
362                }
363            }
364        }
365        Ok(written)
366    }
367}
368
369fn git_error_to_io(err: sley_core::GitError) -> std::io::Error {
370    std::io::Error::new(ErrorKind::InvalidData, err.to_string())
371}
372
373pub fn shallow_info_from_protocol_v2_fetch_header(
374    header: &ProtocolV2FetchResponseHeader,
375) -> Vec<ProtocolV2FetchShallowInfo> {
376    let mut shallow_info = Vec::new();
377    for section in &header.sections {
378        if let ProtocolV2FetchResponseSection::ShallowInfo(entries) = section {
379            shallow_info.extend(entries.clone());
380        }
381    }
382    shallow_info
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use sley_core::ObjectFormat;
389    use sley_object::{EncodedObject, ObjectType};
390    use sley_odb::{FileObjectDatabase, ObjectDatabase, ObjectReader};
391    use sley_pack::PackFile;
392    use sley_protocol::{
393        SideBandChannel, SideBandPacket, UploadPackAcknowledgment, UploadPackRawPackfileResponse,
394        encode_sideband_packet, encode_upload_pack_raw_packfile_response, write_pkt_line_payload,
395        write_protocol_v2_fetch_response, write_upload_pack_raw_packfile_response,
396    };
397    use std::fs;
398    use std::path::Path;
399    use std::sync::atomic::{AtomicU64, Ordering};
400
401    static TEST_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
402
403    #[test]
404    fn raw_upload_pack_response_stream_installs_pack_without_buffering_response() {
405        let root = test_temp_root("sley-remote-install-upload-pack-raw-stream-install");
406        let format = ObjectFormat::Sha1;
407        let object = EncodedObject::new(ObjectType::Blob, b"raw streamed upload-pack\n".to_vec());
408        let oid = object
409            .object_id(format)
410            .expect("test operation should succeed");
411        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
412            .expect("test operation should succeed");
413        let response = UploadPackRawPackfileResponse {
414            acknowledgments: vec![UploadPackAcknowledgment::Nak],
415            packfile: pack.pack,
416        };
417        let encoded =
418            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
419        let destination = FileObjectDatabase::new(root.join("objects"), format);
420        let mut reader = encoded.as_slice();
421
422        let result =
423            install_upload_pack_raw_response_from_reader(format, &mut reader, &destination, None)
424                .expect("test operation should succeed");
425
426        assert_eq!(result.object_ids, vec![oid]);
427        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
428        let _ = fs::remove_dir_all(&root);
429    }
430
431    #[test]
432    fn shallow_raw_upload_pack_response_stream_installs_pack_without_buffering_response() {
433        let root = test_temp_root("sley-remote-install-upload-pack-shallow-raw-stream-install");
434        let format = ObjectFormat::Sha1;
435        let shallow_oid =
436            sley_core::ObjectId::from_hex(format, "1111111111111111111111111111111111111111")
437                .expect("test operation should succeed");
438        let object =
439            EncodedObject::new(ObjectType::Blob, b"shallow streamed upload-pack\n".to_vec());
440        let oid = object
441            .object_id(format)
442            .expect("test operation should succeed");
443        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
444            .expect("test operation should succeed");
445        let response = UploadPackRawPackfileResponse {
446            acknowledgments: vec![UploadPackAcknowledgment::Nak],
447            packfile: pack.pack,
448        };
449        let mut encoded = Vec::new();
450        write_pkt_line_payload(&mut encoded, format!("shallow {shallow_oid}\n").as_bytes())
451            .expect("test operation should succeed");
452        encoded.extend_from_slice(b"0000");
453        write_upload_pack_raw_packfile_response(&mut encoded, &response)
454            .expect("test operation should succeed");
455        let destination = FileObjectDatabase::new(root.join("objects"), format);
456        let mut reader = encoded.as_slice();
457
458        let (shallow, result) =
459            install_upload_pack_shallow_raw_response_from_reader(format, &mut reader, &destination, None)
460                .expect("test operation should succeed");
461
462        assert_eq!(
463            shallow,
464            vec![ProtocolV2FetchShallowInfo::Shallow(shallow_oid)]
465        );
466        assert_eq!(result.object_ids, vec![oid]);
467        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
468        let _ = fs::remove_dir_all(&root);
469    }
470
471    #[test]
472    fn raw_upload_pack_response_installs_promisor_pack_sidecar() {
473        let root = test_temp_root("sley-remote-install-upload-pack-promisor-install");
474        let format = ObjectFormat::Sha1;
475        let object = EncodedObject::new(ObjectType::Blob, b"promisor upload-pack\n".to_vec());
476        let oid = object
477            .object_id(format)
478            .expect("test operation should succeed");
479        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
480            .expect("test operation should succeed");
481        let response = UploadPackRawPackfileResponse {
482            acknowledgments: Vec::new(),
483            packfile: pack.pack,
484        };
485        let mut encoded = Vec::new();
486        write_upload_pack_raw_packfile_response(&mut encoded, &response)
487            .expect("test operation should succeed");
488        let destination = FileObjectDatabase::new(root.join("objects"), format);
489        let mut reader = encoded.as_slice();
490
491        let result = install_upload_pack_raw_promisor_response_from_reader(
492            format,
493            &mut reader,
494            &destination,
495            None,
496        )
497        .expect("test operation should succeed");
498
499        assert_eq!(result.object_ids, vec![oid]);
500        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
501        assert_promisor_sidecar(&root.join("objects"));
502        let _ = fs::remove_dir_all(&root);
503    }
504
505    #[test]
506    fn protocol_v2_fetch_response_packfile_demuxes_and_installs_pack() {
507        let root = test_temp_root("sley-remote-install-v2-response-install");
508        let format = ObjectFormat::Sha1;
509        let object = EncodedObject::new(ObjectType::Blob, b"v2 response packfile\n".to_vec());
510        let oid = object
511            .object_id(format)
512            .expect("test operation should succeed");
513        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
514            .expect("test operation should succeed");
515        let sections = vec![ProtocolV2FetchResponseSection::Packfile(vec![
516            encode_sideband_packet(&SideBandPacket {
517                channel: SideBandChannel::Progress,
518                data: b"counting objects\n".to_vec(),
519            })
520            .expect("test operation should succeed"),
521            encode_sideband_packet(&SideBandPacket {
522                channel: SideBandChannel::Data,
523                data: pack.pack,
524            })
525            .expect("test operation should succeed"),
526        ])];
527        let mut encoded = Vec::new();
528        write_protocol_v2_fetch_response(&mut encoded, &sections)
529            .expect("test operation should succeed");
530        let destination = FileObjectDatabase::new(root.join("objects"), format);
531        let mut reader = encoded.as_slice();
532
533        let (header, result) = install_protocol_v2_fetch_response_from_reader(
534            format,
535            &mut reader,
536            false,
537            &destination,
538            None,
539        )
540        .expect("test operation should succeed");
541        let result = result.expect("packfile should be installed");
542
543        assert!(header.has_packfile);
544        assert!(header.sections.is_empty());
545        assert_eq!(result.object_ids, vec![oid]);
546        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
547        let _ = fs::remove_dir_all(&root);
548    }
549
550    #[test]
551    fn protocol_v2_fetch_response_packfile_installs_promisor_sidecar() {
552        let root = test_temp_root("sley-remote-install-v2-response-promisor-install");
553        let format = ObjectFormat::Sha1;
554        let object = EncodedObject::new(ObjectType::Blob, b"v2 promisor packfile\n".to_vec());
555        let oid = object
556            .object_id(format)
557            .expect("test operation should succeed");
558        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
559            .expect("test operation should succeed");
560        let sections = vec![ProtocolV2FetchResponseSection::Packfile(vec![
561            encode_sideband_packet(&SideBandPacket {
562                channel: SideBandChannel::Data,
563                data: pack.pack,
564            })
565            .expect("test operation should succeed"),
566        ])];
567        let mut encoded = Vec::new();
568        write_protocol_v2_fetch_response(&mut encoded, &sections)
569            .expect("test operation should succeed");
570        let destination = FileObjectDatabase::new(root.join("objects"), format);
571        let mut reader = encoded.as_slice();
572
573        let (header, result) = install_protocol_v2_fetch_promisor_response_from_reader(
574            format,
575            &mut reader,
576            false,
577            &destination,
578            None,
579        )
580        .expect("test operation should succeed");
581        let result = result.expect("packfile should be installed");
582
583        assert!(header.has_packfile);
584        assert_eq!(result.object_ids, vec![oid]);
585        assert_pack_install(&root.join("objects"), &destination, &oid, &object);
586        assert_promisor_sidecar(&root.join("objects"));
587        let _ = fs::remove_dir_all(&root);
588    }
589
590    #[test]
591    fn protocol_v2_fetch_response_without_packfile_installs_nothing() {
592        let root = test_temp_root("sley-remote-install-v2-response-empty");
593        let destination = FileObjectDatabase::new(root.join("objects"), ObjectFormat::Sha1);
594        let sections = vec![ProtocolV2FetchResponseSection::Acknowledgments(Vec::new())];
595        let mut encoded = Vec::new();
596        write_protocol_v2_fetch_response(&mut encoded, &sections)
597            .expect("test operation should succeed");
598        let mut reader = encoded.as_slice();
599
600        let (header, result) = install_protocol_v2_fetch_response_from_reader(
601            ObjectFormat::Sha1,
602            &mut reader,
603            false,
604            &destination,
605            None,
606        )
607        .expect("test operation should succeed");
608
609        assert!(!header.has_packfile);
610        assert!(result.is_none());
611        assert!(!root.join("objects").join("pack").exists());
612        let _ = fs::remove_dir_all(&root);
613    }
614
615    #[test]
616    fn raw_upload_pack_response_rejects_pack_exceeding_max_input_size() {
617        let root = test_temp_root("sley-remote-install-upload-pack-max-size");
618        let format = ObjectFormat::Sha1;
619        let object = EncodedObject::new(ObjectType::Blob, b"oversized fetch pack\n".to_vec());
620        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
621            .expect("test operation should succeed");
622        let response = UploadPackRawPackfileResponse {
623            acknowledgments: Vec::new(),
624            packfile: pack.pack,
625        };
626        let encoded =
627            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
628        let destination = FileObjectDatabase::new(root.join("objects"), format);
629        let mut reader = encoded.as_slice();
630        let limit = 32u64;
631
632        let err = install_upload_pack_raw_response_from_reader(
633            format,
634            &mut reader,
635            &destination,
636            Some(limit),
637        )
638        .expect_err("oversized pack should be rejected");
639
640        assert!(
641            err.to_string().contains("pack exceeds maximum allowed size"),
642            "unexpected error: {err}"
643        );
644        let pack_dir = root.join("objects").join("pack");
645        let installed = fs::read_dir(&pack_dir)
646            .map(|entries| {
647                entries
648                    .filter_map(|entry| entry.ok())
649                    .filter(|entry| {
650                        entry
651                            .path()
652                            .extension()
653                            .and_then(|ext| ext.to_str())
654                            == Some("pack")
655                    })
656                    .count()
657            })
658            .unwrap_or_default();
659        assert_eq!(installed, 0);
660        let _ = fs::remove_dir_all(&root);
661    }
662
663    #[test]
664    fn install_helpers_accept_custom_raw_pack_installer() {
665        #[derive(Default)]
666        struct RecordingInstaller {
667            packs: std::cell::RefCell<Vec<Vec<u8>>>,
668        }
669
670        impl RawPackInstaller for RecordingInstaller {
671            fn install_raw_pack_from_reader_with_options<R>(
672                &self,
673                reader: &mut R,
674                _options: RawPackInstallOptions,
675            ) -> Result<RawPackInstallResult>
676            where
677                R: Read,
678            {
679                let mut pack_bytes = Vec::new();
680                reader.read_to_end(&mut pack_bytes)?;
681                self.packs.borrow_mut().push(pack_bytes.to_vec());
682                Ok(RawPackInstallResult {
683                    object_ids: Vec::new(),
684                })
685            }
686        }
687
688        let installer = RecordingInstaller::default();
689        let response = UploadPackRawPackfileResponse {
690            acknowledgments: Vec::new(),
691            packfile: b"PACKcustom".to_vec(),
692        };
693        let encoded =
694            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
695        let mut reader = encoded.as_slice();
696
697        let result = install_upload_pack_raw_response_from_reader(
698            ObjectFormat::Sha1,
699            &mut reader, &installer, None)
700        .expect("test operation should succeed");
701
702        assert!(result.object_ids.is_empty());
703        assert_eq!(installer.packs.into_inner(), vec![b"PACKcustom".to_vec()]);
704    }
705
706    #[test]
707    fn raw_upload_pack_response_installs_into_in_memory_database() {
708        let format = ObjectFormat::Sha1;
709        let object = EncodedObject::new(ObjectType::Blob, b"in memory fetch pack\n".to_vec());
710        let oid = object
711            .object_id(format)
712            .expect("test operation should succeed");
713        let pack = PackFile::write_undeltified(std::slice::from_ref(&object), format)
714            .expect("test operation should succeed");
715        let response = UploadPackRawPackfileResponse {
716            acknowledgments: Vec::new(),
717            packfile: pack.pack,
718        };
719        let encoded =
720            encode_upload_pack_raw_packfile_response(&response).expect("response should encode");
721        let destination = ObjectDatabase::new(format);
722        let mut reader = encoded.as_slice();
723
724        let result =
725            install_upload_pack_raw_response_from_reader(format, &mut reader, &destination, None)
726                .expect("test operation should succeed");
727
728        assert_eq!(result.object_ids, vec![oid]);
729        assert_eq!(
730            destination
731                .read_object(&oid)
732                .expect("test operation should succeed")
733                .as_ref(),
734            &object
735        );
736    }
737
738    fn test_temp_root(prefix: &str) -> std::path::PathBuf {
739        let root = std::env::temp_dir().join(format!(
740            "{prefix}-{}-{}",
741            std::process::id(),
742            TEST_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
743        ));
744        let _ = fs::remove_dir_all(&root);
745        root
746    }
747
748    fn assert_pack_install(
749        objects_dir: &Path,
750        db: &FileObjectDatabase,
751        oid: &sley_core::ObjectId,
752        object: &EncodedObject,
753    ) {
754        assert!(
755            !db.loose()
756                .object_path(oid)
757                .expect("test operation should succeed")
758                .exists()
759        );
760        let pack_dir = objects_dir.join("pack");
761        let packs = fs::read_dir(&pack_dir)
762            .expect("test operation should succeed")
763            .map(|entry| entry.expect("test operation should succeed").path())
764            .collect::<Vec<_>>();
765        assert!(
766            packs
767                .iter()
768                .any(|path| path.extension().and_then(|ext| ext.to_str()) == Some("pack"))
769        );
770        assert!(
771            packs
772                .iter()
773                .any(|path| path.extension().and_then(|ext| ext.to_str()) == Some("idx"))
774        );
775        assert!(db.contains(oid).expect("test operation should succeed"));
776        assert_eq!(
777            db.read_object(oid)
778                .expect("test operation should succeed")
779                .as_ref(),
780            object
781        );
782    }
783
784    fn assert_promisor_sidecar(objects_dir: &Path) {
785        let pack_dir = objects_dir.join("pack");
786        let promisors = fs::read_dir(&pack_dir)
787            .expect("test operation should succeed")
788            .map(|entry| entry.expect("test operation should succeed").path())
789            .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("promisor"))
790            .collect::<Vec<_>>();
791        assert_eq!(promisors.len(), 1);
792        assert_eq!(
793            fs::read(&promisors[0]).expect("test operation should succeed"),
794            b""
795        );
796    }
797}