1#![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
15pub(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 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
104pub 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
200pub 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, §ions)
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, §ions)
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, §ions)
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 for sample in &samples {
850 assert_eq!(sample.total_objects, Some(total));
851 }
852
853 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 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 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 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 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}