Skip to content

ESP-IDF-SVC NVS contains() Bug Report - String Values Not Detected #585

Description

@danielmeza

ESP-IDF-SVC NVS contains() Bug Report - String Values Not Detected

Bug description

The contains() method in EspNvs returns false for string values that actually exist in NVS partitions, even though the same strings can be successfully retrieved using get_str(). This creates inconsistent behavior where data exists in flash but appears missing to the application.

Root Cause: The contains() method internally calls len(), which only checks for u64 and blob data types via nvs_get_u64() and nvs_get_blob(). It never calls nvs_get_str() to check for string values stored with nvs_set_str().

Would you like to work on a fix? Yes, we have identified the exact issue and can provide a complete solution.

To Reproduce

  1. Create an NVS partition in your ESP32 project (default or custom)
  2. Initialize an EspNvs or EspCustomNvs instance with a namespace
  3. Store a string value using set_str(key, "some_string")
  4. Call contains(key) - it returns false
  5. Call get_str(key, buffer) - it successfully retrieves the string
  6. Reboot the device and repeat steps 4-5 - same inconsistent behavior

Minimal reproduction code:

use esp_idf_svc::nvs::{EspDefaultNvsPartition, EspNvs};
// or for custom partitions: use esp_idf_svc::nvs::{EspCustomNvsPartition, EspCustomNvs};

fn reproduce_bug() -> Result<(), Box<dyn std::error::Error>> {
    // Using default partition (bug affects all partition types)
    let partition = EspDefaultNvsPartition::take()?;
    let mut nvs = EspNvs::new(partition, "test_ns", true)?;
    
    // Alternative: Custom partition
    // let partition = EspCustomNvsPartition::take("custom_partition")?;
    // let mut nvs = EspCustomNvs::new(partition, "test_ns", true)?;
    
    // Set a string value
    nvs.set_str("test_key", "test_value")?;
    
    // BUG: contains() returns false for string values
    let contains_result = nvs.contains("test_key")?;
    println!("contains(): {}", contains_result); // Prints: false
    
    // But get_str() works correctly
    let mut buffer = [0u8; 64];
    let get_result = nvs.get_str("test_key", &mut buffer)?;
    println!("get_str(): {:?}", get_result); // Prints: Some("test_value")
    
    // Inconsistent behavior: data exists but contains() can't find it
    assert!(contains_result != get_result.is_some()); // This assertion passes, proving the bug
    
    Ok(())
}

Expected behavior

contains(key) should return true for any existing NVS entry regardless of data type (u8, u16, u32, u64, i8, i16, i32, i64, string, blob). The method should be consistent with other NVS operations like get_str(), get_u32(), etc.

Expected behavior:

  • nvs.set_str("key", "value") - stores string successfully ✅
  • nvs.contains("key") - returns true ✅ (currently returns false ❌)
  • nvs.get_str("key", buffer) - retrieves string successfully ✅

Root Cause Analysis

The bug is in esp-idf-svc/src/nvs.rs:

pub fn contains(&self, name: &str) -> Result<bool, EspError> {
    self.len(name).map(|v| v.is_some())  // ← calls len()
}

fn len(&self, name: &str) -> Result<Option<usize>, EspError> {
    let c_key = to_cstring_arg(name)?;
    let mut value: u_int64_t = 0;

    // check for u64 value
    match unsafe { nvs_get_u64(self.1, c_key.as_ptr(), &mut value as *mut _) } {
        ESP_ERR_NVS_NOT_FOUND => {
            // check for blob value, by getting blob length
            let mut len = 0;
            match unsafe {
                nvs_get_blob(self.1, c_key.as_ptr(), ptr::null_mut(), &mut len as *mut _)
            } {
                ESP_ERR_NVS_NOT_FOUND => Ok(None),  // ← STOPS HERE, never checks strings!
                // ... only handles blob case
            }
        }
        // ... only handles u64 case
    }
}

Problem: The len() method only checks nvs_get_u64() and nvs_get_blob(). It never calls nvs_get_str() for string values.

Proposed Solution

Option 1: Fix the len() method to check string values

