This repository has no description
1use std::collections::HashMap;
2use std::io::Write;
3use std::path::Path;
4
5use knot_git::{Filter, RefTxn, RefUpdate, Repo};
6use knot_messages::{RefKey, RejectMessages};
7use knot_types::{ObjectFormat, Oid, PushOption, PushOptions, RefName};
8
9use crate::error::PackError;
10use crate::meter::PackLimits;
11use crate::objects;
12use crate::pkt;
13use crate::quarantine::Quarantine;
14use crate::receiver::ReceivedPack;
15use crate::{HaveOids, WantOids};
16
17fn stage_pack_bytes(
18 dir: &Path,
19 pack: &[u8],
20 kind: gix::hash::Kind,
21) -> Result<Option<(tempfile::NamedTempFile, gix_pack::data::File)>, PackError> {
22 if pack.is_empty() {
23 return Ok(None);
24 }
25 if !pack.starts_with(b"PACK") {
26 return Err(PackError::Pack(
27 "packfile is missing its PACK signature".to_string(),
28 ));
29 }
30 let mut tmp = tempfile::NamedTempFile::new_in(dir)?;
31 tmp.write_all(pack)?;
32 tmp.flush()?;
33 let file = gix_pack::data::File::at(tmp.path(), kind)
34 .map_err(|error| PackError::Pack(error.to_string()))?;
35 Ok(Some((tmp, file)))
36}
37
38pub fn handle_bytes(repo: &Repo, body: &[u8], limits: &PackLimits) -> Result<Vec<u8>, PackError> {
39 let kind = repo.object_format().kind();
40 let messages = &crate::default_catalog().reject;
41 match stage_pack_bytes(&repo.objects_dir(), pkt::split_receive(body)?.pack, kind) {
42 Ok(staged) => handle(
43 repo,
44 body,
45 Ok(staged.as_ref().map(|(_, file)| file)),
46 limits,
47 messages,
48 ),
49 Err(error) => handle(repo, body, Err(error), limits, messages),
50 }
51}
52
53pub fn handle_guarded_bytes(
54 live: &Repo,
55 body: &[u8],
56 limits: &PackLimits,
57 guard: &dyn ReceiveGuard,
58 seal: &dyn Fn(&[RefUpdate]),
59 messages: &RejectMessages,
60) -> Result<ReceiveOutcome, PackError> {
61 let kind = live.object_format().kind();
62 match stage_pack_bytes(&live.objects_dir(), pkt::split_receive(body)?.pack, kind) {
63 Ok(staged) => handle_guarded(
64 live,
65 body,
66 Ok(staged.as_ref().map(|(_, file)| file)),
67 limits,
68 guard,
69 seal,
70 messages,
71 ),
72 Err(error) => handle_guarded(live, body, Err(error), limits, guard, seal, messages),
73 }
74}
75
76pub fn handle_guarded_streamed(
77 live: &Repo,
78 received: &ReceivedPack,
79 limits: &PackLimits,
80 guard: &dyn ReceiveGuard,
81 seal: &dyn Fn(&[RefUpdate]),
82 messages: &RejectMessages,
83) -> Result<ReceiveOutcome, PackError> {
84 match received.open_pack() {
85 Ok(pack) => handle_guarded(
86 live,
87 received.preamble(),
88 Ok(pack.as_ref()),
89 limits,
90 guard,
91 seal,
92 messages,
93 ),
94 Err(error) => handle_guarded(
95 live,
96 received.preamble(),
97 Err(error),
98 limits,
99 guard,
100 seal,
101 messages,
102 ),
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum RefDecision {
108 Allow,
109 Reject(String),
110}
111
112pub struct ReceiveOutcome {
113 pub report: Vec<u8>,
114 pub side_band: bool,
115 pub push_options: PushOptions,
116}
117
118pub trait ReceiveGuard {
119 fn authorize(&self, staged: &Repo, commands: &[ReceiveCommand]) -> Vec<RefDecision>;
120}
121
122const CAPS_BASE: &str =
123 "report-status delete-refs atomic ofs-delta side-band-64k push-options agent=knot/0";
124
125fn caps(format: ObjectFormat) -> String {
126 format!("{CAPS_BASE} object-format={}", format.capability())
127}
128
129pub fn advertise(repo: &Repo) -> Result<Vec<u8>, PackError> {
130 let mut buf = Vec::new();
131 pkt::write_data(&mut buf, b"# service=git-receive-pack\n")?;
132 pkt::write_flush(&mut buf)?;
133 write_advert(&mut buf, repo)?;
134 Ok(buf)
135}
136
137pub fn advertise_ssh(repo: &Repo) -> Result<Vec<u8>, PackError> {
138 let mut buf = Vec::new();
139 write_advert(&mut buf, repo)?;
140 Ok(buf)
141}
142
143fn write_advert(buf: &mut Vec<u8>, repo: &Repo) -> Result<(), PackError> {
144 let format = repo.object_format();
145 let caps = caps(format);
146 let refs = repo.advertised_refs_for(knot_git::AdvertScope::Receive)?;
147 match refs.split_first() {
148 Some((first, rest)) => {
149 let mut line = format!("{} {}", first.target, first.name).into_bytes();
150 line.push(0);
151 line.extend_from_slice(caps.as_bytes());
152 line.push(b'\n');
153 pkt::write_data(buf, &line)?;
154 rest.iter().try_fold(&mut *buf, |buf, record| {
155 pkt::write_data(
156 buf,
157 format!("{} {}\n", record.target, record.name).as_bytes(),
158 )?;
159 Ok::<_, PackError>(buf)
160 })?;
161 }
162 None => {
163 let mut line = format!("{} capabilities^{{}}", format.null_oid()).into_bytes();
164 line.push(0);
165 line.extend_from_slice(caps.as_bytes());
166 line.push(b'\n');
167 pkt::write_data(buf, &line)?;
168 }
169 }
170 pkt::write_flush(buf)?;
171 Ok(())
172}
173
174enum CommandRef {
175 Named(RefName),
176 Unparsed(String),
177}
178
179pub struct ReceiveCommand {
180 old: Oid,
181 new: Oid,
182 name: CommandRef,
183}
184
185impl ReceiveCommand {
186 pub fn refname(&self) -> &str {
187 match &self.name {
188 CommandRef::Named(name) => name.as_str(),
189 CommandRef::Unparsed(raw) => raw,
190 }
191 }
192
193 pub fn name(&self) -> Option<&RefName> {
194 match &self.name {
195 CommandRef::Named(name) => Some(name),
196 CommandRef::Unparsed(_) => None,
197 }
198 }
199
200 pub fn is_delete(&self) -> bool {
201 self.new.is_null()
202 }
203
204 pub fn is_create(&self) -> bool {
205 self.old.is_null()
206 }
207
208 fn parse(line: &[u8], first: bool) -> Option<ReceiveCommand> {
209 let line = if first {
210 line.split(|byte| *byte == 0).next().unwrap_or(line)
211 } else {
212 line
213 };
214 let text = std::str::from_utf8(line).ok()?;
215 let mut parts = text.trim_end().split(' ');
216 let old = Oid::from_hex(parts.next()?).ok()?;
217 let new = Oid::from_hex(parts.next()?).ok()?;
218 let raw = parts.next()?.to_string();
219 let name = match RefName::new(raw.as_str()) {
220 Ok(name) => CommandRef::Named(name),
221 Err(_) => CommandRef::Unparsed(raw),
222 };
223 Some(ReceiveCommand { old, new, name })
224 }
225
226 fn to_update(&self) -> Result<RefUpdate, PackError> {
227 let name = self
228 .name()
229 .cloned()
230 .ok_or_else(|| PackError::Protocol(invalid_refname(self.refname())))?;
231 Ok(match (self.old.is_null(), self.new.is_null()) {
232 (_, true) => RefUpdate::Delete {
233 name,
234 old: self.old,
235 },
236 (true, false) => RefUpdate::Create {
237 name,
238 new: self.new,
239 },
240 (false, false) => RefUpdate::Update {
241 name,
242 old: self.old,
243 new: self.new,
244 },
245 })
246 }
247}
248
249pub(crate) fn invalid_refname(raw: &str) -> String {
250 format!("invalid ref name {raw:?}")
251}
252
253fn forbidden_ref(command: &ReceiveCommand, messages: &RejectMessages) -> Option<String> {
254 match command.name() {
255 Some(name) => (!knot_git::is_public_ref(name)).then(|| messages.reserved_refs.text()),
256 None => Some(invalid_refname(command.refname())),
257 }
258}
259
260fn reserved_create_only(command: &ReceiveCommand, messages: &RejectMessages) -> Option<String> {
261 (command.name().is_some_and(knot_git::is_reserved) && !command.is_create())
262 .then(|| messages.cob_create_only.text())
263}
264
265struct RefSnapshot {
266 by_name: HashMap<RefName, Oid>,
267}
268
269impl RefSnapshot {
270 fn capture(repo: &Repo) -> Result<RefSnapshot, PackError> {
271 let by_name = repo
272 .references()?
273 .into_iter()
274 .map(|record| (record.name, record.target))
275 .collect();
276 Ok(RefSnapshot { by_name })
277 }
278
279 fn tips(&self) -> HaveOids {
280 self.by_name.values().copied().collect()
281 }
282
283 fn conflict(&self, command: &ReceiveCommand, messages: &RejectMessages) -> Option<String> {
284 match (
285 command.old.is_null(),
286 command
287 .name()
288 .and_then(|name| self.by_name.get(name).copied()),
289 ) {
290 (true, Some(_)) => Some(messages.ref_exists.text()),
291 (false, found) if found != Some(command.old) => Some(messages.stale_old_value.text()),
292 _ => None,
293 }
294 }
295}
296
297struct RefResult {
298 refname: String,
299 failure: Option<String>,
300}
301
302impl RefResult {
303 fn of(command: &ReceiveCommand, failure: Option<String>) -> RefResult {
304 RefResult {
305 refname: command.refname().to_string(),
306 failure,
307 }
308 }
309}
310
311struct Conflict {
312 refname: String,
313 reason: String,
314}
315
316fn first_conflict(
317 snapshot: &RefSnapshot,
318 commands: &[ReceiveCommand],
319 messages: &RejectMessages,
320) -> Option<Conflict> {
321 commands.iter().find_map(|command| {
322 snapshot.conflict(command, messages).map(|reason| Conflict {
323 refname: command.refname().to_string(),
324 reason,
325 })
326 })
327}
328
329fn objects_present(
330 repo: &Repo,
331 wants: &WantOids,
332 haves: &HaveOids,
333 closure: Option<&objects::FreshClosure>,
334) -> bool {
335 if let Some(closure) = closure {
336 return closure.self_contained
337 && wants
338 .as_slice()
339 .iter()
340 .all(|want| closure.present.contains(want) || repo.contains(*want));
341 }
342 matches!(
343 repo.select_pack_objects_filtered(
344 wants.wants(),
345 haves.haves(),
346 Filter::None,
347 crate::upload::selection_budget(),
348 ),
349 Ok(selection) if selection.send.iter().all(|oid| repo.contains(*oid))
350 )
351}
352
353fn connectivity_reasons(
354 repo: &Repo,
355 commands: &[ReceiveCommand],
356 haves: &HaveOids,
357 closure: Option<&objects::FreshClosure>,
358 messages: &RejectMessages,
359) -> Vec<Option<String>> {
360 let news: WantOids = commands
361 .iter()
362 .map(|command| command.new)
363 .filter(|new| !new.is_null())
364 .collect();
365 let batched_ok = news.is_empty() || objects_present(repo, &news, haves, closure);
366 commands
367 .iter()
368 .map(|command| {
369 if command.new.is_null()
370 || batched_ok
371 || objects_present(repo, &WantOids::new(vec![command.new]), haves, closure)
372 {
373 None
374 } else {
375 Some(messages.missing_objects.text())
376 }
377 })
378 .collect()
379}
380
381pub(crate) fn fuzz(body: &[u8]) {
382 if let Ok(parsed) = pkt::split_receive(body) {
383 parsed
384 .commands
385 .iter()
386 .enumerate()
387 .for_each(|(index, line)| {
388 if let Some(command) = ReceiveCommand::parse(line, index == 0) {
389 let _ = command.to_update();
390 }
391 });
392 }
393}
394
395pub(crate) fn is_empty(repo: &Repo) -> bool {
396 repo.references()
397 .map(|refs| refs.is_empty())
398 .unwrap_or(false)
399}
400
401pub(crate) fn ingest(
402 objects_dir: &Path,
403 pack: Option<&gix_pack::data::File>,
404 limits: &PackLimits,
405 kind: gix::hash::Kind,
406 live_empty: bool,
407) -> (Result<(), PackError>, Option<objects::FreshClosure>) {
408 let Some(pack) = pack else {
409 return (Ok(()), None);
410 };
411 if let Err(error) = objects::admit_ingest(pack, kind) {
412 return (Err(error), None);
413 }
414 if live_empty {
415 match objects::ingest_and_close(
416 objects_dir,
417 pack,
418 limits,
419 kind,
420 knot_resource::ingest_base_budget(),
421 false,
422 ) {
423 Ok(Some(closure)) => return (Ok(()), Some(closure)),
424 Ok(None) => {}
425 Err(error) => return (Err(error), None),
426 }
427 }
428 (
429 objects::index_pack_bounded(objects_dir, pack, limits, kind),
430 None,
431 )
432}
433
434pub fn handle(
435 repo: &Repo,
436 body: &[u8],
437 pack: Result<Option<&gix_pack::data::File>, PackError>,
438 limits: &PackLimits,
439 messages: &RejectMessages,
440) -> Result<Vec<u8>, PackError> {
441 let parsed = pkt::split_receive(body)?;
442 let atomic = parsed.caps.atomic;
443 let commands = parse_commands(&parsed);
444 let kind = repo.object_format().kind();
445
446 let (unpack, closure) = match pack {
447 Ok(pack) => ingest(&repo.objects_dir(), pack, limits, kind, is_empty(repo)),
448 Err(error) => (Err(error), None),
449 };
450 let results: Vec<RefResult> = match &unpack {
451 Err(_) => all_failed(&commands, &messages.unpacker_error.text()),
452 Ok(()) => match RefSnapshot::capture(repo) {
453 Err(_) => all_failed(&commands, &messages.ref_snapshot_unavailable.text()),
454 Ok(snapshot) => {
455 let haves = snapshot.tips();
456 if atomic {
457 apply_atomic(
458 repo,
459 &snapshot,
460 &commands,
461 &haves,
462 closure.as_ref(),
463 messages,
464 )
465 } else {
466 commands
467 .iter()
468 .zip(connectivity_reasons(
469 repo,
470 &commands,
471 &haves,
472 closure.as_ref(),
473 messages,
474 ))
475 .map(|(command, connectivity)| {
476 RefResult::of(command, apply_one(repo, command, connectivity, messages))
477 })
478 .collect()
479 }
480 }
481 },
482 };
483 report(&unpack, &results)
484 .map(|report| pkt::frame_report(&report, &[], parsed.caps.side_band_64k))
485}
486
487fn apply_one(
488 repo: &Repo,
489 command: &ReceiveCommand,
490 connectivity: Option<String>,
491 messages: &RejectMessages,
492) -> Option<String> {
493 if let Some(reason) = forbidden_ref(command, messages) {
494 return Some(reason);
495 }
496 if let Some(reason) = connectivity {
497 return Some(reason);
498 }
499 command
500 .to_update()
501 .and_then(|update| repo.update_ref(&update).map_err(PackError::from))
502 .err()
503 .map(|error| error.to_string().replace('\n', " "))
504}
505
506fn atomic_failure(
507 snapshot: &RefSnapshot,
508 commands: &[ReceiveCommand],
509 messages: &RejectMessages,
510) -> Vec<RefResult> {
511 let conflict = first_conflict(snapshot, commands, messages);
512 commands
513 .iter()
514 .map(|command| match &conflict {
515 Some(conflict) if conflict.refname == command.refname() => {
516 RefResult::of(command, Some(conflict.reason.clone()))
517 }
518 _ => RefResult::of(command, Some(messages.atomic_failed.text())),
519 })
520 .collect()
521}
522
523fn apply_atomic(
524 repo: &Repo,
525 snapshot: &RefSnapshot,
526 commands: &[ReceiveCommand],
527 haves: &HaveOids,
528 closure: Option<&objects::FreshClosure>,
529 messages: &RejectMessages,
530) -> Vec<RefResult> {
531 let fail = |reason: String| all_failed(commands, &reason);
532 if commands
533 .iter()
534 .any(|command| forbidden_ref(command, messages).is_some())
535 {
536 return commands
537 .iter()
538 .map(|command| {
539 let reason = forbidden_ref(command, messages)
540 .unwrap_or_else(|| messages.atomic_aborted.text());
541 RefResult::of(command, Some(reason))
542 })
543 .collect();
544 }
545 if let Some(command) = commands
546 .iter()
547 .zip(connectivity_reasons(
548 repo, commands, haves, closure, messages,
549 ))
550 .find_map(|(command, reason)| reason.map(|_| command))
551 {
552 return fail(
553 messages
554 .missing_objects_for
555 .line(|RefKey::Ref| command.refname().to_string()),
556 );
557 }
558 let updates = match commands
559 .iter()
560 .map(ReceiveCommand::to_update)
561 .collect::<Result<Vec<_>, _>>()
562 {
563 Ok(updates) => updates,
564 Err(error) => return fail(error.to_string().replace('\n', " ")),
565 };
566 match repo.update_refs(&updates) {
567 Ok(()) => commands
568 .iter()
569 .map(|command| RefResult::of(command, None))
570 .collect(),
571 Err(_) => atomic_failure(snapshot, commands, messages),
572 }
573}
574
575fn report(unpack: &Result<(), PackError>, results: &[RefResult]) -> Result<Vec<u8>, PackError> {
576 let mut buf = Vec::new();
577 match unpack {
578 Ok(()) => pkt::write_data(&mut buf, b"unpack ok\n")?,
579 Err(error) => pkt::write_data(
580 &mut buf,
581 format!("unpack {}\n", error.to_string().replace('\n', " ")).as_bytes(),
582 )?,
583 }
584 results.iter().try_fold(&mut buf, |buf, result| {
585 let line = match &result.failure {
586 None => format!("ok {}\n", result.refname),
587 Some(reason) => format!("ng {} {reason}\n", result.refname),
588 };
589 pkt::write_data(buf, line.as_bytes())?;
590 Ok::<_, PackError>(buf)
591 })?;
592 pkt::write_flush(&mut buf)?;
593 Ok(buf)
594}
595
596fn parse_commands(parsed: &pkt::Receive) -> Vec<ReceiveCommand> {
597 parsed
598 .commands
599 .iter()
600 .enumerate()
601 .filter_map(|(index, line)| ReceiveCommand::parse(line, index == 0))
602 .collect()
603}
604
605pub struct Preflight {
606 pub creates_branch: bool,
607}
608
609pub(crate) fn preflight(body: &[u8]) -> Preflight {
610 pkt::split_receive(body)
611 .map(|parsed| {
612 let commands = parse_commands(&parsed);
613 Preflight {
614 creates_branch: commands.iter().any(|command| {
615 command.is_create() && command.name().is_some_and(knot_git::is_branch)
616 }),
617 }
618 })
619 .unwrap_or(Preflight {
620 creates_branch: false,
621 })
622}
623
624fn all_failed(commands: &[ReceiveCommand], reason: &str) -> Vec<RefResult> {
625 commands
626 .iter()
627 .map(|command| RefResult::of(command, Some(reason.to_string())))
628 .collect()
629}
630
631fn stage_to_quarantine(staged: &Repo, commands: &[ReceiveCommand]) {
632 commands
633 .iter()
634 .filter(|command| !command.is_delete())
635 .for_each(|command| {
636 if let Some(name) = command.name() {
637 let _ = staged.update_ref(&RefUpdate::Create {
638 name: name.clone(),
639 new: command.new,
640 });
641 }
642 });
643}
644
645fn apply_guarded_atomic(
646 txn: &RefTxn<'_>,
647 snapshot: &RefSnapshot,
648 commands: &[ReceiveCommand],
649 seal: &dyn Fn(&[RefUpdate]),
650 messages: &RejectMessages,
651) -> Vec<RefResult> {
652 let updates = match commands
653 .iter()
654 .map(ReceiveCommand::to_update)
655 .collect::<Result<Vec<_>, _>>()
656 {
657 Ok(updates) => updates,
658 Err(error) => return all_failed(commands, &error.to_string().replace('\n', " ")),
659 };
660 match txn.update_refs(&updates) {
661 Ok(()) => {
662 seal(&updates);
663 commands
664 .iter()
665 .map(|command| RefResult::of(command, None))
666 .collect()
667 }
668 Err(_) => atomic_failure(snapshot, commands, messages),
669 }
670}
671
672fn apply_guarded_update(
673 txn: &RefTxn<'_>,
674 command: &ReceiveCommand,
675 seal: &dyn Fn(&[RefUpdate]),
676) -> Option<String> {
677 let update = match command.to_update() {
678 Ok(update) => update,
679 Err(error) => return Some(error.to_string().replace('\n', " ")),
680 };
681 match txn.update_ref(&update) {
682 Ok(()) => {
683 seal(std::slice::from_ref(&update));
684 None
685 }
686 Err(error) => Some(PackError::from(error).to_string().replace('\n', " ")),
687 }
688}
689
690fn parse_push_options(parsed: &pkt::Receive) -> PushOptions {
691 PushOptions::new(parsed.options.iter().filter_map(|option| {
692 PushOption::new(
693 String::from_utf8_lossy(option).trim_matches(|byte: char| byte == '\n' || byte == '\r'),
694 )
695 .ok()
696 }))
697}
698
699#[allow(clippy::too_many_arguments)]
700pub fn handle_guarded(
701 live: &Repo,
702 body: &[u8],
703 pack: Result<Option<&gix_pack::data::File>, PackError>,
704 limits: &PackLimits,
705 guard: &dyn ReceiveGuard,
706 seal: &dyn Fn(&[RefUpdate]),
707 messages: &RejectMessages,
708) -> Result<ReceiveOutcome, PackError> {
709 let parsed = pkt::split_receive(body)?;
710 let atomic = parsed.caps.atomic;
711 let side_band = parsed.caps.side_band_64k;
712 let push_options = parse_push_options(&parsed);
713 let build = |report: Vec<u8>| ReceiveOutcome {
714 report,
715 side_band,
716 push_options: push_options.clone(),
717 };
718 let commands = parse_commands(&parsed);
719 if commands.is_empty() {
720 return report(&Ok(()), &[]).map(build);
721 }
722
723 let pack = match pack {
724 Ok(pack) => pack,
725 Err(error) => {
726 let results = all_failed(&commands, &messages.unpacker_error.text());
727 return report(&Err(error), &results).map(build);
728 }
729 };
730 let (quarantine, closure) = match Quarantine::stage(
731 live,
732 pack,
733 limits,
734 live.object_format().kind(),
735 is_empty(live),
736 ) {
737 Ok(staged) => staged,
738 Err(error) => {
739 let results = all_failed(&commands, &messages.unpacker_error.text());
740 return report(&Err(error), &results).map(build);
741 }
742 };
743 let staged = quarantine.repo();
744 stage_to_quarantine(staged, &commands);
745 let snapshot = match RefSnapshot::capture(live) {
746 Ok(snapshot) => snapshot,
747 Err(_) => {
748 let results = all_failed(&commands, &messages.ref_snapshot_unavailable.text());
749 return report(&Ok(()), &results).map(build);
750 }
751 };
752 let haves = snapshot.tips();
753
754 let verdicts = guard.authorize(staged, &commands);
755 let reasons: Vec<Option<String>> = if verdicts.len() == commands.len() {
756 commands
757 .iter()
758 .zip(verdicts)
759 .zip(connectivity_reasons(
760 staged,
761 &commands,
762 &haves,
763 closure.as_ref(),
764 messages,
765 ))
766 .map(|((command, verdict), connectivity)| match verdict {
767 RefDecision::Reject(reason) => Some(reason),
768 RefDecision::Allow => reserved_create_only(command, messages)
769 .or(connectivity)
770 .or_else(|| snapshot.conflict(command, messages)),
771 })
772 .collect()
773 } else {
774 commands
775 .iter()
776 .map(|_| Some(messages.authorization_unavailable.text()))
777 .collect()
778 };
779
780 let any_reject = reasons.iter().any(Option::is_some);
781 if atomic && any_reject {
782 let results = commands
783 .iter()
784 .zip(reasons)
785 .map(|(command, reason)| {
786 RefResult::of(
787 command,
788 Some(reason.unwrap_or_else(|| messages.atomic_aborted.text())),
789 )
790 })
791 .collect::<Vec<_>>();
792 return report(&Ok(()), &results).map(build);
793 }
794
795 let applied = live.with_ref_txn(|txn| {
796 if reasons.iter().any(Option::is_none) {
797 quarantine.migrate_into(live)?;
798 }
799 let results = if atomic {
800 apply_guarded_atomic(txn, &snapshot, &commands, seal, messages)
801 } else {
802 commands
803 .iter()
804 .zip(reasons)
805 .map(|(command, reason)| match reason {
806 Some(reason) => RefResult::of(command, Some(reason)),
807 None => RefResult::of(command, apply_guarded_update(txn, command, seal)),
808 })
809 .collect()
810 };
811 Ok::<_, PackError>(results)
812 });
813 match applied {
814 Ok(results) => report(&Ok(()), &results).map(build),
815 Err(error) => {
816 let results = all_failed(&commands, &messages.object_migration_failed.text());
817 report(&Err(error), &results).map(build)
818 }
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::parse_push_options;
825 use crate::pkt;
826 use knot_types::{PushOption, PushOptions};
827
828 fn options(raw: &[&[u8]]) -> PushOptions {
829 parse_push_options(&pkt::Receive {
830 commands: Vec::new(),
831 options: raw.to_vec(),
832 pack: &[],
833 caps: pkt::Caps::default(),
834 })
835 }
836
837 #[test]
838 fn a_push_option_the_lexicon_rejects_never_reaches_the_event() {
839 let long = vec![b'x'; 1025];
840 let parsed = options(&[b"verbose-ci\n", b"", &long, b"has\nnewline", b"ci-skip\r"]);
841 assert_eq!(
842 parsed
843 .as_slice()
844 .iter()
845 .map(PushOption::as_str)
846 .collect::<Vec<&str>>(),
847 vec!["verbose-ci", "ci-skip"],
848 "parsing trims trailing end-of-line and rejects empty, oversized, and multi-line options"
849 );
850
851 let raw: Vec<Vec<u8>> = (0..PushOptions::MAX + 10)
852 .map(|index| format!("option-{index}").into_bytes())
853 .collect();
854 let borrowed: Vec<&[u8]> = raw.iter().map(Vec::as_slice).collect();
855 assert_eq!(
856 options(&borrowed).as_slice().len(),
857 PushOptions::MAX,
858 "directives parse from the same truncated list the event reports"
859 );
860 }
861}