Establish pass compatibility fixture harness (#1)
This commit is contained in:
236
crates/storage/tests/compatibility_fixtures.rs
Normal file
236
crates/storage/tests/compatibility_fixtures.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod support;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use support::compatibility::{
|
||||
FixtureSet, TestResult, decrypt_entry_with_passphrase, validate_entry_record,
|
||||
validate_key_record, validate_recipient_signature, validate_repository,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn pins_the_selected_upstream_revisions() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
assert_eq!(fixture.upstream.format, 1);
|
||||
assert_eq!(fixture.upstream.project.len(), 2);
|
||||
|
||||
let projects = fixture
|
||||
.upstream
|
||||
.project
|
||||
.iter()
|
||||
.map(|project| (project.name.as_str(), project))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let password_store = projects["password-store"];
|
||||
assert_eq!(password_store.version, "1.7.4");
|
||||
assert_eq!(password_store.reported_version, "1.7.4");
|
||||
assert_eq!(
|
||||
password_store.commit,
|
||||
"1078f2514d579178d5df7042c6a790e9c9b731ad"
|
||||
);
|
||||
let pass_otp = projects["pass-otp"];
|
||||
assert_eq!(pass_otp.version, "1.2.0");
|
||||
assert_eq!(pass_otp.reported_version, "1.1.1");
|
||||
assert_eq!(pass_otp.commit, "1e9d10ca75ae1a8672a7f192809713463657778e");
|
||||
for project in projects.values() {
|
||||
assert_eq!(project.commit.len(), 40);
|
||||
assert!(project.commit.bytes().all(|byte| byte.is_ascii_hexdigit()));
|
||||
assert!(project.source.starts_with("https://"));
|
||||
assert!(!project.behavior_source.is_empty());
|
||||
assert!(project.license.starts_with("GPL-"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn behavior_catalog_covers_the_milestone_contract() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
assert_eq!(fixture.behavior.format, 1);
|
||||
assert!(fixture.behavior.cases.len() >= 65);
|
||||
|
||||
let mut ids = BTreeSet::new();
|
||||
let mut commands = BTreeSet::new();
|
||||
let mut flags = BTreeSet::new();
|
||||
let mut areas = BTreeSet::new();
|
||||
let mut has_success = false;
|
||||
let mut has_failure = false;
|
||||
|
||||
for case in &fixture.behavior.cases {
|
||||
assert!(
|
||||
ids.insert(case.id.as_str()),
|
||||
"duplicate case ID {}",
|
||||
case.id
|
||||
);
|
||||
assert!(
|
||||
matches!(case.status, 0 | 1),
|
||||
"invalid status in {}",
|
||||
case.id
|
||||
);
|
||||
has_success |= case.status == 0;
|
||||
has_failure |= case.status != 0;
|
||||
areas.insert(case.area.as_str());
|
||||
if let Some(command) = case.argv.first() {
|
||||
commands.insert(command.as_str());
|
||||
}
|
||||
for argument in &case.argv {
|
||||
if argument.starts_with('-') {
|
||||
let flag = argument.split('=').next().unwrap_or(argument);
|
||||
flags.insert(flag);
|
||||
}
|
||||
}
|
||||
if case.status != 0 {
|
||||
assert!(case.recipients.is_empty(), "failed case has recipients");
|
||||
assert!(case.commit.is_empty(), "failed case has a commit");
|
||||
}
|
||||
assert!(!case.outcome.is_empty());
|
||||
assert!(!case.stdin.is_empty());
|
||||
}
|
||||
|
||||
assert!(has_success && has_failure);
|
||||
for area in [
|
||||
"init", "show", "list", "find", "grep", "insert", "edit", "generate", "remove", "move",
|
||||
"copy", "git", "otp", "meta",
|
||||
] {
|
||||
assert!(areas.contains(area), "missing behavior area {area}");
|
||||
}
|
||||
for command in [
|
||||
"init",
|
||||
"show",
|
||||
"ls",
|
||||
"list",
|
||||
"find",
|
||||
"search",
|
||||
"grep",
|
||||
"insert",
|
||||
"add",
|
||||
"edit",
|
||||
"generate",
|
||||
"rm",
|
||||
"remove",
|
||||
"delete",
|
||||
"mv",
|
||||
"rename",
|
||||
"cp",
|
||||
"copy",
|
||||
"git",
|
||||
"otp",
|
||||
"help",
|
||||
"version",
|
||||
"--help",
|
||||
"--version",
|
||||
] {
|
||||
assert!(
|
||||
commands.contains(command),
|
||||
"missing command or alias {command}"
|
||||
);
|
||||
}
|
||||
for flag in [
|
||||
"-p",
|
||||
"--path",
|
||||
"-c",
|
||||
"--clip",
|
||||
"-q2",
|
||||
"--qrcode",
|
||||
"-e",
|
||||
"--echo",
|
||||
"-m",
|
||||
"--multiline",
|
||||
"-f",
|
||||
"--force",
|
||||
"-n",
|
||||
"--no-symbols",
|
||||
"-i",
|
||||
"--in-place",
|
||||
"-r",
|
||||
"--recursive",
|
||||
"-s",
|
||||
"--secret",
|
||||
"-a",
|
||||
"--account",
|
||||
"--issuer",
|
||||
] {
|
||||
assert!(flags.contains(flag), "missing option spelling {flag}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_openpgp_fixtures_are_self_consistent() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
assert_eq!(fixture.generated.format, 1);
|
||||
assert_eq!(fixture.generated.keys.len(), 2);
|
||||
assert!(fixture.generated.entries.len() >= 8);
|
||||
for key in &fixture.generated.keys {
|
||||
validate_key_record(&fixture, key)?;
|
||||
}
|
||||
for entry in &fixture.generated.entries {
|
||||
validate_entry_record(&fixture, entry)?;
|
||||
}
|
||||
let bob_entry = fixture
|
||||
.generated
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.store == "basic" && entry.path == "team/service.gpg")
|
||||
.expect("Bob-only entry");
|
||||
let bob = fixture.key("bob")?;
|
||||
assert!(
|
||||
decrypt_entry_with_passphrase(&fixture, bob_entry, bob, "fixture-alice-passphrase",)
|
||||
.is_err(),
|
||||
"the encrypted secret-key passphrase must be required"
|
||||
);
|
||||
validate_recipient_signature(&fixture, "basic", "alice")?;
|
||||
validate_recipient_signature(&fixture, "basic/team", "bob")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_git_repositories_have_valid_objects_and_refs() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
assert_eq!(fixture.generated.repositories.len(), 3);
|
||||
for repository in &fixture.generated.repositories {
|
||||
validate_repository(&fixture, repository)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixtures_materialize_without_touching_a_user_store() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
assert!(store.path().join(".gpg-id").is_file());
|
||||
assert!(store.path().join("email/personal.gpg").is_file());
|
||||
assert!(store.path().join("team/.gpg-id").is_file());
|
||||
|
||||
let repository = fixture.materialize_repository("basic")?;
|
||||
assert!(repository.path().join("HEAD").is_file());
|
||||
assert!(repository.path().join("objects").is_dir());
|
||||
assert!(repository.path().join("refs/heads/main").is_file());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_and_interruption_recipes_are_present() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let nodes = fixture
|
||||
.layouts
|
||||
.get("synthetic_node")
|
||||
.and_then(toml::Value::as_array)
|
||||
.expect("synthetic_node array");
|
||||
assert!(nodes.len() >= 4);
|
||||
let outcomes = nodes
|
||||
.iter()
|
||||
.filter_map(|node| node.get("expected"))
|
||||
.filter_map(toml::Value::as_str)
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(outcomes.contains("reject-symlink-escape"));
|
||||
assert!(outcomes.contains("reject-unsupported-file-type"));
|
||||
assert!(outcomes.contains("require-trailing-slash-disambiguation"));
|
||||
|
||||
let interruptions = fixture
|
||||
.layouts
|
||||
.get("interruption")
|
||||
.and_then(toml::Value::as_array)
|
||||
.expect("interruption array");
|
||||
assert_eq!(interruptions.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
52
crates/storage/tests/fixtures/compatibility/README.md
vendored
Normal file
52
crates/storage/tests/fixtures/compatibility/README.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# Compatibility fixtures
|
||||
|
||||
These fixtures pin password-store 1.7.4 at commit
|
||||
`1078f2514d579178d5df7042c6a790e9c9b731ad` and pass-otp 1.2.0 at commit
|
||||
`1e9d10ca75ae1a8672a7f192809713463657778e`. The behavioral catalog was
|
||||
written from the projects' published command documentation and pinned source;
|
||||
no upstream source or test code is copied here.
|
||||
|
||||
The v1.2.0 pass-otp tag reports `1.1.1` from its `version` command. The
|
||||
fixture records that observable upstream behavior explicitly rather than
|
||||
silently correcting it.
|
||||
|
||||
All identities, passphrases, plaintexts, OTP secrets, Git identities, and
|
||||
remote names are synthetic and public. They must never be reused for real
|
||||
secrets. The generator reuses the checked-in identities and produces:
|
||||
|
||||
- ASCII-armored and binary public keys;
|
||||
- passphrase-protected ASCII-armored and binary secret keys;
|
||||
- binary OpenPGP messages in an ordinary pass directory tree;
|
||||
- root, nested, signed, and multi-recipient `.gpg-id` policies;
|
||||
- TOTP and HOTP entries;
|
||||
- valid loose-object Git repository templates, including an automatic-commit
|
||||
history and nested-repository boundaries.
|
||||
|
||||
Rust tests parse keys, verify signatures, decrypt every entry, compare exact
|
||||
plaintext bytes, validate ciphertext digests, validate Git objects and refs,
|
||||
and materialize isolated stores and repositories. They never invoke `pass`,
|
||||
`gpg`, `git`, a shell, or an OTP/QR helper.
|
||||
|
||||
## Refreshing fixtures
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
cargo run -p ironstorage --example refresh_compatibility_fixtures
|
||||
cargo test -p ironstorage --test compatibility_fixtures
|
||||
```
|
||||
|
||||
Normal refreshes are byte-for-byte reproducible at the locked dependency
|
||||
versions because checked-in identities are reused. Deleting the key fixtures
|
||||
explicitly rotates the synthetic identities; OpenPGP self-signature timestamps
|
||||
then make that rotation intentionally non-reproducible. Review all binary
|
||||
changes and the generated fingerprints, digests, and Git object IDs before
|
||||
committing them.
|
||||
|
||||
An optional developer interoperability audit may import the generated keys
|
||||
into a disposable GnuPG home, decrypt the `.gpg` entries, and point an
|
||||
upstream `pass` 1.7.4 checkout at a materialized store. That audit is never a
|
||||
test prerequisite and must use a temporary keyring and the synthetic fixture
|
||||
passphrases. Refreshing observable behavior against upstream `pass` or
|
||||
`pass-otp` is similarly development-only; application/runtime code may not
|
||||
execute either tool.
|
||||
1205
crates/storage/tests/fixtures/compatibility/behavior.toml
vendored
Normal file
1205
crates/storage/tests/fixtures/compatibility/behavior.toml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
crates/storage/tests/fixtures/compatibility/expected/basic/email/personal.txt
vendored
Normal file
3
crates/storage/tests/fixtures/compatibility/expected/basic/email/personal.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
correct horse fixture
|
||||
login: alice@example.test
|
||||
url: https://example.test
|
||||
1
crates/storage/tests/fixtures/compatibility/expected/basic/otp/hotp.txt
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/expected/basic/otp/hotp.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
otpauth://hotp/IronStorage:counter?secret=JBSWY3DPEHPK3PXP&issuer=IronStorage&counter=0&digits=8
|
||||
2
crates/storage/tests/fixtures/compatibility/expected/basic/otp/totp.txt
vendored
Normal file
2
crates/storage/tests/fixtures/compatibility/expected/basic/otp/totp.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
fixture-password
|
||||
otpauth://totp/IronStorage:alice%40example.test?secret=JBSWY3DPEHPK3PXP&issuer=IronStorage&algorithm=SHA1&digits=6&period=30
|
||||
2
crates/storage/tests/fixtures/compatibility/expected/basic/shared/multiple.txt
vendored
Normal file
2
crates/storage/tests/fixtures/compatibility/expected/basic/shared/multiple.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
shared-fixture-password
|
||||
recipients: alice,bob
|
||||
2
crates/storage/tests/fixtures/compatibility/expected/basic/team/service.txt
vendored
Normal file
2
crates/storage/tests/fixtures/compatibility/expected/basic/team/service.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
team-fixture-password
|
||||
owner: bob
|
||||
3
crates/storage/tests/fixtures/compatibility/expected/basic/unicode/咖啡.txt
vendored
Normal file
3
crates/storage/tests/fixtures/compatibility/expected/basic/unicode/咖啡.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
pässwörd-猫
|
||||
login: 用户@example.test
|
||||
notes: café
|
||||
1
crates/storage/tests/fixtures/compatibility/expected/nested/outer/inner/nested-entry.txt
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/expected/nested/outer/inner/nested-entry.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
inner repository entry
|
||||
1
crates/storage/tests/fixtures/compatibility/expected/nested/outer/root-entry.txt
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/expected/nested/outer/root-entry.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
outer repository entry
|
||||
105
crates/storage/tests/fixtures/compatibility/generated.toml
vendored
Normal file
105
crates/storage/tests/fixtures/compatibility/generated.toml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
format = 1
|
||||
generated_by = "cargo run -p ironstorage --example refresh_compatibility_fixtures"
|
||||
fixture_seed = "49 repeated 32 times (hex); checked-in keys are reused on refresh"
|
||||
|
||||
[[keys]]
|
||||
name = "alice"
|
||||
user_id = "Alice Fixture <alice@ironstorage.invalid>"
|
||||
primary_fingerprint = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30"
|
||||
encryption_subkey_fingerprint = "10208D0B84AD13D2157795D5EB55885EC93C0A26"
|
||||
passphrase = "fixture-alice-passphrase"
|
||||
public_armor = "keys/alice-public.asc"
|
||||
public_binary = "keys/alice-public.pgp"
|
||||
secret_armor = "keys/alice-secret.asc"
|
||||
secret_binary = "keys/alice-secret.pgp"
|
||||
|
||||
[[keys]]
|
||||
name = "bob"
|
||||
user_id = "Bob Fixture <bob@ironstorage.invalid>"
|
||||
primary_fingerprint = "B37027B56FC406BD3F6A622B2AC03492B992D06F"
|
||||
encryption_subkey_fingerprint = "46E34F32333752A01E4E019B4AC0B1A0B4D6278D"
|
||||
passphrase = "fixture-bob-passphrase"
|
||||
public_armor = "keys/bob-public.asc"
|
||||
public_binary = "keys/bob-public.pgp"
|
||||
secret_armor = "keys/bob-secret.asc"
|
||||
secret_binary = "keys/bob-secret.pgp"
|
||||
|
||||
[[entries]]
|
||||
store = "basic"
|
||||
path = "email/personal.gpg"
|
||||
plaintext = "email/personal.txt"
|
||||
recipients = ["7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30"]
|
||||
ciphertext_sha256 = "8259de0c1ae6b5d4fc3ebd410452f96a266b0d55d111cfaf9c609baf3deda1b2"
|
||||
|
||||
[[entries]]
|
||||
store = "basic"
|
||||
path = "unicode/咖啡.gpg"
|
||||
plaintext = "unicode/咖啡.txt"
|
||||
recipients = ["7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30"]
|
||||
ciphertext_sha256 = "c5c829b46e517109bcfea7731c846ca6d09eff8076fe4444aaa1c2512fc06d44"
|
||||
|
||||
[[entries]]
|
||||
store = "basic"
|
||||
path = "otp/totp.gpg"
|
||||
plaintext = "otp/totp.txt"
|
||||
recipients = ["7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30"]
|
||||
ciphertext_sha256 = "260a0d7371964ca9d115e2d818cf1a8a63e2a4ff9a172ca421c569aa649cf014"
|
||||
|
||||
[[entries]]
|
||||
store = "basic"
|
||||
path = "otp/hotp.gpg"
|
||||
plaintext = "otp/hotp.txt"
|
||||
recipients = ["7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30"]
|
||||
ciphertext_sha256 = "337bb6568bc3805b8c269463654b081da88d3755da62603b263b6865babf9bd2"
|
||||
|
||||
[[entries]]
|
||||
store = "basic"
|
||||
path = "team/service.gpg"
|
||||
plaintext = "team/service.txt"
|
||||
recipients = ["B37027B56FC406BD3F6A622B2AC03492B992D06F"]
|
||||
ciphertext_sha256 = "b325904fbdbe335bc9cd2319ace3e598da194e6450bf3f47371507b1f47306ae"
|
||||
|
||||
[[entries]]
|
||||
store = "basic"
|
||||
path = "shared/multiple.gpg"
|
||||
plaintext = "shared/multiple.txt"
|
||||
recipients = [
|
||||
"7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30",
|
||||
"B37027B56FC406BD3F6A622B2AC03492B992D06F",
|
||||
]
|
||||
ciphertext_sha256 = "7a8ac913dfb930d6ed8d98d40b12be9f522685c5be45d6b2b30b1bc407b5996b"
|
||||
|
||||
[[entries]]
|
||||
store = "nested"
|
||||
path = "outer/root-entry.gpg"
|
||||
plaintext = "outer/root-entry.txt"
|
||||
recipients = ["7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30"]
|
||||
ciphertext_sha256 = "58c1470b623ec7076ddf52e8c3a8372e508ab4b84b7ad146e7878d85e4b872de"
|
||||
|
||||
[[entries]]
|
||||
store = "nested"
|
||||
path = "outer/inner/nested-entry.gpg"
|
||||
plaintext = "outer/inner/nested-entry.txt"
|
||||
recipients = ["B37027B56FC406BD3F6A622B2AC03492B992D06F"]
|
||||
ciphertext_sha256 = "b8757d983ad7d78205f3f7b74cfb22b949aebae25bc70febcffe0c5d6ec4882b"
|
||||
|
||||
[[repositories]]
|
||||
name = "basic"
|
||||
template = "repositories/basic.git"
|
||||
head = "c1bec8815d40fab6e23162443ef55ce1c46bddf6"
|
||||
commits = [
|
||||
"a7d88415df6027082096f6fd035387ac76726920",
|
||||
"c1bec8815d40fab6e23162443ef55ce1c46bddf6",
|
||||
]
|
||||
|
||||
[[repositories]]
|
||||
name = "outer"
|
||||
template = "repositories/outer.git"
|
||||
head = "2c32c59867d2e4b4ab07ca8e9225e8eaa887a58e"
|
||||
commits = ["2c32c59867d2e4b4ab07ca8e9225e8eaa887a58e"]
|
||||
|
||||
[[repositories]]
|
||||
name = "inner"
|
||||
template = "repositories/inner.git"
|
||||
head = "308371ba7fd91255536f4e6afee2e23a02af6915"
|
||||
commits = ["308371ba7fd91255536f4e6afee2e23a02af6915"]
|
||||
13
crates/storage/tests/fixtures/compatibility/keys/alice-public.asc
vendored
Normal file
13
crates/storage/tests/fixtures/compatibility/keys/alice-public.asc
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xjMEZZIAgBYJKwYBBAHaRw8BAQdAZihauC+eAYkipuDUFUMqq+UVCvWVZCK8gvTi
|
||||
rkx1grXNKUFsaWNlIEZpeHR1cmUgPGFsaWNlQGlyb25zdG9yYWdlLmludmFsaWQ+
|
||||
wokEExYIADEFAmp457IWIQR+XFJBsl9v+q1xfr+hMtyy3COuMAIbAwIeAQMLCQcC
|
||||
FQgBFgEnAhkBAAoJEKEy3LLcI64wEAABAKqDzgiRwCBFC+kcbKGNOWO9O/nKVDHR
|
||||
Q2d4r8HOUXItAQCTGZHFaBkaZNyDjAOLTBBWMBxa0i5WatkAx5ZnKJT8Cs44BGWS
|
||||
AIASCisGAQQBl1UBBQEBB0BIPO/SwIrdEJLCGmXEG1/szwa9u7WK0rPQArtvrYMx
|
||||
PAMBCAfCeAQYFggAIAUCanjnswIbDBYhBH5cUkGyX2/6rXF+v6Ey3LLcI64wAAoJ
|
||||
EKEy3LLcI64wyKIA/i7fuf/N31swIwzJXLBkzU4BvWJfv6NuHuvQej4qmnhrAQDi
|
||||
ZJ+POdCbAxeTE7wNT5llULI8oowyUXfcF7C7UzDCAw==
|
||||
=Mbap
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
BIN
crates/storage/tests/fixtures/compatibility/keys/alice-public.pgp
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/keys/alice-public.pgp
vendored
Normal file
Binary file not shown.
17
crates/storage/tests/fixtures/compatibility/keys/alice-secret.asc
vendored
Normal file
17
crates/storage/tests/fixtures/compatibility/keys/alice-secret.asc
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
xYYEZZIAgBYJKwYBBAHaRw8BAQdAZihauC+eAYkipuDUFUMqq+UVCvWVZCK8gvTi
|
||||
rkx1grX+CQMIY/R1q1/mEXzgJkJdUlNlns+1/EtfSNxLhlzT8XaaLL/pf7ZPI7e0
|
||||
mvzQuBEvkEGTVAxt2u80sGMY7mbZvcG6L++dUpP1FqvPdM8fjCv8PM0pQWxpY2Ug
|
||||
Rml4dHVyZSA8YWxpY2VAaXJvbnN0b3JhZ2UuaW52YWxpZD7CiQQTFggAMQUCanjn
|
||||
shYhBH5cUkGyX2/6rXF+v6Ey3LLcI64wAhsDAh4BAwsJBwIVCAEWAScCGQEACgkQ
|
||||
oTLcstwjrjAQAAEAqoPOCJHAIEUL6RxsoY05Y707+cpUMdFDZ3ivwc5Rci0BAJMZ
|
||||
kcVoGRpk3IOMA4tMEFYwHFrSLlZq2QDHlmcolPwKx4sEZZIAgBIKKwYBBAGXVQEF
|
||||
AQEHQEg879LAit0QksIaZcQbX+zPBr27tYrSs9ACu2+tgzE8AwEIB/4JAwhRDjBN
|
||||
dWgYi+AGAFqqDR6n0O+kc1WyeUYI+G8+R9EDz6Ar0xGGaZZ8GUU17BWGDY/UbxUU
|
||||
z5LHAQ2KB05GYf/9VjHTaKZCSiKFJlCElEASwngEGBYIACAFAmp457MCGwwWIQR+
|
||||
XFJBsl9v+q1xfr+hMtyy3COuMAAKCRChMtyy3COuMMiiAP4u37n/zd9bMCMMyVyw
|
||||
ZM1OAb1iX7+jbh7r0Ho+Kpp4awEA4mSfjznQmwMXkxO8DU+ZZVCyPKKMMlF33Bew
|
||||
u1MwwgM=
|
||||
=ij6E
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
BIN
crates/storage/tests/fixtures/compatibility/keys/alice-secret.pgp
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/keys/alice-secret.pgp
vendored
Normal file
Binary file not shown.
13
crates/storage/tests/fixtures/compatibility/keys/bob-public.asc
vendored
Normal file
13
crates/storage/tests/fixtures/compatibility/keys/bob-public.asc
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xjMEZZIAgBYJKwYBBAHaRw8BAQdANd08TxPj0ZuIl1cMvEf0FMCxubDwlzXDLX6w
|
||||
9M+959XNJUJvYiBGaXh0dXJlIDxib2JAaXJvbnN0b3JhZ2UuaW52YWxpZD7CiQQT
|
||||
FggAMQUCanjntBYhBLNwJ7VvxAa9P2piKyrANJK5ktBvAhsDAh4BAwsJBwIVCAEW
|
||||
AScCGQEACgkQKsA0krmS0G9BCwD/daZy2BdmNQX8TldPBtoLTrugeaFmtrcVDQZ3
|
||||
AzWEMcMA/16q87Zkaw2ozanLSBcRiq4iZnBTAmU/7bUSlA2myZoHzjgEZZIAgBIK
|
||||
KwYBBAGXVQEFAQEHQByJpci1aMxEcUVlw9mByZUPyaQ7kI1emdJOuN1hcLEKAwEI
|
||||
B8J4BBgWCAAgBQJqeOe1AhsMFiEEs3AntW/EBr0/amIrKsA0krmS0G8ACgkQKsA0
|
||||
krmS0G+2lwEAvD9R7o52H37GsfeMS15UJBeIHBaYHjpsIyfoOWq0vsEA/j0oL4IG
|
||||
SIakL3z1C1KHU+4iz3rzz2vNnhgGQd7+DS8H
|
||||
=vS0q
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
BIN
crates/storage/tests/fixtures/compatibility/keys/bob-public.pgp
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/keys/bob-public.pgp
vendored
Normal file
Binary file not shown.
17
crates/storage/tests/fixtures/compatibility/keys/bob-secret.asc
vendored
Normal file
17
crates/storage/tests/fixtures/compatibility/keys/bob-secret.asc
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
xYYEZZIAgBYJKwYBBAHaRw8BAQdANd08TxPj0ZuIl1cMvEf0FMCxubDwlzXDLX6w
|
||||
9M+959X+CQMIx1A5+eCcr6bgYLtP6nkTdu/qlT/dwB7B38PIlZekNRoN2yd48DcO
|
||||
WmOqWrkE+Fxovj8zITOJUGP16kHJjW0727wsY1LI4BrX2Gsa3m0jV80lQm9iIEZp
|
||||
eHR1cmUgPGJvYkBpcm9uc3RvcmFnZS5pbnZhbGlkPsKJBBMWCAAxBQJqeOe0FiEE
|
||||
s3AntW/EBr0/amIrKsA0krmS0G8CGwMCHgEDCwkHAhUIARYBJwIZAQAKCRAqwDSS
|
||||
uZLQb0ELAP91pnLYF2Y1BfxOV08G2gtOu6B5oWa2txUNBncDNYQxwwD/XqrztmRr
|
||||
DajNqctIFxGKriJmcFMCZT/ttRKUDabJmgfHiwRlkgCAEgorBgEEAZdVAQUBAQdA
|
||||
HImlyLVozERxRWXD2YHJlQ/JpDuQjV6Z0k643WFwsQoDAQgH/gkDCJdPSdzYfIpD
|
||||
4M3cgwcGp2Ok/jR9F18ObkKhc/M6UfllIkZKLNl9zjUnDagCIzy+ZiLWTxAAKl02
|
||||
bGkW56gOcrp+PIARjn85GEBRXXYogE7CeAQYFggAIAUCanjntQIbDBYhBLNwJ7Vv
|
||||
xAa9P2piKyrANJK5ktBvAAoJECrANJK5ktBvtpcBALw/Ue6Odh9+xrH3jEteVCQX
|
||||
iBwWmB46bCMn6DlqtL7BAP49KC+CBkiGpC989QtSh1PuIs96889rzZ4YBkHe/g0v
|
||||
Bw==
|
||||
=w2Yf
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
BIN
crates/storage/tests/fixtures/compatibility/keys/bob-secret.pgp
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/keys/bob-secret.pgp
vendored
Normal file
Binary file not shown.
51
crates/storage/tests/fixtures/compatibility/layouts.toml
vendored
Normal file
51
crates/storage/tests/fixtures/compatibility/layouts.toml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
format = 1
|
||||
|
||||
[[layout]]
|
||||
id = "basic"
|
||||
store = "stores/basic"
|
||||
purpose = "ordinary, Unicode, OTP, nested recipient, signature, and multi-recipient entries"
|
||||
|
||||
[[layout]]
|
||||
id = "nested"
|
||||
store = "stores/nested/outer"
|
||||
purpose = "an outer store and an inner recipient/Git boundary"
|
||||
|
||||
[[synthetic_node]]
|
||||
layout = "hostile"
|
||||
kind = "symlink"
|
||||
path = "escape.gpg"
|
||||
target = "../outside.gpg"
|
||||
expected = "reject-symlink-escape"
|
||||
|
||||
[[synthetic_node]]
|
||||
layout = "hostile"
|
||||
kind = "symlink"
|
||||
path = "directory-link"
|
||||
target = "../outside"
|
||||
expected = "reject-symlink-escape"
|
||||
|
||||
[[synthetic_node]]
|
||||
layout = "hostile"
|
||||
kind = "fifo"
|
||||
path = "unsupported.gpg"
|
||||
target = ""
|
||||
expected = "reject-unsupported-file-type"
|
||||
|
||||
[[synthetic_node]]
|
||||
layout = "collision"
|
||||
kind = "entry-directory-collision"
|
||||
path = "ambiguous"
|
||||
target = "ambiguous.gpg"
|
||||
expected = "require-trailing-slash-disambiguation"
|
||||
|
||||
[[interruption]]
|
||||
id = "before-temp-fsync"
|
||||
expected = "original-entry-intact"
|
||||
|
||||
[[interruption]]
|
||||
id = "after-temp-fsync-before-rename"
|
||||
expected = "original-entry-intact"
|
||||
|
||||
[[interruption]]
|
||||
id = "after-rename-before-directory-fsync"
|
||||
expected = "new-entry-complete"
|
||||
1
crates/storage/tests/fixtures/compatibility/repositories/basic.git/HEAD
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/repositories/basic.git/HEAD
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/main
|
||||
6
crates/storage/tests/fixtures/compatibility/repositories/basic.git/config
vendored
Normal file
6
crates/storage/tests/fixtures/compatibility/repositories/basic.git/config
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[pass]
|
||||
signcommits = false
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
crates/storage/tests/fixtures/compatibility/repositories/basic.git/refs/heads/main
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/repositories/basic.git/refs/heads/main
vendored
Normal file
@@ -0,0 +1 @@
|
||||
c1bec8815d40fab6e23162443ef55ce1c46bddf6
|
||||
1
crates/storage/tests/fixtures/compatibility/repositories/inner.git/HEAD
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/repositories/inner.git/HEAD
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/main
|
||||
6
crates/storage/tests/fixtures/compatibility/repositories/inner.git/config
vendored
Normal file
6
crates/storage/tests/fixtures/compatibility/repositories/inner.git/config
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[pass]
|
||||
signcommits = false
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
xœŽK
|
||||
Â0@]÷³Ê˜¤IÝ®=A>S
|
||||
ØD¦Sõø¢½‚»/ÖqÌʘ•0D¯·Q‡Áû¤u¯¬²
|
||||
U?8§BLØYÝãûÆÏr«g®å"•ý•à”ß23ÁnXà<58>¹–i‘m.OÏi‡Sˆ°FDlâoAè/±æ˜Ä™™Š@¬E¨Èu€\
|
||||
Binary file not shown.
Binary file not shown.
1
crates/storage/tests/fixtures/compatibility/repositories/inner.git/refs/heads/main
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/repositories/inner.git/refs/heads/main
vendored
Normal file
@@ -0,0 +1 @@
|
||||
308371ba7fd91255536f4e6afee2e23a02af6915
|
||||
1
crates/storage/tests/fixtures/compatibility/repositories/outer.git/HEAD
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/repositories/outer.git/HEAD
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/main
|
||||
6
crates/storage/tests/fixtures/compatibility/repositories/outer.git/config
vendored
Normal file
6
crates/storage/tests/fixtures/compatibility/repositories/outer.git/config
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[pass]
|
||||
signcommits = false
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
crates/storage/tests/fixtures/compatibility/repositories/outer.git/refs/heads/main
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/repositories/outer.git/refs/heads/main
vendored
Normal file
@@ -0,0 +1 @@
|
||||
2c32c59867d2e4b4ab07ca8e9225e8eaa887a58e
|
||||
1
crates/storage/tests/fixtures/compatibility/stores/basic/.gpg-id
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/stores/basic/.gpg-id
vendored
Normal file
@@ -0,0 +1 @@
|
||||
7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30
|
||||
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/.gpg-id.sig
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/.gpg-id.sig
vendored
Normal file
Binary file not shown.
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/email/personal.gpg
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/email/personal.gpg
vendored
Normal file
Binary file not shown.
2
crates/storage/tests/fixtures/compatibility/stores/basic/otp/hotp.gpg
vendored
Normal file
2
crates/storage/tests/fixtures/compatibility/stores/basic/otp/hotp.gpg
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Á^ëUˆ^É<
|
||||
&@ÞLT5µ–« w캩ÆÉaО<13>—Õ×¶Ú8%£–ï@0~±`ÇAEŠÄauÚ#‰¡‚kröøáÆ@ÓFãMiÜJ1Z›†ðy ûFÛ…”}ý˜>Ò’G™M+n²¼/xu†=³uÙ;C¸ùV½n<C2BD>AÛ¬v!7ÿaÞ]XV@x]þˆ ðÁîÓÃæ“Ã{<GB<47>úï’JÓç‘e5u¨€]ƒþf¶†eÝ è<0E>S¼D³žÐMÜ¥@ÂÍ_ÓO‘°`švÂþ¾·×…'þ.¿e|n„Ò!Ñ
|
||||
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/otp/totp.gpg
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/otp/totp.gpg
vendored
Normal file
Binary file not shown.
2
crates/storage/tests/fixtures/compatibility/stores/basic/shared/.gpg-id
vendored
Normal file
2
crates/storage/tests/fixtures/compatibility/stores/basic/shared/.gpg-id
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30
|
||||
B37027B56FC406BD3F6A622B2AC03492B992D06F
|
||||
3
crates/storage/tests/fixtures/compatibility/stores/basic/shared/multiple.gpg
vendored
Normal file
3
crates/storage/tests/fixtures/compatibility/stores/basic/shared/multiple.gpg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Á^ëUˆ^É<
|
||||
&@ÆÀ€–D¾bó
|
||||
ƒ–²ÁÎUšIJà ‚x¸Ä[¦üO0l‹]¦žÍé2+û'µþ¡jfEÜd+ܲɜE“¦b™x4”Öð™Fõ¾Ðj‡'ÕM÷»šÁ^JÀ± ´Ö'<27>@«>Ó˜ÖÁ§·‚&Ί¡àc™Íë€Èâ¶d\ô_x0#À—Û´Œ6‹AûŠ˜þ¥X ·uë'c¿°(„<>¬hÈŽ¢<C5BD>´x<C2B4>`ËŃë—hrØÞmÒ_Á𨗲w:â^–>ûzûêó#7:ã§$U€¦ñ<C2A6><C3B1>êãJ×óÅEN6V•®Ol$<24>óÕ½ÅZ•5)µ–®þÁÅ^/™›.Š£/Vn_[&ú%…Ý‚—ùýEÍ{Ž&œ
|
||||
1
crates/storage/tests/fixtures/compatibility/stores/basic/team/.gpg-id
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/stores/basic/team/.gpg-id
vendored
Normal file
@@ -0,0 +1 @@
|
||||
B37027B56FC406BD3F6A622B2AC03492B992D06F
|
||||
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/team/.gpg-id.sig
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/stores/basic/team/.gpg-id.sig
vendored
Normal file
Binary file not shown.
1
crates/storage/tests/fixtures/compatibility/stores/basic/team/service.gpg
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/stores/basic/team/service.gpg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Á^JÀ± ´Ö'<27>@˜RžEmF¶z}àŠÜwMþŽÖy³?T9ôL0÷Y³é¦ºõ"‹Gw©<77>/§á„ÒÓJfƒ¸Žy2àªVëtÏ«¹ô±XÐÆÉ;¾h¥ÒR!ÜŽbd‘'E^[Ì“_|“ã¿Åû¿cwk˜ 0j¡ÑÚ=xÔü8(ˆjqVý¿¢˜7®ž¸qW<71>K‘Ð.+'Î×ß9)<29>W
|
||||
2
crates/storage/tests/fixtures/compatibility/stores/basic/unicode/咖啡.gpg
vendored
Normal file
2
crates/storage/tests/fixtures/compatibility/stores/basic/unicode/咖啡.gpg
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
а^КU┬^и<
|
||||
&@╧c┐╩▓f≤╟}▐Т╔ц│[v┐ЦТк`%@╙Вvw0ЕвБ╜К├┬©©SЭgя┬┤жbжЯ■l║≥Х Р┐HЪhФGG╡╛.д┼≈╔Kырh∙╦й└ус·┬╝┌─PРЬ~x2Б°IБ4<÷╙%1Bпmиk?ЦYГ╒o▄з╩\л2млХTbеВ%┴╫F,■▀gВ∙И┬┐уЗ°Уhm┤C?{g╖ъ▐i╣╠z_gкщ▓Щр╚Д
|
||||
1
crates/storage/tests/fixtures/compatibility/stores/nested/outer/.gpg-id
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/stores/nested/outer/.gpg-id
vendored
Normal file
@@ -0,0 +1 @@
|
||||
7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30
|
||||
1
crates/storage/tests/fixtures/compatibility/stores/nested/outer/inner/.gpg-id
vendored
Normal file
1
crates/storage/tests/fixtures/compatibility/stores/nested/outer/inner/.gpg-id
vendored
Normal file
@@ -0,0 +1 @@
|
||||
B37027B56FC406BD3F6A622B2AC03492B992D06F
|
||||
2
crates/storage/tests/fixtures/compatibility/stores/nested/outer/inner/nested-entry.gpg
vendored
Normal file
2
crates/storage/tests/fixtures/compatibility/stores/nested/outer/inner/nested-entry.gpg
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Á^JÀ± ´Ö'<27>@YC
|
||||
ÔEÕõÚËêPô˜l¼w`|÷ǵkPsSk©µ•H0Ð_ç´²ka¢g#Ôú‚õ¿½<O¼(jYÚЧ§ÄèäÑì-°Ä‘j'³ñLÒHå:o£|·Ö!=:c:ƒé²ãEÑw÷R¬»—cÕ›bÍYôvÐ.U¨Bä}ZÈB;ž.£éœ\V}ów¼‚<C2BC>êyï`
|
||||
BIN
crates/storage/tests/fixtures/compatibility/stores/nested/outer/root-entry.gpg
vendored
Normal file
BIN
crates/storage/tests/fixtures/compatibility/stores/nested/outer/root-entry.gpg
vendored
Normal file
Binary file not shown.
21
crates/storage/tests/fixtures/compatibility/upstream.toml
vendored
Normal file
21
crates/storage/tests/fixtures/compatibility/upstream.toml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
format = 1
|
||||
|
||||
[[project]]
|
||||
name = "password-store"
|
||||
version = "1.7.4"
|
||||
reported_version = "1.7.4"
|
||||
tag = "1.7.4"
|
||||
commit = "1078f2514d579178d5df7042c6a790e9c9b731ad"
|
||||
source = "https://git.zx2c4.com/password-store/"
|
||||
behavior_source = "src/password-store.sh"
|
||||
license = "GPL-2.0-or-later"
|
||||
|
||||
[[project]]
|
||||
name = "pass-otp"
|
||||
version = "1.2.0"
|
||||
reported_version = "1.1.1"
|
||||
tag = "v1.2.0"
|
||||
commit = "1e9d10ca75ae1a8672a7f192809713463657778e"
|
||||
source = "https://github.com/tadfisher/pass-otp"
|
||||
behavior_source = "otp.bash"
|
||||
license = "GPL-3.0-only"
|
||||
406
crates/storage/tests/support/compatibility.rs
Normal file
406
crates/storage/tests/support/compatibility.rs
Normal file
@@ -0,0 +1,406 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
fs,
|
||||
io::{Cursor, Read},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use flate2::read::ZlibDecoder;
|
||||
use pgp::{
|
||||
composed::{Deserializable, DetachedSignature, Message, SignedPublicKey, SignedSecretKey},
|
||||
types::{KeyDetails as _, Password},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use sha1::{Digest as _, Sha1};
|
||||
use sha2::Sha256;
|
||||
use tempfile::TempDir;
|
||||
|
||||
pub type TestResult<T = ()> = Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpstreamManifest {
|
||||
pub format: u8,
|
||||
pub project: Vec<UpstreamProject>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpstreamProject {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub reported_version: String,
|
||||
pub tag: String,
|
||||
pub commit: String,
|
||||
pub source: String,
|
||||
pub behavior_source: String,
|
||||
pub license: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BehaviorManifest {
|
||||
pub format: u8,
|
||||
#[serde(rename = "case")]
|
||||
pub cases: Vec<BehaviorCase>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BehaviorCase {
|
||||
pub id: String,
|
||||
pub area: String,
|
||||
pub argv: Vec<String>,
|
||||
pub stdin: String,
|
||||
pub tty: bool,
|
||||
pub status: i32,
|
||||
pub outcome: String,
|
||||
pub recipients: Vec<String>,
|
||||
pub commit: String,
|
||||
pub random_stream: Option<String>,
|
||||
pub clock: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GeneratedManifest {
|
||||
pub format: u8,
|
||||
pub generated_by: String,
|
||||
pub fixture_seed: String,
|
||||
pub keys: Vec<KeyRecord>,
|
||||
pub entries: Vec<EntryRecord>,
|
||||
pub repositories: Vec<RepositoryRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct KeyRecord {
|
||||
pub name: String,
|
||||
pub user_id: String,
|
||||
pub primary_fingerprint: String,
|
||||
pub encryption_subkey_fingerprint: String,
|
||||
pub passphrase: String,
|
||||
pub public_armor: String,
|
||||
pub public_binary: String,
|
||||
pub secret_armor: String,
|
||||
pub secret_binary: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EntryRecord {
|
||||
pub store: String,
|
||||
pub path: String,
|
||||
pub plaintext: String,
|
||||
pub recipients: Vec<String>,
|
||||
pub ciphertext_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RepositoryRecord {
|
||||
pub name: String,
|
||||
pub template: String,
|
||||
pub head: String,
|
||||
pub commits: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct FixtureSet {
|
||||
root: PathBuf,
|
||||
pub upstream: UpstreamManifest,
|
||||
pub behavior: BehaviorManifest,
|
||||
pub generated: GeneratedManifest,
|
||||
pub layouts: toml::Value,
|
||||
}
|
||||
|
||||
impl FixtureSet {
|
||||
pub fn load() -> TestResult<Self> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/compatibility");
|
||||
let upstream = parse_toml(root.join("upstream.toml"))?;
|
||||
let behavior = parse_toml(root.join("behavior.toml"))?;
|
||||
let generated = parse_toml(root.join("generated.toml"))?;
|
||||
let layouts = parse_toml(root.join("layouts.toml"))?;
|
||||
Ok(Self {
|
||||
root,
|
||||
upstream,
|
||||
behavior,
|
||||
generated,
|
||||
layouts,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path(&self, relative: impl AsRef<Path>) -> PathBuf {
|
||||
self.root.join(relative)
|
||||
}
|
||||
|
||||
pub fn read(&self, relative: impl AsRef<Path>) -> TestResult<Vec<u8>> {
|
||||
Ok(fs::read(self.path(relative))?)
|
||||
}
|
||||
|
||||
pub fn key(&self, name: &str) -> TestResult<&KeyRecord> {
|
||||
self.generated
|
||||
.keys
|
||||
.iter()
|
||||
.find(|key| key.name == name)
|
||||
.ok_or_else(|| test_error(format!("missing fixture key {name}")))
|
||||
}
|
||||
|
||||
pub fn key_by_fingerprint(&self, fingerprint: &str) -> TestResult<&KeyRecord> {
|
||||
self.generated
|
||||
.keys
|
||||
.iter()
|
||||
.find(|key| key.primary_fingerprint == fingerprint)
|
||||
.ok_or_else(|| test_error(format!("missing fixture key {fingerprint}")))
|
||||
}
|
||||
|
||||
pub fn parse_secret_key(&self, key: &KeyRecord) -> TestResult<SignedSecretKey> {
|
||||
let bytes = self.read(&key.secret_armor)?;
|
||||
let (secret, _) = SignedSecretKey::from_armor_single(Cursor::new(bytes))?;
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
pub fn parse_public_key(&self, key: &KeyRecord) -> TestResult<SignedPublicKey> {
|
||||
let bytes = self.read(&key.public_armor)?;
|
||||
let (public, _) = SignedPublicKey::from_armor_single(Cursor::new(bytes))?;
|
||||
Ok(public)
|
||||
}
|
||||
|
||||
pub fn materialize_store(&self, name: &str) -> TestResult<TempDir> {
|
||||
let temporary = tempfile::tempdir()?;
|
||||
copy_tree(&self.path(Path::new("stores").join(name)), temporary.path())?;
|
||||
Ok(temporary)
|
||||
}
|
||||
|
||||
pub fn materialize_repository(&self, name: &str) -> TestResult<TempDir> {
|
||||
let repository = self
|
||||
.generated
|
||||
.repositories
|
||||
.iter()
|
||||
.find(|repository| repository.name == name)
|
||||
.ok_or_else(|| test_error(format!("missing repository fixture {name}")))?;
|
||||
let temporary = tempfile::tempdir()?;
|
||||
copy_tree(&self.path(&repository.template), temporary.path())?;
|
||||
Ok(temporary)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt_entry(
|
||||
fixture: &FixtureSet,
|
||||
entry: &EntryRecord,
|
||||
key: &KeyRecord,
|
||||
) -> TestResult<Vec<u8>> {
|
||||
decrypt_entry_with_passphrase(fixture, entry, key, &key.passphrase)
|
||||
}
|
||||
|
||||
pub fn decrypt_entry_with_passphrase(
|
||||
fixture: &FixtureSet,
|
||||
entry: &EntryRecord,
|
||||
key: &KeyRecord,
|
||||
passphrase: &str,
|
||||
) -> TestResult<Vec<u8>> {
|
||||
let secret = fixture.parse_secret_key(key)?;
|
||||
let ciphertext = fixture.read(Path::new("stores").join(&entry.store).join(&entry.path))?;
|
||||
let message = Message::from_bytes(Cursor::new(ciphertext))?;
|
||||
let mut decrypted = message.decrypt(&Password::from(passphrase), &secret)?;
|
||||
Ok(decrypted.as_data_vec()?)
|
||||
}
|
||||
|
||||
pub fn validate_key_record(fixture: &FixtureSet, key: &KeyRecord) -> TestResult {
|
||||
let secret = fixture.parse_secret_key(key)?;
|
||||
let public = fixture.parse_public_key(key)?;
|
||||
secret.verify_bindings()?;
|
||||
public.verify_bindings()?;
|
||||
|
||||
let primary = format!("{:X}", secret.primary_key.fingerprint());
|
||||
let encryption = format!("{:X}", secret.secret_subkeys[0].fingerprint());
|
||||
if primary != key.primary_fingerprint || encryption != key.encryption_subkey_fingerprint {
|
||||
return Err(test_error(format!("fingerprint mismatch for {}", key.name)));
|
||||
}
|
||||
if secret.to_public_key() != public {
|
||||
return Err(test_error(format!(
|
||||
"public and secret fixture mismatch for {}",
|
||||
key.name
|
||||
)));
|
||||
}
|
||||
|
||||
let binary_secret = fixture.read(&key.secret_binary)?;
|
||||
let binary_public = fixture.read(&key.public_binary)?;
|
||||
let parsed_secret = SignedSecretKey::from_bytes(Cursor::new(binary_secret))?;
|
||||
let parsed_public = SignedPublicKey::from_bytes(Cursor::new(binary_public))?;
|
||||
if parsed_secret != secret || parsed_public != public {
|
||||
return Err(test_error(format!(
|
||||
"armored and binary key mismatch for {}",
|
||||
key.name
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_entry_record(fixture: &FixtureSet, entry: &EntryRecord) -> TestResult {
|
||||
let ciphertext = fixture.read(Path::new("stores").join(&entry.store).join(&entry.path))?;
|
||||
let digest = hex::encode(Sha256::digest(&ciphertext));
|
||||
if digest != entry.ciphertext_sha256 {
|
||||
return Err(test_error(format!(
|
||||
"ciphertext digest mismatch for {}/{}",
|
||||
entry.store, entry.path
|
||||
)));
|
||||
}
|
||||
|
||||
let expected = fixture.read(
|
||||
Path::new("expected")
|
||||
.join(&entry.store)
|
||||
.join(&entry.plaintext),
|
||||
)?;
|
||||
for recipient in &entry.recipients {
|
||||
let key = fixture.key_by_fingerprint(recipient)?;
|
||||
let actual = decrypt_entry(fixture, entry, key)?;
|
||||
if actual != expected {
|
||||
return Err(test_error(format!(
|
||||
"plaintext mismatch for {}/{} and recipient {}",
|
||||
entry.store, entry.path, key.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_recipient_signature(fixture: &FixtureSet, store: &str, signer: &str) -> TestResult {
|
||||
let key = fixture.key(signer)?;
|
||||
let public = fixture.parse_public_key(key)?;
|
||||
let recipient_path = Path::new("stores").join(store).join(".gpg-id");
|
||||
let signature_path = Path::new("stores").join(store).join(".gpg-id.sig");
|
||||
let contents = fixture.read(recipient_path)?;
|
||||
let signature = DetachedSignature::from_bytes(Cursor::new(fixture.read(signature_path)?))?;
|
||||
signature.verify(&public.primary_key, &contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_repository(fixture: &FixtureSet, repository: &RepositoryRecord) -> TestResult {
|
||||
let root = fixture.path(&repository.template);
|
||||
let head_ref = fs::read_to_string(root.join("HEAD"))?;
|
||||
if head_ref != "ref: refs/heads/main\n" {
|
||||
return Err(test_error(format!(
|
||||
"unexpected HEAD for {}",
|
||||
repository.name
|
||||
)));
|
||||
}
|
||||
let head = fs::read_to_string(root.join("refs/heads/main"))?
|
||||
.trim()
|
||||
.to_owned();
|
||||
if head != repository.head {
|
||||
return Err(test_error(format!("head mismatch for {}", repository.name)));
|
||||
}
|
||||
|
||||
for path in loose_object_paths(&root.join("objects"))? {
|
||||
let canonical = inflate(&path)?;
|
||||
let actual = hex::encode(Sha1::digest(&canonical));
|
||||
let directory = path
|
||||
.parent()
|
||||
.and_then(Path::file_name)
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| test_error("invalid loose object directory"))?;
|
||||
let file = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| test_error("invalid loose object name"))?;
|
||||
let expected = format!("{directory}{file}");
|
||||
if actual != expected {
|
||||
return Err(test_error(format!(
|
||||
"invalid loose object {expected} in {}",
|
||||
repository.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
for commit in &repository.commits {
|
||||
let canonical = read_git_object(&root, commit)?;
|
||||
if !canonical.starts_with(b"commit ") {
|
||||
return Err(test_error(format!(
|
||||
"{commit} is not a commit in {}",
|
||||
repository.name
|
||||
)));
|
||||
}
|
||||
let separator = canonical
|
||||
.iter()
|
||||
.position(|byte| *byte == 0)
|
||||
.ok_or_else(|| test_error("Git object has no header separator"))?;
|
||||
let body = std::str::from_utf8(&canonical[separator + 1..])?;
|
||||
let tree = body
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("tree "))
|
||||
.ok_or_else(|| test_error("commit has no tree"))?;
|
||||
let tree_object = read_git_object(&root, tree)?;
|
||||
if !tree_object.starts_with(b"tree ") {
|
||||
return Err(test_error("commit tree points to a non-tree object"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_toml<T>(path: PathBuf) -> TestResult<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
Ok(toml::from_str(&fs::read_to_string(path)?)?)
|
||||
}
|
||||
|
||||
fn copy_tree(source: &Path, destination: &Path) -> TestResult {
|
||||
if !source.is_dir() {
|
||||
return Err(test_error(format!(
|
||||
"fixture source is not a directory: {}",
|
||||
source.display()
|
||||
)));
|
||||
}
|
||||
for entry in fs::read_dir(source)? {
|
||||
let entry = entry?;
|
||||
let source_path = entry.path();
|
||||
let destination_path = destination.join(entry.file_name());
|
||||
let kind = entry.file_type()?;
|
||||
if kind.is_dir() {
|
||||
fs::create_dir_all(&destination_path)?;
|
||||
copy_tree(&source_path, &destination_path)?;
|
||||
} else if kind.is_file() {
|
||||
fs::copy(source_path, destination_path)?;
|
||||
} else {
|
||||
return Err(test_error(format!(
|
||||
"unsupported checked-in fixture type: {}",
|
||||
source_path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn loose_object_paths(objects: &Path) -> TestResult<Vec<PathBuf>> {
|
||||
let mut paths = Vec::new();
|
||||
for directory in fs::read_dir(objects)? {
|
||||
let directory = directory?;
|
||||
if !directory.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let directory_name = directory.file_name();
|
||||
if directory_name.to_string_lossy().len() != 2 {
|
||||
continue;
|
||||
}
|
||||
for object in fs::read_dir(directory.path())? {
|
||||
let object = object?;
|
||||
if object.file_type()?.is_file() {
|
||||
paths.push(object.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
paths.sort();
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
fn read_git_object(repository: &Path, id: &str) -> TestResult<Vec<u8>> {
|
||||
if id.len() != 40 || !id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||
return Err(test_error(format!("invalid Git object ID {id}")));
|
||||
}
|
||||
inflate(&repository.join("objects").join(&id[..2]).join(&id[2..]))
|
||||
}
|
||||
|
||||
fn inflate(path: &Path) -> TestResult<Vec<u8>> {
|
||||
let mut decoder = ZlibDecoder::new(fs::File::open(path)?);
|
||||
let mut canonical = Vec::new();
|
||||
decoder.read_to_end(&mut canonical)?;
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
fn test_error(message: impl Into<String>) -> Box<dyn Error> {
|
||||
Box::new(std::io::Error::other(message.into()))
|
||||
}
|
||||
1
crates/storage/tests/support/mod.rs
Normal file
1
crates/storage/tests/support/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod compatibility;
|
||||
Reference in New Issue
Block a user