#include "chat/chat_protocol.h" #include #include #include #include #include namespace fs = std::filesystem; namespace { using dragonx::chat::HushChatCompatibilityFixtureImportCandidate; using dragonx::chat::HushChatCompatibilityFixtureKind; std::string baseName(const std::string& path) { return fs::path(path).filename().string(); } std::vector jsonFilesInDirectory(const fs::path& directory) { std::vector paths; if (!fs::exists(directory) || !fs::is_directory(directory)) return paths; for (const auto& entry : fs::directory_iterator(directory)) { if (!entry.is_regular_file()) continue; if (entry.path().extension() != ".json") continue; paths.push_back(entry.path().string()); } std::sort(paths.begin(), paths.end()); return paths; } HushChatCompatibilityFixtureKind expectedKindFromPath(const std::string& path) { const std::string name = baseName(path); if (name.find("outgoing") != std::string::npos) return HushChatCompatibilityFixtureKind::OutgoingMemo; if (name.find("seed") != std::string::npos) return HushChatCompatibilityFixtureKind::SeedPublicKeyProjection; if (name.find("corrupted") != std::string::npos) return HushChatCompatibilityFixtureKind::CorruptedAuthFailure; if (name.find("cont") != std::string::npos) return HushChatCompatibilityFixtureKind::ContactExclusion; return HushChatCompatibilityFixtureKind::IncomingMemo; } void printUsage(const char* program) { std::cerr << "Usage: " << program << " [--allow-pending] [--replacement-dry-run] [--validate-capture-manifest]" << " ...\n"; } const char* boolText(bool value) { return value ? "1" : "0"; } void printReplacementDryRun(const dragonx::chat::HushChatCompatibilityFixtureReplacementDryRunResult& dryRun) { std::cout << "HushChat fixture replacement dry run: " << (dryRun.ok ? "Ready" : "Refused") << "\n"; std::cout << "error=" << dryRun.error_name << " dry_run_only=" << boolText(dryRun.dry_run_only) << " redacted=" << boolText(dryRun.redacted_report) << " would_replace=" << boolText(dryRun.would_replace) << " replacement_refused=" << boolText(dryRun.replacement_refused) << "\n"; std::cout << "required=" << dryRun.required_count << " supplied=" << dryRun.supplied_count << " missing=" << dryRun.missing_count << " verified=" << dryRun.verified_count << " seed_projected=" << dryRun.seed_projection_verified_count << " future_auth_required=" << dryRun.future_auth_failure_required_count << " auth_structural_ready=" << dryRun.auth_failure_structural_ready_count << " cont_excluded=" << dryRun.excluded_count << " pending=" << dryRun.pending_count << " rejected=" << dryRun.rejected_count << "\n"; for (const auto& item : dryRun.report_items) { std::cout << dragonx::chat::hushChatCompatibilityFixtureKindName(item.expected_kind) << " decision=" << (item.replacement_eligible ? "ready" : "refused") << " error=" << item.error_name << " supplied=" << boolText(item.supplied) << " pending=" << boolText(item.pending) << " seed_projected=" << boolText(item.seed_projection_verified) << " future_auth_required=" << boolText(item.future_auth_failure_required) << " auth_structural_ready=" << boolText(item.structurally_ready_for_future_auth_check) << " cont_excluded=" << boolText(item.cont_excluded) << " decrypted=" << boolText(item.decrypted) << " authenticated=" << boolText(item.authenticated); if (item.supplied) std::cout << " file=" << baseName(item.path); std::cout << "\n"; } } std::string captureManifestPathFromInput(const std::string& input) { fs::path path(input); if (fs::exists(path) && fs::is_directory(path)) return (path / "capture-manifest.json").string(); return input; } void printCaptureManifestValidation(const dragonx::chat::HushChatCaptureManifestValidationResult& validation) { std::cout << "HushChat capture manifest: " << (validation.ok ? "Ready" : "Refused") << "\n"; std::cout << "error=" << validation.error_name << " redacted=" << boolText(validation.redacted_report) << " provenance_only=" << boolText(validation.validates_provenance_only) << " no_sensitive_material=" << boolText(validation.no_sensitive_material_declared) << " dry_run_instruction=" << boolText(validation.has_dry_run_command); if (!validation.manifest_path.empty()) std::cout << " manifest=" << baseName(validation.manifest_path); if (!validation.fixture_directory.empty()) std::cout << " fixture_dir=" << baseName(validation.fixture_directory); std::cout << "\n"; std::cout << "required=" << validation.required_count << " declared=" << validation.declared_count << " missing=" << validation.missing_count << " duplicate=" << validation.duplicate_count << " handling_flags=" << validation.handling_flag_count << " prohibited=" << validation.prohibited_field_count << "\n"; for (const auto& category : validation.categories) { std::cout << dragonx::chat::hushChatCompatibilityFixtureKindName(category.kind) << " declared=" << boolText(category.declared); if (!category.staged_filename.empty()) std::cout << " file=" << baseName(category.staged_filename); std::cout << "\n"; } } } // namespace int main(int argc, char** argv) { bool allowPending = false; bool replacementDryRun = false; bool validateCaptureManifest = false; std::vector inputPaths; for (int index = 1; index < argc; ++index) { const std::string arg = argv[index]; if (arg == "--allow-pending") { allowPending = true; } else if (arg == "--replacement-dry-run") { replacementDryRun = true; } else if (arg == "--validate-capture-manifest") { validateCaptureManifest = true; } else if (arg == "--help" || arg == "-h") { printUsage(argv[0]); return 0; } else { inputPaths.push_back(arg); } } if (inputPaths.empty()) { printUsage(argv[0]); return 2; } if (validateCaptureManifest && (allowPending || replacementDryRun)) { std::cerr << "Capture manifest validation is separate from fixture and replacement checks.\n"; return 2; } if (replacementDryRun && allowPending) { std::cerr << "Replacement dry run is strict; remove --allow-pending.\n"; return 2; } if (validateCaptureManifest) { bool allValid = true; for (const auto& input : inputPaths) { const auto validation = dragonx::chat::loadHushChatCaptureManifestFile( captureManifestPathFromInput(input), true); printCaptureManifestValidation(validation); if (!validation.ok) allValid = false; } return allValid ? 0 : 1; } std::vector paths; for (const auto& input : inputPaths) { fs::path path(input); if (fs::exists(path) && fs::is_directory(path)) { auto directoryFiles = jsonFilesInDirectory(path); paths.insert(paths.end(), directoryFiles.begin(), directoryFiles.end()); } else { paths.push_back(input); } } std::vector candidates; candidates.reserve(paths.size()); for (const auto& path : paths) { candidates.push_back(HushChatCompatibilityFixtureImportCandidate{expectedKindFromPath(path), path}); } if (replacementDryRun) { const auto dryRun = dragonx::chat::inspectHushChatCompatibilityFixtureReplacementDryRun(candidates, true); printReplacementDryRun(dryRun); return dryRun.ok ? 0 : 1; } const auto checklist = dragonx::chat::inspectHushChatCompatibilityFixtureImportChecklist(candidates, true); std::cout << "HushChat fixture import checklist: " << checklist.error_name << "\n"; std::cout << "required=" << checklist.required_count << " supplied=" << checklist.supplied_count << " missing=" << checklist.missing_count << " verified=" << checklist.verified_count << " seed_projected=" << checklist.seed_projection_verified_count << " future_auth_required=" << checklist.future_auth_failure_required_count << " auth_structural_ready=" << checklist.auth_failure_structural_ready_count << " cont_excluded=" << checklist.excluded_count << " pending=" << checklist.pending_count << " rejected=" << checklist.rejected_count << "\n"; for (const auto& item : checklist.items) { std::cout << dragonx::chat::hushChatCompatibilityFixtureKindName(item.expected_kind) << " " << item.error_name; if (item.future_auth_failure_required) std::cout << " FutureAuthFailureRequired"; if (item.structurally_ready_for_future_auth_check) std::cout << " StructurallyReadyForFutureAuthCheck"; if (item.supplied) std::cout << " " << baseName(item.path); std::cout << "\n"; } if (checklist.replacement_ready) return 0; if (allowPending && checklist.supplied_count == checklist.required_count && checklist.missing_count == 0 && checklist.pending_count > 0 && checklist.rejected_count == 0) return 0; return 1; }