This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-pack / src / receive.rs
26 kB 861 lines
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}