fn len(&self, name: &str) -> Result<Option<usize>, EspError> {
    let c_key = to_cstring_arg(name)?;
    
    // Check for u64 value
    let mut u64_value: u_int64_t = 0;
    match unsafe { nvs_get_u64(self.1, c_key.as_ptr(), &mut u64_value as *mut _) } {
        ESP_ERR_NVS_NOT_FOUND => {
            // Check for blob value
            let mut blob_len = 0;
            match unsafe { nvs_get_blob(self.1, c_key.as_ptr(), ptr::null_mut(), &mut blob_len as *mut _) } {
                ESP_ERR_NVS_NOT_FOUND => {
                    // ← ADD THIS: Check for string value
                    let mut str_len = 0;
                    match unsafe { nvs_get_str(self.1, c_key.as_ptr(), ptr::null_mut(), &mut str_len as *mut _) } {
                        ESP_ERR_NVS_NOT_FOUND => Ok(None),
                        err => {
                            esp!(err)?;
                            Ok(Some(str_len))
                        }
                    }
                }
                err => {
                    esp!(err)?;
                    Ok(Some(blob_len))
                }
            }
        }
        err => {
            esp!(err)?;
            Ok(Some(8)) // u64 is always 8 bytes
        }
    }
}

Option 2: Use nvs_find_key() in contains() method (Recommended)

pub fn contains(&self, name: &str) -> Result<bool, EspError> {
    let c_key = to_cstring_arg(name)?;
    let mut entry_type: nvs_type_t = nvs_type_t_NVS_TYPE_ANY;
    
    let result = unsafe { 
        nvs_find_key(self.1, c_key.as_ptr(), &mut entry_type as *mut _) 
    };
    
    match result {
        ESP_OK => Ok(true),
        ESP_ERR_NVS_NOT_FOUND => Ok(false),
        err => {
            esp!(err)?;
            Ok(false) // This line should never be reached due to esp!() macro
        }
    }
}

Option 3: Enhanced find_key() method that returns data type (Additional Feature)

The nvs_find_key function can also return the data type of the found entry. Consider adding this as a new method:

pub fn find_key(&self, name: &str) -> Result<Option<nvs_type_t>, EspError> {
    let c_key = to_cstring_arg(name)?;
    let mut entry_type: nvs_type_t = nvs_type_t_NVS_TYPE_ANY;
    
    let result = unsafe { 
        nvs_find_key(self.1, c_key.as_ptr(), &mut entry_type as *mut _) 
    };
    
    match result {
        ESP_OK => Ok(Some(entry_type)),
        ESP_ERR_NVS_NOT_FOUND => Ok(None),
        err => {
            esp!(err)?;
            Ok(None) // This line should never be reached due to esp!() macro
        }
    }
}

This would allow applications to:

// Check existence and get type in one call
if let Some(data_type) = nvs.find_key("my_key")? {
    match data_type {
        nvs_type_t_NVS_TYPE_STR => { /* handle string */ },
        nvs_type_t_NVS_TYPE_U32 => { /* handle u32 */ },
        nvs_type_t_NVS_TYPE_BLOB => { /* handle blob */ },
        _ => { /* handle other types */ },
    }
}

Options 2 and 3 are recommended because:

  • nvs_find_key() is designed specifically to check for key existence regardless of data type
  • It's more efficient than trying multiple nvs_get_*() calls
  • It's more robust and handles all current and future NVS data types
  • It matches the semantic expectation of a contains() method
  • Uses the exact pattern from ESP-IDF commit 8740888 that introduced nvs_find_key() on Dec 8, 2023
  • Option 3 additionally provides data type information, which can be useful for applications

Note: The nvs_find_key function was added to ESP-IDF specifically for this use case - checking key existence regardless of data type. The implementation above follows the exact error handling pattern used in the official ESP-IDF test suite.

Environment

  • Crate (esp-idf-svc) version: 0.51.0
  • ESP-IDF branch or tag: release/v5.2
  • Target device (MCU): esp32
  • OS: Windows 11

Impact

This bug affects anyone using string values in NVS partitions with esp-idf-svc, making it a critical issue for production applications. It causes:

  1. Logic errors where applications think data doesn't exist when it actually does
  2. Unnecessary re-initialization of configuration values
  3. Data inconsistency between different NVS operation results
  4. Debugging confusion where data appears in partition dumps but contains() returns false

Additional Notes

  • The bug affects all NVS partitions (default and custom) and string values
  • get_str(), set_str(), and other string operations work correctly
  • Only the contains() method is affected
  • Binary partition dumps show the string entries exist in flash
  • This suggests the bug is in the esp-idf-svc wrapper, not the underlying ESP-IDF NVS implementation

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions