Normalize entry parsing and display order

This commit is contained in:
2026-08-11 07:28:15 +02:00
parent 2a8c941a65
commit d21f5bd467
6 changed files with 262 additions and 74 deletions

View File

@@ -79,7 +79,7 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
document.password().expect("password").value(),
"pässwörd".as_bytes()
);
assert_eq!(document.fields().len(), 11);
assert_eq!(document.fields().len(), 9);
let kinds = document
.fields()
@@ -96,8 +96,6 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
EntryFieldKind::Field,
EntryFieldKind::Field,
EntryFieldKind::OtpUri,
EntryFieldKind::Blank,
EntryFieldKind::Note,
EntryFieldKind::Note,
EntryFieldKind::Field,
]
@@ -123,11 +121,15 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
assert_eq!(document.fields()[5].metadata().name(), Some("custom"));
assert!(document.fields()[5].value().is_empty());
assert_ne!(document.fields()[4].id(), document.fields()[5].id());
assert_eq!(document.fields()[10].metadata().name(), Some(""));
assert_eq!(document.fields()[8].metadata().name(), Some(""));
assert_eq!(
document.fields()[10].value(),
document.fields()[8].value(),
"\r\nunrecognized line".as_bytes()
);
assert_eq!(
document.fields()[7].value(),
b"\r\nfirst note\r\nsecond note"
);
assert_eq!(
document.fields()[6].metadata().sensitivity(),
EntrySensitivity::Sensitive
@@ -175,7 +177,119 @@ fn named_multiline_fields_are_one_lossless_logical_field() -> TestResult {
document.replace_field_value(comments.id(), b"New heading\nalpha\nbeta".to_vec())?;
assert_eq!(
document.serialize().expose(),
b"password\ncomments: New heading\nalpha\nbeta\nurl: https://example.test\n"
b"password\ncomments: New heading\n alpha\n beta\nurl: https://example.test\n"
);
Ok(())
}
#[test]
fn only_whitespace_free_names_end_multiline_fields() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let mut secrets = FixtureSecrets::all(&fixture);
let plaintext = "password\ncomments: PIN: 5678\nVertragsnummer 1234\n\nwww.bahn.de Login:\n\nbenutzer / passwort\n\n(über Geschäftskundenlogin gehen!)\n\nautotype_enabled: True\nicon: 0\n";
write_plaintext(
&repository,
&keys,
"documents/comments",
plaintext.as_bytes(),
)?;
let document =
EntryDocumentService::new(&repository, &keys).open("documents/comments", &mut secrets)?;
assert_eq!(document.serialize().expose(), plaintext.as_bytes());
assert_eq!(document.fields().len(), 4);
assert_eq!(document.fields()[1].metadata().name(), Some("comments"));
assert_eq!(
document.fields()[1].value(),
"PIN: 5678\nVertragsnummer 1234\n\nwww.bahn.de Login:\n\nbenutzer / passwort\n\n(über Geschäftskundenlogin gehen!)\n"
.as_bytes()
);
assert_eq!(
document.fields()[2].metadata().name(),
Some("autotype_enabled")
);
assert_eq!(document.fields()[3].metadata().name(), Some("icon"));
assert!(matches!(
EntryFieldDraft::field("www.bahn.de Login", Vec::new()),
Err(DocumentError::InvalidFieldName)
));
write_plaintext(
&repository,
&keys,
"documents/plain-note",
b"password\n\nordinary note\ncontinued",
)?;
let note =
EntryDocumentService::new(&repository, &keys).open("documents/plain-note", &mut secrets)?;
assert_eq!(note.password().expect("password").value(), b"password");
assert_eq!(note.fields().len(), 2);
assert_eq!(note.fields()[1].value(), b"\nordinary note\ncontinued");
let unordered = "password\nzeta: last\nnote: named\ncomments: heading\notpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example\nordinary note\nurl: https://example.test\nlogin: alice\nalpha: first\n";
write_plaintext(
&repository,
&keys,
"documents/unordered",
unordered.as_bytes(),
)?;
let mut unordered_document =
EntryDocumentService::new(&repository, &keys).open("documents/unordered", &mut secrets)?;
assert_eq!(
unordered_document.serialize().expose(),
unordered.as_bytes()
);
assert_eq!(
unordered_document
.display_fields()
.into_iter()
.map(|field| field.metadata().name().unwrap_or("note"))
.collect::<Vec<_>>(),
[
"login", "password", "url", "otp", "note", "note", "comments", "alpha", "zeta"
]
);
let comments = unordered_document
.fields()
.iter()
.find(|field| field.metadata().name() == Some("comments"))
.expect("comments")
.id();
unordered_document
.replace_field_value(comments, b"heading\nline: still a comment\n\nend".to_vec())?;
assert_eq!(
unordered_document.serialize().expose(),
b"password\nzeta: last\nnote: named\ncomments: heading\n line: still a comment\n \n end\notpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example\n ordinary note\nurl: https://example.test\nlogin: alice\nalpha: first\n"
);
let normalized = unordered_document.serialize();
write_plaintext(
&repository,
&keys,
"documents/normalized",
normalized.expose(),
)?;
let normalized_document =
EntryDocumentService::new(&repository, &keys).open("documents/normalized", &mut secrets)?;
assert_eq!(
normalized_document
.fields()
.iter()
.find(|field| field.metadata().name() == Some("comments"))
.expect("normalized comments")
.value(),
b"heading\nline: still a comment\n\nend"
);
assert_eq!(
normalized_document
.fields()
.iter()
.find(|field| field.metadata().kind() == EntryFieldKind::Note)
.expect("normalized note")
.value(),
b"ordinary note"
);
Ok(())
}
@@ -264,8 +378,8 @@ fn field_ids_survive_updates_removal_and_reordering() -> TestResult {
empty.add(0, EntryFieldDraft::line(b"password".to_vec())?)?;
empty.add(1, EntryFieldDraft::blank())?;
assert_eq!(empty.fields().len(), 2);
assert_eq!(empty.fields()[1].metadata().kind(), EntryFieldKind::Blank);
assert_eq!(empty.serialize().expose(), b"password\n");
assert_eq!(empty.fields()[1].metadata().kind(), EntryFieldKind::Note);
assert_eq!(empty.serialize().expose(), b"password\n ");
assert!(matches!(
EntryFieldDraft::line(b"two\nlines".to_vec()),