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
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 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
103pub 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
211pub 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, §ions)
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, §ions)
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, §ions)
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 for sample in &samples {
870 assert_eq!(sample.total_objects, Some(total));
871 }
872
873 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 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 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 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 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}