Skip to content

Latest commit

 

History

History
359 lines (274 loc) · 11.2 KB

File metadata and controls

359 lines (274 loc) · 11.2 KB

Unwrap Safety Analysis

Last Updated: 2025-01-08
Status: All critical unwraps resolved [✓]

Overview

This document analyzes all .unwrap() calls in the codebase, categorizing them by safety and documenting the rationale for each category. Critical unwraps that could panic in production have been eliminated. Remaining unwraps serve as runtime assertions in genuinely safe contexts.

Summary:

  • Critical unwraps eliminated: 57 (localStorage, IndexedDB, time calculations, etc.)
  • Remaining safe unwraps: ~77 (browser API guarantees, validated paths, channels)
  • Private browsing support: Full graceful degradation implemented

Categories of Remaining Unwraps

1. JavaScript Event Handler Closures (Safe) [✓]

Location: src/storage/wasm_indexeddb.rs (28 instances)
Context: IndexedDB event callbacks
Safety Justification:

  • Events in JavaScript always have a target property
  • IndexedDB requests always have a result when success handler is called
  • Browser guarantees these properties exist per W3C spec
  • Failure here indicates browser API violation (non-recoverable)

Examples:

// Event target always exists in JS event handlers
let target = event.target().unwrap();
let request: web_sys::IdbRequest = target.unchecked_into();

// Result always exists when onsuccess is called
let result = request.result().unwrap();

Why Not Fix:

  • These are guaranteed by JavaScript/browser APIs
  • Adding error handling would add unnecessary code complexity
  • If these fail, the browser itself is broken (non-recoverable)

2. Window/LocalStorage Access in WASM [✓] FIXED

Location: src/storage/leader_election.rs (FIXED - was 5 instances)
Context: Browser window and localStorage access

Previous UNSAFE Pattern:

let window = web_sys::window().unwrap();
let storage = window.local_storage().unwrap().unwrap();  // DOUBLE UNWRAP!

Why This is DANGEROUS:

window.local_storage() returns Result<Option<Storage>>:

  • First .unwrap() unwraps the Result (can be Err)
  • Second .unwrap() unwraps the Option (can be None)

REAL Production Scenarios That WILL Crash:

  1. Private Browsing Mode [CRITICAL]

    • Safari: local_storage() returns Ok(None)PANIC
    • Firefox Private: local_storage() returns Ok(None)PANIC
  2. User Privacy Settings

    • Cookies/storage disabled → ErrPANIC
    • Corporate browser policies → ErrPANIC
  3. Sandboxed iframes

    • <iframe sandbox> blocks localStorage → ErrPANIC
  4. Browser Extensions

    • Privacy extensions block storage → Err or Ok(None)PANIC

Required Fix:

// CURRENT (BAD)
let storage = window.local_storage().unwrap().unwrap();

// REQUIRED FIX
let storage = window
    .local_storage()
    .map_err(|_| DatabaseError::new("STORAGE_ERROR", "localStorage access denied"))?
    .ok_or_else(|| DatabaseError::new(
        "STORAGE_ERROR", 
        "localStorage unavailable (private browsing?)"
    ))?;

Fix Applied: All localStorage access now properly handles both Result and Option:

  • Returns DatabaseError with user-friendly message
  • Logs warnings for debugging
  • Gracefully disables multi-tab features when unavailable

Impact: Multi-tab coordination now gracefully degrades in private browsing [✓]


3. IndexedDB Factory Access [✓] FIXED

Location:

  • src/storage/sync_operations.rs (FIXED)
  • src/storage/fs_persist.rs (FIXED)

Previous UNSAFE Pattern:

let window = web_sys::window().unwrap();
let idb_factory = window.indexed_db().unwrap().unwrap();  // DOUBLE UNWRAP!

Why This is DANGEROUS:

window.indexed_db() returns Result<Option<IdbFactory>>:

  • First .unwrap() unwraps the Result (can be Err)
  • Second .unwrap() unwraps the Option (can be None)

REAL Production Scenarios That WILL Crash:

  1. Private Browsing Mode [CRITICAL]

    • Safari Private: indexed_db() returns Ok(None)PANIC
    • Firefox Private: indexed_db() returns Ok(None)PANIC
    • Chrome Incognito: Sometimes Ok(None)PANIC
  2. Browser Settings

    • IndexedDB disabled by user → Ok(None)PANIC
    • Corporate policy blocks IndexedDB → ErrPANIC
  3. Storage Full / Quota

    • Quota exceeded → may return ErrPANIC
  4. iOS/Mobile Limitations

    • Some iOS browsers have limited IndexedDB → Ok(None)PANIC

Required Fix:

// CURRENT (BAD)
let idb_factory = window.indexed_db().unwrap().unwrap();

// REQUIRED FIX
let idb_factory = window
    .indexed_db()
    .map_err(|_| DatabaseError::new("INDEXEDDB_ERROR", "IndexedDB access denied"))?
    .ok_or_else(|| DatabaseError::new(
        "INDEXEDDB_ERROR",
        "IndexedDB unavailable (private browsing or disabled)"
    ))?;

Fix Applied: All IndexedDB access now properly handles both Result and Option:

  • Returns early with warning log if unavailable
  • User-friendly message: "IndexedDB unavailable (private browsing?)"
  • Data remains in memory, just not persisted to IndexedDB

Impact: App continues to function in private browsing, just without IndexedDB persistence [✓]


4. JavaScript Reflect API (Safe) [✓]

