1use crate::error::{Error, ParseError, Result};
21use crate::mainconfig::MainConfig;
22use crate::repo::Repo;
23use crate::types::*;
24use camino::Utf8PathBuf;
25use indexmap::IndexMap;
26use std::str::FromStr;
27use url::Url;
28
29macro_rules! try_parse_nutype {
34 ($val:expr, $typ:ty, $inner:ty) => {
35 $val.trim()
36 .parse::<$inner>()
37 .ok()
38 .and_then(|n| <$typ>::try_new(n).ok())
39 };
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct SectionBlock<T> {
67 pub header_comments: Vec<String>,
69 pub data: T,
71 pub item_comments: IndexMap<String, String>,
73 pub item_order: Vec<String>,
75 pub raw_entries: Vec<RawEntry>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct RawEntry {
85 pub key: String,
87 pub value: String,
89 pub inline_comment: Option<String>,
91 pub leading_comments: Vec<String>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct RepoFile {
118 pub preamble: Vec<String>,
120 pub main: Option<SectionBlock<MainConfig>>,
122 pub repos: IndexMap<RepoId, SectionBlock<Repo>>,
124}
125
126impl std::fmt::Display for RepoFile {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(f, "{}", self.render())
129 }
130}
131
132impl std::str::FromStr for RepoFile {
133 type Err = ParseError;
134 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
135 Self::parse(s)
136 }
137}
138
139impl From<RepoFile> for String {
140 fn from(rf: RepoFile) -> Self {
141 rf.render()
142 }
143}
144
145impl<'a> IntoIterator for &'a RepoFile {
146 type Item = (&'a RepoId, &'a SectionBlock<Repo>);
147 type IntoIter = indexmap::map::Iter<'a, RepoId, SectionBlock<Repo>>;
148 fn into_iter(self) -> Self::IntoIter {
149 self.repos.iter()
150 }
151}
152
153#[derive(Debug)]
158struct ParseState {
159 preamble: Vec<String>,
160 pending_comments: Vec<String>,
161 current_section: Option<String>,
162 current_entries: Vec<RawLine>,
163 sections: IndexMap<String, Vec<RawLine>>,
164 section_header_comments: IndexMap<String, Vec<String>>,
165}
166
167#[derive(Debug, Clone)]
169struct RawLine {
170 key: String,
171 value: String,
172 inline_comment: Option<String>,
173 leading_comments: Vec<String>,
174}
175
176fn split_value_and_comment(value_part: &str) -> (String, Option<String>) {
181 let mut in_quotes = false;
182 for (i, ch) in value_part.char_indices() {
183 if ch == '"' {
184 in_quotes = !in_quotes;
185 }
186 if ch == '#' && !in_quotes {
187 return (
188 value_part[..i].to_string(),
189 Some(value_part[i + 1..].trim().to_string()),
190 );
191 }
192 }
193 (value_part.to_string(), None)
194}
195
196fn parse_ip_resolve(val: &str) -> Option<IpResolve> {
201 match val.trim().to_lowercase().as_str() {
202 "4" | "ipv4" => Some(IpResolve::V4),
203 "6" | "ipv6" => Some(IpResolve::V6),
204 _ => None,
205 }
206}
207
208fn parse_proxy_auth_method(val: &str) -> Option<ProxyAuthMethod> {
209 match val.trim().to_lowercase().as_str() {
210 "any" => Some(ProxyAuthMethod::Any),
211 "none" => Some(ProxyAuthMethod::None_),
212 "basic" => Some(ProxyAuthMethod::Basic),
213 "digest" => Some(ProxyAuthMethod::Digest),
214 "negotiate" => Some(ProxyAuthMethod::Negotiate),
215 "ntlm" => Some(ProxyAuthMethod::Ntlm),
216 "digest_ie" | "digestie" => Some(ProxyAuthMethod::DigestIe),
217 "ntlm_wb" | "ntlmwb" => Some(ProxyAuthMethod::NtlmWb),
218 _ => None,
219 }
220}
221
222fn parse_proxy(val: &str) -> ProxySetting {
223 let trimmed = val.trim();
224 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("_none_") {
225 return ProxySetting::Disabled;
226 }
227 match Url::from_str(trimmed) {
228 Ok(url) => ProxySetting::Url(url),
229 Err(_) => ProxySetting::Raw(trimmed.to_owned()),
230 }
231}
232
233fn parse_multilib_policy(val: &str) -> Option<MultilibPolicy> {
234 match val.trim().to_lowercase().as_str() {
235 "best" => Some(MultilibPolicy::Best),
236 "all" => Some(MultilibPolicy::All),
237 _ => None,
238 }
239}
240
241fn parse_persistence(val: &str) -> Option<Persistence> {
242 match val.trim().to_lowercase().as_str() {
243 "auto" => Some(Persistence::Auto),
244 "transient" => Some(Persistence::Transient),
245 "persist" => Some(Persistence::Persist),
246 _ => None,
247 }
248}
249
250fn parse_rpmverbosity(val: &str) -> Option<RpmVerbosity> {
251 match val.trim().to_lowercase().as_str() {
252 "critical" => Some(RpmVerbosity::Critical),
253 "emergency" => Some(RpmVerbosity::Emergency),
254 "error" => Some(RpmVerbosity::Error),
255 "warn" => Some(RpmVerbosity::Warn),
256 "info" => Some(RpmVerbosity::Info),
257 "debug" => Some(RpmVerbosity::Debug),
258 _ => None,
259 }
260}
261
262fn parse_tsflags(val: &str) -> Vec<TsFlag> {
263 val.split(|c: char| c == ',' || c.is_whitespace())
264 .filter_map(|s| match s.trim().to_lowercase().as_str() {
265 "notriggers" | "notrigger" => Some(TsFlag::NoTriggers),
266 "noscripts" | "noscript" => Some(TsFlag::NoScripts),
267 "test" => Some(TsFlag::Test),
268 "nodocs" | "nodoc" => Some(TsFlag::NoDocs),
269 "justdb" => Some(TsFlag::JustDb),
270 "nocontexts" => Some(TsFlag::NoContexts),
271 "nocaps" => Some(TsFlag::NoCaps),
272 "nocrypto" => Some(TsFlag::NoCrypto),
273 "deploops" => Some(TsFlag::Deploops),
274 "noplugins" => Some(TsFlag::NoPlugins),
275 _ => None,
276 })
277 .collect()
278}
279
280fn parse_storage_size(val: &str) -> Option<StorageSize> {
281 let trimmed = val.trim();
282 if trimmed.is_empty() {
283 return None;
284 }
285 let (num_str, multiplier) = match trimmed.chars().last() {
286 Some('k') | Some('K') => (&trimmed[..trimmed.len() - 1], 1024),
287 Some('M') => (&trimmed[..trimmed.len() - 1], 1024 * 1024),
288 Some('G') => (&trimmed[..trimmed.len() - 1], 1024 * 1024 * 1024),
289 _ => (trimmed, 1),
290 };
291 let num: u64 = num_str.trim().parse().ok()?;
292 let bytes = num.checked_mul(multiplier)?;
293 Some(StorageSize(bytes))
294}
295
296fn parse_metadata_expire(val: &str) -> Option<MetadataExpire> {
297 let trimmed = val.trim();
298 if trimmed.eq_ignore_ascii_case("never") {
299 return Some(MetadataExpire::Never);
300 }
301 if let Ok(secs) = trimmed.parse::<u64>() {
302 return Some(MetadataExpire::Duration(secs));
303 }
304 None
305}
306
307fn parse_throttle(val: &str) -> Option<Throttle> {
308 let trimmed = val.trim();
309 if trimmed.is_empty() {
310 return None;
311 }
312 if let Some(pct_str) = trimmed.strip_suffix('%') {
314 if let Ok(pct) = pct_str.trim().parse::<u8>() {
315 if pct <= 100 {
316 return Some(Throttle::Percent(pct));
317 }
318 }
319 }
320 parse_storage_size(trimmed).map(Throttle::Absolute)
322}
323
324fn parse_repo_type(val: &str) -> Option<RepoMetadataType> {
325 match val.trim().to_lowercase().as_str() {
326 "rpm-md" | "rpm" => Some(RepoMetadataType::RpmMd),
327 _ => None,
328 }
329}
330
331fn parse_entries_into_repo(
338 repo: &mut Repo,
339 entries: &[RawLine],
340) -> (Vec<String>, IndexMap<String, String>, Vec<RawEntry>) {
341 let mut item_order = Vec::new();
342 let mut item_comments = IndexMap::new();
343 let mut raw_entries = Vec::new();
344
345 for entry in entries {
346 let key = entry.key.clone();
347 let value = entry.value.clone();
348
349 raw_entries.push(RawEntry {
351 key: key.clone(),
352 value: value.clone(),
353 inline_comment: entry.inline_comment.clone(),
354 leading_comments: entry.leading_comments.clone(),
355 });
356
357 item_order.push(key.clone());
358 if let Some(ref ic) = entry.inline_comment {
359 item_comments.insert(key.clone(), ic.clone());
360 }
361
362 match key.as_str() {
363 "name" => {
365 if let Ok(v) = RepoName::from_str(&value) {
366 repo.name = Some(v);
367 }
368 }
369 "mediaid" => {
370 repo.mediaid = Some(value.clone());
371 }
372
373 "baseurl" => {
375 if let Ok(v) = Url::from_str(&value) {
376 repo.baseurl.push(v);
377 }
378 }
379 "mirrorlist" => {
380 if let Ok(v) = Url::from_str(&value) {
381 repo.mirrorlist = Some(v);
382 }
383 }
384 "metalink" => {
385 if let Ok(v) = Url::from_str(&value) {
386 repo.metalink = Some(v);
387 }
388 }
389
390 "gpgkey" => {
392 repo.gpgkey.push(value.clone());
393 }
394 "enabled_metadata" => {
395 repo.enabled_metadata.push(value.clone());
396 }
397 "excludepkgs" => {
398 repo.excludepkgs.push(value.clone());
399 }
400 "includepkgs" => {
401 repo.includepkgs.push(value.clone());
402 }
403
404 "enabled" => {
406 if let Ok(v) = DnfBool::parse(&value) {
407 repo.enabled = Some(v);
408 }
409 }
410 "module_hotfixes" => {
411 if let Ok(v) = DnfBool::parse(&value) {
412 repo.module_hotfixes = Some(v);
413 }
414 }
415 "gpgcheck" => {
416 if let Ok(v) = DnfBool::parse(&value) {
417 repo.gpgcheck = Some(v);
418 }
419 }
420 "repo_gpgcheck" => {
421 if let Ok(v) = DnfBool::parse(&value) {
422 repo.repo_gpgcheck = Some(v);
423 }
424 }
425 "localpkg_gpgcheck" => {
426 if let Ok(v) = DnfBool::parse(&value) {
427 repo.localpkg_gpgcheck = Some(v);
428 }
429 }
430 "skip_if_unavailable" => {
431 if let Ok(v) = DnfBool::parse(&value) {
432 repo.skip_if_unavailable = Some(v);
433 }
434 }
435 "deltarpm" => {
436 if let Ok(v) = DnfBool::parse(&value) {
437 repo.deltarpm = Some(v);
438 }
439 }
440 "enablegroups" => {
441 if let Ok(v) = DnfBool::parse(&value) {
442 repo.enablegroups = Some(v);
443 }
444 }
445 "fastestmirror" => {
446 if let Ok(v) = DnfBool::parse(&value) {
447 repo.fastestmirror = Some(v);
448 }
449 }
450 "countme" => {
451 if let Ok(v) = DnfBool::parse(&value) {
452 repo.countme = Some(v);
453 }
454 }
455 "sslverify" => {
456 if let Ok(v) = DnfBool::parse(&value) {
457 repo.sslverify = Some(v);
458 }
459 }
460 "sslverifystatus" => {
461 if let Ok(v) = DnfBool::parse(&value) {
462 repo.sslverifystatus = Some(v);
463 }
464 }
465 "proxy_sslverify" => {
466 if let Ok(v) = DnfBool::parse(&value) {
467 repo.proxy_sslverify = Some(v);
468 }
469 }
470
471 "priority" => {
473 if let Some(v) = try_parse_nutype!(&value, Priority, i32) {
474 repo.priority = Some(v);
475 }
476 }
477 "cost" => {
478 if let Some(v) = try_parse_nutype!(&value, Cost, i32) {
479 repo.cost = Some(v);
480 }
481 }
482 "deltarpm_percentage" => {
483 if let Some(v) = try_parse_nutype!(&value, DeltaRpmPercentage, u32) {
484 repo.deltarpm_percentage = Some(v);
485 }
486 }
487 "retries" => {
488 if let Some(v) = try_parse_nutype!(&value, Retries, u32) {
489 repo.retries = Some(v);
490 }
491 }
492 "timeout" => {
493 if let Some(v) = try_parse_nutype!(&value, TimeoutSeconds, u32) {
494 repo.timeout = Some(v);
495 }
496 }
497 "max_parallel_downloads" => {
498 if let Some(v) = try_parse_nutype!(&value, MaxParallelDownloads, u32) {
499 repo.max_parallel_downloads = Some(v);
500 }
501 }
502
503 "bandwidth" => {
505 if let Some(v) = parse_storage_size(&value) {
506 repo.bandwidth = Some(v);
507 }
508 }
509 "minrate" => {
510 if let Some(v) = parse_storage_size(&value) {
511 repo.minrate = Some(v);
512 }
513 }
514
515 "throttle" => {
517 if let Some(v) = parse_throttle(&value) {
518 repo.throttle = Some(v);
519 }
520 }
521
522 "metadata_expire" => {
524 if let Some(v) = parse_metadata_expire(&value) {
525 repo.metadata_expire = Some(v);
526 }
527 }
528
529 "ip_resolve" => {
531 if let Some(v) = parse_ip_resolve(&value) {
532 repo.ip_resolve = Some(v);
533 }
534 }
535
536 "sslcacert" => {
538 repo.sslcacert = Some(Utf8PathBuf::from(&value));
539 }
540 "sslclientcert" => {
541 repo.sslclientcert = Some(Utf8PathBuf::from(&value));
542 }
543 "sslclientkey" => {
544 repo.sslclientkey = Some(Utf8PathBuf::from(&value));
545 }
546
547 "proxy" => {
549 repo.proxy = parse_proxy(&value);
550 }
551 "proxy_username" => {
552 repo.proxy_username = ProxyUsername::from_str(&value).ok();
553 }
554 "proxy_password" => {
555 repo.proxy_password = ProxyPassword::from_str(&value).ok();
556 }
557 "proxy_auth_method" => {
558 if let Some(v) = parse_proxy_auth_method(&value) {
559 repo.proxy_auth_method = Some(v);
560 }
561 }
562 "proxy_sslcacert" => {
563 repo.proxy_sslcacert = Some(Utf8PathBuf::from(&value));
564 }
565 "proxy_sslclientcert" => {
566 repo.proxy_sslclientcert = Some(Utf8PathBuf::from(&value));
567 }
568 "proxy_sslclientkey" => {
569 repo.proxy_sslclientkey = Some(Utf8PathBuf::from(&value));
570 }
571
572 "username" => {
574 repo.username = Username::from_str(&value).ok();
575 }
576 "password" => {
577 repo.password = Password::from_str(&value).ok();
578 }
579 "user_agent" => {
580 repo.user_agent = UserAgent::from_str(&value).ok();
581 }
582
583 "type" => {
585 if let Some(v) = parse_repo_type(&value) {
586 repo.metadata_type = Some(v);
587 }
588 }
589
590 _ => {
592 repo.extras
593 .entry(key.clone())
594 .or_default()
595 .push(value.clone());
596 }
597 }
598 }
599
600 (item_order, item_comments, raw_entries)
601}
602
603fn parse_entries_into_mainconfig(
605 config: &mut MainConfig,
606 entries: &[RawLine],
607) -> (Vec<String>, IndexMap<String, String>, Vec<RawEntry>) {
608 let mut item_order = Vec::new();
609 let mut item_comments = IndexMap::new();
610 let mut raw_entries = Vec::new();
611
612 for entry in entries {
613 let key = entry.key.clone();
614 let value = entry.value.clone();
615
616 raw_entries.push(RawEntry {
617 key: key.clone(),
618 value: value.clone(),
619 inline_comment: entry.inline_comment.clone(),
620 leading_comments: entry.leading_comments.clone(),
621 });
622
623 item_order.push(key.clone());
624 if let Some(ref ic) = entry.inline_comment {
625 item_comments.insert(key.clone(), ic.clone());
626 }
627
628 match key.as_str() {
629 "arch" => config.arch = Some(value.clone()),
631 "basearch" => config.basearch = Some(value.clone()),
632 "releasever" => config.releasever = Some(value.clone()),
633
634 "cachedir" => config.cachedir = Some(Utf8PathBuf::from(&value)),
636 "persistdir" => config.persistdir = Some(Utf8PathBuf::from(&value)),
637 "logdir" => config.logdir = Some(Utf8PathBuf::from(&value)),
638 "config_file_path" => config.config_file_path = Some(Utf8PathBuf::from(&value)),
639 "installroot" => config.installroot = Some(Utf8PathBuf::from(&value)),
640
641 "reposdir" => config.reposdir.push(Utf8PathBuf::from(&value)),
643 "varsdir" => config.varsdir.push(Utf8PathBuf::from(&value)),
644 "pluginconfpath" => config.pluginconfpath.push(Utf8PathBuf::from(&value)),
645 "pluginpath" => config.pluginpath.push(Utf8PathBuf::from(&value)),
646
647 "installonlypkgs" => config.installonlypkgs.push(value.clone()),
649 "protected_packages" => config.protected_packages.push(value.clone()),
650 "exclude_from_weak" => config.exclude_from_weak.push(value.clone()),
651 "group_package_types" => config.group_package_types.push(value.clone()),
652 "optional_metadata_types" => config.optional_metadata_types.push(value.clone()),
653 "usr_drift_protected_paths" => config.usr_drift_protected_paths.push(value.clone()),
654
655 "allow_vendor_change" => {
657 if let Ok(v) = DnfBool::parse(&value) {
658 config.allow_vendor_change = Some(v);
659 }
660 }
661 "assumeno" => {
662 if let Ok(v) = DnfBool::parse(&value) {
663 config.assumeno = Some(v);
664 }
665 }
666 "assumeyes" => {
667 if let Ok(v) = DnfBool::parse(&value) {
668 config.assumeyes = Some(v);
669 }
670 }
671 "autocheck_running_kernel" => {
672 if let Ok(v) = DnfBool::parse(&value) {
673 config.autocheck_running_kernel = Some(v);
674 }
675 }
676 "best" => {
677 if let Ok(v) = DnfBool::parse(&value) {
678 config.best = Some(v);
679 }
680 }
681 "cacheonly" => {
682 if let Ok(v) = DnfBool::parse(&value) {
683 config.cacheonly = Some(v);
684 }
685 }
686 "check_config_file_age" => {
687 if let Ok(v) = DnfBool::parse(&value) {
688 config.check_config_file_age = Some(v);
689 }
690 }
691 "clean_requirements_on_remove" => {
692 if let Ok(v) = DnfBool::parse(&value) {
693 config.clean_requirements_on_remove = Some(v);
694 }
695 }
696 "debug_solver" => {
697 if let Ok(v) = DnfBool::parse(&value) {
698 config.debug_solver = Some(v);
699 }
700 }
701 "defaultyes" => {
702 if let Ok(v) = DnfBool::parse(&value) {
703 config.defaultyes = Some(v);
704 }
705 }
706 "diskspacecheck" => {
707 if let Ok(v) = DnfBool::parse(&value) {
708 config.diskspacecheck = Some(v);
709 }
710 }
711 "exclude_from_weak_autodetect" => {
712 if let Ok(v) = DnfBool::parse(&value) {
713 config.exclude_from_weak_autodetect = Some(v);
714 }
715 }
716 "exit_on_lock" => {
717 if let Ok(v) = DnfBool::parse(&value) {
718 config.exit_on_lock = Some(v);
719 }
720 }
721 "gpgkey_dns_verification" => {
722 if let Ok(v) = DnfBool::parse(&value) {
723 config.gpgkey_dns_verification = Some(v);
724 }
725 }
726 "ignorearch" => {
727 if let Ok(v) = DnfBool::parse(&value) {
728 config.ignorearch = Some(v);
729 }
730 }
731 "install_weak_deps" => {
732 if let Ok(v) = DnfBool::parse(&value) {
733 config.install_weak_deps = Some(v);
734 }
735 }
736 "keepcache" => {
737 if let Ok(v) = DnfBool::parse(&value) {
738 config.keepcache = Some(v);
739 }
740 }
741 "log_compress" => {
742 if let Ok(v) = DnfBool::parse(&value) {
743 config.log_compress = Some(v);
744 }
745 }
746 "module_obsoletes" => {
747 if let Ok(v) = DnfBool::parse(&value) {
748 config.module_obsoletes = Some(v);
749 }
750 }
751 "module_stream_switch" => {
752 if let Ok(v) = DnfBool::parse(&value) {
753 config.module_stream_switch = Some(v);
754 }
755 }
756 "obsoletes" => {
757 if let Ok(v) = DnfBool::parse(&value) {
758 config.obsoletes = Some(v);
759 }
760 }
761 "plugins" => {
762 if let Ok(v) = DnfBool::parse(&value) {
763 config.plugins = Some(v);
764 }
765 }
766 "protect_running_kernel" => {
767 if let Ok(v) = DnfBool::parse(&value) {
768 config.protect_running_kernel = Some(v);
769 }
770 }
771 "strict" => {
772 if let Ok(v) = DnfBool::parse(&value) {
773 config.strict = Some(v);
774 }
775 }
776 "upgrade_group_objects_upgrade" => {
777 if let Ok(v) = DnfBool::parse(&value) {
778 config.upgrade_group_objects_upgrade = Some(v);
779 }
780 }
781 "zchunk" => {
782 if let Ok(v) = DnfBool::parse(&value) {
783 config.zchunk = Some(v);
784 }
785 }
786
787 "debuglevel" => {
789 if let Some(v) = try_parse_nutype!(&value, DebugLevel, u8) {
790 config.debuglevel = Some(v);
791 }
792 }
793 "logfilelevel" => {
794 if let Some(v) = try_parse_nutype!(&value, LogLevel, u8) {
795 config.logfilelevel = Some(v);
796 }
797 }
798 "log_rotate" => {
799 if let Some(v) = try_parse_nutype!(&value, LogRotate, u32) {
800 config.log_rotate = Some(v);
801 }
802 }
803 "installonly_limit" => {
804 if let Some(v) = try_parse_nutype!(&value, InstallOnlyLimit, u32) {
805 config.installonly_limit = Some(v);
806 }
807 }
808 "errorlevel" => {
809 if let Some(v) = try_parse_nutype!(&value, ErrorLevel, u8) {
810 config.errorlevel = Some(v);
811 }
812 }
813 "metadata_timer_sync" => {
814 if let Some(v) = try_parse_nutype!(&value, MetadataTimerSync, u32) {
815 config.metadata_timer_sync = Some(v);
816 }
817 }
818
819 "log_size" => {
821 if let Some(v) = parse_storage_size(&value) {
822 config.log_size = Some(v);
823 }
824 }
825
826 "multilib_policy" => {
828 if let Some(v) = parse_multilib_policy(&value) {
829 config.multilib_policy = Some(v);
830 }
831 }
832 "persistence" => {
833 if let Some(v) = parse_persistence(&value) {
834 config.persistence = Some(v);
835 }
836 }
837 "rpmverbosity" => {
838 if let Some(v) = parse_rpmverbosity(&value) {
839 config.rpmverbosity = Some(v);
840 }
841 }
842
843 "tsflags" => {
845 let flags = parse_tsflags(&value);
846 config.tsflags.extend(flags);
847 }
848
849 "module_platform_id" => {
851 config.module_platform_id = ModulePlatformId::from_str(&value).ok();
852 }
853
854 _ => {
856 config
857 .extras
858 .entry(key.clone())
859 .or_default()
860 .push(value.clone());
861 }
862 }
863 }
864
865 (item_order, item_comments, raw_entries)
866}
867
868fn build_repofile(state: ParseState) -> std::result::Result<RepoFile, ParseError> {
873 let mut rf = RepoFile::new();
874 rf.preamble = state.preamble;
875
876 for (sec_name, entries) in &state.sections {
877 let header_comments = state
878 .section_header_comments
879 .get(sec_name)
880 .cloned()
881 .unwrap_or_default();
882 if sec_name == "main" {
883 let mut mc = MainConfig::default();
884 let (io, ic, re) = parse_entries_into_mainconfig(&mut mc, entries);
885 rf.main = Some(SectionBlock {
886 header_comments,
887 data: mc,
888 item_comments: ic,
889 item_order: io,
890 raw_entries: re,
891 });
892 } else {
893 let repo_id =
894 RepoId::try_new(sec_name.as_str()).map_err(|_| ParseError::InvalidRepoId {
895 id: sec_name.clone(),
896 reason: "invalid characters in repo ID".into(),
897 })?;
898 let mut repo = Repo::new(repo_id);
899 let (io, ic, re) = parse_entries_into_repo(&mut repo, entries);
900 rf.repos.insert(
901 repo.id.clone(),
902 SectionBlock {
903 header_comments,
904 data: repo,
905 item_comments: ic,
906 item_order: io,
907 raw_entries: re,
908 },
909 );
910 }
911 }
912 Ok(rf)
913}
914
915impl RepoFile {
920 pub fn new() -> Self {
932 RepoFile {
933 preamble: Vec::new(),
934 main: None,
935 repos: IndexMap::new(),
936 }
937 }
938
939 pub fn parse(input: &str) -> std::result::Result<Self, ParseError> {
961 let mut state = ParseState {
962 preamble: Vec::new(),
963 pending_comments: Vec::new(),
964 current_section: None,
965 current_entries: Vec::new(),
966 sections: IndexMap::new(),
967 section_header_comments: IndexMap::new(),
968 };
969
970 for (line_idx, raw_line) in input.lines().enumerate() {
971 let trimmed = raw_line.trim();
972
973 if trimmed.is_empty() {
974 if state.current_section.is_some() {
975 state.pending_comments.push(String::new());
976 } else {
977 state.preamble.push(String::new());
978 }
979 continue;
980 }
981
982 if trimmed.starts_with('#') || trimmed.starts_with(';') {
983 if state.current_section.is_some() {
984 state.pending_comments.push(raw_line.to_owned());
985 } else {
986 state.preamble.push(raw_line.to_owned());
987 }
988 continue;
989 }
990
991 if trimmed.starts_with('[') && trimmed.ends_with(']') {
992 if let Some(ref sec_name) = state.current_section.take() {
994 state
995 .sections
996 .insert(sec_name.clone(), std::mem::take(&mut state.current_entries));
997 }
998
999 let section_name = trimmed[1..trimmed.len() - 1].trim().to_string();
1000 if section_name.is_empty() {
1001 return Err(ParseError::EmptySectionName);
1002 }
1003 if section_name != "main" && RepoId::try_new(section_name.as_str()).is_err() {
1004 return Err(ParseError::InvalidRepoId {
1005 id: section_name.clone(),
1006 reason: "invalid characters in repo ID".into(),
1007 });
1008 }
1009
1010 if !state.pending_comments.is_empty() {
1012 state.section_header_comments.insert(
1013 section_name.clone(),
1014 std::mem::take(&mut state.pending_comments),
1015 );
1016 }
1017
1018 state.current_section = Some(section_name);
1019 continue;
1020 }
1021
1022 if let Some(eq_pos) = trimmed.find('=') {
1023 let key = trimmed[..eq_pos].trim().to_string();
1024 let value_part = &trimmed[eq_pos + 1..];
1025 let (value, inline_comment) = split_value_and_comment(value_part);
1026
1027 if key.is_empty() {
1028 return Err(ParseError::MissingEquals {
1029 line: line_idx + 1,
1030 line_text: raw_line.to_owned(),
1031 });
1032 }
1033
1034 let entry = RawLine {
1035 key,
1036 value: value.trim().to_string(),
1037 inline_comment,
1038 leading_comments: std::mem::take(&mut state.pending_comments),
1039 };
1040
1041 if state.current_section.is_some() {
1042 state.current_entries.push(entry);
1043 } else {
1044 state.preamble.push(raw_line.to_owned());
1045 }
1046 } else {
1047 return Err(ParseError::MissingEquals {
1048 line: line_idx + 1,
1049 line_text: raw_line.to_owned(),
1050 });
1051 }
1052 }
1053
1054 if let Some(ref sec_name) = state.current_section.take() {
1056 if !state.pending_comments.is_empty()
1057 || !state.section_header_comments.contains_key(sec_name)
1058 {
1059 state.section_header_comments.insert(
1060 sec_name.clone(),
1061 std::mem::take(&mut state.pending_comments),
1062 );
1063 }
1064 state
1065 .sections
1066 .insert(sec_name.clone(), std::mem::take(&mut state.current_entries));
1067 } else if !state.pending_comments.is_empty() {
1068 state
1069 .preamble
1070 .extend(std::mem::take(&mut state.pending_comments));
1071 }
1072
1073 build_repofile(state)
1074 }
1075
1076 #[must_use]
1094 pub fn render(&self) -> String {
1095 let mut out = String::new();
1096 for line in &self.preamble {
1097 render_line(&mut out, line);
1098 }
1099 if let Some(ref block) = self.main {
1100 for c in &block.header_comments {
1101 render_line(&mut out, c);
1102 }
1103 out.push_str("[main]\n");
1104 render_section_entries(&mut out, block);
1105 }
1106 for (repo_id, block) in &self.repos {
1107 for c in &block.header_comments {
1108 render_line(&mut out, c);
1109 }
1110 out.push_str(&format!("[{}]\n", repo_id.as_ref()));
1111 render_section_entries(&mut out, block);
1112 }
1113 out
1114 }
1115
1116 pub fn get(&self, id: &RepoId) -> Option<&SectionBlock<Repo>> {
1132 self.repos.get(id)
1133 }
1134
1135 pub fn get_mut(&mut self, id: &RepoId) -> Option<&mut SectionBlock<Repo>> {
1149 self.repos.get_mut(id)
1150 }
1151
1152 pub fn add(&mut self, repo: Repo) -> Result<()> {
1172 let id = repo.id.clone();
1173 if self.repos.contains_key(&id) {
1174 return Err(Error::DuplicateRepo(id.to_string()));
1175 }
1176 self.repos.insert(
1177 id,
1178 SectionBlock {
1179 header_comments: Vec::new(),
1180 data: repo,
1181 item_comments: IndexMap::new(),
1182 item_order: Vec::new(),
1183 raw_entries: Vec::new(),
1184 },
1185 );
1186 Ok(())
1187 }
1188
1189 pub fn set(&mut self, repo: Repo) {
1204 let id = repo.id.clone();
1205 self.repos.insert(
1206 id,
1207 SectionBlock {
1208 header_comments: Vec::new(),
1209 data: repo,
1210 item_comments: IndexMap::new(),
1211 item_order: Vec::new(),
1212 raw_entries: Vec::new(),
1213 },
1214 );
1215 }
1216
1217 pub fn remove(&mut self, id: &RepoId) -> Option<SectionBlock<Repo>> {
1231 self.repos.shift_remove(id)
1232 }
1233
1234 pub fn contains(&self, id: &RepoId) -> bool {
1246 self.repos.contains_key(id)
1247 }
1248
1249 pub fn len(&self) -> usize {
1262 self.repos.len()
1263 }
1264
1265 pub fn is_empty(&self) -> bool {
1276 self.repos.is_empty()
1277 }
1278
1279 pub fn iter(&self) -> impl Iterator<Item = (&RepoId, &SectionBlock<Repo>)> {
1293 self.repos.iter()
1294 }
1295
1296 pub fn repos(&self) -> impl Iterator<Item = &Repo> {
1310 self.repos.values().map(|block| &block.data)
1311 }
1312
1313 pub fn repo_ids(&self) -> impl Iterator<Item = &RepoId> {
1326 self.repos.keys()
1327 }
1328
1329 pub fn main(&self) -> Option<&SectionBlock<MainConfig>> {
1340 self.main.as_ref()
1341 }
1342
1343 pub fn main_mut(&mut self) -> Option<&mut SectionBlock<MainConfig>> {
1356 self.main.as_mut()
1357 }
1358
1359 pub fn set_main(&mut self, config: MainConfig) {
1371 self.main = Some(SectionBlock {
1372 header_comments: Vec::new(),
1373 data: config,
1374 item_comments: IndexMap::new(),
1375 item_order: Vec::new(),
1376 raw_entries: Vec::new(),
1377 });
1378 }
1379
1380 pub fn remove_main(&mut self) {
1393 self.main = None;
1394 }
1395
1396 pub fn merge(&mut self, other: RepoFile) {
1422 if let Some(other_main) = other.main {
1423 if let Some(ref mut self_main) = self.main {
1424 merge_mainconfig(&mut self_main.data, &other_main.data);
1425 for (k, v) in other_main.item_comments {
1426 self_main.item_comments.entry(k).or_insert(v);
1427 }
1428 } else {
1429 self.main = Some(other_main);
1430 }
1431 }
1432 for (id, block) in other.repos {
1433 self.repos.insert(id, block);
1434 }
1435 }
1436}
1437
1438fn merge_mainconfig(dest: &mut MainConfig, src: &MainConfig) {
1439 macro_rules! merge_opt {
1440 ($field:ident) => {
1441 if src.$field.is_some() && dest.$field.is_none() {
1442 dest.$field = src.$field.clone();
1443 }
1444 };
1445 }
1446 macro_rules! merge_vec {
1447 ($field:ident) => {
1448 dest.$field.extend(src.$field.iter().cloned());
1449 };
1450 }
1451 merge_opt!(arch);
1452 merge_opt!(basearch);
1453 merge_opt!(releasever);
1454 merge_opt!(cachedir);
1455 merge_opt!(persistdir);
1456 merge_opt!(logdir);
1457 merge_opt!(config_file_path);
1458 merge_opt!(installroot);
1459 merge_opt!(debuglevel);
1460 merge_opt!(logfilelevel);
1461 merge_opt!(log_rotate);
1462 merge_opt!(log_size);
1463 merge_opt!(installonly_limit);
1464 merge_opt!(errorlevel);
1465 merge_opt!(metadata_timer_sync);
1466 merge_opt!(allow_vendor_change);
1467 merge_opt!(assumeyes);
1468 merge_opt!(assumeno);
1469 merge_opt!(autocheck_running_kernel);
1470 merge_opt!(best);
1471 merge_opt!(cacheonly);
1472 merge_opt!(check_config_file_age);
1473 merge_opt!(clean_requirements_on_remove);
1474 merge_opt!(debug_solver);
1475 merge_opt!(defaultyes);
1476 merge_opt!(diskspacecheck);
1477 merge_opt!(exclude_from_weak_autodetect);
1478 merge_opt!(exit_on_lock);
1479 merge_opt!(gpgkey_dns_verification);
1480 merge_opt!(ignorearch);
1481 merge_opt!(install_weak_deps);
1482 merge_opt!(keepcache);
1483 merge_opt!(log_compress);
1484 merge_opt!(module_obsoletes);
1485 merge_opt!(module_stream_switch);
1486 merge_opt!(obsoletes);
1487 merge_opt!(plugins);
1488 merge_opt!(protect_running_kernel);
1489 merge_opt!(strict);
1490 merge_opt!(upgrade_group_objects_upgrade);
1491 merge_opt!(zchunk);
1492 merge_opt!(multilib_policy);
1493 merge_opt!(persistence);
1494 merge_opt!(rpmverbosity);
1495 merge_opt!(module_platform_id);
1496 merge_vec!(reposdir);
1497 merge_vec!(varsdir);
1498 merge_vec!(pluginconfpath);
1499 merge_vec!(pluginpath);
1500 merge_vec!(installonlypkgs);
1501 merge_vec!(protected_packages);
1502 merge_vec!(exclude_from_weak);
1503 merge_vec!(group_package_types);
1504 merge_vec!(optional_metadata_types);
1505 merge_vec!(tsflags);
1506 merge_vec!(usr_drift_protected_paths);
1507 for (k, v) in &src.extras {
1508 if !dest.extras.contains_key(k) {
1509 dest.extras.insert(k.clone(), v.clone());
1510 }
1511 }
1512}
1513
1514impl Default for RepoFile {
1515 fn default() -> Self {
1516 Self::new()
1517 }
1518}
1519
1520fn render_line(out: &mut String, line: &str) {
1525 out.push_str(line);
1526 if !line.ends_with('\n') {
1527 out.push('\n');
1528 }
1529}
1530
1531fn render_section_entries<T>(out: &mut String, block: &SectionBlock<T>) {
1532 for entry in &block.raw_entries {
1533 for c in &entry.leading_comments {
1534 render_line(out, c);
1535 }
1536 let mut line = format!("{}={}", entry.key, entry.value);
1537 if let Some(ref ic) = entry.inline_comment {
1538 line.push_str(&format!(" #{}", ic));
1539 }
1540 out.push_str(&line);
1541 out.push('\n');
1542 }
1543}