To extract a file extension in Arduino C, use the standard C library function strrchr(filename, '.') to locate the last dot in a null-terminated char array, or use filename.substring(filename.lastIndexOf('.') + 1) if you are using the Arduino String class. For SD card directory parsing where memory efficiency matters, the C-string approach via strrchr is the industry standard, provided you implement strict null-pointer and hidden-file checks.

Unlike Python or PHP, Arduino C++ does not have a built-in pathinfo() or os.path.splitext() function. When iterating through directories on a FAT32 SD card or ESP32 LittleFS, the native file objects return standard C-strings. Mishandling these pointers is the number one cause of hard crashes in embedded file-logging projects. This guide walks through the exact hardware setup, memory-safe C-string parsing, and the specific debugging steps required when your ESP32 throws a memory protection fault.

Hardware BOM and SPI Pin Mapping (ESP32 DevKit V1)

The code and logic in this guide specifically target the ESP32 DevKit V1 (38-pin variant, NodeMCU-32S architecture) running the Arduino Core for ESP32 (v3.x). The ESP32 is chosen here over the ATmega328P (Arduino Uno) because its 520KB of SRAM and native SPI DMA make it the standard for high-throughput SD card data logging in 2026.

Tip: Never wire a 5V Arduino Uno directly to a 3.3V microSD breakout without a logic level shifter. The ESP32 operates natively at 3.3V, eliminating the need for bulky level-shifting modules like the BSS138, provided you use a breakout with an onboard 3.3V LDO regulator.

Bill of Materials

Component Exact Model / Variant Qty Specifications & Notes
Microcontroller ESP32 DevKit V1 (38-pin) 1 NodeMCU-32S, Dual-core 240MHz, 520KB SRAM. (~$6.00)
Storage Module Adafruit MicroSD Breakout (254) 1 Includes 3.3V LDO and level shifters. HW-125 clones work but often lack decoupling caps. (~$7.50)
MicroSD Card SanDisk Ultra 16GB 1 Must be formatted as FAT32. SDHC (Class 10). Avoid SDXC (>32GB) for standard SD.h. (~$8.00)
Wiring 24 AWG Silicone Wire 6 Keep SPI traces under 4 inches to prevent signal degradation at 20MHz+ clock speeds.
Bypass Capacitor 100nF Ceramic (0.1µF) 1 Solder directly across VCC/GND on the SD module if using a cheap clone board.

SPI Pin Mapping Table

ESP32 GPIO Pin SD Module Pin Wire Color (Std) Function / Notes
GPIO 23 (VSPI MOSI) DI / MOSI Blue Master Out Slave In (Data to SD)
GPIO 19 (VSPI MISO) DO / MISO Green Master In Slave Out (Data from SD)
GPIO 18 (VSPI SCK) CLK / SCK Yellow SPI Clock Signal
GPIO 5 (VSPI CS) CS / SS Orange Chip Select (Active LOW)
3V3 VCC / 3V3 Red Power (Ensure module has LDO if feeding 5V)
GND GND Black Common Ground

C-Strings vs. Arduino Strings for File Parsing

When you call file.name() using the standard Arduino SD library, it returns a const char* (a standard C-string). You have two choices for extracting the extension: stick to native C-strings or cast it to an Arduino String object. Here is how they compare in an embedded environment.

Criteria Native C-String (strrchr) Arduino String Object (lastIndexOf)
Memory Allocation Zero dynamic allocation. Operates on existing stack/heap pointers. Requires heap allocation. Creates a new String copy in SRAM.
Execution Speed Extremely fast. Single pass from the end of the string backward. Slower. Involves object instantiation, copying, and method overhead.
Fragmentation Risk None. Safe for infinite loops and long-running data loggers. High. Repeated allocation/deallocation causes heap fragmentation, leading to reboots.
Syntax Complexity High. Requires manual pointer arithmetic and null-termination checks. Low. Familiar dot-notation syntax for web developers.
Verdict Use for production SD/LittleFS logging. Use only for quick prototyping.

Complete Compilable Code: SD Card File Extension Filter

The following code targets the ESP32 DevKit V1. It mounts a FAT32 SD card, iterates through the root directory, and uses a memory-safe custom function extractExtension() to isolate the file suffix. It includes explicit error handling for hidden files (like .Trashes) and files without extensions.

#include <SPI.h>
#include <SD.h>

// --- PIN DEFINITIONS (ESP32 DevKit V1 VSPI) ---
#define PIN_SD_MOSI 23
#define PIN_SD_MISO 19
#define PIN_SD_SCK  18
#define PIN_SD_CS   5

// Buffer size for extension (e.g., "jpeg" + null terminator = 5)
#define EXT_BUFFER_SIZE 8 

/**
 * Safely extracts the file extension from a C-string filename.
 * Returns true if a valid extension was found, false otherwise.
 */
bool extractExtension(const char* filename, char* extBuffer, size_t bufferSize) {
    if (filename == NULL || extBuffer == NULL || bufferSize == 0) {
        return false; // Prevent null pointer dereference
    }

    // Find the last occurrence of the dot character
    const char* dot = strrchr(filename, '.');

    // Check 1: No dot found in the entire string
    if (dot == NULL) {
        extBuffer[0] = '\0';
        return false;
    }

    // Check 2: Dot is the very first character (e.g., hidden file ".gitignore")
    if (dot == filename) {
        extBuffer[0] = '\0';
        return false;
    }

    // Move pointer past the dot to the actual extension characters
    const char* extStart = dot + 1;

    // Check 3: Ensure the extension isn't longer than our buffer to prevent overflow
    size_t extLen = strlen(extStart);
    if (extLen >= bufferSize) {
        // Truncate safely if extension is abnormally long
        strncpy(extBuffer, extStart, bufferSize - 1);
        extBuffer[bufferSize - 1] = '\0';
        return true;
    }

    // Safe to copy
    strcpy(extBuffer, extStart);
    return true;
}

void setup() {
    Serial.begin(115200);
    while(!Serial) { delay(10); }
    Serial.println("\n--- ESP32 SD Card Extension Parser ---");

    // Initialize SPI and SD Card
    SPI.begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, PIN_SD_CS);
    if (!SD.begin(PIN_SD_CS)) {
        Serial.println("FATAL: SD Card Mount Failed. Check wiring and FAT32 format.");
        while(1) { delay(1000); } // Halt execution
    }
    
    uint8_t cardType = SD.cardType();
    if(cardType == CARD_NONE) {
        Serial.println("No SD card attached.");
        return;
    }

    File root = SD.open("/");
    if(!root) {
        Serial.println("Failed to open root directory.");
        return;
    }
    if(!root.isDirectory()) {
        Serial.println("Root is not a directory.");
        return;
    }

    // Iterate through files
    char extBuffer[EXT_BUFFER_SIZE];
    File file = root.openNextFile();
    while(file) {
        const char* fName = file.name();
        
        if (extractExtension(fName, extBuffer, EXT_BUFFER_SIZE)) {
            Serial.printf("Found valid file: %s | Extension: %s\n", fName, extBuffer);
            
            // Example: Filter only for CSV data logs
            if (strcasecmp(extBuffer, "csv") == 0) {
                Serial.println("  -> Processing CSV data log...");
            }
        } else {
            Serial.printf("Skipped file (no ext/hidden): %s\n", fName);
        }
        
        file.close();
        file = root.openNextFile();
    }
    root.close();
    Serial.println("Directory scan complete.");
}

void loop() {
    // Nothing to do in loop for this demonstration
    delay(10000);
}

Debugging: Pointer Crashes and Parse Errors

When manipulating C-strings on the ESP32, a single missed boundary check will bypass the Arduino wrapper and trigger a hardware-level memory protection fault. If your serial monitor spits out the following exact error string, your extension parser has failed:

Exact Error String:
Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.
Core 1 register dump:
PC: 0x400d1234 PS: 0x00060030 A0: 0x800d1234 A1: 0x3ffb1234

Alternatively, if you are using a custom logging wrapper, you might see a logical error string like: ERR_EXT_PARSE: Null pointer dereference at 0x00000001.

The First Three Things to Check When It Fails

  1. Check for NULL returns from strrchr: The LoadProhibited crash almost always happens when a file has no dot (e.g., a directory named SYSTEM~1 or a file named README). strrchr returns NULL. If your next line of code is const char* ext = dot + 1;, you are attempting to read memory address 0x00000001, triggering the ESP32 hardware watchdog to panic and reboot. Always wrap the pointer increment in an if (dot != NULL) block.
  2. Check for Hidden Files (Index 0 Dot): macOS and Linux create hidden files like .DS_Store or .Trashes. In these cases, strrchr finds the dot, but it is the very first character of the string. If you parse this as an extension, your code will treat the entire filename as the extension. You must check if (dot == filename) and reject it.
  3. Check Destination Buffer Overflows: If you use strcpy(extBuffer, extStart) without checking the length of extStart, a corrupted FAT32 directory entry returning a 20-character garbage string will overwrite adjacent stack variables. This causes erratic behavior, Wi-Fi drops, or delayed reboots. Always use strncpy or enforce a length check against your buffer size, as demonstrated in the code above.

For deeper debugging on ESP32 memory faults, utilize the Espressif Arduino Core documentation to decode the stack trace using the esp32_exception_decoder tool in PlatformIO.

Extending and Simplifying the Build

How to Simplify (The RAM Trade-off)

If you are building a quick prototype and do not care about heap fragmentation, you can entirely bypass C-string pointer math by casting the filename to an Arduino String object. This reduces the extraction logic to a single, highly readable line:

String fName = String(file.name());
int dotIndex = fName.lastIndexOf('.');
if (dotIndex > 0) {
    String ext = fName.substring(dotIndex + 1);
    Serial.println(ext);
}

Warning: Do not use this simplification in a while() loop that runs thousands of times (like a continuous data logger). The repeated creation and destruction of String objects will fragment the ESP32's heap, eventually causing an alloc failed panic after a few hours of uptime.

How to Extend (MIME-Type Mapping for Web Servers)

If you are building an ESP32 web server that serves files from the SD card, the extracted extension is the key to setting the correct HTTP Content-Type header. You can extend the build by passing the extracted extBuffer into a lookup function:

const char* getMimeType(const char* ext) {
    if (strcasecmp(ext, "htm") == 0 || strcasecmp(ext, "html") == 0) return "text/html";
    if (strcasecmp(ext, "css") == 0) return "text/css";
    if (strcasecmp(ext, "js") == 0)  return "application/javascript";
    if (strcasecmp(ext, "json") == 0) return "application/json";
    if (strcasecmp(ext, "png") == 0) return "image/png";
    if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0) return "image/jpeg";
    return "application/octet-stream"; // Default fallback for binary/unknown
}

By mastering strrchr and implementing strict boundary checks, you ensure your embedded file systems remain stable, memory-efficient, and immune to the pointer crashes that plague beginner SD card projects. Always verify your FAT32 formatting and keep your SPI wiring short to maintain signal integrity at the hardware level.