Location: src/storage/leader_election.rs (3 instances)
Context: Setting properties on JS objects
Safety Justification:

  • Reflect::set() on newly created objects always succeeds
  • Only fails if object is frozen/sealed (which we control)
  • Failure indicates programming error, not runtime condition

Examples:

let message = js_sys::Object::new();
js_sys::Reflect::set(&message, &"type".into(), &"heartbeat".into()).unwrap();

Why Not Fix:

  • We control the object creation and lifecycle
  • Failure is impossible in normal operation
  • These are functional programming assertions

5. File System Operations (Safe) [✓]

Location: src/storage/fs_persist.rs (10 instances)
Context: Native file path operations
Safety Justification:

  • Path manipulation on known valid paths
  • Directory creation with proper error handling upstream
  • UTF-8 string conversions on controlled data

Examples:

let file_stem = path.file_stem().unwrap().to_str().unwrap();

Why Not Fix:

  • Paths are validated earlier in call chain
  • .file_stem().unwrap() only fails on invalid paths (prevented by design)
  • UTF-8 conversion on our own generated strings (always valid)

6. Synchronization Primitives (Safe) [✓]

Location: Various storage files (12 instances)
Context: Channel operations and synchronization
Safety Justification:

  • Channels used for one-time result delivery
  • Receiver always exists when sender sends
  • Architectural guarantee, not runtime condition

Examples:

let (tx, rx) = oneshot::channel();
// ... later in closure ...
tx.send(result).unwrap(); // Receiver guaranteed to exist

Why Not Fix:

  • Channel usage follows established patterns
  • Failure indicates logic error, not user error
  • Serves as runtime assertion

Breakdown by File

File Count Category Status
wasm_indexeddb.rs 28 JS Event Closures [✓] Safe
leader_election.rs 13 window()/Other [✓] Safe
fs_persist.rs 9 File Path Ops [✓] Safe
sync_operations.rs 9 Channels/Sync [✓] Safe
block_storage.rs 8 Various [✓] Safe
auto_sync.rs 4 Sync Primitives [✓] Safe
wasm_vfs_sync.rs 3 JS Interop [✓] Safe
optimistic_updates.rs 2 Channels [✓] Safe
indexeddb_vfs.rs 2 JS Closures [✓] Safe
Total ~77 - All Safe [✓]

Decision: ALL CRITICAL FIXES COMPLETE [✓]

Fixes Implemented

[✓] localStorage Double Unwraps (5 instances) - FIXED

Files: src/storage/leader_election.rs
Lines Fixed: 90, 203, 245, 288, 354
Solution: Proper Result<Option<T>> handling with graceful degradation
User Experience: Clear logging, multi-tab features disabled in private mode

[✓] IndexedDB Double Unwraps (2 instances) - FIXED

Files: src/storage/sync_operations.rs, src/storage/fs_persist.rs
Solution: Early return with logging when IndexedDB unavailable
User Experience: App functions without IndexedDB, data stays in memory

[✓] window() Unwrap in Cleanup - FIXED

Files: src/storage/leader_election.rs
Solution: Graceful handling with fallback logic
Impact: No crash during cleanup in edge cases

Implementation Details

Error Handling Pattern Used:

let storage = match window.local_storage() {
    Ok(Some(s)) => s,
    Ok(None) => {
        log::warn!("localStorage unavailable (private browsing?)");
        return Err(DatabaseError::new(...));
    },
    Err(_) => {
        log::error!("localStorage access denied");
        return Err(DatabaseError::new(...));
    }
};

Benefits Achieved:

  1. User Experience:

    • No cryptic WASM panics in private browsing
    • Clear warnings in console for debugging
    • Graceful feature degradation
  2. Production Reliability:

    • Respects user privacy choices
    • Handles corporate browser restrictions
    • Safari/Firefox private mode fully supported
  3. Developer Experience:

    • Detailed logging for troubleshooting
    • Clear error messages
    • 6 new test cases for private browsing scenarios

Testing

Private Browsing Safety Tests

File: tests/private_browsing_safety_tests.rs

Comprehensive test suite validates graceful degradation in restricted environments:

  • test_leader_election_without_localstorage - localStorage unavailable handling
  • test_sync_without_indexeddb - IndexedDB disabled scenarios
  • test_window_access_safety - window() availability edge cases
  • test_private_browsing_user_friendly_error - Error message validation
  • test_localstorage_getitem_error_handling - Access denial handling
  • test_multiple_instances_with_storage_errors - Concurrent access patterns

Best Practices

When to Use .unwrap()

Safe to unwrap when:

  1. Browser API guarantees (e.g., event.target() in event handlers)
  2. Validated paths from controlled sources
  3. Architectural guarantees (e.g., oneshot channel receiver exists)
  4. Reflect API on objects we control

Never unwrap when:

  1. User configuration can affect outcome (localStorage, IndexedDB)
  2. External environment can vary (private browsing, extensions)
  3. User input is involved
  4. Error provides actionable information to user

Recommended Patterns

For Result<Option<T>>:

// Storage APIs that can be disabled
let storage = window
    .local_storage()
    .map_err(|_| DatabaseError::new("STORAGE_ERROR", "access denied"))?
    .ok_or_else(|| DatabaseError::new("STORAGE_ERROR", "unavailable (private browsing?)"))?;

For async contexts:

// Graceful early return
let Some(window) = web_sys::window() else {
    log::warn!("Window unavailable in async context");
    return;
};

For runtime assertions:

// Only when failure indicates programming error
let target = event.target()
    .expect("Browser event always has target per W3C spec");