|
1 | 1 | use anyhow::{Context, Result}; |
2 | | -use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; |
| 2 | + |
3 | 3 | use log::debug; |
4 | | -use serde::Deserialize; |
| 4 | +use serde::{Deserialize, Serialize}; |
5 | 5 | use std::fs::File; |
6 | 6 | use std::io::{Read, Write}; |
7 | 7 | use std::path::Path; |
@@ -460,136 +460,198 @@ impl EncfsConfig { |
460 | 460 | /// Saves the configuration file as XML, emulating the Boost Serialization format |
461 | 461 | /// used for V6 configs. |
462 | 462 | fn save_xml(&self, path: &Path) -> Result<()> { |
463 | | - let mut file = File::create(path).context("Failed to create config file")?; |
464 | | - |
465 | | - // Write XML header |
466 | | - writeln!(file, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?; |
467 | | - writeln!(file, "<!DOCTYPE boost_serialization>")?; |
468 | | - writeln!( |
469 | | - file, |
470 | | - "<boost_serialization signature=\"serialization::archive\" version=\"7\">" |
471 | | - )?; |
472 | | - writeln!( |
473 | | - file, |
474 | | - " <cfg class_id=\"0\" tracking_level=\"0\" version=\"20\">" |
475 | | - )?; |
476 | | - |
477 | | - // Write version |
478 | | - writeln!(file, " <version>{}</version>", self.version)?; |
479 | | - |
480 | | - // Write creator |
481 | | - writeln!(file, " <creator>{}</creator>", self.creator)?; |
482 | | - |
483 | | - // Write cipherAlg |
484 | | - writeln!( |
485 | | - file, |
486 | | - " <cipherAlg class_id=\"1\" tracking_level=\"0\" version=\"0\">" |
487 | | - )?; |
488 | | - writeln!(file, " <name>{}</name>", self.cipher_iface.name)?; |
489 | | - writeln!( |
490 | | - file, |
491 | | - " <major>{}</major>", |
492 | | - self.cipher_iface.major |
493 | | - )?; |
494 | | - writeln!( |
495 | | - file, |
496 | | - " <minor>{}</minor>", |
497 | | - self.cipher_iface.minor |
498 | | - )?; |
499 | | - writeln!(file, " </cipherAlg>")?; |
500 | | - |
501 | | - // Write nameAlg |
502 | | - writeln!(file, " <nameAlg>")?; |
503 | | - writeln!(file, " <name>{}</name>", self.name_iface.name)?; |
504 | | - writeln!(file, " <major>{}</major>", self.name_iface.major)?; |
505 | | - writeln!(file, " <minor>{}</minor>", self.name_iface.minor)?; |
506 | | - writeln!(file, " </nameAlg>")?; |
507 | | - |
508 | | - // Write other fields |
509 | | - writeln!(file, " <keySize>{}</keySize>", self.key_size)?; |
510 | | - writeln!(file, " <blockSize>{}</blockSize>", self.block_size)?; |
511 | | - writeln!( |
512 | | - file, |
513 | | - " <plainData>{}</plainData>", |
514 | | - if self.plain_data { 1 } else { 0 } |
515 | | - )?; |
516 | | - writeln!( |
517 | | - file, |
518 | | - " <uniqueIV>{}</uniqueIV>", |
519 | | - if self.unique_iv { 1 } else { 0 } |
520 | | - )?; |
521 | | - writeln!( |
522 | | - file, |
523 | | - " <chainedNameIV>{}</chainedNameIV>", |
524 | | - if self.chained_name_iv { 1 } else { 0 } |
525 | | - )?; |
526 | | - writeln!( |
527 | | - file, |
528 | | - " <externalIVChaining>{}</externalIVChaining>", |
529 | | - if self.external_iv_chaining { 1 } else { 0 } |
530 | | - )?; |
531 | | - writeln!( |
532 | | - file, |
533 | | - " <blockMACBytes>{}</blockMACBytes>", |
534 | | - self.block_mac_bytes |
535 | | - )?; |
536 | | - writeln!( |
537 | | - file, |
538 | | - " <blockMACRandBytes>{}</blockMACRandBytes>", |
539 | | - self.block_mac_rand_bytes |
540 | | - )?; |
541 | | - writeln!( |
542 | | - file, |
543 | | - " <allowHoles>{}</allowHoles>", |
544 | | - if self.allow_holes { 1 } else { 0 } |
545 | | - )?; |
546 | | - |
547 | | - // Write encodedKeySize and encodedKeyData |
548 | | - writeln!( |
549 | | - file, |
550 | | - " <encodedKeySize>{}</encodedKeySize>", |
551 | | - self.key_data.len() |
552 | | - )?; |
553 | | - writeln!(file, " <encodedKeyData>")?; |
554 | | - let key_data_b64 = BASE64.encode(&self.key_data); |
555 | | - // Split into lines of reasonable length (76 chars is standard) |
556 | | - for chunk in key_data_b64 |
557 | | - .as_bytes() |
558 | | - .chunks(crate::constants::XML_BASE64_LINE_LEN) |
559 | | - { |
560 | | - writeln!(file, "{}", String::from_utf8_lossy(chunk))?; |
| 463 | + let file = File::create(path).context("Failed to create config file")?; |
| 464 | + let mut writer = std::io::BufWriter::new(file); |
| 465 | + |
| 466 | + let root = xml_ser::BoostSerializationRoot::from_config(self); |
| 467 | + |
| 468 | + // Write XML header and DOCTYPE manually as quick-xml doesn't support custom DOCTYPE well |
| 469 | + // with the default serializer, and we want exact control over the format. |
| 470 | + writer.write_all(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")?; |
| 471 | + writer.write_all(b"<!DOCTYPE boost_serialization>\n")?; |
| 472 | + |
| 473 | + let mut xml_buffer = String::new(); |
| 474 | + let mut serializer = quick_xml::se::Serializer::new(&mut xml_buffer); |
| 475 | + serializer.indent(' ', 4); |
| 476 | + |
| 477 | + root.serialize(serializer) |
| 478 | + .context("Failed to serialize XML")?; |
| 479 | + |
| 480 | + writer.write_all(xml_buffer.as_bytes())?; |
| 481 | + |
| 482 | + // Append a newline at the end |
| 483 | + writer.write_all(b"\n")?; |
| 484 | + |
| 485 | + Ok(()) |
| 486 | + } |
| 487 | +} |
| 488 | + |
| 489 | +mod xml_ser { |
| 490 | + use super::*; |
| 491 | + use serde::Serialize; |
| 492 | + |
| 493 | + #[derive(Serialize)] |
| 494 | + #[serde(rename = "boost_serialization")] |
| 495 | + pub struct BoostSerializationRoot<'a> { |
| 496 | + #[serde(rename = "@signature")] |
| 497 | + signature: &'static str, |
| 498 | + #[serde(rename = "@version")] |
| 499 | + version: &'static str, |
| 500 | + |
| 501 | + cfg: EncfsConfigXml<'a>, |
| 502 | + } |
| 503 | + |
| 504 | + impl<'a> BoostSerializationRoot<'a> { |
| 505 | + pub fn from_config(config: &'a EncfsConfig) -> Self { |
| 506 | + Self { |
| 507 | + signature: "serialization::archive", |
| 508 | + version: "7", |
| 509 | + cfg: EncfsConfigXml::from_config(config), |
| 510 | + } |
561 | 511 | } |
562 | | - writeln!(file, "</encodedKeyData>")?; |
563 | | - |
564 | | - // Write saltLen and saltData |
565 | | - writeln!(file, " <saltLen>{}</saltLen>", self.salt.len())?; |
566 | | - writeln!(file, " <saltData>")?; |
567 | | - let salt_b64 = BASE64.encode(&self.salt); |
568 | | - for chunk in salt_b64 |
569 | | - .as_bytes() |
570 | | - .chunks(crate::constants::XML_BASE64_LINE_LEN) |
571 | | - { |
572 | | - writeln!(file, "{}", String::from_utf8_lossy(chunk))?; |
| 512 | + } |
| 513 | + |
| 514 | + #[derive(Serialize)] |
| 515 | + struct EncfsConfigXml<'a> { |
| 516 | + #[serde(rename = "@class_id")] |
| 517 | + class_id: &'static str, |
| 518 | + #[serde(rename = "@tracking_level")] |
| 519 | + tracking_level: &'static str, |
| 520 | + #[serde(rename = "@version")] |
| 521 | + version_attr: &'static str, |
| 522 | + |
| 523 | + version: i32, |
| 524 | + creator: &'a str, |
| 525 | + |
| 526 | + #[serde(rename = "cipherAlg")] |
| 527 | + cipher_alg: InterfaceXml<'a>, |
| 528 | + |
| 529 | + #[serde(rename = "nameAlg")] |
| 530 | + name_alg: InterfaceXml<'a>, |
| 531 | + |
| 532 | + #[serde(rename = "keySize")] |
| 533 | + key_size: i32, |
| 534 | + |
| 535 | + #[serde(rename = "blockSize")] |
| 536 | + block_size: i32, |
| 537 | + |
| 538 | + #[serde(rename = "plainData")] |
| 539 | + plain_data: u8, |
| 540 | + |
| 541 | + #[serde(rename = "uniqueIV")] |
| 542 | + unique_iv: u8, |
| 543 | + |
| 544 | + #[serde(rename = "chainedNameIV")] |
| 545 | + chained_name_iv: u8, |
| 546 | + |
| 547 | + #[serde(rename = "externalIVChaining")] |
| 548 | + external_iv_chaining: u8, |
| 549 | + |
| 550 | + #[serde(rename = "blockMACBytes")] |
| 551 | + block_mac_bytes: i32, |
| 552 | + |
| 553 | + #[serde(rename = "blockMACRandBytes")] |
| 554 | + block_mac_rand_bytes: i32, |
| 555 | + |
| 556 | + #[serde(rename = "allowHoles")] |
| 557 | + allow_holes: u8, |
| 558 | + |
| 559 | + #[serde(rename = "encodedKeySize")] |
| 560 | + encoded_key_size: usize, |
| 561 | + |
| 562 | + #[serde(rename = "encodedKeyData")] |
| 563 | + encoded_key_data: Base64Xml<'a>, |
| 564 | + |
| 565 | + #[serde(rename = "saltLen")] |
| 566 | + salt_len: usize, |
| 567 | + |
| 568 | + #[serde(rename = "saltData")] |
| 569 | + salt_data: Base64Xml<'a>, |
| 570 | + |
| 571 | + #[serde(rename = "kdfIterations")] |
| 572 | + kdf_iterations: i32, |
| 573 | + |
| 574 | + #[serde(rename = "desiredKDFDuration")] |
| 575 | + desired_kdf_duration: i64, |
| 576 | + } |
| 577 | + |
| 578 | + impl<'a> EncfsConfigXml<'a> { |
| 579 | + fn from_config(config: &'a EncfsConfig) -> Self { |
| 580 | + Self { |
| 581 | + class_id: "0", |
| 582 | + tracking_level: "0", |
| 583 | + version_attr: "20", |
| 584 | + version: config.version, |
| 585 | + creator: &config.creator, |
| 586 | + cipher_alg: InterfaceXml::new(&config.cipher_iface, "1"), |
| 587 | + name_alg: InterfaceXml::new(&config.name_iface, "0"), |
| 588 | + key_size: config.key_size, |
| 589 | + block_size: config.block_size, |
| 590 | + plain_data: if config.plain_data { 1 } else { 0 }, |
| 591 | + unique_iv: if config.unique_iv { 1 } else { 0 }, |
| 592 | + chained_name_iv: if config.chained_name_iv { 1 } else { 0 }, |
| 593 | + external_iv_chaining: if config.external_iv_chaining { 1 } else { 0 }, |
| 594 | + block_mac_bytes: config.block_mac_bytes, |
| 595 | + block_mac_rand_bytes: config.block_mac_rand_bytes, |
| 596 | + allow_holes: if config.allow_holes { 1 } else { 0 }, |
| 597 | + encoded_key_size: config.key_data.len(), |
| 598 | + encoded_key_data: Base64Xml { |
| 599 | + data: &config.key_data, |
| 600 | + }, |
| 601 | + salt_len: config.salt.len(), |
| 602 | + salt_data: Base64Xml { data: &config.salt }, |
| 603 | + kdf_iterations: config.kdf_iterations, |
| 604 | + desired_kdf_duration: config.desired_kdf_duration, |
| 605 | + } |
573 | 606 | } |
574 | | - writeln!(file, "</saltData>")?; |
575 | | - |
576 | | - // Write KDF fields |
577 | | - writeln!( |
578 | | - file, |
579 | | - " <kdfIterations>{}</kdfIterations>", |
580 | | - self.kdf_iterations |
581 | | - )?; |
582 | | - writeln!( |
583 | | - file, |
584 | | - " <desiredKDFDuration>{}</desiredKDFDuration>", |
585 | | - self.desired_kdf_duration |
586 | | - )?; |
587 | | - |
588 | | - // Close tags |
589 | | - writeln!(file, " </cfg>")?; |
590 | | - writeln!(file, "</boost_serialization>")?; |
| 607 | + } |
591 | 608 |
|
592 | | - Ok(()) |
| 609 | + #[derive(Serialize)] |
| 610 | + struct InterfaceXml<'a> { |
| 611 | + #[serde(rename = "@class_id", skip_serializing_if = "Option::is_none")] |
| 612 | + class_id: Option<&'static str>, |
| 613 | + #[serde(rename = "@tracking_level", skip_serializing_if = "Option::is_none")] |
| 614 | + tracking_level: Option<&'static str>, |
| 615 | + #[serde(rename = "@version", skip_serializing_if = "Option::is_none")] |
| 616 | + version: Option<&'static str>, |
| 617 | + |
| 618 | + name: &'a str, |
| 619 | + major: i32, |
| 620 | + minor: i32, |
| 621 | + } |
| 622 | + |
| 623 | + impl<'a> InterfaceXml<'a> { |
| 624 | + fn new(iface: &'a Interface, class_id: &'static str) -> Self { |
| 625 | + let (cid, tl, v) = if class_id == "1" { |
| 626 | + (Some("1"), Some("0"), Some("0")) |
| 627 | + } else { |
| 628 | + (None, None, None) |
| 629 | + }; |
| 630 | + |
| 631 | + Self { |
| 632 | + class_id: cid, |
| 633 | + tracking_level: tl, |
| 634 | + version: v, |
| 635 | + name: &iface.name, |
| 636 | + major: iface.major, |
| 637 | + minor: iface.minor, |
| 638 | + } |
| 639 | + } |
| 640 | + } |
| 641 | + |
| 642 | + struct Base64Xml<'a> { |
| 643 | + data: &'a [u8], |
| 644 | + } |
| 645 | + |
| 646 | + impl Serialize for Base64Xml<'_> { |
| 647 | + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
| 648 | + where |
| 649 | + S: serde::Serializer, |
| 650 | + { |
| 651 | + use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; |
| 652 | + let encoded = BASE64.encode(self.data); |
| 653 | + serializer.serialize_str(&encoded) |
| 654 | + } |
593 | 655 | } |
594 | 656 | } |
595 | 657 |
|
@@ -636,14 +698,6 @@ mod tests { |
636 | 698 | // 4. Compare |
637 | 699 | assert_eq!(loaded_config, reloaded_config); |
638 | 700 |
|
639 | | - // 5. Compare text content |
640 | | - let original_text = fs::read_to_string(fixture_path)?; |
641 | | - let saved_text = fs::read_to_string(&saved_path)?; |
642 | | - assert_eq!( |
643 | | - original_text, saved_text, |
644 | | - "Saved XML text differs from original" |
645 | | - ); |
646 | | - |
647 | 701 | // Cleanup |
648 | 702 | let _ = fs::remove_file(saved_path); |
649 | 703 |
|
|
0 commit comments