Skip to content

refactor(modules): split attachments and oauth client directories#14

Merged
BjornMelin merged 5 commits intomainfrom
refactor/attachments-and-oauth-client-modules
Apr 22, 2026
Merged

refactor(modules): split attachments and oauth client directories#14
BjornMelin merged 5 commits intomainfrom
refactor/attachments-and-oauth-client-modules

Conversation

@BjornMelin
Copy link
Copy Markdown
Owner

@BjornMelin BjornMelin commented Apr 22, 2026

Summary

  • split src/attachments.rs into focused directory modules for errors, reports, service flows, vault IO, export helpers, and relocated tests
  • split src/auth/oauth_client.rs into focused directory modules for resolve, import, interactive prompts, storage, serde types, and relocated tests
  • keep the module roots thin and preserve existing CLI/report behavior, including warning-free subprocess stderr for CLI contract tests

Commits

  • a94f638 refactor(modules): split attachments and oauth client directories

Verification

Copilot AI review requested due to automatic review settings April 22, 2026 21:05
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Walkthrough

The PR replaces the previous monolithic attachments and OAuth client code with modular implementations. Attachments are split into submodules (error, export, reports, service, vault, tests) adding AttachmentServiceError, report structs, async service entrypoints (list, show, fetch, export), content-addressed vault writing with blake3 hashing and atomic persistence, export path/copy logic with conflict detection, and comprehensive tests. The OAuth client layer is restructured into import, interactive, resolve, storage, types, and mod modules (removing the old src/auth/oauth_client.rs), adding import/preparation/storage/discovery/resolve helpers, constants, and tests.

Possibly related PRs

🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.66% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Docstring & Research Standards ❓ Inconclusive Custom check designed for TypeScript/JavaScript projects with Biome 2.x cannot be applied to a Rust project using Cargo. Replace with Rust-appropriate quality gate validating Rust doc comments, cargo fmt compliance, rustdoc standards, and cargo test/clippy verification.
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: refactoring src/attachments.rs and src/auth/oauth_client.rs into focused directory modules.
Description check ✅ Passed The description clearly relates to the changeset by detailing the module splits, preservation of existing behavior, and listing verification steps performed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
React + Next.Js Best Practices ✅ Passed The custom check for React + Next.js best practices is not applicable to this pull request. The PR is a refactoring of a Rust project that restructures modules into focused subdirectories.
Web Interface Guidelines ✅ Passed This check is not applicable to the provided PR. The Web Interface Guidelines check is designed for web/frontend development, while this PR is exclusively a backend refactoring of Rust modules.
Google Python Style Compliance ✅ Passed This PR refactors Rust code files (.rs extension); Google Python Style Guide applies only to Python codebases.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/attachments-and-oauth-client-modules
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch refactor/attachments-and-oauth-client-modules

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the codebase to split two large modules (attachments handling and OAuth client setup/import) into focused submodules, keeping existing CLI/report behavior intact while improving maintainability.

Changes:

  • Split src/attachments.rs into src/attachments/ submodules (error/export/reports/service/vault) and moved tests accordingly.
  • Split src/auth/oauth_client.rs into src/auth/oauth_client/ submodules (import/interactive/resolve/storage/types) and moved tests accordingly.
  • Updated auth reporting code to accommodate the new OAuth client module structure.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/auth/oauth_client/types.rs Introduces serde types for stored/downloaded OAuth client credential shapes.
src/auth/oauth_client/tests.rs Relocates and preserves OAuth client behavior tests after the refactor.
src/auth/oauth_client/storage.rs Moves OAuth client file IO, candidate discovery, and ADC parsing into a dedicated module.
src/auth/oauth_client/resolve.rs Isolates OAuth client resolution + source detection + error types.
src/auth/oauth_client/mod.rs Defines the new oauth_client module surface and re-exports.
src/auth/oauth_client/interactive.rs Extracts interactive prompting and selection logic.
src/auth/oauth_client/import.rs Extracts import/setup preparation flows and normalization utilities.
src/auth/oauth_client.rs Removes the former monolithic OAuth client implementation file.
src/auth/mod.rs Updates setup reporting output to use the refactored oauth_client API.
src/attachments/vault.rs Extracts vault IO (atomic writes, hashing, safe path resolution).
src/attachments/tests.rs Relocates and preserves attachment behavior tests after the refactor.
src/attachments/service.rs Extracts attachment service operations (list/show/fetch/export).
src/attachments/reports.rs Extracts attachment report types and printing behavior.
src/attachments/mod.rs Defines the new attachments module surface and re-exports.
src/attachments/export.rs Extracts export path resolution, naming, and vault-to-destination copying.
src/attachments/error.rs Extracts attachment-specific error types.
src/attachments.rs Removes the former monolithic attachments implementation file.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/auth/oauth_client/storage.rs Outdated
Comment thread src/auth/oauth_client/resolve.rs Outdated
Comment thread src/auth/mod.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/attachments/tests.rs`:
- Around line 404-509: Multiple export integration tests (e.g.
export_preserves_preexisting_matching_file_when_event_persistence_fails)
duplicate the same account/message/attachment/vault setup; extract that shared
setup into a helper (struct or function) such as ExportTestFixture or
setup_export_test_fixture that performs TempDir/WorkspacePaths creation,
paths.ensure_runtime_dirs(), resolve/init, accounts::upsert_active,
crate::store::mailbox::upsert_messages, write_vault_bytes, and
set_attachment_vault_state, then return the temp_dir, paths, config_report, and
vault_write so each test can call this helper and use the returned values when
calling export and assertions, reducing duplication across the tests that call
export/write_vault_bytes.

In `@src/auth/oauth_client/resolve.rs`:
- Around line 114-123: normalize_optional_string is duplicated in resolve.rs and
import.rs; remove the private copy in resolve.rs and reuse the existing
pub(super) normalize_optional_string from import.rs by importing it (e.g., add
use super::import::normalize_optional_string;) or move the helper into a shared
util module and import from there; update any call sites in resolve.rs to call
the single shared function (symbol: normalize_optional_string) so
trimming-and-empty-to-None behavior is centralized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6f4632d3-120e-402f-975d-c301b0a80f2a

📥 Commits

Reviewing files that changed from the base of the PR and between 90d8cac and a94f638.

📒 Files selected for processing (17)
  • src/attachments.rs
  • src/attachments/error.rs
  • src/attachments/export.rs
  • src/attachments/mod.rs
  • src/attachments/reports.rs
  • src/attachments/service.rs
  • src/attachments/tests.rs
  • src/attachments/vault.rs
  • src/auth/mod.rs
  • src/auth/oauth_client.rs
  • src/auth/oauth_client/import.rs
  • src/auth/oauth_client/interactive.rs
  • src/auth/oauth_client/mod.rs
  • src/auth/oauth_client/resolve.rs
  • src/auth/oauth_client/storage.rs
  • src/auth/oauth_client/tests.rs
  • src/auth/oauth_client/types.rs
💤 Files with no reviewable changes (2)
  • src/attachments.rs
  • src/auth/oauth_client.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: rust
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Use anyhow for command dispatch and top-level context in src/lib.rs; prefer typed thiserror errors in Gmail, workflow, and store layers in Rust
Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load
For new CLI JSON contracts, normalize success and failure to one top-level shape: { "success": true, "data": ... } for success and { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } } for failure
Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only in JSON error contracts
Do not include debug or backtrace payloads in the JSON error contract; use stderr and Rust backtrace env vars for deep diagnostics instead
Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant
Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable
Add focused error-path tests for every new failure class; if CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass

Files:

  • src/auth/mod.rs
  • src/auth/oauth_client/types.rs
  • src/auth/oauth_client/import.rs
  • src/auth/oauth_client/resolve.rs
  • src/auth/oauth_client/tests.rs
  • src/attachments/mod.rs
  • src/auth/oauth_client/mod.rs
  • src/attachments/export.rs
  • src/attachments/reports.rs
  • src/attachments/service.rs
  • src/auth/oauth_client/interactive.rs
  • src/auth/oauth_client/storage.rs
  • src/attachments/error.rs
  • src/attachments/tests.rs
  • src/attachments/vault.rs
🔍 Remote MCP Context7, Firecrawl

Excellent! Now I have all the necessary context. Let me compile the summary of findings:

Summary of Key Findings for PR #14 Review

OAuth Client Credential Security

According to Google's OAuth 2.0 policies, developers must "Store your OAuth client information in a secure place and protect it, especially your client secret, just as you would a password." The PR's implementation properly addresses this through:

  1. File Permissions Handling: On Unix-based platforms, the code uses PermissionsExt trait to set mode bits with from_mode(0o600) for files and 0o700 for directories, which restricts access to the owner only. This aligns with security best practices.

  2. Atomic File Writes: The vault storage pattern uses temporary files with create_new(true) to prevent partial writes and race conditions, and includes explicit sync_all() calls to ensure durability.

  3. JSON Pretty-printing: The code uses serde_json::to_vec_pretty() for credentials file serialization, which is the correct Rust pattern for readable JSON output while maintaining validation.

Tokio spawn_blocking Error Handling

According to the Tokio documentation, spawn_blocking returns a JoinHandle that can produce a JoinError when awaited, which occurs if the task was cancelled or panicked. The PR correctly maps JoinError into a dedicated AttachmentServiceError::BlockingTask variant with proper error source attribution using the #[source] macro.

Error Type Design

The use of thiserror with #[source] attributes aligns with Rust best practices. The #[source] attribute (or a field named source) implements the Error::source() method to indicate the underlying error, which the PR uses correctly for all wrapped error types (tokio::task::JoinError, std::io::Error, anyhow::Error, store::mailbox::MailboxReadError).

Dependency Usage [::GitHub_Copilot_get_file_contents::]

The Cargo.toml shows mature, well-maintained dependencies:

  • blake3 = "1.8.4" for cryptographic hashing (appropriate for content-addressed vault)
  • tokio with required features for async operations
  • thiserror = "2.0.18" for ergonomic error handling
  • serde_json = "1.0.145" for JSON serialization

Module Organization [::Firecrawl_firecrawl_search::]

The refactoring follows established Rust patterns:

  • Each module has a focused responsibility (error types, service flows, vault IO, export helpers, reports)
  • The root mod.rs remains thin with only public re-exports
  • Tests are co-located with their modules in a dedicated tests.rs file
  • Cross-module dependencies are minimal and explicit via super:: paths

Security Considerations Verified

  1. Vault File Permissions: The Blake3 content-addressed vault writes files with 0o600 permissions on Unix systems, protecting cryptographic hashes from unauthorized access.

  2. Export Cleanup on Failure: The cleanup_export_file_task properly handles transient errors (NotFound) versus persistent failures, with warnings logged only for genuine issues.

  3. Conflict Detection: Exported files are validated against content hashes before overwriting, preventing silent data loss when the same attachment is exported multiple times.

Compilation & Testing

The PR includes comprehensive test coverage:

  • Unit tests for sanitization, path validation, hash verification
  • Integration tests for export scenarios (destination conflicts, cleanup on persistence failure)
  • A benchmark for comparing copy vs. hash-compare performance across file sizes
  • All verification commands pass (cargo fmt, cargo test --lib, cargo clippy with -D warnings)
🔇 Additional comments (47)
src/auth/mod.rs (2)

13-16: LGTM!

The import restructuring correctly aligns with the new modular OAuth client architecture. The resolve as resolve_oauth_client alias provides clear disambiguation when calling the resolve function.


85-86: LGTM!

The explicit type annotation on source_kind improves readability by making the ImportedOAuthClientSourceKind type visible at the call site. The behavior is unchanged since as_str() takes self by value (Copy type).

src/auth/oauth_client/interactive.rs (3)

1-69: LGTM!

The interactive setup flow is well-structured:

  • Menu construction correctly orders downloaded JSON candidates first, then manual paste, then ADC option
  • Selection mapping via into_iter().nth(selection) is safe since dialoguer::Select::interact() returns a valid index
  • The None branch at line 67 is a reasonable defensive guard against unexpected edge cases

71-102: LGTM!

The manual entry flow correctly:

  • Guards against non-interactive terminals before prompting
  • Validates client_id is non-empty
  • Allows empty client_secret via allow_empty_password(true) (appropriate for public clients)
  • Trims and normalizes input before passing to the preparation function

104-110: LGTM!

The interactivity checks are correct:

  • JSON output mode disables interactive prompts to ensure machine-parseable output
  • Both stdin and stdout must be terminals for interactive mode
src/auth/oauth_client/types.rs (1)

1-61: LGTM!

Well-designed type hierarchy with appropriate serde attributes:

  • StoredOAuthClientFile/StoredInstalledOAuthClient for workspace persistence with proper defaults
  • DownloadedGoogleCredentials/DownloadedInstalledClient with all-optional fields for lenient parsing
  • LegacyStoredOAuthClient for backwards compatibility with older stored formats
  • ADC types with proper field renaming for "type"credential_type

The pub(super) visibility correctly scopes these types to the oauth_client module.

src/auth/oauth_client/mod.rs (1)

1-25: LGTM!

Clean module organization with appropriate visibility levels:

  • Public re-exports (ImportedOAuthClient, ImportedOAuthClientSourceKind, OAuthClientError, etc.) form a clear public API
  • Internal re-exports (PreparedSetup, persist_prepared_google_desktop_client, etc.) are properly scoped to pub(crate)
  • Constants have correct visibility: public for user-facing URLs, pub(crate) for internal defaults
  • Test-only setup_guidance is correctly gated with #[cfg(test)]
src/auth/oauth_client/import.rs (5)

25-54: LGTM!

Well-designed public types for import metadata:

  • ImportedOAuthClientSourceKind covers all import paths (JSON, manual, ADC) with snake_case serialization
  • ImportedOAuthClient captures complete import context including source path, auto-discovery flag, and credential presence indicators
  • #[serde(skip_serializing_if = "...")] attributes appropriately omit optional fields from JSON output

56-101: LGTM!

The prepared import types provide clean separation between:

  • PreparedOAuthClientImport for standard client imports
  • PreparedAdcOAuthClientImport wrapping client import with refresh token (using SecretString appropriately)
  • PreparedSetup enum modeling the three possible setup outcomes

Accessor methods return references, avoiding unnecessary clones until needed.


114-139: LGTM!

The prepare_setup flow correctly prioritizes:

  1. Explicit credentials file → direct import
  2. Existing workspace/inline config → reuse
  3. Otherwise → interactive or non-interactive discovery

The should_use_interactive_setup check properly gates interactive prompts based on JSON mode and terminal state.


234-266: LGTM!

Non-interactive setup handles all cases correctly:

  • Single candidate → import it
  • No candidates but ADC available → import ADC
  • No candidates and no ADC → clear error with guidance
  • Multiple candidates → error listing all candidates with guidance

Error messages include setup_guidance() to help users resolve the situation.


341-396: LGTM!

Normalization helpers are defensive and consistent:

  • normalize_optional_string trims and converts empty strings to None
  • normalize_required_* variants return typed errors for missing fields
  • normalize_redirect_uris ensures at least one redirect URI exists, defaulting to localhost
src/auth/oauth_client/resolve.rs (3)

7-29: LGTM!

Clean public types for resolution:

  • ResolvedOAuthClient captures the resolved credentials with appropriate serde attributes
  • OAuthClientSource enum correctly models the three possible sources with stable string representations

31-63: LGTM!

Comprehensive error enum with user-friendly messages:

  • Each variant includes actionable guidance (e.g., "run mailroom auth setup")
  • Error messages distinguish between missing, malformed, and unsupported credential types
  • Using thiserror aligns with coding guidelines for typed errors in the Gmail/auth layer

65-98: LGTM!

Resolution logic correctly implements the documented precedence:

  1. Workspace file (if valid) takes priority
  2. Inline config (gmail.client_id) as fallback
  3. MissingClientConfiguration error if neither available

The oauth_client_source function mirrors this logic but returns Unconfigured instead of erroring, appropriate for status reporting.

src/auth/oauth_client/tests.rs (1)

1-419: LGTM!

Comprehensive test coverage including:

  • Resolution precedence (workspace file > inline config)
  • Import flows (downloaded JSON, manual paste, ADC)
  • Legacy format backwards compatibility
  • Error cases (malformed files, unsupported ADC types, missing candidates)
  • Interactive/non-interactive selection logic
  • Path normalization and deduplication

Tests use tempfile::TempDir for proper isolation and clean assertions.

src/auth/oauth_client/storage.rs (5)

16-46: LGTM!

Robust loading logic with backwards compatibility:

  • Gracefully handles missing files by returning Ok(None)
  • Tries current format first, falls back to legacy format
  • Validates required fields after parsing
  • Proper error context for debugging

62-75: LGTM!

Secure file persistence pattern:

  • Creates parent directory with owner-only permissions (0o700 on Unix)
  • Uses temp file + rename for atomic writes (avoids partial writes on crash)
  • Sets owner-only file permissions (0o600 on Unix) before rename
  • Uses serde_json::to_vec_pretty for readable output

77-114: LGTM!

Discovery logic provides clear feedback:

  • Returns immediately if explicit path provided (with existence check)
  • Single candidate → auto-discover
  • No candidates → error with setup guidance
  • Multiple candidates → lists all candidates with setup guidance

165-194: LGTM!

Directory scanning is defensive:

  • Handles NotFound gracefully (returns empty vec)
  • Filters to regular files only
  • Pattern matches client_secret_*.json naming convention
  • Uses to_string_lossy() appropriately for filename comparison

243-275: LGTM!

Platform-specific handling is correct:

  • Unix: Sets restrictive permissions (0o700 dirs, 0o600 files)
  • Non-Unix: No-op (Windows ACLs managed differently)
  • Windows atomic rename workaround: removes destination first since rename doesn't overwrite on Windows
src/attachments/tests.rs (7)

1-14: LGTM! Test setup and imports are well-organized.

The imports correctly reference the new modular structure via super:: paths. Test dependencies (tempfile, rusqlite, blake3) are appropriate for the test scenarios.


16-63: LGTM! Good coverage of sanitization edge cases and security validation.

Tests properly verify:

  • Filename fallback when Gmail filename is blank or sanitizes to empty
  • Path partitioning with thread/message IDs
  • Parent traversal rejection in vault paths (security-critical)

65-129: LGTM! Vault integrity tests are thorough.

Good coverage of:

  • Hash mismatch detection preventing reuse (line 79 uses "invalid-hash")
  • Successful reuse when hash/size match
  • Overwriting corrupted blobs with correct content

131-166: LGTM! Error mapping and conflict detection tests are correct.

Tests verify:

  • MailboxWriteError::AttachmentNotFound maps to AttachmentServiceError::AttachmentNotFound
  • Hash comparison detects conflicts when destination exists with different content

168-208: LGTM! Service validation tests are well-structured.

Async tests correctly validate:

  • InvalidLimit error for zero limit
  • NoActiveAccount error when no account state exists

The error matching pattern with downcast_ref is appropriate for anyhow::Error.


301-402: Excellent test coverage for cleanup semantics.

The SQLite trigger injection (RAISE(FAIL, ...)) is a clever technique to test persistence failure handling. The test correctly verifies that:

  • New files are cleaned up on persistence failure (line 401: !destination_path.exists())
  • The error is properly propagated as StoreWrite

511-553: LGTM! Benchmark is properly structured.

Good practices observed:

  • #[ignore] with clear run instructions
  • JSON output format for easy CI integration
  • Realistic file size tiers for performance characterization
src/attachments/mod.rs (1)

1-26: LGTM! Clean module organization with well-defined public API.

The module root is appropriately thin:

  • Declares internal submodules without exposing implementation details
  • Re-exports only the intended public API (AttachmentServiceError, report types, service functions)
  • #[cfg(test)] correctly gates the test module

This aligns with the PR objective of keeping module root files thin.

src/attachments/reports.rs (2)

1-53: LGTM! Report structs are well-defined and Serialize-compatible.

The structs appropriately:

  • Derive Serialize for JSON output
  • Include all relevant metadata fields
  • Use PathBuf for filesystem paths (serializes correctly via Serde)

54-138: LGTM! Print methods correctly delegate to the CLI output contract.

The JSON path uses print_json_success which produces the required { "success": true, "data": ... } shape per coding guidelines. Human-readable output uses consistent key=value formatting with proper PathBuf::display() usage.

src/attachments/error.rs (1)

1-57: LGTM! Error enum follows coding guidelines for typed errors.

The error design is well-structured:

  • Uses thiserror with #[source] for proper error chaining
  • Variants cover all failure modes (IO, store, blocking task, validation)
  • Messages are operator-oriented (e.g., "run mailroom auth login first")
  • Correctly matches the pattern matching in cli_output.rs (per relevant snippet showing all variants are handled)

As per coding guidelines: "prefer typed thiserror errors in Gmail, workflow, and store layers" and "keep error enums local to the layer that owns the failure semantics."

src/attachments/export.rs (3)

1-69: LGTM! Path resolution and sanitization are correctly implemented.

Functions properly handle:

  • Filename sanitization with meaningful fallbacks
  • Deterministic path construction from identifiers
  • Directory vs. file destination resolution

76-146: LGTM! Atomic copy with conflict detection is correctly implemented.

Good practices:

  • create_new(true) prevents accidental overwrites
  • Hash comparison on AlreadyExists distinguishes "same content" from "conflict"
  • sync_all() before returning ensures durability
  • Cleanup on write failure prevents partial files

148-163: LGTM! Cleanup task correctly handles edge cases.

The function:

  • Uses spawn_blocking for filesystem work per coding guidelines
  • Treats NotFound as success (idempotent)
  • Logs warnings to stderr (not JSON output) for genuine failures
src/attachments/vault.rs (6)

15-64: LGTM! Vault reuse validation is thorough.

existing_vault_report correctly validates:

  • Presence of vault metadata (relative_path, content_hash, fetched_at_epoch_s)
  • File existence and type (must be regular file)
  • Optional size check
  • Critical: Blake3 hash verification before reuse (line 46)

This prevents serving corrupted or tampered vault content.


66-86: LGTM! Path validation correctly rejects traversal attacks.

The check for ParentDir, RootDir, and Prefix components prevents:

  • Path traversal (e.g., ../escape.bin)
  • Absolute paths
  • Windows prefix paths

This is security-critical for content-addressed storage.


88-118: LGTM! Content-addressed write with Blake3 is correct.

The blake3/{prefix}/{hash} path structure:

  • Uses first 2 characters as directory prefix for filesystem distribution
  • Hash serves as filename for deduplication
  • write_vault_file_atomically ensures crash-safe persistence

120-190: LGTM! Atomic write implementation is robust.

Key safety features:

  • Unique temp file naming with PID, nanoseconds, and attempt counter
  • Bounded retry (8 attempts) prevents infinite loops
  • sync_all() before rename ensures durability
  • Temp file cleanup on any error path

229-281: Windows backup handling is correct but backup removal failure is strict.

The Windows-specific rename pattern correctly handles:

  • Backing up existing destination before overwrite
  • Restoring backup on rename failure

However, if backup removal fails (lines 254-259), the function returns an error even though the actual write succeeded. This could be changed to a warning, though keeping it as an error ensures cleanup consistency.


298-312: LGTM! Permissions hardening correctly restricts vault file access.

The 0o600 mode ensures only the owner can read/write vault files, protecting sensitive attachment content. The conditional compilation (#[cfg(unix)] / #[cfg(not(unix))]) correctly handles platform differences.

src/attachments/service.rs (6)

1-56: LGTM! list function correctly uses spawn_blocking for DB access.

The implementation:

  • Validates limit before any blocking work
  • Uses spawn_blocking for list_attachments DB query
  • Properly maps JoinError to BlockingTask and read errors to StoreRead

As per coding guidelines: "Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking."


58-70: LGTM! show function is correctly implemented.


72-143: LGTM! fetch correctly implements vault deduplication.

Key behaviors:

  • Checks existing vault state first to avoid redundant downloads
  • Downloads via async Gmail client (not blocking)
  • Uses spawn_blocking for vault write and DB update
  • Returns downloaded: false for cache hits, true for new fetches

145-217: LGTM! export correctly handles cleanup semantics.

The cleanup logic (lines 198-203) is correct:

  • Only cleans up if copy_result.copied is true (new file was created)
  • Preserves preexisting matching files on persistence failure
  • This matches the test coverage in tests.rs

242-254: LGTM! Account resolution with fallback is sensible.

The function tries:

  1. Active account from accounts table
  2. Sync state from mailbox (for cases where account state exists but isn't "active")
  3. Returns NoActiveAccount error

This provides resilience while maintaining clear error semantics.


285-296: The map_mailbox_write_error function mapping all non-AttachmentNotFound variants to AttachmentServiceError::StoreWrite is intentional and correct. The AttachmentServiceError enum does not have an AccountMismatch variant; account-level concerns are store-layer semantics and should not be exposed at the service layer. This follows the coding guideline to keep error enums local to the layer that owns the failure semantics.

Comment thread src/attachments/tests.rs
Comment thread src/auth/oauth_client/resolve.rs Outdated
@BjornMelin BjornMelin self-assigned this Apr 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/attachments/tests.rs`:
- Around line 32-50: Tests in
default_export_path_uses_thread_and_message_partitions and
default_export_path_falls_back_when_partition_ids_sanitize_to_empty use
hardcoded POSIX absolute string literals ("/tmp/...") which fail on non-Unix
platforms; update both tests to build the expected PathBuf using
WorkspacePaths::from_repo_root(PathBuf::from(...)) and PathBuf::join (or
join-like operations) so the expected path mirrors how default_export_path
constructs it (use the same root PathBuf and append ".mailroom", "exports", and
the sanitized partition/filename segments produced by default_export_path),
ensuring comparisons use PathBuf values rather than raw string literals.
- Around line 63-68: The test
resolve_vault_relative_path_rejects_parent_traversal currently asserts on the
error string; change it to assert the concrete error variant instead. After
calling resolve_vault_relative_path (using WorkspacePaths), pattern-match the
returned Err for AttachmentServiceError::InvalidVaultPath (e.g., using matches!
or if let Err(AttachmentServiceError::InvalidVaultPath) = error) so the test
verifies the typed error variant rather than depending on the error message
text.

In `@src/auth/oauth_client/storage.rs`:
- Around line 198-209: The candidate discovery currently aborts on a
PermissionDenied when calling collect_candidate_files(dir); change the read_dir
error handling to treat std::io::ErrorKind::PermissionDenied the same as
NotFound (i.e., return Ok(Vec::new()) and skip the directory) rather than
returning an Err with context. Update the match in collect_candidate_files to
add an arm for Err(error) if error.kind() ==
std::io::ErrorKind::PermissionDenied that returns Ok(Vec::new()), leaving the
existing Err(error) => { ... } arm for other errors unchanged.
- Around line 298-307: In persist_tmp_file, remove the Windows-specific
pre-deletion block so the function simply calls fs::rename(tmp_path,
destination) and returns Ok(()); do not call fs::remove_file(destination)
anywhere in persist_tmp_file (remove the #[cfg(windows)] block) because
fs::rename already performs atomic replacement on supported platforms and
pre-deleting the destination risks data loss if the rename fails.
- Around line 229-250: detect_adc_path_from_env currently only checks the Unix
well-known path under HOME and therefore misses Windows ADC stored under
%APPDATA%; update detect_adc_path_from_env (and keep detect_adc_path unchanged)
to also probe the Windows well-known location when on Windows by adding a
platform-specific branch (#[cfg(windows)]) that reads the APPDATA env var
(std::env::var_os("APPDATA")) and checks
APPDATA.join("gcloud").join("application_default_credentials.json") for
existence, returning it when present; preserve the existing Unix check in a
#[cfg(not(windows))] block or use conditional code paths so both platforms are
handled correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 084cb859-bd6b-4a13-8fb6-c434969c4a1c

📥 Commits

Reviewing files that changed from the base of the PR and between a94f638 and ae7a6a5.

📒 Files selected for processing (6)
  • src/attachments/tests.rs
  • src/auth/oauth_client/import.rs
  • src/auth/oauth_client/interactive.rs
  • src/auth/oauth_client/mod.rs
  • src/auth/oauth_client/resolve.rs
  • src/auth/oauth_client/storage.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: rust
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Use anyhow for command dispatch and top-level context in src/lib.rs; prefer typed thiserror errors in Gmail, workflow, and store layers in Rust
Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load
For new CLI JSON contracts, normalize success and failure to one top-level shape: { "success": true, "data": ... } for success and { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } } for failure
Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only in JSON error contracts
Do not include debug or backtrace payloads in the JSON error contract; use stderr and Rust backtrace env vars for deep diagnostics instead
Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant
Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable
Add focused error-path tests for every new failure class; if CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass

Files:

  • src/auth/oauth_client/interactive.rs
  • src/auth/oauth_client/resolve.rs
  • src/auth/oauth_client/mod.rs
  • src/attachments/tests.rs
  • src/auth/oauth_client/storage.rs
  • src/auth/oauth_client/import.rs
🔍 Remote MCP Firecrawl, GitHub Copilot

Summary of additional facts relevant to reviewing PR #14

  • Cargo.toml dependency versions (relevant for crypto/hashing, async, errors): blake3 = 1.8.4, tokio = 1.52.0, thiserror = 2.0.18, serde_json = 1.0.145. These confirm the PR can use Blake3 hashing, Tokio spawn_blocking/JoinError, and thiserror-based error wrappers as implemented.

  • Tokio spawn_blocking / JoinHandle behavior: spawn_blocking returns a JoinHandle whose await yields Result<T, JoinError>; JoinError exposes is_cancelled/is_panic and Into/try_into helpers, and cannot be used to abort an already-running spawn_blocking task. This validates mapping tokio::task::JoinError into a service-level BlockingTask error variant and explains why cleanup/logging for blocking-task join failures is needed.

Sources used

  • Repository Cargo.toml (dependencies) — GitHub Copilot get_file_contents.
  • Tokio docs (spawn_blocking and JoinError) — Firecrawl search + scraped docs.rs pages.
🔇 Additional comments (2)
src/attachments/tests.rs (2)

16-19: Nice fixture extraction; this improves test maintainability

ExportTestFixture plus setup_export_test_fixture cleanly removes repeated setup across export tests and keeps scenarios focused on behavior under test.

Also applies to: 385-460


238-271: Strong coverage of export error cleanup semantics

These two tests clearly validate both branches: removing newly copied files on persistence failure and preserving preexisting matching files.

As per coding guidelines, "Add focused error-path tests for every new failure class; if CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass".

Also applies to: 273-311

Comment thread src/attachments/tests.rs
Comment thread src/attachments/tests.rs
Comment thread src/auth/oauth_client/storage.rs
Comment thread src/auth/oauth_client/storage.rs
Comment thread src/auth/oauth_client/storage.rs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1759317fbf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/auth/oauth_client/storage.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/attachments/tests.rs`:
- Around line 371-397: The fixture helpers (e.g., detail_with_vault and its
counterpart used at 399-466) currently hardcode
AttachmentDetailRecord.size_bytes to 5 while accepting variable-size vault
payloads; update both helpers to accept the actual payload (or its byte slice)
as an argument, compute size_bytes = payload.len() as i64 and use that value for
both size_bytes and vault_size_bytes (instead of the hardcoded 5), and update
all call sites/tests to pass the payload so mailbox/vault metadata stays
consistent with the blob length.

In `@src/auth/oauth_client/storage.rs`:
- Around line 315-318: persist_tmp_file in src/auth/oauth_client/storage.rs
currently calls fs::rename directly; to unify atomic persistence and Windows
safety with src/auth/file_store.rs, modify the persist_tmp_file function to
perform a conditional pre-delete on Windows (as in the other module) before
calling fs::rename, i.e., add the same #[cfg(windows)] removal of the
destination path if it exists, keeping the final fs::rename unchanged; also
consider applying the same pattern where vault.rs uses fs::rename to ensure
consistent behavior across auth and vault modules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 65d94548-753f-4acc-84bd-f9d18d6d2b2e

📥 Commits

Reviewing files that changed from the base of the PR and between ae7a6a5 and 1759317.

📒 Files selected for processing (3)
  • src/attachments/tests.rs
  • src/auth/oauth_client/storage.rs
  • src/auth/oauth_client/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: rust
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Use anyhow for command dispatch and top-level context in src/lib.rs; prefer typed thiserror errors in Gmail, workflow, and store layers in Rust
Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load
For new CLI JSON contracts, normalize success and failure to one top-level shape: { "success": true, "data": ... } for success and { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } } for failure
Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only in JSON error contracts
Do not include debug or backtrace payloads in the JSON error contract; use stderr and Rust backtrace env vars for deep diagnostics instead
Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant
Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable
Add focused error-path tests for every new failure class; if CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass

Files:

  • src/attachments/tests.rs
  • src/auth/oauth_client/storage.rs
  • src/auth/oauth_client/tests.rs
🔍 Remote MCP Firecrawl

Relevant facts found

  • blake3 crate (1.8.4) provides incremental hashing APIs suitable for streaming/file hashing: blake3::Hasher with update, update_reader, update_mmap/update_mmap_rayon, and blake3::hash helper; docs note update_reader and update_mmap are provided for efficient file hashing and that update_mmap_rayon requires mmap+rayon features for multithreaded hashing.

  • blake3 crate is widely used and the crate docs (crates.io + docs.rs) describe expected behavior and feature flags (std, rayon, mmap, zeroize, serde). Using blake3 for file content hashing (to validate vault blobs) is consistent with crate intent.,

  • tokio::task::spawn_blocking runs closures on a dedicated blocking thread pool and returns a JoinHandle; spawn_blocking tasks cannot be aborted once started (abort has no effect after the task started). This explains why mapping tokio::task::JoinError into a service-level BlockingTask variant and logging/cleanup on join failures is appropriate.

  • tokio::task::JoinError exposes is_cancelled() and is_panic(), and provides into_panic()/try_into_panic() to extract a panic payload; JoinError implements Error/Display and can be inspected to distinguish cancellation vs panic. This is useful when converting join failures into AttachmentServiceError::BlockingTask or when deciding whether to treat the failure as transient vs fatal.

🔇 Additional comments (15)
src/auth/oauth_client/tests.rs (7)

1-46: Well-structured test helpers and imports.

The test module imports are comprehensive and the helper functions workspace_for and gmail_config provide clean, reusable fixtures that isolate tests via TempDir. Good foundation for the test suite.


48-96: Good end-to-end test for import-then-resolve flow.

This test validates the complete happy path: importing a Google Desktop client JSON, verifying the ImportedOAuthClient fields, confirming resolution produces correct credentials, and checking the persisted file format. Covers the key contract between import_google_desktop_client and resolve.


126-163: ADC import test validates refresh token extraction.

The test correctly verifies that prepare_google_desktop_client_from_adc extracts client_id, client_secret, and refresh_token from an authorized_user ADC file. Using expose_secret() appropriately for assertion.


165-192: Good coverage for ADC fallback paths.

Tests at lines 165-179 and 181-192 validate the fallback logic in detect_adc_path_from_env: missing env path falls back to Unix well-known location, and a separate test for Windows APPDATA well-known location. This aligns with the storage module's cross-platform ADC detection.


270-276: Exhaustive boolean matrix for interactive setup logic.

All four combinations of (json, interactive_terminal) are tested, verifying the logic from interactive.rs:104-110. This ensures the function behaves correctly across all input states.


278-299: Legacy schema backward compatibility test.

This test validates that stored client files using the old flat schema (client_id/client_secret at root level without the installed wrapper) are still correctly resolved. This exercises the LegacyStoredOAuthClient migration path in load_imported_client.


376-423: Error-path coverage for validation failures.

Tests for malformed workspace file (empty client_id) and unsupported ADC type (service_account) validate error propagation. Using downcast_ref and pattern matching on OAuthClientError variants ensures typed error handling is working correctly.

src/auth/oauth_client/storage.rs (7)

1-12: Clean module structure with appropriate imports.

Imports are well-organized, separating internal types, constants, and external dependencies. The conditional Unix import for PermissionsExt is correctly scoped.


13-43: Legacy schema migration handles backward compatibility correctly.

The fallback logic tries the current StoredOAuthClientFile format first, then falls back to LegacyStoredOAuthClient parsing with sensible defaults for auth_uri, token_uri, and redirect_uris. Both paths validate the result before returning.


45-79: Normalization helpers provide consistent empty-string handling.

The trim-and-empty-as-None semantics are applied consistently across optional strings, required option strings, ADC fields, and raw input strings. The differentiated error types (MissingField vs MissingAdcField vs MissingStoredField) allow callers to distinguish failure sources.


95-108: Atomic file persistence with owner-only permissions.

The save_imported_client function correctly:

  1. Creates parent directories
  2. Sets restrictive directory permissions (0o700 on Unix)
  3. Writes to a temp file first
  4. Sets restrictive file permissions (0o600 on Unix)
  5. Atomically renames to destination

This pattern protects credentials from transient partial writes.


198-217: PermissionDenied now handled gracefully.

The collect_candidate_files function correctly treats PermissionDenied the same as NotFound, returning an empty vec instead of failing the entire discovery flow. This addresses the previous review feedback.


244-270: Windows ADC path detection now includes APPDATA.

The detect_adc_path_from_env function now checks three locations in order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var
  2. Unix well-known: $HOME/.config/gcloud/application_default_credentials.json
  3. Windows well-known: %APPDATA%\gcloud\application_default_credentials.json

This addresses the previous review feedback about missing Windows support.


272-291: ADC parsing correctly validates credential type.

The parse_authorized_user_adc function:

  1. Reads and parses the ADC file
  2. Rejects non-authorized_user types with a descriptive error
  3. Validates required fields (client_id, refresh_token)
  4. Normalizes optional fields (client_secret, quota_project_id)

This ensures only usable ADC credentials are processed.

src/attachments/tests.rs (1)

73-81: The review comment's core assertion is incorrect. The resolve_vault_relative_path function does not canonicalize the path before validation. Instead, it validates path components directly via path.components().any(|component| matches!(component, Component::ParentDir | ...)), which catches parent-directory traversal (..) attempts at the token level before any filesystem operations. The test correctly exercises this check—passing "../escape.bin" triggers the ParentDir component match and returns InvalidVaultPath immediately, regardless of whether the file exists. The suggested adjustment is unnecessary.

			> Likely an incorrect or invalid review comment.

Comment thread src/attachments/tests.rs
Comment thread src/auth/oauth_client/storage.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/auth/oauth_client/storage.rs (1)

315-323: ⚠️ Potential issue | 🔴 Critical

Prevent credential loss in Windows overwrite flow.

Line 318 deletes the destination before Line 323 commits the replacement. If the rename fails, the stored OAuth client is lost.

♻️ Suggested fix
 fn persist_tmp_file(tmp_path: &Path, destination: &Path) -> Result<()> {
     #[cfg(windows)]
     {
-        if destination.exists() {
-            fs::remove_file(destination)?;
-        }
+        let backup_path = destination.with_extension("bak");
+        if destination.exists() {
+            fs::rename(destination, &backup_path)?;
+        }
+        if let Err(error) = fs::rename(tmp_path, destination) {
+            if backup_path.exists() {
+                let _ = fs::rename(&backup_path, destination);
+            }
+            return Err(error.into());
+        }
+        if backup_path.exists() {
+            let _ = fs::remove_file(backup_path);
+        }
+        return Ok(());
     }
 
     fs::rename(tmp_path, destination)?;
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auth/oauth_client/storage.rs` around lines 315 - 323, The current
persist_tmp_file deletes destination on Windows before renaming, risking
credential loss if rename fails; change persist_tmp_file to attempt
fs::rename(tmp_path, destination) first and only if that fails with
AlreadyExists (or equivalent) perform fs::remove_file(destination) and retry
fs::rename(tmp_path, destination), returning any errors if the second rename
fails; keep the Windows-specific logic around the conditional removal and ensure
errors are propagated from persist_tmp_file (refer to function persist_tmp_file
and the Windows cfg block).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/auth/oauth_client/storage.rs`:
- Around line 112-114: The current check only tests path.exists() and can allow
directories; change the validation to ensure the import path is a regular file
(use path.is_file() or combine exists() && is_file()) and return
OAuthClientError::MissingImportFile { path } when it's not a file so directories
fail fast with a clear error; update the conditional around the variable path
(and keep the same error type) to enforce is_file().
- Around line 220-223: Change the strict uses of `?` inside the `for entry in
entries { ... }` loop to best-effort error handling so a single unreadable entry
doesn't abort discovery: replace `let entry = entry?;` with matching on the
entry result and, if Err(e) and e.kind() == PermissionDenied, continue,
otherwise propagate the error; likewise replace `entry.file_type()?` with a
match that skips on PermissionDenied but returns other errors. Apply these
checks inside the loop that iterates `entries` and reference the `entry`
variable and its `file_type()` call when implementing the conditional continues.
- Around line 7-8: The storage layer currently exported anyhow::Result and used
anyhow::{Context, anyhow}, which prevents callers from matching structured
errors; create a typed error enum (e.g., StorageError) using thiserror::Error in
this module and replace all public function signatures that return
anyhow::Result<T> with Result<T, StorageError>; convert existing uses of
anyhow::Context/.context() and calls to anyhow!(...) into mapping functions that
produce StorageError variants (implement From for underlying errors like
std::io::Error, sqlx::Error, serde_json::Error as needed) and update internal
error construction to use explicit StorageError variants so callers/tests can
match on the typed errors.

---

Duplicate comments:
In `@src/auth/oauth_client/storage.rs`:
- Around line 315-323: The current persist_tmp_file deletes destination on
Windows before renaming, risking credential loss if rename fails; change
persist_tmp_file to attempt fs::rename(tmp_path, destination) first and only if
that fails with AlreadyExists (or equivalent) perform
fs::remove_file(destination) and retry fs::rename(tmp_path, destination),
returning any errors if the second rename fails; keep the Windows-specific logic
around the conditional removal and ensure errors are propagated from
persist_tmp_file (refer to function persist_tmp_file and the Windows cfg block).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c869a1c6-680c-41e0-9e1f-c2ac5c48ca56

📥 Commits

Reviewing files that changed from the base of the PR and between 1759317 and c92a00e.

📒 Files selected for processing (3)
  • src/attachments/tests.rs
  • src/auth/oauth_client/storage.rs
  • src/auth/oauth_client/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: rust
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Use anyhow for command dispatch and top-level context in src/lib.rs; prefer typed thiserror errors in Gmail, workflow, and store layers in Rust
Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load
For new CLI JSON contracts, normalize success and failure to one top-level shape: { "success": true, "data": ... } for success and { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } } for failure
Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only in JSON error contracts
Do not include debug or backtrace payloads in the JSON error contract; use stderr and Rust backtrace env vars for deep diagnostics instead
Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant
Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable
Add focused error-path tests for every new failure class; if CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass

Files:

  • src/auth/oauth_client/tests.rs
  • src/attachments/tests.rs
  • src/auth/oauth_client/storage.rs
🔍 Remote MCP Firecrawl

Summary of additional facts relevant to this PR (file I/O, hashing, and platform semantics)

  • std::fs::rename semantics vary by platform; POSIX rename replaces atomically, Windows has different semantics and historically required ReplaceFile/MoveFileEx/SetFileInformationByHandle for superseding-renames; superseding renames on NTFS are usually atomic but have edge cases (large-file transaction splitting) and network/share differences. Consider the PR’s vault atomic-rename and Windows backup/replace logic against these platform caveats.,

  • There is active std discussion / proposal for a rename-without-replace API (rename_noreplace) because cross-platform TOCTOU-safe rename semantics are subtle; code that relies on atomic non-replacing rename must handle platform differences or use explicit platform APIs/hardlink fallbacks.

  • OpenOptions behavior: create_new(true) is the atomic way to create-only-if-missing (returns AlreadyExists when target exists); create(true) opens existing or creates. Using OpenOptions::create_new with create_new semantics (and handling AlreadyExists) is the correct atomic pattern for "create-only" destination files. (Docs and historical doc tweaks noted.) This matters for copy_from_vault which uses create_new(true) and treats AlreadyExists specially.,

  • File durability: Write::flush is an application-level buffer flush (noop on File on many platforms); to ensure data reaches disk use File::sync_data or File::sync_all. sync_data/sync_all semantics differ (sync_all also flushes metadata). For atomic-write-to-temp-then-rename patterns, best practice is: write to temp file, flush app buffers (if any), call sync_data/sync_all on temp file before rename, then rename, and if desired sync parent directory. The PR’s vault write logic that writes, calls sync_all/sync_data, and then renames is consistent with these recommendations; ensure code calls appropriate sync on target platforms and documents tradeoffs.,

  • blake3 crate: blake3::Hasher/update/update_reader/hash APIs (v1.8.4) are intended for file hashing; update_reader/update_mmap exist and mmap/rayon features may speed large-file hashing. Using blake3 for vault content-addressed hashing matches crate intent; consider feature choices (mmap/rayon) and streaming hashing patterns used in vault.hash_file_blake3.,

Caveats to call out when reviewing the PR

  • Windows rename/replace is nuanced — verify ReplaceFile/SetFileInformationByHandle usage (or MoveFileEx flags) and documented fallback behavior (the PR mentions Windows-specific backup/rename behavior). Ensure tests and code handle NTFS/network/share limits and document expectations.

  • Ensure create_new handling and AlreadyExists error mapping align with OpenOptions semantics (PR uses create_new(true) + content-hash compare to detect matching file vs conflict — this is correct pattern).

  • Ensure sync_all/sync_data calls are placed where the PR intends durability (before rename), and consider whether parent-directory sync is needed on the target platform for full durability guarantees. Make sure the PR’s error handling on failed sync/rename matches desired invariants.

Sources / tool calls used

  • Firecrawl (web searches / scraped docs):
    • firecrawl_search -> Rust/Windows rename atomicity discussion (StackOverflow + Rust internals),
    • firecrawl_search -> std::fs::OpenOptions docs and related issue,
    • firecrawl_search -> flush vs sync_all guidance and best-practices for atomic temp-file write,
    • firecrawl_search -> blake3 crate docs and Hasher APIs (docs.rs),
🔇 Additional comments (12)
src/auth/oauth_client/tests.rs (1)

48-480: Good coverage across OAuth source resolution and ADC/import paths.

These tests exercise precedence, legacy compatibility, and key failure paths in the new module boundaries.

src/attachments/tests.rs (11)

16-19: LGTM!

The ExportTestFixture struct appropriately holds the TempDir for RAII cleanup and the ConfigReport for test configuration. Tests can derive paths from temp_dir.path() as needed.


21-29: LGTM!

Good coverage of filename fallback edge cases: blank input and inputs that sanitize to empty.


31-70: LGTM!

The path construction tests now use relative paths with .join() for assertions, addressing the previous cross-platform concern. The uniqueness test at lines 63-70 only checks inequality, so the absolute path there is acceptable.


72-82: LGTM!

Good improvement from the previous string-based assertion. The test now properly matches the AttachmentServiceError::InvalidVaultPath variant and verifies the relative_path field.


84-127: LGTM!

Excellent test coverage for the vault reuse semantics. The first test verifies that hash mismatches prevent reuse, and the second confirms that matching content hashes enable reuse with downloaded: false.


129-148: LGTM!

Good test for the content-addressed vault write semantics—verifies that pre-existing corrupt data at the hash-derived path is correctly overwritten with valid content.


150-185: LGTM!

Error mapping and destination conflict tests are well-structured. The copy conflict test properly sets up the scenario with matching source hash but different destination content to trigger DestinationConflict.


187-227: LGTM!

Focused error-path tests for InvalidLimit and NoActiveAccount. The tests correctly assume fail-fast validation before database operations, which aligns with the coding guidelines for focused error-path coverage.


229-325: LGTM!

Comprehensive export error handling tests. The SQLite trigger technique for simulating persistence failures is effective and correctly tests both cleanup scenarios:

  • New copy is removed when persistence fails (line 284)
  • Preexisting matching file is preserved when persistence fails (line 324)

These align with the cleanup logic in service.rs where copy_result.copied determines whether to call cleanup_export_file_task.


327-369: LGTM!

Well-designed benchmark with tiered blob sizes and structured JSON output. The #[ignore] annotation appropriately keeps it out of regular test runs while remaining accessible for manual performance characterization.


371-475: LGTM!

The helper functions now properly derive sizes from actual payload data:

  • detail_with_vault accepts vault_size_bytes as a parameter
  • setup_export_test_fixture computes vault_size_bytes from vault_bytes.len() and threads it through both the attachment record and vault state

This addresses the previous feedback about ensuring mailbox/vault metadata consistency with blob length.

Comment thread src/auth/oauth_client/storage.rs
Comment thread src/auth/oauth_client/storage.rs Outdated
Comment thread src/auth/oauth_client/storage.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/auth/oauth_client/storage.rs (2)

324-333: ⚠️ Potential issue | 🟠 Major

Avoid deleting the destination before rename (credentials loss window).

At Line 327-Line 329, removing the existing file first creates a destructive gap: if remove_file succeeds and rename then fails, the last-good credentials are lost. Keep replacement single-step and fail without deleting the current file first.

💡 Proposed fix
 fn persist_tmp_file(tmp_path: &Path, destination: &Path) -> Result<()> {
-    #[cfg(windows)]
-    {
-        if destination.exists() {
-            fs::remove_file(destination)?;
-        }
-    }
-
     fs::rename(tmp_path, destination)?;
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auth/oauth_client/storage.rs` around lines 324 - 333, In
persist_tmp_file, do not remove the existing destination before calling
fs::rename (avoid the remove_file call inside the #[cfg(windows)] block) because
that creates a window where credentials can be lost; instead attempt a
single-step replacement by calling fs::rename(tmp_path, destination) and
propagate the error if it fails so the existing destination remains intact.
Update the function body (persist_tmp_file, tmp_path, destination, fs::rename)
to remove the pre-rename removal logic and rely on rename to fail atomically on
error.

7-8: 🛠️ Refactor suggestion | 🟠 Major

Use typed thiserror results at this storage-layer boundary.

Line 7 exposes anyhow::Result from storage helpers, which weakens structured matching for callers/tests in the auth store path. Please move this module to a local typed error enum and return Result<T, StorageError> from its public functions.

As per coding guidelines, "Use anyhow for command dispatch and top-level context in src/lib.rs; prefer typed thiserror errors in Gmail, workflow, and store layers in Rust".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auth/oauth_client/storage.rs` around lines 7 - 8, The storage module
currently exposes anyhow::Result (use anyhow::{Context, Result, anyhow}) which
weakens typed error handling; replace that by defining a local thiserror-backed
enum StorageError (#[derive(thiserror::Error, Debug)]) covering IO, Serde, and
domain variants, remove anyhow::Result import, and change all public function
signatures to return Result<T, StorageError>. Update error conversions by
implementing From<std::io::Error> and From<serde_...::Error> for StorageError
and replace uses of anyhow::Context/anyhow! with .map_err(StorageError::from) or
StorageError::... variants so callers/tests can pattern-match the typed
StorageError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/auth/oauth_client/storage.rs`:
- Around line 324-333: In persist_tmp_file, do not remove the existing
destination before calling fs::rename (avoid the remove_file call inside the
#[cfg(windows)] block) because that creates a window where credentials can be
lost; instead attempt a single-step replacement by calling fs::rename(tmp_path,
destination) and propagate the error if it fails so the existing destination
remains intact. Update the function body (persist_tmp_file, tmp_path,
destination, fs::rename) to remove the pre-rename removal logic and rely on
rename to fail atomically on error.
- Around line 7-8: The storage module currently exposes anyhow::Result (use
anyhow::{Context, Result, anyhow}) which weakens typed error handling; replace
that by defining a local thiserror-backed enum StorageError
(#[derive(thiserror::Error, Debug)]) covering IO, Serde, and domain variants,
remove anyhow::Result import, and change all public function signatures to
return Result<T, StorageError>. Update error conversions by implementing
From<std::io::Error> and From<serde_...::Error> for StorageError and replace
uses of anyhow::Context/anyhow! with .map_err(StorageError::from) or
StorageError::... variants so callers/tests can pattern-match the typed
StorageError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b8f105bb-07ed-4423-977e-3e5ac8a2fa46

📥 Commits

Reviewing files that changed from the base of the PR and between c92a00e and 72e1a66.

📒 Files selected for processing (2)
  • src/auth/oauth_client/storage.rs
  • src/auth/oauth_client/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: rust
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Use anyhow for command dispatch and top-level context in src/lib.rs; prefer typed thiserror errors in Gmail, workflow, and store layers in Rust
Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load
For new CLI JSON contracts, normalize success and failure to one top-level shape: { "success": true, "data": ... } for success and { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } } for failure
Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only in JSON error contracts
Do not include debug or backtrace payloads in the JSON error contract; use stderr and Rust backtrace env vars for deep diagnostics instead
Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant
Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable
Add focused error-path tests for every new failure class; if CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass

Files:

  • src/auth/oauth_client/tests.rs
  • src/auth/oauth_client/storage.rs
🔍 Remote MCP Firecrawl

Relevant facts for reviewing the PR (concise):

  • std::fs::rename semantics and Windows caveats

    • rename is atomic on the same filesystem but behavior/atomicy differs by platform; Windows historically had race/access issues when replacing an open file; Rust std now uses POSIX/Win FILE_RENAME_POSIX_SEMANTICS where available (resolved in std updates) but platform/filesystem edge-cases remain. Cite: Rust issue & discussion and forum threads.,
  • Cross-filesystem moves

    • fs::rename does not work across mount points (must copy-to-temp-on-dest then rename). This matters for vault->export copy/atomicity if destination may be on a different filesystem.
  • OpenOptions::create_new and create_new(true) behavior

    • OpenOptions::create_new(true) atomically creates-only-if-missing and returns AlreadyExists on existing target; it’s the right primitive for "create only" semantics used in create_new + handling AlreadyExists for hash-compare vs conflict detection.
  • Durability (sync) best practices for temp-file-then-rename

    • Correct pattern to avoid partial/zero-length results on crash: write temp file, call File::sync_all (or sync_data) on temp file, rename, then sync the containing directory. Many discussions warn that omitting syncs can let a rename succeed while data not fully persisted. The PR’s use of sync_all before/after rename is consistent with recommendations; consider whether parent-dir sync is needed for the project’s durability guarantees.,
  • blake3 usage guidance

    • blake3::Hasher provides update/update_reader/update_mmap and finalize/finalize_xof; crates support mmap and rayon features for faster hashing of large files. Using streaming update or update_mmap (with appropriate features) is standard for hashing files in vault logic; be aware of performance trade-offs (rayon/mmap can hurt on spinning disks).

Sources searched (Firecrawl searches used):

  • std::fs rename / Windows atomicity discussion:,
  • fs::rename across filesystems:
  • OpenOptions::create_new docs:
  • Temp-file sync/atomic-write best practices:,
  • blake3 hasher API and features:
🔇 Additional comments (1)
src/auth/oauth_client/tests.rs (1)

98-113: Good error-path coverage for the refactor.

These tests validate both fast-fail import path validation and malformed workspace-file handling, which materially reduces regression risk in the split OAuth storage/resolve flow.

Also applies to: 441-463

@BjornMelin BjornMelin merged commit e0c408b into main Apr 22, 2026
3 checks passed
@BjornMelin BjornMelin deleted the refactor/attachments-and-oauth-client-modules branch April 22, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants