fork the esp-idf fatfs for f_forward and exfat support

custom
jacqueline 2 years ago
parent 9287c4eb8c
commit 7b72e5479e
  1. 7
      lib/fatfs/.build-test-rules.yml
  2. 16
      lib/fatfs/CMakeLists.txt
  3. 233
      lib/fatfs/Kconfig
  4. 117
      lib/fatfs/diskio/diskio.c
  5. 72
      lib/fatfs/diskio/diskio_impl.h
  6. 134
      lib/fatfs/diskio/diskio_rawflash.c
  7. 37
      lib/fatfs/diskio/diskio_rawflash.h
  8. 142
      lib/fatfs/diskio/diskio_sdmmc.c
  9. 42
      lib/fatfs/diskio/diskio_sdmmc.h
  10. 118
      lib/fatfs/diskio/diskio_wl.c
  11. 39
      lib/fatfs/diskio/diskio_wl.h
  12. 0
      lib/fatfs/fatfs_utils/__init__.py
  13. 168
      lib/fatfs/fatfs_utils/boot_sector.py
  14. 213
      lib/fatfs/fatfs_utils/cluster.py
  15. 253
      lib/fatfs/fatfs_utils/entry.py
  16. 54
      lib/fatfs/fatfs_utils/exceptions.py
  17. 100
      lib/fatfs/fatfs_utils/fat.py
  18. 17
      lib/fatfs/fatfs_utils/fatfs_parser.py
  19. 170
      lib/fatfs/fatfs_utils/fatfs_state.py
  20. 343
      lib/fatfs/fatfs_utils/fs_object.py
  21. 98
      lib/fatfs/fatfs_utils/long_filename_utils.py
  22. 299
      lib/fatfs/fatfs_utils/utils.py
  23. 245
      lib/fatfs/fatfsgen.py
  24. 167
      lib/fatfs/fatfsparse.py
  25. 212
      lib/fatfs/port/freertos/ffsystem.c
  26. 45
      lib/fatfs/port/linux/ffsystem.c
  27. 119
      lib/fatfs/project_include.cmake
  28. 368
      lib/fatfs/src/00history.txt
  29. 20
      lib/fatfs/src/00readme.txt
  30. 228
      lib/fatfs/src/diskio.c
  31. 79
      lib/fatfs/src/diskio.h
  32. 7085
      lib/fatfs/src/ff.c
  33. 429
      lib/fatfs/src/ff.h
  34. 332
      lib/fatfs/src/ffconf.h
  35. 208
      lib/fatfs/src/ffsystem.c
  36. 15593
      lib/fatfs/src/ffunicode.c
  37. 19
      lib/fatfs/test_apps/README.md
  38. 8
      lib/fatfs/test_apps/flash_ro/CMakeLists.txt
  39. 14
      lib/fatfs/test_apps/flash_ro/README.md
  40. 41
      lib/fatfs/test_apps/flash_ro/main/CMakeLists.txt
  41. 311
      lib/fatfs/test_apps/flash_ro/main/test_fatfs_flash_ro.c
  42. 3
      lib/fatfs/test_apps/flash_ro/partitions.csv
  43. 15
      lib/fatfs/test_apps/flash_ro/pytest_fatfs_flash_ro.py
  44. 14
      lib/fatfs/test_apps/flash_ro/sdkconfig.defaults
  45. 8
      lib/fatfs/test_apps/flash_wl/CMakeLists.txt
  46. 8
      lib/fatfs/test_apps/flash_wl/README.md
  47. 4
      lib/fatfs/test_apps/flash_wl/main/CMakeLists.txt
  48. 276
      lib/fatfs/test_apps/flash_wl/main/test_fatfs_flash_wl.c
  49. 3
      lib/fatfs/test_apps/flash_wl/partitions.csv
  50. 39
      lib/fatfs/test_apps/flash_wl/pytest_fatfs_flash_wl.py
  51. 0
      lib/fatfs/test_apps/flash_wl/sdkconfig.ci.default
  52. 2
      lib/fatfs/test_apps/flash_wl/sdkconfig.ci.fastseek
  53. 3
      lib/fatfs/test_apps/flash_wl/sdkconfig.ci.psram
  54. 2
      lib/fatfs/test_apps/flash_wl/sdkconfig.ci.release
  55. 18
      lib/fatfs/test_apps/flash_wl/sdkconfig.defaults
  56. 8
      lib/fatfs/test_apps/sdcard/CMakeLists.txt
  57. 14
      lib/fatfs/test_apps/sdcard/README.md
  58. 8
      lib/fatfs/test_apps/sdcard/main/CMakeLists.txt
  59. 11
      lib/fatfs/test_apps/sdcard/main/test_fatfs_sdcard_main.c
  60. 364
      lib/fatfs/test_apps/sdcard/main/test_fatfs_sdmmc.c
  61. 199
      lib/fatfs/test_apps/sdcard/main/test_fatfs_sdspi.c
  62. 3
      lib/fatfs/test_apps/sdcard/partitions.csv
  63. 75
      lib/fatfs/test_apps/sdcard/pytest_fatfs_sdcard.py
  64. 0
      lib/fatfs/test_apps/sdcard/sdkconfig.ci.default
  65. 3
      lib/fatfs/test_apps/sdcard/sdkconfig.ci.psram
  66. 2
      lib/fatfs/test_apps/sdcard/sdkconfig.ci.release
  67. 19
      lib/fatfs/test_apps/sdcard/sdkconfig.defaults
  68. 3
      lib/fatfs/test_apps/test_fatfs_common/CMakeLists.txt
  69. 979
      lib/fatfs/test_apps/test_fatfs_common/test_fatfs_common.c
  70. 76
      lib/fatfs/test_apps/test_fatfs_common/test_fatfs_common.h
  71. 106
      lib/fatfs/test_fatfs_host/Makefile
  72. 44
      lib/fatfs/test_fatfs_host/Makefile.files
  73. 17
      lib/fatfs/test_fatfs_host/component.mk
  74. 2
      lib/fatfs/test_fatfs_host/main.cpp
  75. 6
      lib/fatfs/test_fatfs_host/partition_table.csv
  76. 94
      lib/fatfs/test_fatfs_host/test_fatfs.cpp
  77. 535
      lib/fatfs/test_fatfsgen/test_fatfsgen.py
  78. 354
      lib/fatfs/test_fatfsgen/test_fatfsparse.py
  79. 67
      lib/fatfs/test_fatfsgen/test_utils.py
  80. 142
      lib/fatfs/test_fatfsgen/test_wl_fatfsgen.py
  81. 356
      lib/fatfs/vfs/esp_vfs_fat.h
  82. 1117
      lib/fatfs/vfs/vfs_fat.c
  83. 30
      lib/fatfs/vfs/vfs_fat_internal.h
  84. 498
      lib/fatfs/vfs/vfs_fat_sdmmc.c
  85. 335
      lib/fatfs/vfs/vfs_fat_spiflash.c
  86. 215
      lib/fatfs/wl_fatfsgen.py

@ -0,0 +1,7 @@
# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps
components/fatfs/test_apps/sdcard:
disable_test:
- if: IDF_TARGET in ["esp32s3", "esp32c2", "esp32c6", "esp32h2"]
temporary: true
reason: No sdspi runners for these targets

@ -0,0 +1,16 @@
set(srcs "diskio/diskio.c"
"diskio/diskio_rawflash.c"
"diskio/diskio_sdmmc.c"
"diskio/diskio_wl.c"
"src/ff.c"
"port/freertos/ffsystem.c"
"src/ffunicode.c"
"vfs/vfs_fat.c"
"vfs/vfs_fat_sdmmc.c"
"vfs/vfs_fat_spiflash.c")
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS diskio vfs src
REQUIRES wear_levelling sdmmc
PRIV_REQUIRES vfs
)

@ -0,0 +1,233 @@
menu "FAT Filesystem support"
config FATFS_VOLUME_COUNT
int "Number of volumes"
default 2
range 1 10
help
Number of volumes (logical drives) to use.
choice FATFS_LONG_FILENAMES
prompt "Long filename support"
default FATFS_LFN_NONE
help
Support long filenames in FAT. Long filename data increases
memory usage. FATFS can be configured to store the buffer for
long filename data in stack or heap.
config FATFS_LFN_NONE
bool "No long filenames"
config FATFS_LFN_HEAP
bool "Long filename buffer in heap"
config FATFS_LFN_STACK
bool "Long filename buffer on stack"
endchoice
choice FATFS_SECTOR_SIZE
prompt "Sector size"
default FATFS_SECTOR_4096
help
Specify the size of the sector in bytes for FATFS partition generator.
config FATFS_SECTOR_512
bool "512"
config FATFS_SECTOR_4096
bool "4096"
endchoice
choice FATFS_CHOOSE_CODEPAGE
prompt "OEM Code Page"
default FATFS_CODEPAGE_437
help
OEM code page used for file name encodings.
If "Dynamic" is selected, code page can be chosen at runtime using
f_setcp function. Note that choosing this option will increase
application size by ~480kB.
config FATFS_CODEPAGE_DYNAMIC
bool "Dynamic (all code pages supported)"
config FATFS_CODEPAGE_437
bool "US (CP437)"
config FATFS_CODEPAGE_720
bool "Arabic (CP720)"
config FATFS_CODEPAGE_737
bool "Greek (CP737)"
config FATFS_CODEPAGE_771
bool "KBL (CP771)"
config FATFS_CODEPAGE_775
bool "Baltic (CP775)"
config FATFS_CODEPAGE_850
bool "Latin 1 (CP850)"
config FATFS_CODEPAGE_852
bool "Latin 2 (CP852)"
config FATFS_CODEPAGE_855
bool "Cyrillic (CP855)"
config FATFS_CODEPAGE_857
bool "Turkish (CP857)"
config FATFS_CODEPAGE_860
bool "Portugese (CP860)"
config FATFS_CODEPAGE_861
bool "Icelandic (CP861)"
config FATFS_CODEPAGE_862
bool "Hebrew (CP862)"
config FATFS_CODEPAGE_863
bool "Canadian French (CP863)"
config FATFS_CODEPAGE_864
bool "Arabic (CP864)"
config FATFS_CODEPAGE_865
bool "Nordic (CP865)"
config FATFS_CODEPAGE_866
bool "Russian (CP866)"
config FATFS_CODEPAGE_869
bool "Greek 2 (CP869)"
config FATFS_CODEPAGE_932
bool "Japanese (DBCS) (CP932)"
config FATFS_CODEPAGE_936
bool "Simplified Chinese (DBCS) (CP936)"
config FATFS_CODEPAGE_949
bool "Korean (DBCS) (CP949)"
config FATFS_CODEPAGE_950
bool "Traditional Chinese (DBCS) (CP950)"
endchoice
config FATFS_CODEPAGE
int
default 0 if FATFS_CODEPAGE_DYNAMIC
default 437 if FATFS_CODEPAGE_437
default 720 if FATFS_CODEPAGE_720
default 737 if FATFS_CODEPAGE_737
default 771 if FATFS_CODEPAGE_771
default 775 if FATFS_CODEPAGE_775
default 850 if FATFS_CODEPAGE_850
default 852 if FATFS_CODEPAGE_852
default 855 if FATFS_CODEPAGE_855
default 857 if FATFS_CODEPAGE_857
default 860 if FATFS_CODEPAGE_860
default 861 if FATFS_CODEPAGE_861
default 862 if FATFS_CODEPAGE_862
default 863 if FATFS_CODEPAGE_863
default 864 if FATFS_CODEPAGE_864
default 865 if FATFS_CODEPAGE_865
default 866 if FATFS_CODEPAGE_866
default 869 if FATFS_CODEPAGE_869
default 932 if FATFS_CODEPAGE_932
default 936 if FATFS_CODEPAGE_936
default 949 if FATFS_CODEPAGE_949
default 950 if FATFS_CODEPAGE_950
default 437
config FATFS_MAX_LFN
int "Max long filename length"
depends on !FATFS_LFN_NONE
default 255
range 12 255
help
Maximum long filename length. Can be reduced to save RAM.
choice FATFS_API_ENCODING
prompt "API character encoding"
depends on !FATFS_LFN_NONE
default FATFS_API_ENCODING_ANSI_OEM
help
Choose encoding for character and string arguments/returns when using
FATFS APIs. The encoding of arguments will usually depend on text
editor settings.
config FATFS_API_ENCODING_ANSI_OEM
bool "API uses ANSI/OEM encoding"
config FATFS_API_ENCODING_UTF_8
bool "API uses UTF-8 encoding"
endchoice
config FATFS_FS_LOCK
int "Number of simultaneously open files protected by lock function"
default 0
range 0 65535
help
This option sets the FATFS configuration value _FS_LOCK.
The option _FS_LOCK switches file lock function to control duplicated file open
and illegal operation to open objects.
* 0: Disable file lock function. To avoid volume corruption, application
should avoid illegal open, remove and rename to the open objects.
* >0: Enable file lock function. The value defines how many files/sub-directories
can be opened simultaneously under file lock control.
Note that the file lock control is independent of re-entrancy.
config FATFS_TIMEOUT_MS
int "Timeout for acquiring a file lock, ms"
default 10000
help
This option sets FATFS configuration value _FS_TIMEOUT, scaled to milliseconds.
Sets the number of milliseconds FATFS will wait to acquire a mutex when
operating on an open file. For example, if one task is performing a lenghty
operation, another task will wait for the first task to release the lock,
and time out after amount of time set by this option.
config FATFS_PER_FILE_CACHE
bool "Use separate cache for each file"
default y
help
This option affects FATFS configuration value _FS_TINY.
If this option is set, _FS_TINY is 0, and each open file has its own cache,
size of the cache is equal to the _MAX_SS variable (512 or 4096 bytes).
This option uses more RAM if more than 1 file is open, but needs less reads
and writes to the storage for some operations.
If this option is not set, _FS_TINY is 1, and single cache is used for
all open files, size is also equal to _MAX_SS variable. This reduces the
amount of heap used when multiple files are open, but increases the number
of read and write operations which FATFS needs to make.
config FATFS_ALLOC_PREFER_EXTRAM
bool "Perfer external RAM when allocating FATFS buffers"
default y
depends on SPIRAM_USE_CAPS_ALLOC || SPIRAM_USE_MALLOC
help
When the option is enabled, internal buffers used by FATFS will be allocated
from external RAM. If the allocation from external RAM fails, the buffer will
be allocated from the internal RAM.
Disable this option if optimizing for performance. Enable this option if
optimizing for internal memory size.
config FATFS_USE_FASTSEEK
bool "Enable fast seek algorithm when using lseek function through VFS FAT"
default n
help
The fast seek feature enables fast backward/long seek operations without
FAT access by using an in-memory CLMT (cluster link map table).
Please note, fast-seek is only allowed for read-mode files, if a
file is opened in write-mode, the seek mechanism will automatically fallback
to the default implementation.
config FATFS_FAST_SEEK_BUFFER_SIZE
int "Fast seek CLMT buffer size"
default 64
depends on FATFS_USE_FASTSEEK
help
If fast seek algorithm is enabled, this defines the size of
CLMT buffer used by this algorithm in 32-bit word units.
This value should be chosen based on prior knowledge of
maximum elements of each file entry would store.
config FATFS_VFS_FSTAT_BLKSIZE
int "Default block size"
default 0
help
If set to 0, the 'newlib' library's default size (BLKSIZ) is used (128 B).
If set to a non-zero value, the value is used as the block size.
Default file buffer size is set to this value
and the buffer is allocated when first attempt of reading/writing to a file is made.
Increasing this value improves fread() speed, however the heap usage is increased as well.
NOTE: The block size value is shared by all the filesystem functions
accessing target media for given file descriptor!
See 'Improving I/O performance' section of 'Maximizing Execution Speed' documentation page
for more details.
endmenu

@ -0,0 +1,117 @@
/*-----------------------------------------------------------------------*/
/* Low level disk I/O module skeleton for FatFs (C)ChaN, 2016 */
/* ESP-IDF port Copyright 2016 Espressif Systems (Shanghai) PTE LTD */
/*-----------------------------------------------------------------------*/
/* If a working storage control module is available, it should be */
/* attached to the FatFs via a glue function rather than modifying it. */
/* This is an example of glue functions to attach various exsisting */
/* storage control modules to the FatFs module with a defined API. */
/*-----------------------------------------------------------------------*/
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <sys/time.h>
#include "diskio_impl.h"
#include "ffconf.h"
#include "ff.h"
static ff_diskio_impl_t * s_impls[FF_VOLUMES] = { NULL };
#if FF_MULTI_PARTITION /* Multiple partition configuration */
const PARTITION VolToPart[FF_VOLUMES] = {
{0, 0}, /* Logical drive 0 ==> Physical drive 0, auto detection */
{1, 0}, /* Logical drive 1 ==> Physical drive 1, auto detection */
#if FF_VOLUMES > 2
{2, 0}, /* Logical drive 2 ==> Physical drive 2, auto detection */
#endif
#if FF_VOLUMES > 3
{3, 0}, /* Logical drive 3 ==> Physical drive 3, auto detection */
#endif
#if FF_VOLUMES > 4
{4, 0}, /* Logical drive 4 ==> Physical drive 4, auto detection */
#endif
#if FF_VOLUMES > 5
{5, 0}, /* Logical drive 5 ==> Physical drive 5, auto detection */
#endif
#if FF_VOLUMES > 6
{6, 0}, /* Logical drive 6 ==> Physical drive 6, auto detection */
#endif
#if FF_VOLUMES > 7
{7, 0}, /* Logical drive 7 ==> Physical drive 7, auto detection */
#endif
#if FF_VOLUMES > 8
{8, 0}, /* Logical drive 8 ==> Physical drive 8, auto detection */
#endif
#if FF_VOLUMES > 9
{9, 0}, /* Logical drive 9 ==> Physical drive 9, auto detection */
#endif
};
#endif
esp_err_t ff_diskio_get_drive(BYTE* out_pdrv)
{
BYTE i;
for(i=0; i<FF_VOLUMES; i++) {
if (!s_impls[i]) {
*out_pdrv = i;
return ESP_OK;
}
}
return ESP_ERR_NOT_FOUND;
}
void ff_diskio_register(BYTE pdrv, const ff_diskio_impl_t* discio_impl)
{
assert(pdrv < FF_VOLUMES);
if (s_impls[pdrv]) {
ff_diskio_impl_t* im = s_impls[pdrv];
s_impls[pdrv] = NULL;
free(im);
}
if (!discio_impl) {
return;
}
ff_diskio_impl_t * impl = (ff_diskio_impl_t *)malloc(sizeof(ff_diskio_impl_t));
assert(impl != NULL);
memcpy(impl, discio_impl, sizeof(ff_diskio_impl_t));
s_impls[pdrv] = impl;
}
DSTATUS ff_disk_initialize (BYTE pdrv)
{
return s_impls[pdrv]->init(pdrv);
}
DSTATUS ff_disk_status (BYTE pdrv)
{
return s_impls[pdrv]->status(pdrv);
}
DRESULT ff_disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count)
{
return s_impls[pdrv]->read(pdrv, buff, sector, count);
}
DRESULT ff_disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count)
{
return s_impls[pdrv]->write(pdrv, buff, sector, count);
}
DRESULT ff_disk_ioctl (BYTE pdrv, BYTE cmd, void* buff)
{
return s_impls[pdrv]->ioctl(pdrv, cmd, buff);
}
DWORD get_fattime(void)
{
time_t t = time(NULL);
struct tm tmr;
localtime_r(&t, &tmr);
int year = tmr.tm_year < 80 ? 0 : tmr.tm_year - 80;
return ((DWORD)(year) << 25)
| ((DWORD)(tmr.tm_mon + 1) << 21)
| ((DWORD)tmr.tm_mday << 16)
| (WORD)(tmr.tm_hour << 11)
| (WORD)(tmr.tm_min << 5)
| (WORD)(tmr.tm_sec >> 1);
}

@ -0,0 +1,72 @@
// Copyright 2017-2019 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
typedef unsigned int UINT;
typedef unsigned char BYTE;
typedef uint32_t DWORD;
#define FF_DRV_NOT_USED 0xFF
#include "diskio.h"
#include "esp_err.h"
/**
* Structure of pointers to disk IO driver functions.
*
* See FatFs documentation for details about these functions
*/
typedef struct {
DSTATUS (*init) (unsigned char pdrv); /*!< disk initialization function */
DSTATUS (*status) (unsigned char pdrv); /*!< disk status check function */
DRESULT (*read) (unsigned char pdrv, unsigned char* buff, uint32_t sector, unsigned count); /*!< sector read function */
DRESULT (*write) (unsigned char pdrv, const unsigned char* buff, uint32_t sector, unsigned count); /*!< sector write function */
DRESULT (*ioctl) (unsigned char pdrv, unsigned char cmd, void* buff); /*!< function to get info about disk and do some misc operations */
} ff_diskio_impl_t;
/**
* Register or unregister diskio driver for given drive number.
*
* When FATFS library calls one of disk_xxx functions for driver number pdrv,
* corresponding function in discio_impl for given pdrv will be called.
*
* @param pdrv drive number
* @param discio_impl pointer to ff_diskio_impl_t structure with diskio functions
* or NULL to unregister and free previously registered drive
*/
void ff_diskio_register(BYTE pdrv, const ff_diskio_impl_t* discio_impl);
#define ff_diskio_unregister(pdrv_) ff_diskio_register(pdrv_, NULL)
/**
* Get next available drive number
*
* @param out_pdrv pointer to the byte to set if successful
*
* @return ESP_OK on success
* ESP_ERR_NOT_FOUND if all drives are attached
*/
esp_err_t ff_diskio_get_drive(BYTE* out_pdrv);
#ifdef __cplusplus
}
#endif

@ -0,0 +1,134 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "diskio_impl.h"
#include "ffconf.h"
#include "ff.h"
#include "esp_log.h"
#include "diskio_rawflash.h"
#include "esp_compiler.h"
static const char* TAG = "diskio_rawflash";
static const esp_partition_t* s_ff_raw_handles[FF_VOLUMES];
// Determine the sector size and sector count by parsing the boot sector
static size_t s_sector_size[FF_VOLUMES];
static size_t s_sectors_count[FF_VOLUMES];
#define BPB_BytsPerSec 11
#define BPB_TotSec16 19
#define BPB_TotSec32 32
DSTATUS ff_raw_initialize (BYTE pdrv)
{
uint16_t sector_size_tmp;
uint16_t sectors_count_tmp_16;
uint32_t sectors_count_tmp_32;
const esp_partition_t* part = s_ff_raw_handles[pdrv];
assert(part);
esp_err_t err = esp_partition_read(part, BPB_BytsPerSec, &sector_size_tmp, sizeof(sector_size_tmp));
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "esp_partition_read failed (0x%x)", err);
return RES_ERROR;
}
s_sector_size[pdrv] = sector_size_tmp;
err = esp_partition_read(part, BPB_TotSec16, &sectors_count_tmp_16, sizeof(sectors_count_tmp_16));
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "esp_partition_read failed (0x%x)", err);
return RES_ERROR;
}
s_sectors_count[pdrv] = sectors_count_tmp_16;
// For FAT32, the number of sectors is stored in a different field
if (sectors_count_tmp_16 == 0){
err = esp_partition_read(part, BPB_TotSec32, &sectors_count_tmp_32, sizeof(sectors_count_tmp_32));
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "esp_partition_read failed (0x%x)", err);
return RES_ERROR;
}
s_sectors_count[pdrv] = sectors_count_tmp_32;
}
return 0;
}
DSTATUS ff_raw_status (BYTE pdrv)
{
return 0;
}
DRESULT ff_raw_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
{
ESP_LOGV(TAG, "ff_raw_read - pdrv=%i, sector=%i, count=%in", (unsigned int)pdrv, (unsigned int)sector, (unsigned int)count);
const esp_partition_t* part = s_ff_raw_handles[pdrv];
assert(part);
esp_err_t err = esp_partition_read(part, sector * s_sector_size[pdrv], buff, count * s_sector_size[pdrv]);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "esp_partition_read failed (0x%x)", err);
return RES_ERROR;
}
return RES_OK;
}
DRESULT ff_raw_write (BYTE pdrv, const BYTE *buff, DWORD sector, UINT count)
{
return RES_ERROR;
}
DRESULT ff_raw_ioctl (BYTE pdrv, BYTE cmd, void *buff)
{
const esp_partition_t* part = s_ff_raw_handles[pdrv];
ESP_LOGV(TAG, "ff_raw_ioctl: cmd=%in", cmd);
assert(part);
switch (cmd) {
case CTRL_SYNC:
return RES_OK;
case GET_SECTOR_COUNT:
*((DWORD *) buff) = s_sectors_count[pdrv];
return RES_OK;
case GET_SECTOR_SIZE:
*((WORD *) buff) = s_sector_size[pdrv];
return RES_OK;
case GET_BLOCK_SIZE:
return RES_ERROR;
}
return RES_ERROR;
}
esp_err_t ff_diskio_register_raw_partition(BYTE pdrv, const esp_partition_t* part_handle)
{
if (pdrv >= FF_VOLUMES) {
return ESP_ERR_INVALID_ARG;
}
static const ff_diskio_impl_t raw_impl = {
.init = &ff_raw_initialize,
.status = &ff_raw_status,
.read = &ff_raw_read,
.write = &ff_raw_write,
.ioctl = &ff_raw_ioctl
};
ff_diskio_register(pdrv, &raw_impl);
s_ff_raw_handles[pdrv] = part_handle;
return ESP_OK;
}
BYTE ff_diskio_get_pdrv_raw(const esp_partition_t* part_handle)
{
for (int i = 0; i < FF_VOLUMES; i++) {
if (part_handle == s_ff_raw_handles[i]) {
return i;
}
}
return 0xff;
}

@ -0,0 +1,37 @@
// Copyright 2015-2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _DISKIO_RAWFLASH_DEFINED
#define _DISKIO_RAWFLASH_DEFINED
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_partition.h"
/**
* Register spi flash partition
*
* @param pdrv drive number
* @param part_handle pointer to raw flash partition.
*/
esp_err_t ff_diskio_register_raw_partition(unsigned char pdrv, const esp_partition_t* part_handle);
unsigned char ff_diskio_get_pdrv_raw(const esp_partition_t* part_handle);
#ifdef __cplusplus
}
#endif
#endif // _DISKIO_RAWFLASH_DEFINED

@ -0,0 +1,142 @@
/*
* SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "diskio_impl.h"
#include "ffconf.h"
#include "ff.h"
#include "sdmmc_cmd.h"
#include "esp_log.h"
#include "esp_compiler.h"
static sdmmc_card_t* s_cards[FF_VOLUMES] = { NULL };
static bool s_disk_status_check_en[FF_VOLUMES] = { };
static const char* TAG = "diskio_sdmmc";
//Check if SD/MMC card is present
static DSTATUS ff_sdmmc_card_available(BYTE pdrv)
{
sdmmc_card_t* card = s_cards[pdrv];
assert(card);
esp_err_t err = sdmmc_get_status(card);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "Check status failed (0x%x)", err);
return STA_NOINIT;
}
return 0;
}
/**
* ff_sdmmc_status() and ff_sdmmc_initialize() return STA_NOINIT when sdmmc_get_status()
* fails. This error value is checked throughout the FATFS code.
* Both functions return 0 on success.
*/
DSTATUS ff_sdmmc_initialize (BYTE pdrv)
{
return ff_sdmmc_card_available(pdrv);
}
DSTATUS ff_sdmmc_status(BYTE pdrv)
{
if (s_disk_status_check_en[pdrv]) {
return ff_sdmmc_card_available(pdrv);
}
return 0;
}
DRESULT ff_sdmmc_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count)
{
sdmmc_card_t* card = s_cards[pdrv];
assert(card);
esp_err_t err = sdmmc_read_sectors(card, buff, sector, count);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "sdmmc_read_blocks failed (%d)", err);
return RES_ERROR;
}
return RES_OK;
}
DRESULT ff_sdmmc_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count)
{
sdmmc_card_t* card = s_cards[pdrv];
assert(card);
esp_err_t err = sdmmc_write_sectors(card, buff, sector, count);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "sdmmc_write_blocks failed (%d)", err);
return RES_ERROR;
}
return RES_OK;
}
#if FF_USE_TRIM
DRESULT ff_sdmmc_trim (BYTE pdrv, DWORD start_sector, DWORD sector_count)
{
sdmmc_card_t* card = s_cards[pdrv];
assert(card);
sdmmc_erase_arg_t arg;
arg = sdmmc_can_discard(card) == ESP_OK ? SDMMC_DISCARD_ARG : SDMMC_ERASE_ARG;
esp_err_t err = sdmmc_erase_sectors(card, start_sector, sector_count, arg);
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "sdmmc_erase_sectors failed (%d)", err);
return RES_ERROR;
}
return RES_OK;
}
#endif //FF_USE_TRIM
DRESULT ff_sdmmc_ioctl (BYTE pdrv, BYTE cmd, void* buff)
{
sdmmc_card_t* card = s_cards[pdrv];
assert(card);
switch(cmd) {
case CTRL_SYNC:
return RES_OK;
case GET_SECTOR_COUNT:
*((DWORD*) buff) = card->csd.capacity;
return RES_OK;
case GET_SECTOR_SIZE:
*((WORD*) buff) = card->csd.sector_size;
return RES_OK;
case GET_BLOCK_SIZE:
return RES_ERROR;
#if FF_USE_TRIM
case CTRL_TRIM:
return ff_sdmmc_trim (pdrv, *((DWORD*)buff), //start_sector
(*((DWORD*)buff + 1) - *((DWORD*)buff) + 1)); //sector_count
#endif //FF_USE_TRIM
}
return RES_ERROR;
}
void ff_sdmmc_set_disk_status_check(BYTE pdrv, bool enable)
{
s_disk_status_check_en[pdrv] = enable;
}
void ff_diskio_register_sdmmc(BYTE pdrv, sdmmc_card_t* card)
{
static const ff_diskio_impl_t sdmmc_impl = {
.init = &ff_sdmmc_initialize,
.status = &ff_sdmmc_status,
.read = &ff_sdmmc_read,
.write = &ff_sdmmc_write,
.ioctl = &ff_sdmmc_ioctl
};
s_cards[pdrv] = card;
s_disk_status_check_en[pdrv] = false;
ff_diskio_register(pdrv, &sdmmc_impl);
}
BYTE ff_diskio_get_pdrv_card(const sdmmc_card_t* card)
{
for (int i = 0; i < FF_VOLUMES; i++) {
if (card == s_cards[i]) {
return i;
}
}
return 0xff;
}

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2017-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "sdmmc_cmd.h"
#include "driver/sdmmc_defs.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Enable/disable SD card status checking
*
* @param pdrv drive number
* @param enable mock ff_sdmmc_status function (return 0)
*/
void ff_sdmmc_set_disk_status_check(BYTE pdrv, bool enable);
/**
* Register SD/MMC diskio driver
*
* @param pdrv drive number
* @param card pointer to sdmmc_card_t structure describing a card; card should be initialized before calling f_mount.
*/
void ff_diskio_register_sdmmc(unsigned char pdrv, sdmmc_card_t* card);
/**
* @brief Get the driver number corresponding to a card
*
* @param card The card to get its driver
* @return Driver number to the card
*/
BYTE ff_diskio_get_pdrv_card(const sdmmc_card_t* card);
#ifdef __cplusplus
}
#endif

@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "diskio_impl.h"
#include "ffconf.h"
#include "ff.h"
#include "esp_log.h"
#include "diskio_wl.h"
#include "wear_levelling.h"
#include "esp_compiler.h"
static const char* TAG = "ff_diskio_spiflash";
wl_handle_t ff_wl_handles[FF_VOLUMES] = {
[0 ... FF_VOLUMES - 1] = WL_INVALID_HANDLE
};
DSTATUS ff_wl_initialize (BYTE pdrv)
{
return 0;
}
DSTATUS ff_wl_status (BYTE pdrv)
{
return 0;
}
DRESULT ff_wl_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
{
ESP_LOGV(TAG, "ff_wl_read - pdrv=%i, sector=%i, count=%i\n", (unsigned int)pdrv, (unsigned int)sector, (unsigned int)count);
wl_handle_t wl_handle = ff_wl_handles[pdrv];
assert(wl_handle + 1);
esp_err_t err = wl_read(wl_handle, sector * wl_sector_size(wl_handle), buff, count * wl_sector_size(wl_handle));
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "wl_read failed (%d)", err);
return RES_ERROR;
}
return RES_OK;
}
DRESULT ff_wl_write (BYTE pdrv, const BYTE *buff, DWORD sector, UINT count)
{
ESP_LOGV(TAG, "ff_wl_write - pdrv=%i, sector=%i, count=%i\n", (unsigned int)pdrv, (unsigned int)sector, (unsigned int)count);
wl_handle_t wl_handle = ff_wl_handles[pdrv];
assert(wl_handle + 1);
esp_err_t err = wl_erase_range(wl_handle, sector * wl_sector_size(wl_handle), count * wl_sector_size(wl_handle));
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "wl_erase_range failed (%d)", err);
return RES_ERROR;
}
err = wl_write(wl_handle, sector * wl_sector_size(wl_handle), buff, count * wl_sector_size(wl_handle));
if (unlikely(err != ESP_OK)) {
ESP_LOGE(TAG, "wl_write failed (%d)", err);
return RES_ERROR;
}
return RES_OK;
}
DRESULT ff_wl_ioctl (BYTE pdrv, BYTE cmd, void *buff)
{
wl_handle_t wl_handle = ff_wl_handles[pdrv];
ESP_LOGV(TAG, "ff_wl_ioctl: cmd=%i\n", cmd);
assert(wl_handle + 1);
switch (cmd) {
case CTRL_SYNC:
return RES_OK;
case GET_SECTOR_COUNT:
*((DWORD *) buff) = wl_size(wl_handle) / wl_sector_size(wl_handle);
return RES_OK;
case GET_SECTOR_SIZE:
*((WORD *) buff) = wl_sector_size(wl_handle);
return RES_OK;
case GET_BLOCK_SIZE:
return RES_ERROR;
}
return RES_ERROR;
}
esp_err_t ff_diskio_register_wl_partition(BYTE pdrv, wl_handle_t flash_handle)
{
if (pdrv >= FF_VOLUMES) {
return ESP_ERR_INVALID_ARG;
}
static const ff_diskio_impl_t wl_impl = {
.init = &ff_wl_initialize,
.status = &ff_wl_status,
.read = &ff_wl_read,
.write = &ff_wl_write,
.ioctl = &ff_wl_ioctl
};
ff_wl_handles[pdrv] = flash_handle;
ff_diskio_register(pdrv, &wl_impl);
return ESP_OK;
}
BYTE ff_diskio_get_pdrv_wl(wl_handle_t flash_handle)
{
for (int i = 0; i < FF_VOLUMES; i++) {
if (flash_handle == ff_wl_handles[i]) {
return i;
}
}
return 0xff;
}
void ff_diskio_clear_pdrv_wl(wl_handle_t flash_handle)
{
for (int i = 0; i < FF_VOLUMES; i++) {
if (flash_handle == ff_wl_handles[i]) {
ff_wl_handles[i] = WL_INVALID_HANDLE;
}
}
}

@ -0,0 +1,39 @@
// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _DISKIO_WL_DEFINED
#define _DISKIO_WL_DEFINED
#ifdef __cplusplus
extern "C" {
#endif
#include "wear_levelling.h"
/**
* Register spi flash partition
*
* @param pdrv drive number
* @param flash_handle handle of the wear levelling partition.
*/
esp_err_t ff_diskio_register_wl_partition(unsigned char pdrv, wl_handle_t flash_handle);
unsigned char ff_diskio_get_pdrv_wl(wl_handle_t flash_handle);
void ff_diskio_clear_pdrv_wl(wl_handle_t flash_handle);
#ifdef __cplusplus
}
#endif
#endif // _DISKIO_WL_DEFINED

@ -0,0 +1,168 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from inspect import getmembers, isroutine
from typing import Optional
from construct import Const, Int8ul, Int16ul, Int32ul, PaddedString, Struct, core
from .exceptions import InconsistentFATAttributes, NotInitialized
from .fatfs_state import BootSectorState
from .utils import (ALLOWED_SECTOR_SIZES, ALLOWED_SECTORS_PER_CLUSTER, EMPTY_BYTE, FAT32, FULL_BYTE,
SHORT_NAMES_ENCODING, FATDefaults, generate_4bytes_random, pad_string)
class BootSector:
"""
This class describes the first sector of the volume in the Reserved Region.
It contains data from BPB (BIOS Parameter Block) and BS (Boot sector). The fields of the BPB and BS are mixed in
the header of the physical boot sector. Fields with prefix BPB belongs to BPB block and with prefix BS
belongs to the actual boot sector.
Please beware, that the name of class BootSector refer to data both from the boot sector and BPB.
ESP32 ignores fields with prefix "BS_"! Fields with prefix BPB_ are essential to read the filesystem.
"""
MAX_VOL_LAB_SIZE = 11
MAX_OEM_NAME_SIZE = 8
MAX_FS_TYPE_SIZE = 8
# the FAT specification defines 512 bytes for the boot sector header
BOOT_HEADER_SIZE = 512
BOOT_SECTOR_HEADER = Struct(
# this value reflects BS_jmpBoot used for ESP32 boot sector (any other accepted)
'BS_jmpBoot' / Const(b'\xeb\xfe\x90'),
'BS_OEMName' / PaddedString(MAX_OEM_NAME_SIZE, SHORT_NAMES_ENCODING),
'BPB_BytsPerSec' / Int16ul,
'BPB_SecPerClus' / Int8ul,
'BPB_RsvdSecCnt' / Int16ul,
'BPB_NumFATs' / Int8ul,
'BPB_RootEntCnt' / Int16ul,
'BPB_TotSec16' / Int16ul, # zero if the FAT type is 32, otherwise number of sectors
'BPB_Media' / Int8ul,
'BPB_FATSz16' / Int16ul, # for FAT32 always zero, for FAT12/FAT16 number of sectors per FAT
'BPB_SecPerTrk' / Int16ul,
'BPB_NumHeads' / Int16ul,
'BPB_HiddSec' / Int32ul,
'BPB_TotSec32' / Int32ul, # zero if the FAT type is 12/16, otherwise number of sectors
'BS_DrvNum' / Const(b'\x80'),
'BS_Reserved1' / Const(EMPTY_BYTE),
'BS_BootSig' / Const(b'\x29'),
'BS_VolID' / Int32ul,
'BS_VolLab' / PaddedString(MAX_VOL_LAB_SIZE, SHORT_NAMES_ENCODING),
'BS_FilSysType' / PaddedString(MAX_FS_TYPE_SIZE, SHORT_NAMES_ENCODING),
'BS_EMPTY' / Const(448 * EMPTY_BYTE),
'Signature_word' / Const(FATDefaults.SIGNATURE_WORD)
)
assert BOOT_SECTOR_HEADER.sizeof() == BOOT_HEADER_SIZE
def __init__(self, boot_sector_state: Optional[BootSectorState] = None) -> None:
self._parsed_header: dict = {}
self.boot_sector_state: BootSectorState = boot_sector_state
def generate_boot_sector(self) -> None:
boot_sector_state: BootSectorState = self.boot_sector_state
if boot_sector_state is None:
raise NotInitialized('The BootSectorState instance is not initialized!')
volume_uuid = generate_4bytes_random()
pad_header: bytes = (boot_sector_state.sector_size - BootSector.BOOT_HEADER_SIZE) * EMPTY_BYTE
data_content: bytes = boot_sector_state.data_sectors * boot_sector_state.sector_size * FULL_BYTE
root_dir_content: bytes = boot_sector_state.root_dir_sectors_cnt * boot_sector_state.sector_size * EMPTY_BYTE
fat_tables_content: bytes = (boot_sector_state.sectors_per_fat_cnt
* boot_sector_state.fat_tables_cnt
* boot_sector_state.sector_size
* EMPTY_BYTE)
self.boot_sector_state.binary_image = (
BootSector.BOOT_SECTOR_HEADER.build(
dict(BS_OEMName=pad_string(boot_sector_state.oem_name, size=BootSector.MAX_OEM_NAME_SIZE),
BPB_BytsPerSec=boot_sector_state.sector_size,
BPB_SecPerClus=boot_sector_state.sectors_per_cluster,
BPB_RsvdSecCnt=boot_sector_state.reserved_sectors_cnt,
BPB_NumFATs=boot_sector_state.fat_tables_cnt,
BPB_RootEntCnt=boot_sector_state.entries_root_count,
# if fat type is 12 or 16 BPB_TotSec16 is filled and BPB_TotSec32 is 0x00 and vice versa
BPB_TotSec16=0x00 if boot_sector_state.fatfs_type == FAT32 else boot_sector_state.sectors_count,
BPB_Media=boot_sector_state.media_type,
BPB_FATSz16=boot_sector_state.sectors_per_fat_cnt,
BPB_SecPerTrk=boot_sector_state.sec_per_track,
BPB_NumHeads=boot_sector_state.num_heads,
BPB_HiddSec=boot_sector_state.hidden_sectors,
BPB_TotSec32=boot_sector_state.sectors_count if boot_sector_state.fatfs_type == FAT32 else 0x00,
BS_VolID=volume_uuid,
BS_VolLab=pad_string(boot_sector_state.volume_label,
size=BootSector.MAX_VOL_LAB_SIZE),
BS_FilSysType=pad_string(boot_sector_state.file_sys_type,
size=BootSector.MAX_FS_TYPE_SIZE)
)
) + pad_header + fat_tables_content + root_dir_content + data_content
)
def parse_boot_sector(self, binary_data: bytes) -> None:
"""
Checks the validity of the boot sector and derives the metadata from boot sector to the structured shape.
"""
try:
self._parsed_header = BootSector.BOOT_SECTOR_HEADER.parse(binary_data)
except core.StreamError:
raise NotInitialized('The boot sector header is not parsed successfully!')
if self._parsed_header['BPB_TotSec16'] != 0x00:
sectors_count_: int = self._parsed_header['BPB_TotSec16']
elif self._parsed_header['BPB_TotSec32'] != 0x00:
# uncomment for FAT32 implementation
# sectors_count_ = self._parsed_header['BPB_TotSec32']
# possible_fat_types = [FAT32]
assert self._parsed_header['BPB_TotSec16'] == 0
raise NotImplementedError('FAT32 not implemented!')
else:
raise InconsistentFATAttributes('The number of FS sectors cannot be zero!')
if self._parsed_header['BPB_BytsPerSec'] not in ALLOWED_SECTOR_SIZES:
raise InconsistentFATAttributes(f'The number of bytes '
f"per sector is {self._parsed_header['BPB_BytsPerSec']}! "
f'The accepted values are {ALLOWED_SECTOR_SIZES}')
if self._parsed_header['BPB_SecPerClus'] not in ALLOWED_SECTORS_PER_CLUSTER:
raise InconsistentFATAttributes(f'The number of sectors per cluster '
f"is {self._parsed_header['BPB_SecPerClus']}"
f'The accepted values are {ALLOWED_SECTORS_PER_CLUSTER}')
total_root_bytes: int = self._parsed_header['BPB_RootEntCnt'] * FATDefaults.ENTRY_SIZE
root_dir_sectors_cnt_: int = total_root_bytes // self._parsed_header['BPB_BytsPerSec']
self.boot_sector_state = BootSectorState(oem_name=self._parsed_header['BS_OEMName'],
sector_size=self._parsed_header['BPB_BytsPerSec'],
sectors_per_cluster=self._parsed_header['BPB_SecPerClus'],
reserved_sectors_cnt=self._parsed_header['BPB_RsvdSecCnt'],
fat_tables_cnt=self._parsed_header['BPB_NumFATs'],
root_dir_sectors_cnt=root_dir_sectors_cnt_,
sectors_count=sectors_count_,
media_type=self._parsed_header['BPB_Media'],
sec_per_track=self._parsed_header['BPB_SecPerTrk'],
num_heads=self._parsed_header['BPB_NumHeads'],
hidden_sectors=self._parsed_header['BPB_HiddSec'],
volume_label=self._parsed_header['BS_VolLab'],
file_sys_type=self._parsed_header['BS_FilSysType'],
volume_uuid=self._parsed_header['BS_VolID'])
self.boot_sector_state.binary_image = binary_data
assert self.boot_sector_state.file_sys_type in (f'FAT{self.boot_sector_state.fatfs_type} ', 'FAT ')
def __str__(self) -> str:
"""
FATFS properties parser (internal helper tool for fatfsgen.py/fatfsparse.py)
Provides all the properties of given FATFS instance by parsing its boot sector (returns formatted string)
"""
if self._parsed_header == {}:
return 'Boot sector is not initialized!'
res: str = 'FATFS properties:\n'
for member in getmembers(self.boot_sector_state, lambda a: not (isroutine(a))):
prop_ = getattr(self.boot_sector_state, member[0])
if isinstance(prop_, int) or isinstance(prop_, str) and not member[0].startswith('_'):
res += f'{member[0]}: {prop_}\n'
return res
@property
def binary_image(self) -> bytes:
# when BootSector is not instantiated, self.boot_sector_state might be None
if self.boot_sector_state is None or len(self.boot_sector_state.binary_image) == 0:
raise NotInitialized('Boot sector is not initialized!')
bin_image_: bytes = self.boot_sector_state.binary_image
return bin_image_

@ -0,0 +1,213 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from typing import Dict, Optional
from construct import Int16ul
from .fatfs_state import BootSectorState
from .utils import (EMPTY_BYTE, FAT12, FAT16, build_byte, merge_by_half_byte_12_bit_little_endian,
split_by_half_byte_12_bit_little_endian)
def get_dir_size(is_root: bool, boot_sector: BootSectorState) -> int:
dir_size_: int = boot_sector.root_dir_sectors_cnt * boot_sector.sector_size if is_root else boot_sector.sector_size
return dir_size_
class Cluster:
"""
class Cluster handles values in FAT table and allocates sectors in data region.
"""
RESERVED_BLOCK_ID: int = 0
ROOT_BLOCK_ID: int = 1
ALLOCATED_BLOCK_FAT12: int = 0xFFF
ALLOCATED_BLOCK_FAT16: int = 0xFFFF
ALLOCATED_BLOCK_SWITCH = {FAT12: ALLOCATED_BLOCK_FAT12, FAT16: ALLOCATED_BLOCK_FAT16}
INITIAL_BLOCK_SWITCH: Dict[int, int] = {FAT12: 0xFF8, FAT16: 0xFFF8}
def __init__(self,
cluster_id: int,
boot_sector_state: BootSectorState,
init_: bool) -> None:
"""
Initially, if init_ is False, the cluster is virtual and is not allocated (doesn't do changes in the FAT).
:param cluster_id: the cluster ID - a key value linking the file's cluster,
the corresponding physical cluster (data region) and the FAT table cluster.
:param boot_sector_state: auxiliary structure holding the file-system's metadata
:param init_: True for allocation the cluster on instantiation, otherwise False.
:returns: None
"""
self.id: int = cluster_id
self.boot_sector_state: BootSectorState = boot_sector_state
self._next_cluster = None # type: Optional[Cluster]
# First cluster in FAT is reserved, low 8 bits contains BPB_Media and the rest is filled with 1
# e.g. the esp32 media type is 0xF8 thus the FAT[0] = 0xFF8 for FAT12, 0xFFF8 for FAT16
if self.id == Cluster.RESERVED_BLOCK_ID and init_:
self.set_in_fat(self.INITIAL_BLOCK_SWITCH[self.boot_sector_state.fatfs_type])
return
self.cluster_data_address: int = self._compute_cluster_data_address()
assert self.cluster_data_address
@property
def next_cluster(self): # type: () -> Optional[Cluster]
return self._next_cluster
@next_cluster.setter
def next_cluster(self, value): # type: (Optional[Cluster]) -> None
self._next_cluster = value
def _cluster_id_to_fat_position_in_bits(self, _id: int) -> int:
"""
This private method calculates the position of the memory block (cluster) in the FAT table.
:param _id: the cluster ID - a key value linking the file's cluster,
the corresponding physical cluster (data region) and the FAT table cluster.
:returns: bit offset of the cluster in FAT
e.g.:
00003000: 42 65 00 2E 00 74 00 78 00 74 00 0F 00 43 FF FF
For FAT12 the third cluster has value = 0x02E and ID = 2.
Its bit-address is 24 (24 bits preceding, 0-indexed), because 0x2E starts at the bit-offset 24.
"""
logical_position_: int = self.boot_sector_state.fatfs_type * _id
return logical_position_
@staticmethod
def compute_cluster_data_address(boot_sector_state: BootSectorState, id_: int) -> int:
"""
This method translates the id of the cluster to the address in data region.
:param boot_sector_state: the class with FS shared data
:param id_: id of the cluster
:returns: integer denoting the address of the cluster in the data region
"""
data_address_: int = boot_sector_state.root_directory_start
if not id_ == Cluster.ROOT_BLOCK_ID:
# the first data cluster id is 2 (we have to subtract reserved cluster and cluster for root)
data_address_ = boot_sector_state.sector_size * (id_ - 2) + boot_sector_state.data_region_start
return data_address_
def _compute_cluster_data_address(self) -> int:
return self.compute_cluster_data_address(self.boot_sector_state, self.id)
@property
def fat_cluster_address(self) -> int:
"""Determines how many bits precede the first bit of the cluster in FAT"""
return self._cluster_id_to_fat_position_in_bits(self.id)
@property
def real_cluster_address(self) -> int:
"""
The property method computes the real address of the cluster in the FAT region. Result is simply
address of the cluster in fat + fat table address.
"""
cluster_address: int = self.boot_sector_state.fat_table_start_address + self.fat_cluster_address // 8
return cluster_address
def get_from_fat(self) -> int:
"""
Calculating the value in the FAT block, that denotes if the block is full, empty, or chained to other block.
For FAT12 is the block stored in one and half byte. If the order of the block is even the first byte and second
half of the second byte belongs to the block. First half of the second byte and the third byte belongs to
the second block.
e.g. b'\xff\x0f\x00' stores two blocks. First of them is evenly ordered (index 0) and is set to 0xfff,
that means full block that is final in chain of blocks
and second block is set to 0x000 that means empty block.
three bytes - AB XC YZ - stores two blocks - CAB YZX
"""
address_: int = self.real_cluster_address
bin_img_: bytearray = self.boot_sector_state.binary_image
if self.boot_sector_state.fatfs_type == FAT12:
if self.fat_cluster_address % 8 == 0:
# even block
return bin_img_[self.real_cluster_address] | ((bin_img_[self.real_cluster_address + 1] & 0x0F) << 8)
# odd block
return ((bin_img_[self.real_cluster_address] & 0xF0) >> 4) | (bin_img_[self.real_cluster_address + 1] << 4)
if self.boot_sector_state.fatfs_type == FAT16:
return int.from_bytes(bin_img_[address_:address_ + 2], byteorder='little')
raise NotImplementedError('Only valid fatfs types are FAT12 and FAT16.')
@property
def is_empty(self) -> bool:
"""
The property method takes a look into the binary array and checks if the bytes ordered by little endian
and relates to the current cluster are all zeros (which denotes they are empty).
"""
return self.get_from_fat() == 0x00
def set_in_fat(self, value: int) -> None:
"""
Sets cluster in FAT to certain value.
Firstly, we split the target value into 3 half bytes (max value is 0xfff).
Then we could encounter two situations:
1. if the cluster index (indexed from zero) is even, we set the full byte computed by
self.cluster_id_to_logical_position_in_bits and the second half of the consequent byte.
Order of half bytes is 2, 1, 3.
2. if the cluster index is odd, we set the first half of the computed byte and the full consequent byte.
Order of half bytes is 1, 3, 2.
"""
def _set_msb_half_byte(address: int, value_: int) -> None:
"""
Sets 4 most significant bits (msb half-byte) of 'boot_sector_state.binary_image' at given
'address' to 'value_' (size of variable 'value_' is half byte)
If a byte contents is 0b11110000, the msb half-byte would be 0b1111
"""
self.boot_sector_state.binary_image[address] &= 0x0f
self.boot_sector_state.binary_image[address] |= value_ << 4
def _set_lsb_half_byte(address: int, value_: int) -> None:
"""
Sets 4 least significant bits (lsb half-byte) of 'boot_sector_state.binary_image' at given
'address' to 'value_' (size of variable 'value_' is half byte)
If a byte contents is 0b11110000, the lsb half-byte would be 0b0000
"""
self.boot_sector_state.binary_image[address] &= 0xf0
self.boot_sector_state.binary_image[address] |= value_
# value must fit into number of bits of the fat (12, 16 or 32)
assert value <= (1 << self.boot_sector_state.fatfs_type) - 1
half_bytes = split_by_half_byte_12_bit_little_endian(value)
bin_img_: bytearray = self.boot_sector_state.binary_image
if self.boot_sector_state.fatfs_type == FAT12:
assert merge_by_half_byte_12_bit_little_endian(*half_bytes) == value
if self.fat_cluster_address % 8 == 0:
# even block
bin_img_[self.real_cluster_address] = build_byte(half_bytes[1], half_bytes[0])
_set_lsb_half_byte(self.real_cluster_address + 1, half_bytes[2])
elif self.fat_cluster_address % 8 != 0:
# odd block
_set_msb_half_byte(self.real_cluster_address, half_bytes[0])
bin_img_[self.real_cluster_address + 1] = build_byte(half_bytes[2], half_bytes[1])
elif self.boot_sector_state.fatfs_type == FAT16:
bin_img_[self.real_cluster_address:self.real_cluster_address + 2] = Int16ul.build(value)
assert self.get_from_fat() == value
@property
def is_root(self) -> bool:
"""
The FAT12/FAT16 contains only one root directory,
the root directory allocates the first cluster with the ID `ROOT_BLOCK_ID`.
The method checks if the cluster belongs to the root directory.
"""
return self.id == Cluster.ROOT_BLOCK_ID
def allocate_cluster(self) -> None:
"""
This method sets bits in FAT table to `allocated` and clean the corresponding sector(s)
"""
self.set_in_fat(self.ALLOCATED_BLOCK_SWITCH[self.boot_sector_state.fatfs_type])
cluster_start = self.cluster_data_address
dir_size = get_dir_size(self.is_root, self.boot_sector_state)
cluster_end = cluster_start + dir_size
self.boot_sector_state.binary_image[cluster_start:cluster_end] = dir_size * EMPTY_BYTE

@ -0,0 +1,253 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from typing import List, Optional, Union
from construct import Const, Int8ul, Int16ul, Int32ul, PaddedString, Struct
from .exceptions import LowerCaseException, TooLongNameException
from .fatfs_state import FATFSState
from .utils import (DATETIME, EMPTY_BYTE, FATFS_INCEPTION, MAX_EXT_SIZE, MAX_NAME_SIZE, SHORT_NAMES_ENCODING,
FATDefaults, build_date_entry, build_time_entry, is_valid_fatfs_name, pad_string)
class Entry:
"""
The class Entry represents entry of the directory.
"""
ATTR_READ_ONLY: int = 0x01
ATTR_HIDDEN: int = 0x02
ATTR_SYSTEM: int = 0x04
ATTR_VOLUME_ID: int = 0x08
ATTR_DIRECTORY: int = 0x10 # directory
ATTR_ARCHIVE: int = 0x20 # file
ATTR_LONG_NAME: int = ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID
# indexes in the entry structure and sizes in bytes, not in characters (encoded using 2 bytes for lfn)
LDIR_Name1_IDX: int = 1
LDIR_Name1_SIZE: int = 5
LDIR_Name2_IDX: int = 14
LDIR_Name2_SIZE: int = 6
LDIR_Name3_IDX: int = 28
LDIR_Name3_SIZE: int = 2
# short entry in long file names
LDIR_DIR_NTRES: int = 0x18
# one entry can hold 13 characters with size 2 bytes distributed in three regions of the 32 bytes entry
CHARS_PER_ENTRY: int = LDIR_Name1_SIZE + LDIR_Name2_SIZE + LDIR_Name3_SIZE
# the last 16 bytes record in the LFN entry has first byte masked with the following value
LAST_RECORD_LFN_ENTRY: int = 0x40
SHORT_ENTRY: int = -1
# this value is used for short-like entry but with accepted lower case
SHORT_ENTRY_LN: int = 0
# The 1st January 1980 00:00:00
DEFAULT_DATE: DATETIME = (FATFS_INCEPTION.year, FATFS_INCEPTION.month, FATFS_INCEPTION.day)
DEFAULT_TIME: DATETIME = (FATFS_INCEPTION.hour, FATFS_INCEPTION.minute, FATFS_INCEPTION.second)
ENTRY_FORMAT_SHORT_NAME = Struct(
'DIR_Name' / PaddedString(MAX_NAME_SIZE, SHORT_NAMES_ENCODING),
'DIR_Name_ext' / PaddedString(MAX_EXT_SIZE, SHORT_NAMES_ENCODING),
'DIR_Attr' / Int8ul,
'DIR_NTRes' / Int8ul, # this tagged for lfn (0x00 for lfn prefix, 0x18 for short name in lfn)
'DIR_CrtTimeTenth' / Const(EMPTY_BYTE), # ignored by esp-idf fatfs library
'DIR_CrtTime' / Int16ul, # ignored by esp-idf fatfs library
'DIR_CrtDate' / Int16ul, # ignored by esp-idf fatfs library
'DIR_LstAccDate' / Int16ul, # must be same as DIR_WrtDate
'DIR_FstClusHI' / Const(2 * EMPTY_BYTE),
'DIR_WrtTime' / Int16ul,
'DIR_WrtDate' / Int16ul,
'DIR_FstClusLO' / Int16ul,
'DIR_FileSize' / Int32ul,
)
def __init__(self,
entry_id: int,
parent_dir_entries_address: int,
fatfs_state: FATFSState) -> None:
self.fatfs_state: FATFSState = fatfs_state
self.id: int = entry_id
self.entry_address: int = parent_dir_entries_address + self.id * FATDefaults.ENTRY_SIZE
self._is_alias: bool = False
self._is_empty: bool = True
@staticmethod
def get_cluster_id(obj_: dict) -> int:
cluster_id_: int = obj_['DIR_FstClusLO']
return cluster_id_
@property
def is_empty(self) -> bool:
return self._is_empty
@staticmethod
def _parse_entry(entry_bytearray: Union[bytearray, bytes]) -> dict:
entry_: dict = Entry.ENTRY_FORMAT_SHORT_NAME.parse(entry_bytearray)
return entry_
@staticmethod
def _build_entry(**kwargs) -> bytes: # type: ignore
entry_: bytes = Entry.ENTRY_FORMAT_SHORT_NAME.build(dict(**kwargs))
return entry_
@staticmethod
def _build_entry_long(names: List[bytes], checksum: int, order: int, is_last: bool) -> bytes:
"""
Long entry starts with 1 bytes of the order, if the entry is the last in the chain it is or-masked with 0x40,
otherwise is without change (or masked with 0x00). The following example shows 3 entries:
first two (0x2000-0x2040) are long in the reverse order and the last one (0x2040-0x2060) is short.
The entries define file name "thisisverylongfilenama.txt".
00002000: 42 67 00 66 00 69 00 6C 00 65 00 0F 00 43 6E 00 Bg.f.i.l.e...Cn.
00002010: 61 00 6D 00 61 00 2E 00 74 00 00 00 78 00 74 00 a.m.a...t...x.t.
00002020: 01 74 00 68 00 69 00 73 00 69 00 0F 00 43 73 00 .t.h.i.s.i...Cs.
00002030: 76 00 65 00 72 00 79 00 6C 00 00 00 6F 00 6E 00 v.e.r.y.l...o.n.
00002040: 54 48 49 53 49 53 7E 31 54 58 54 20 00 00 00 00 THISIS~1TXT.....
00002050: 21 00 00 00 00 00 00 00 21 00 02 00 15 00 00 00 !.......!.......
"""
order |= (Entry.LAST_RECORD_LFN_ENTRY if is_last else 0x00)
long_entry: bytes = (Int8ul.build(order) + # order of the long name entry (possibly masked with 0x40)
names[0] + # first 5 characters (10 bytes) of the name part
Int8ul.build(Entry.ATTR_LONG_NAME) + # one byte entity type ATTR_LONG_NAME
Int8ul.build(0) + # one byte of zeros
Int8ul.build(checksum) + # lfn_checksum defined in utils.py
names[1] + # next 6 characters (12 bytes) of the name part
Int16ul.build(0) + # 2 bytes of zeros
names[2]) # last 2 characters (4 bytes) of the name part
return long_entry
@staticmethod
def parse_entry_long(entry_bytes_: bytes, my_check: int) -> dict:
order_ = Int8ul.parse(entry_bytes_[0:1])
names0 = entry_bytes_[1:11]
if Int8ul.parse(entry_bytes_[12:13]) != 0 or Int16ul.parse(entry_bytes_[26:28]) != 0 or Int8ul.parse(entry_bytes_[11:12]) != 15:
return {}
if Int8ul.parse(entry_bytes_[13:14]) != my_check:
return {}
names1 = entry_bytes_[14:26]
names2 = entry_bytes_[28:32]
return {
'order': order_,
'name1': names0,
'name2': names1,
'name3': names2,
'is_last': bool(order_ & Entry.LAST_RECORD_LFN_ENTRY == Entry.LAST_RECORD_LFN_ENTRY)
}
@property
def entry_bytes(self) -> bytes:
"""
:returns: Bytes defining the entry belonging to the given instance.
"""
start_: int = self.entry_address
entry_: bytes = self.fatfs_state.binary_image[start_: start_ + FATDefaults.ENTRY_SIZE]
return entry_
@entry_bytes.setter
def entry_bytes(self, value: bytes) -> None:
"""
:param value: new content of the entry
:returns: None
The setter sets the content of the entry in bytes.
"""
self.fatfs_state.binary_image[self.entry_address: self.entry_address + FATDefaults.ENTRY_SIZE] = value
def _clean_entry(self) -> None:
self.entry_bytes: bytes = FATDefaults.ENTRY_SIZE * EMPTY_BYTE
def allocate_entry(self,
first_cluster_id: int,
entity_name: str,
entity_type: int,
entity_extension: str = '',
size: int = 0,
date: DATETIME = DEFAULT_DATE,
time: DATETIME = DEFAULT_TIME,
lfn_order: int = SHORT_ENTRY,
lfn_names: Optional[List[bytes]] = None,
lfn_checksum_: int = 0,
fits_short: bool = False,
lfn_is_last: bool = False) -> None:
"""
:param first_cluster_id: id of the first data cluster for given entry
:param entity_name: name recorded in the entry
:param entity_extension: extension recorded in the entry
:param size: size of the content of the file
:param date: denotes year (actual year minus 1980), month number day of the month (minimal valid is (0, 1, 1))
:param time: denotes hour, minute and second with granularity 2 seconds (sec // 2)
:param entity_type: type of the entity (file [0x20] or directory [0x10])
:param lfn_order: if long names support is enabled, defines order in long names entries sequence (-1 for short)
:param lfn_names: if the entry is dedicated for long names the lfn_names contains
LDIR_Name1, LDIR_Name2 and LDIR_Name3 in this order
:param lfn_checksum_: use only for long file names, checksum calculated lfn_checksum function
:param fits_short: determines if the name fits in 8.3 filename
:param lfn_is_last: determines if the long file name entry is holds last part of the name,
thus its address is first in the physical order
:returns: None
:raises LowerCaseException: In case when long_names_enabled is set to False and filename exceeds 8 chars
for name or 3 chars for extension the exception is raised
:raises TooLongNameException: When long_names_enabled is set to False and name doesn't fit to 8.3 filename
an exception is raised
"""
valid_full_name: bool = is_valid_fatfs_name(entity_name) and is_valid_fatfs_name(entity_extension)
if not (valid_full_name or lfn_order >= 0):
raise LowerCaseException('Lower case is not supported in short name entry, use upper case.')
if self.fatfs_state.use_default_datetime:
date = self.DEFAULT_DATE
time = self.DEFAULT_TIME
# clean entry before allocation
self._clean_entry()
self._is_empty = False
object_name = entity_name.upper() if not self.fatfs_state.long_names_enabled else entity_name
object_extension = entity_extension.upper() if not self.fatfs_state.long_names_enabled else entity_extension
exceeds_short_name: bool = len(object_name) > MAX_NAME_SIZE or len(object_extension) > MAX_EXT_SIZE
if not self.fatfs_state.long_names_enabled and exceeds_short_name:
raise TooLongNameException(
'Maximal length of the object name is {} characters and {} characters for extension!'.format(
MAX_NAME_SIZE, MAX_EXT_SIZE
)
)
start_address = self.entry_address
end_address = start_address + FATDefaults.ENTRY_SIZE
if lfn_order in (self.SHORT_ENTRY, self.SHORT_ENTRY_LN):
date_entry_: int = build_date_entry(*date)
time_entry: int = build_time_entry(*time)
self.fatfs_state.binary_image[start_address: end_address] = self._build_entry(
DIR_Name=pad_string(object_name, size=MAX_NAME_SIZE),
DIR_Name_ext=pad_string(object_extension, size=MAX_EXT_SIZE),
DIR_Attr=entity_type,
DIR_NTRes=0x00 if (not self.fatfs_state.long_names_enabled) or (not fits_short) else 0x18,
DIR_FstClusLO=first_cluster_id,
DIR_FileSize=size,
DIR_CrtDate=date_entry_, # ignored by esp-idf fatfs library
DIR_LstAccDate=date_entry_, # must be same as DIR_WrtDate
DIR_WrtDate=date_entry_,
DIR_CrtTime=time_entry, # ignored by esp-idf fatfs library
DIR_WrtTime=time_entry
)
else:
assert lfn_names is not None
self.fatfs_state.binary_image[start_address: end_address] = self._build_entry_long(lfn_names,
lfn_checksum_,
lfn_order,
lfn_is_last)
def update_content_size(self, content_size: int) -> None:
"""
:param content_size: the new size of the file content in bytes
:returns: None
This method parses the binary entry to the construct structure, updates the content size of the file
and builds new binary entry.
"""
parsed_entry = self._parse_entry(self.entry_bytes)
parsed_entry.DIR_FileSize = content_size # type: ignore
self.entry_bytes = Entry.ENTRY_FORMAT_SHORT_NAME.build(parsed_entry)

@ -0,0 +1,54 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
class WriteDirectoryException(Exception):
"""
Exception is raised when the user tries to write the content into the directory instead of file
"""
pass
class NoFreeClusterException(Exception):
"""
Exception is raised when the user tries allocate cluster but no free one is available
"""
pass
class LowerCaseException(Exception):
"""
Exception is raised when the user tries to write file or directory with lower case
"""
pass
class TooLongNameException(Exception):
"""
Exception is raised when long name support is not enabled and user tries to write file longer then allowed
"""
pass
class NotInitialized(Exception):
"""
Exception is raised when the user tries to access not initialized property
"""
pass
class WLNotInitialized(Exception):
"""
Exception is raised when the user tries to write fatfs not initialized with wear levelling
"""
pass
class FatalError(Exception):
pass
class InconsistentFATAttributes(Exception):
"""
Caused by e.g. wrong number of clusters for given FAT type
"""
pass

@ -0,0 +1,100 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from typing import List, Optional
from .cluster import Cluster
from .exceptions import NoFreeClusterException
from .fatfs_state import BootSectorState
class FAT:
"""
The FAT represents the FAT region in file system. It is responsible for storing clusters
and chaining them in case we need to extend file or directory to more clusters.
"""
def allocate_root_dir(self) -> None:
"""
The root directory is implicitly created with the FatFS,
its block is on the index 1 (second index) and is allocated implicitly.
"""
self.clusters[Cluster.ROOT_BLOCK_ID].allocate_cluster()
def __init__(self, boot_sector_state: BootSectorState, init_: bool) -> None:
self._first_free_cluster_id = 1
self.boot_sector_state = boot_sector_state
self.clusters: List[Cluster] = [Cluster(cluster_id=i,
boot_sector_state=self.boot_sector_state,
init_=init_) for i in range(self.boot_sector_state.clusters)]
if init_:
self.allocate_root_dir()
def get_cluster_value(self, cluster_id_: int) -> int:
"""
The method retrieves the values of the FAT memory block.
E.g. in case of FAT12:
00000000: F8 FF FF 55 05 00 00 00 00 00 00 00 00 00 00 00
The reserved value is 0xFF8, the value of first cluster if 0xFFF, thus is last in chain,
and the value of the second cluster is 0x555, so refers to the cluster number 0x555.
"""
fat_cluster_value_: int = self.clusters[cluster_id_].get_from_fat()
return fat_cluster_value_
def is_cluster_last(self, cluster_id_: int) -> bool:
"""
Checks if the cluster is last in its cluster chain. If the value of the cluster is
0xFFF for FAT12, 0xFFFF for FAT16 or 0xFFFFFFFF for FAT32, the cluster is the last.
"""
value_ = self.get_cluster_value(cluster_id_)
is_cluster_last_: bool = value_ == (1 << self.boot_sector_state.fatfs_type) - 1
return is_cluster_last_
def get_chained_content(self, cluster_id_: int, size: Optional[int] = None) -> bytearray:
"""
The purpose of the method is retrieving the content from chain of clusters when the FAT FS partition
is analyzed. The file entry provides the reference to the first cluster, this method
traverses linked list of clusters and append partial results to the content.
"""
binary_image: bytearray = self.boot_sector_state.binary_image
data_address_ = Cluster.compute_cluster_data_address(self.boot_sector_state, cluster_id_)
content_ = binary_image[data_address_: data_address_ + self.boot_sector_state.sector_size]
while not self.is_cluster_last(cluster_id_):
cluster_id_ = self.get_cluster_value(cluster_id_)
data_address_ = Cluster.compute_cluster_data_address(self.boot_sector_state, cluster_id_)
content_ += binary_image[data_address_: data_address_ + self.boot_sector_state.sector_size]
# the size is None if the object is directory
if size is None:
return content_
return content_[:size]
def find_free_cluster(self) -> Cluster:
"""
Returns the first free cluster and increments value of `self._first_free_cluster_id`.
The method works only in context of creating a partition from scratch.
In situations where the clusters are allocated and freed during the run of the program,
might the method cause `Out of space` error despite there would be free clusters.
"""
if self._first_free_cluster_id + 1 >= len(self.clusters):
raise NoFreeClusterException('No free cluster available!')
cluster = self.clusters[self._first_free_cluster_id + 1]
if not cluster.is_empty:
raise NoFreeClusterException('No free cluster available!')
cluster.allocate_cluster()
self._first_free_cluster_id += 1
return cluster
def allocate_chain(self, first_cluster: Cluster, size: int) -> None:
"""
Allocates the linked list of clusters needed for the given file or directory.
"""
current = first_cluster
for _ in range(size - 1):
free_cluster = self.find_free_cluster()
current.next_cluster = free_cluster
current.set_in_fat(free_cluster.id)
current = free_cluster

@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from .boot_sector import BootSector
from .utils import read_filesystem
class FATFSParser:
def __init__(self, image_file_path: str, wl_support: bool = False) -> None:
if wl_support:
raise NotImplementedError('Parser is not implemented for WL yet.')
self.fatfs = read_filesystem(image_file_path)
# when wl is not supported we expect boot sector to be the first
self.parsed_header = BootSector.BOOT_SECTOR_HEADER.parse(self.fatfs[:BootSector.BOOT_HEADER_SIZE])
print(BootSector)

@ -0,0 +1,170 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from textwrap import dedent
from typing import Optional
from .exceptions import InconsistentFATAttributes
from .utils import (ALLOWED_SECTOR_SIZES, FAT12, FAT12_MAX_CLUSTERS, FAT16, FAT16_MAX_CLUSTERS,
RESERVED_CLUSTERS_COUNT, FATDefaults, get_fat_sectors_count, get_fatfs_type,
get_non_data_sectors_cnt, number_of_clusters)
class FATFSState:
"""
The class represents the state and the configuration of the FATFS.
"""
def __init__(self,
sector_size: int,
reserved_sectors_cnt: int,
root_dir_sectors_cnt: int,
size: int,
media_type: int,
sectors_per_cluster: int,
volume_label: str,
oem_name: str,
fat_tables_cnt: int,
sec_per_track: int,
num_heads: int,
hidden_sectors: int,
file_sys_type: str,
use_default_datetime: bool,
explicit_fat_type: Optional[int] = None,
long_names_enabled: bool = False):
self.boot_sector_state = BootSectorState(oem_name=oem_name,
sector_size=sector_size,
sectors_per_cluster=sectors_per_cluster,
reserved_sectors_cnt=reserved_sectors_cnt,
fat_tables_cnt=fat_tables_cnt,
root_dir_sectors_cnt=root_dir_sectors_cnt,
sectors_count=size // sector_size,
media_type=media_type,
sec_per_track=sec_per_track,
num_heads=num_heads,
hidden_sectors=hidden_sectors,
volume_label=volume_label,
file_sys_type=file_sys_type,
volume_uuid=-1)
self._explicit_fat_type: Optional[int] = explicit_fat_type
self.long_names_enabled: bool = long_names_enabled
self.use_default_datetime: bool = use_default_datetime
if (size // sector_size) * sectors_per_cluster in (FAT12_MAX_CLUSTERS, FAT16_MAX_CLUSTERS):
print('WARNING: It is not recommended to create FATFS with bounding '
f'count of clusters: {FAT12_MAX_CLUSTERS} or {FAT16_MAX_CLUSTERS}')
self.check_fat_type()
@property
def binary_image(self) -> bytearray:
return self.boot_sector_state.binary_image
@binary_image.setter
def binary_image(self, value: bytearray) -> None:
self.boot_sector_state.binary_image = value
def check_fat_type(self) -> None:
_type = self.boot_sector_state.fatfs_type
if self._explicit_fat_type is not None and self._explicit_fat_type != _type:
raise InconsistentFATAttributes(dedent(
f"""FAT type you specified is inconsistent with other attributes of the system.
The specified FATFS type: FAT{self._explicit_fat_type}
The actual FATFS type: FAT{_type}"""))
if _type not in (FAT12, FAT16):
raise NotImplementedError('FAT32 is currently not supported.')
class BootSectorState:
# pylint: disable=too-many-instance-attributes
def __init__(self,
oem_name: str,
sector_size: int,
sectors_per_cluster: int,
reserved_sectors_cnt: int,
fat_tables_cnt: int,
root_dir_sectors_cnt: int,
sectors_count: int,
media_type: int,
sec_per_track: int,
num_heads: int,
hidden_sectors: int,
volume_label: str,
file_sys_type: str,
volume_uuid: int = -1) -> None:
self.oem_name: str = oem_name
self.sector_size: int = sector_size
assert self.sector_size in ALLOWED_SECTOR_SIZES
self.sectors_per_cluster: int = sectors_per_cluster
self.reserved_sectors_cnt: int = reserved_sectors_cnt
self.fat_tables_cnt: int = fat_tables_cnt
self.root_dir_sectors_cnt: int = root_dir_sectors_cnt
self.sectors_count: int = sectors_count
self.media_type: int = media_type
self.sectors_per_fat_cnt = get_fat_sectors_count(self.size // self.sector_size, self.sector_size)
self.sec_per_track: int = sec_per_track
self.num_heads: int = num_heads
self.hidden_sectors: int = hidden_sectors
self.volume_label: str = volume_label
self.file_sys_type: str = file_sys_type
self.volume_uuid: int = volume_uuid
self._binary_image: bytearray = bytearray(b'')
@property
def binary_image(self) -> bytearray:
return self._binary_image
@binary_image.setter
def binary_image(self, value: bytearray) -> None:
self._binary_image = value
@property
def size(self) -> int:
return self.sector_size * self.sectors_count
@property
def data_region_start(self) -> int:
return self.non_data_sectors * self.sector_size
@property
def fatfs_type(self) -> int:
# variable typed_fatfs_type must be explicitly typed to avoid mypy error
typed_fatfs_type: int = get_fatfs_type(self.clusters)
return typed_fatfs_type
@property
def clusters(self) -> int:
"""
The actual number of clusters is calculated by `number_of_clusters`,
however, the initial two blocks of FAT are reserved (device type and root directory),
despite they don't refer to the data region.
Since that, two clusters are added to use the full potential of the FAT file system partition.
"""
clusters_cnt_: int = number_of_clusters(self.data_sectors, self.sectors_per_cluster) + RESERVED_CLUSTERS_COUNT
return clusters_cnt_
@property
def data_sectors(self) -> int:
# self.sector_size is checked in constructor if has one of allowed values (ALLOWED_SECTOR_SIZES)
return (self.size // self.sector_size) - self.non_data_sectors
@property
def non_data_sectors(self) -> int:
non_data_sectors_: int = get_non_data_sectors_cnt(self.reserved_sectors_cnt,
self.sectors_per_fat_cnt,
self.root_dir_sectors_cnt)
return non_data_sectors_
@property
def fat_table_start_address(self) -> int:
return self.sector_size * self.reserved_sectors_cnt
@property
def entries_root_count(self) -> int:
entries_root_count_: int = (self.root_dir_sectors_cnt * self.sector_size) // FATDefaults.ENTRY_SIZE
return entries_root_count_
@property
def root_directory_start(self) -> int:
root_dir_start: int = (self.reserved_sectors_cnt + self.sectors_per_fat_cnt) * self.sector_size
return root_dir_start

@ -0,0 +1,343 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
from datetime import datetime
from typing import List, Optional, Tuple, Union
from .entry import Entry
from .exceptions import FatalError, WriteDirectoryException
from .fat import FAT, Cluster
from .fatfs_state import FATFSState
from .long_filename_utils import (build_lfn_full_name, build_lfn_unique_entry_name_order,
get_required_lfn_entries_count, split_name_to_lfn_entries,
split_name_to_lfn_entry_blocks)
from .utils import (DATETIME, INVALID_SFN_CHARS_PATTERN, MAX_EXT_SIZE, MAX_NAME_SIZE, FATDefaults,
build_lfn_short_entry_name, build_name, lfn_checksum, required_clusters_count,
split_content_into_sectors, split_to_name_and_extension)
class File:
"""
The class File provides API to write into the files. It represents file in the FS.
"""
ATTR_ARCHIVE: int = 0x20
ENTITY_TYPE: int = ATTR_ARCHIVE
def __init__(self, name: str, fat: FAT, fatfs_state: FATFSState, entry: Entry, extension: str = '') -> None:
self.name: str = name
self.extension: str = extension
self.fatfs_state: FATFSState = fatfs_state
self.fat: FAT = fat
self.size: int = 0
self._first_cluster: Optional[Cluster] = None
self._entry: Entry = entry
@property
def entry(self) -> Entry:
return self._entry
@property
def first_cluster(self) -> Optional[Cluster]:
return self._first_cluster
@first_cluster.setter
def first_cluster(self, value: Cluster) -> None:
self._first_cluster = value
def name_equals(self, name: str, extension: str) -> bool:
equals_: bool = build_name(name, extension) == build_name(self.name, self.extension)
return equals_
def write(self, content: bytes) -> None:
self.entry.update_content_size(len(content))
# we assume that the correct amount of clusters is allocated
current_cluster = self._first_cluster
for content_part in split_content_into_sectors(content, self.fatfs_state.boot_sector_state.sector_size):
content_as_list = content_part
if current_cluster is None:
raise FatalError('No free space left!')
address: int = current_cluster.cluster_data_address
self.fatfs_state.binary_image[address: address + len(content_part)] = content_as_list
current_cluster = current_cluster.next_cluster
class Directory:
"""
The Directory class provides API to add files and directories into the directory
and to find the file according to path and write it.
"""
ATTR_DIRECTORY: int = 0x10
ATTR_ARCHIVE: int = 0x20
ENTITY_TYPE: int = ATTR_DIRECTORY
CURRENT_DIRECTORY = '.'
PARENT_DIRECTORY = '..'
def __init__(self,
name,
fat,
fatfs_state,
entry=None,
cluster=None,
size=None,
extension='',
parent=None):
# type: (str, FAT, FATFSState, Optional[Entry], Cluster, Optional[int], str, Directory) -> None
self.name: str = name
self.fatfs_state: FATFSState = fatfs_state
self.extension: str = extension
self.fat: FAT = fat
self.size: int = size or self.fatfs_state.boot_sector_state.sector_size
# if directory is root its parent is itself
self.parent: Directory = parent or self
self._first_cluster: Cluster = cluster
# entries will be initialized after the cluster allocation
self.entries: List[Entry] = []
self.entities: List[Union[File, Directory]] = [] # type: ignore
self._entry = entry # currently not in use (will use later for e.g. modification time, etc.)
@property
def is_root(self) -> bool:
return self.parent is self
@property
def first_cluster(self) -> Cluster:
return self._first_cluster
@first_cluster.setter
def first_cluster(self, value: Cluster) -> None:
self._first_cluster = value
def name_equals(self, name: str, extension: str) -> bool:
equals_: bool = build_name(name, extension) == build_name(self.name, self.extension)
return equals_
@property
def entries_count(self) -> int:
entries_count_: int = self.size // FATDefaults.ENTRY_SIZE
return entries_count_
def create_entries(self, cluster: Cluster) -> List[Entry]:
return [Entry(entry_id=i,
parent_dir_entries_address=cluster.cluster_data_address,
fatfs_state=self.fatfs_state)
for i in range(self.entries_count)]
def init_directory(self) -> None:
self.entries = self.create_entries(self._first_cluster)
# the root directory doesn't contain link to itself nor the parent
if self.is_root:
return
# if the directory is not root we initialize the reference to itself and to the parent directory
for dir_id, name_ in ((self, self.CURRENT_DIRECTORY), (self.parent, self.PARENT_DIRECTORY)):
new_dir_: Entry = self.find_free_entry() or self.chain_directory()
new_dir_.allocate_entry(first_cluster_id=dir_id.first_cluster.id,
entity_name=name_,
entity_extension='',
entity_type=dir_id.ENTITY_TYPE)
def lookup_entity(self, object_name: str, extension: str): # type: ignore
for entity in self.entities:
if build_name(entity.name, entity.extension) == build_name(object_name, extension):
return entity
return None
@staticmethod
def _is_end_of_path(path_as_list: List[str]) -> bool:
"""
:param path_as_list: path split into the list
:returns: True if the file is the leaf of the directory tree, False otherwise
The method is part of the base of recursion,
determines if the path is target file or directory in the tree folder structure.
"""
return len(path_as_list) == 1
def recursive_search(self, path_as_list, current_dir): # type: ignore
name, extension = split_to_name_and_extension(path_as_list[0])
next_obj = current_dir.lookup_entity(name, extension)
if next_obj is None:
raise FileNotFoundError('No such file or directory!')
if self._is_end_of_path(path_as_list) and next_obj.name_equals(name, extension):
return next_obj
return self.recursive_search(path_as_list[1:], next_obj)
def find_free_entry(self) -> Optional[Entry]:
for entry in self.entries:
if entry.is_empty:
return entry
return None
def _extend_directory(self) -> None:
current: Cluster = self.first_cluster
while current.next_cluster is not None:
current = current.next_cluster
new_cluster: Cluster = self.fat.find_free_cluster()
current.set_in_fat(new_cluster.id)
assert current is not new_cluster
current.next_cluster = new_cluster
self.entries += self.create_entries(new_cluster)
def chain_directory(self) -> Entry:
"""
:returns: First free entry
The method adds new Cluster to the Directory and returns first free entry.
"""
self._extend_directory()
free_entry: Entry = self.find_free_entry()
if free_entry is None:
raise FatalError('No more space left!')
return free_entry
@staticmethod
def allocate_long_name_object(free_entry,
name,
extension,
target_dir,
free_cluster_id,
entity_type,
date,
time):
# type: (Entry, str, str, Directory, int, int, DATETIME, DATETIME) -> Entry
lfn_full_name: str = build_lfn_full_name(name, extension)
lfn_unique_entry_order: int = build_lfn_unique_entry_name_order(target_dir.entities, name)
lfn_short_entry_name: str = build_lfn_short_entry_name(name, extension, lfn_unique_entry_order)
checksum: int = lfn_checksum(lfn_short_entry_name)
entries_count: int = get_required_lfn_entries_count(lfn_full_name)
# entries in long file name entries chain starts with the last entry
split_names_reversed = list(reversed(list(enumerate(split_name_to_lfn_entries(lfn_full_name, entries_count)))))
for i, name_split_to_entry in split_names_reversed:
order: int = i + 1
blocks_: List[bytes] = split_name_to_lfn_entry_blocks(name_split_to_entry)
lfn_names: List[bytes] = list(map(lambda x: x.lower(), blocks_))
free_entry.allocate_entry(first_cluster_id=free_cluster_id,
entity_name=name,
entity_extension=extension,
entity_type=entity_type,
lfn_order=order,
lfn_names=lfn_names,
lfn_checksum_=checksum,
lfn_is_last=order == entries_count)
free_entry = target_dir.find_free_entry() or target_dir.chain_directory()
free_entry.allocate_entry(first_cluster_id=free_cluster_id,
entity_name=lfn_short_entry_name[:MAX_NAME_SIZE],
entity_extension=lfn_short_entry_name[MAX_NAME_SIZE:],
entity_type=entity_type,
lfn_order=Entry.SHORT_ENTRY_LN,
date=date,
time=time)
return free_entry
@staticmethod
def _is_valid_sfn(name: str, extension: str) -> bool:
if INVALID_SFN_CHARS_PATTERN.search(name) or INVALID_SFN_CHARS_PATTERN.search(name):
return False
ret: bool = len(name) <= MAX_NAME_SIZE and len(extension) <= MAX_EXT_SIZE
return ret
def allocate_object(self,
name,
entity_type,
object_timestamp_,
path_from_root=None,
extension='',
is_empty=False):
# type: (str, int, datetime, Optional[List[str]], str, bool) -> Tuple[Cluster, Entry, Directory]
"""
Method finds the target directory in the path
and allocates cluster (both the record in FAT and cluster in the data region)
and entry in the specified directory
"""
free_cluster: Optional[Cluster] = None
free_cluster_id = 0x00
if not is_empty:
free_cluster = self.fat.find_free_cluster()
free_cluster_id = free_cluster.id
target_dir: Directory = self if not path_from_root else self.recursive_search(path_from_root, self)
free_entry: Entry = target_dir.find_free_entry() or target_dir.chain_directory()
fatfs_date_ = (object_timestamp_.year, object_timestamp_.month, object_timestamp_.day)
fatfs_time_ = (object_timestamp_.hour, object_timestamp_.minute, object_timestamp_.second)
if not self.fatfs_state.long_names_enabled or self._is_valid_sfn(name, extension):
free_entry.allocate_entry(first_cluster_id=free_cluster_id,
entity_name=name,
entity_extension=extension,
date=fatfs_date_,
time=fatfs_time_,
fits_short=True,
entity_type=entity_type)
return free_cluster, free_entry, target_dir
return free_cluster, self.allocate_long_name_object(free_entry=free_entry,
name=name,
extension=extension,
target_dir=target_dir,
free_cluster_id=free_cluster_id,
entity_type=entity_type,
date=fatfs_date_,
time=fatfs_time_), target_dir
def new_file(self,
name: str,
extension: str,
path_from_root: Optional[List[str]],
object_timestamp_: datetime,
is_empty: bool) -> None:
free_cluster, free_entry, target_dir = self.allocate_object(name=name,
extension=extension,
entity_type=Directory.ATTR_ARCHIVE,
path_from_root=path_from_root,
object_timestamp_=object_timestamp_,
is_empty=is_empty)
file: File = File(name=name,
fat=self.fat,
extension=extension,
fatfs_state=self.fatfs_state,
entry=free_entry)
file.first_cluster = free_cluster
target_dir.entities.append(file)
def new_directory(self, name, parent, path_from_root, object_timestamp_):
# type: (str, Directory, Optional[List[str]], datetime) -> None
free_cluster, free_entry, target_dir = self.allocate_object(name=name,
entity_type=Directory.ATTR_DIRECTORY,
path_from_root=path_from_root,
object_timestamp_=object_timestamp_)
directory: Directory = Directory(name=name,
fat=self.fat,
parent=parent,
fatfs_state=self.fatfs_state,
entry=free_entry)
directory.first_cluster = free_cluster
directory.init_directory()
target_dir.entities.append(directory)
def write_to_file(self, path: List[str], content: bytes) -> None:
"""
Writes to file existing in the directory structure.
:param path: path split into the list
:param content: content as a string to be written into a file
:returns: None
:raises WriteDirectoryException: raised is the target object for writing is a directory
"""
entity_to_write: Entry = self.recursive_search(path, self)
if isinstance(entity_to_write, File):
clusters_cnt: int = required_clusters_count(cluster_size=self.fatfs_state.boot_sector_state.sector_size,
content=content)
self.fat.allocate_chain(entity_to_write.first_cluster, clusters_cnt)
entity_to_write.write(content)
else:
raise WriteDirectoryException(f'`{os.path.join(*path)}` is a directory!')

@ -0,0 +1,98 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from typing import List
from .entry import Entry
from .exceptions import NoFreeClusterException
from .utils import build_name, convert_to_utf16_and_pad
# File name with long filenames support can be as long as memory allows. It is split into entries
# holding 13 characters of the filename, thus the number of required entries is ceil(len(long_name) / 13).
# This is computed using `get_required_lfn_entries_count`.
# For creating long name entries we need to split the name by 13 characters using `split_name_to_lfn_entries`
# and in every entry into three blocks with sizes 5, 6 and 2 characters using `split_name_to_lfn_entry`.
MAXIMAL_FILES_SAME_PREFIX: int = 127
def get_required_lfn_entries_count(lfn_full_name: str) -> int:
"""
Compute the number of entries required to store the long name.
One long filename entry can hold 13 characters with size 2 bytes.
E.g. "thisisverylongfilenama.txt" with length of 26 needs 2 lfn entries,
but "thisisverylongfilenamax.txt" with 27 characters needs 3 lfn entries.
"""
entries_count: int = (len(lfn_full_name) + Entry.CHARS_PER_ENTRY - 1) // Entry.CHARS_PER_ENTRY
return entries_count
def split_name_to_lfn_entries(name: str, entries: int) -> List[str]:
"""
If the filename is longer than 8 (name) + 3 (extension) characters,
generator uses long name structure and splits the name into suitable amount of blocks.
E.g. 'thisisverylongfilenama.txt' would be split to ['THISISVERYLON', 'GFILENAMA.TXT'],
in case of 'thisisverylongfilenamax.txt' - ['THISISVERYLON', 'GFILENAMAX.TX', 'T']
"""
return [name[i * Entry.CHARS_PER_ENTRY:(i + 1) * Entry.CHARS_PER_ENTRY] for i in range(entries)]
def split_name_to_lfn_entry_blocks(name: str) -> List[bytes]:
"""
Filename is divided into three blocks in every long file name entry. Sizes of the blocks are defined
by LDIR_Name1_SIZE, LDIR_Name2_SIZE and LDIR_Name3_SIZE, thus every block contains LDIR_Name{X}_SIZE * 2 bytes.
If the filename ends in one of the blocks, it is terminated by zero encoded to two bytes (0x0000). Other unused
characters are set to 0xFFFF.
E.g.:
'GFILENAMA.TXT' -> [b'G\x00F\x00I\x00L\x00E\x00', b'N\x00A\x00M\x00A\x00.\x00T\x00', b'X\x00T\x00'];
'T' -> [b'T\x00\x00\x00\xff\xff\xff\xff\xff\xff', b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff',
b'\xff\xff\xff\xff']
Notice that since every character is coded using 2 bytes be must add 0x00 to ASCII symbols ('G' -> 'G\x00', etc.),
since character 'T' ends in the first block, we must add '\x00\x00' after 'T\x00'.
"""
max_entry_size: int = Entry.LDIR_Name1_SIZE + Entry.LDIR_Name2_SIZE + Entry.LDIR_Name2_SIZE
assert len(name) <= max_entry_size
blocks_: List[bytes] = [
convert_to_utf16_and_pad(content=name[:Entry.LDIR_Name1_SIZE],
expected_size=Entry.LDIR_Name1_SIZE),
convert_to_utf16_and_pad(content=name[Entry.LDIR_Name1_SIZE:Entry.LDIR_Name1_SIZE + Entry.LDIR_Name2_SIZE],
expected_size=Entry.LDIR_Name2_SIZE),
convert_to_utf16_and_pad(content=name[Entry.LDIR_Name1_SIZE + Entry.LDIR_Name2_SIZE:],
expected_size=Entry.LDIR_Name3_SIZE)
]
return blocks_
def build_lfn_unique_entry_name_order(entities: list, lfn_entry_name: str) -> int:
"""
The short entry contains only the first 6 characters of the file name,
and we have to distinguish it from other names within the directory starting with the same 6 characters.
To make it unique, we add its order in relation to other names such that lfn_entry_name[:6] == other[:6].
The order is specified by the character, starting with chr(1).
E.g. the file in directory 'thisisverylongfilenama.txt' will be named 'THISIS~1TXT' in its short entry.
If we add another file 'thisisverylongfilenamax.txt' its name in the short entry will be 'THISIS~2TXT'.
"""
preceding_entries: int = 1
for entity in entities:
if entity.name[:6] == lfn_entry_name[:6]:
preceding_entries += 1
if preceding_entries > MAXIMAL_FILES_SAME_PREFIX:
raise NoFreeClusterException('Maximal number of files with the same prefix is 127')
return preceding_entries
def build_lfn_full_name(name: str, extension: str) -> str:
"""
The extension is optional, and the long filename entry explicitly specifies it,
on the opposite as for short file names.
"""
lfn_record: str = build_name(name, extension)
# the name must be terminated with NULL terminator
# if it doesn't fit into the set of long name directory entries
if len(lfn_record) % Entry.CHARS_PER_ENTRY != 0:
return lfn_record + chr(0)
return lfn_record

@ -0,0 +1,299 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import binascii
import os
import re
import uuid
from datetime import datetime
from typing import List, Optional, Tuple
from construct import BitsInteger, BitStruct, Int16ul
# the regex pattern defines symbols that are allowed by long file names but not by short file names
INVALID_SFN_CHARS_PATTERN = re.compile(r'[.+,;=\[\]]')
FATFS_MIN_ALLOC_UNIT: int = 128
FAT12_MAX_CLUSTERS: int = 4085
FAT16_MAX_CLUSTERS: int = 65525
RESERVED_CLUSTERS_COUNT: int = 2
PAD_CHAR: int = 0x20
FAT12: int = 12
FAT16: int = 16
FAT32: int = 32
FULL_BYTE: bytes = b'\xff'
EMPTY_BYTE: bytes = b'\x00'
# redundant
BYTES_PER_DIRECTORY_ENTRY: int = 32
UINT32_MAX: int = (1 << 32) - 1
MAX_NAME_SIZE: int = 8
MAX_EXT_SIZE: int = 3
DATETIME = Tuple[int, int, int]
FATFS_INCEPTION_YEAR: int = 1980
FATFS_INCEPTION: datetime = datetime(FATFS_INCEPTION_YEAR, 1, 1, 0, 0, 0, 0)
FATFS_MAX_HOURS = 24
FATFS_MAX_MINUTES = 60
FATFS_MAX_SECONDS = 60
FATFS_MAX_DAYS = 31
FATFS_MAX_MONTHS = 12
FATFS_MAX_YEARS = 127
FATFS_SECONDS_GRANULARITY: int = 2
# long names are encoded to two bytes in utf-16
LONG_NAMES_ENCODING: str = 'utf-16'
SHORT_NAMES_ENCODING: str = 'utf-8'
# compatible with WL_SECTOR_SIZE
# choices for WL are WL_SECTOR_SIZE_512 and WL_SECTOR_SIZE_4096
ALLOWED_WL_SECTOR_SIZES: List[int] = [512, 4096]
ALLOWED_SECTOR_SIZES: List[int] = [512, 1024, 2048, 4096]
ALLOWED_SECTORS_PER_CLUSTER: List[int] = [1, 2, 4, 8, 16, 32, 64, 128]
def crc32(input_values: List[int], crc: int) -> int:
"""
Name Polynomial Reversed? Init-value XOR-out
crc32 0x104C11DB7 True 4294967295 (UINT32_MAX) 0xFFFFFFFF
"""
return binascii.crc32(bytearray(input_values), crc)
def number_of_clusters(number_of_sectors: int, sectors_per_cluster: int) -> int:
return number_of_sectors // sectors_per_cluster
def get_non_data_sectors_cnt(reserved_sectors_cnt: int, sectors_per_fat_cnt: int, root_dir_sectors_cnt: int) -> int:
return reserved_sectors_cnt + sectors_per_fat_cnt + root_dir_sectors_cnt
def get_fatfs_type(clusters_count: int) -> int:
if clusters_count < FAT12_MAX_CLUSTERS:
return FAT12
if clusters_count <= FAT16_MAX_CLUSTERS:
return FAT16
return FAT32
def get_fat_sectors_count(clusters_count: int, sector_size: int) -> int:
fatfs_type_ = get_fatfs_type(clusters_count)
if fatfs_type_ == FAT32:
raise NotImplementedError('FAT32 is not supported!')
# number of byte halves
cluster_s: int = fatfs_type_ // 4
fat_size_bytes: int = (
clusters_count * 2 + cluster_s) if fatfs_type_ == FAT16 else (clusters_count * 3 + 1) // 2 + cluster_s
return (fat_size_bytes + sector_size - 1) // sector_size
def required_clusters_count(cluster_size: int, content: bytes) -> int:
# compute number of required clusters for file text
return (len(content) + cluster_size - 1) // cluster_size
def generate_4bytes_random() -> int:
return uuid.uuid4().int & 0xFFFFFFFF
def pad_string(content: str, size: Optional[int] = None, pad: int = PAD_CHAR) -> str:
# cut string if longer and fill with pad character if shorter than size
return content.ljust(size or len(content), chr(pad))[:size]
def right_strip_string(content: str, pad: int = PAD_CHAR) -> str:
return content.rstrip(chr(pad))
def build_lfn_short_entry_name(name: str, extension: str, order: int) -> str:
return '{}{}'.format(pad_string(content=name[:MAX_NAME_SIZE - 2] + '~' + chr(order), size=MAX_NAME_SIZE),
pad_string(extension[:MAX_EXT_SIZE], size=MAX_EXT_SIZE))
def lfn_checksum(short_entry_name: str) -> int:
"""
Function defined by FAT specification. Computes checksum out of name in the short file name entry.
"""
checksum_result = 0
for i in range(MAX_NAME_SIZE + MAX_EXT_SIZE):
# operation is a right rotation on 8 bits (Python equivalent for unsigned char in C)
checksum_result = (0x80 if checksum_result & 1 else 0x00) + (checksum_result >> 1) + ord(short_entry_name[i])
checksum_result &= 0xff
return checksum_result
def convert_to_utf16_and_pad(content: str,
expected_size: int,
pad: bytes = FULL_BYTE) -> bytes:
# we need to get rid of the Byte order mark 0xfeff or 0xfffe, fatfs does not use it
bom_utf16: bytes = b'\xfe\xff'
encoded_content_utf16: bytes = content.encode(LONG_NAMES_ENCODING)[len(bom_utf16):]
return encoded_content_utf16.ljust(2 * expected_size, pad)
def split_to_name_and_extension(full_name: str) -> Tuple[str, str]:
name, extension = os.path.splitext(full_name)
return name, extension.replace('.', '')
def is_valid_fatfs_name(string: str) -> bool:
return string == string.upper()
def split_by_half_byte_12_bit_little_endian(value: int) -> Tuple[int, int, int]:
value_as_bytes: bytes = Int16ul.build(value)
return value_as_bytes[0] & 0x0f, value_as_bytes[0] >> 4, value_as_bytes[1] & 0x0f
def merge_by_half_byte_12_bit_little_endian(v1: int, v2: int, v3: int) -> int:
return v1 | v2 << 4 | v3 << 8
def build_byte(first_half: int, second_half: int) -> int:
return (first_half << 4) | second_half
def split_content_into_sectors(content: bytes, sector_size: int) -> List[bytes]:
result = []
clusters_cnt: int = required_clusters_count(cluster_size=sector_size, content=content)
for i in range(clusters_cnt):
result.append(content[sector_size * i:(i + 1) * sector_size])
return result
def get_args_for_partition_generator(desc: str, wl: bool) -> argparse.Namespace:
parser: argparse.ArgumentParser = argparse.ArgumentParser(description=desc)
parser.add_argument('input_directory',
help='Path to the directory that will be encoded into fatfs image')
parser.add_argument('--output_file',
default='fatfs_image.img',
help='Filename of the generated fatfs image')
parser.add_argument('--partition_size',
default=FATDefaults.SIZE,
help='Size of the partition in bytes.' +
('' if wl else ' Use `--partition_size detect` for detecting the minimal partition size.')
)
parser.add_argument('--sector_size',
default=FATDefaults.SECTOR_SIZE,
type=int,
choices=ALLOWED_WL_SECTOR_SIZES if wl else ALLOWED_SECTOR_SIZES,
help='Size of the partition in bytes')
parser.add_argument('--sectors_per_cluster',
default=1,
type=int,
choices=ALLOWED_SECTORS_PER_CLUSTER,
help='Number of sectors per cluster')
parser.add_argument('--root_entry_count',
default=FATDefaults.ROOT_ENTRIES_COUNT,
help='Number of entries in the root directory')
parser.add_argument('--long_name_support',
action='store_true',
help='Set flag to enable long names support.')
parser.add_argument('--use_default_datetime',
action='store_true',
help='For test purposes. If the flag is set the files are created with '
'the default timestamp that is the 1st of January 1980')
parser.add_argument('--fat_type',
default=0,
type=int,
choices=[FAT12, FAT16, 0],
help="""
Type of fat. Select 12 for fat12, 16 for fat16. Don't set, or set to 0 for automatic
calculation using cluster size and partition size.
""")
args = parser.parse_args()
if args.fat_type == 0:
args.fat_type = None
if args.partition_size == 'detect' and not wl:
args.partition_size = -1
args.partition_size = int(str(args.partition_size), 0)
if not os.path.isdir(args.input_directory):
raise NotADirectoryError(f'The target directory `{args.input_directory}` does not exist!')
return args
def read_filesystem(path: str) -> bytearray:
with open(path, 'rb') as fs_file:
return bytearray(fs_file.read())
DATE_ENTRY = BitStruct(
'year' / BitsInteger(7),
'month' / BitsInteger(4),
'day' / BitsInteger(5))
TIME_ENTRY = BitStruct(
'hour' / BitsInteger(5),
'minute' / BitsInteger(6),
'second' / BitsInteger(5),
)
def build_name(name: str, extension: str) -> str:
return f'{name}.{extension}' if len(extension) > 0 else name
def build_date_entry(year: int, mon: int, mday: int) -> int:
"""
:param year: denotes year starting from 1980 (0 ~ 1980, 1 ~ 1981, etc), valid values are 1980 + 0..127 inclusive
thus theoretically 1980 - 2107
:param mon: denotes number of month of year in common order (1 ~ January, 2 ~ February, etc.),
valid values: 1..12 inclusive
:param mday: denotes number of day in month, valid values are 1..31 inclusive
:returns: 16 bit integer number (7 bits for year, 4 bits for month and 5 bits for day of the month)
"""
assert year in range(FATFS_INCEPTION_YEAR, FATFS_INCEPTION_YEAR + FATFS_MAX_YEARS)
assert mon in range(1, FATFS_MAX_MONTHS + 1)
assert mday in range(1, FATFS_MAX_DAYS + 1)
return int.from_bytes(DATE_ENTRY.build(dict(year=year - FATFS_INCEPTION_YEAR, month=mon, day=mday)), 'big')
def build_time_entry(hour: int, minute: int, sec: int) -> int:
"""
:param hour: denotes number of hour, valid values are 0..23 inclusive
:param minute: denotes minutes, valid range 0..59 inclusive
:param sec: denotes seconds with granularity 2 seconds (e.g. 1 ~ 2, 29 ~ 58), valid range 0..29 inclusive
:returns: 16 bit integer number (5 bits for hour, 6 bits for minute and 5 bits for second)
"""
assert hour in range(FATFS_MAX_HOURS)
assert minute in range(FATFS_MAX_MINUTES)
assert sec in range(FATFS_MAX_SECONDS)
return int.from_bytes(TIME_ENTRY.build(
dict(hour=hour, minute=minute, second=sec // FATFS_SECONDS_GRANULARITY)),
byteorder='big'
)
class FATDefaults:
# FATFS defaults
SIZE: int = 1024 * 1024
RESERVED_SECTORS_COUNT: int = 1
FAT_TABLES_COUNT: int = 1
SECTORS_PER_CLUSTER: int = 1
SECTOR_SIZE: int = 0x1000
HIDDEN_SECTORS: int = 0
ENTRY_SIZE: int = 32
NUM_HEADS: int = 0xff
OEM_NAME: str = 'MSDOS5.0'
SEC_PER_TRACK: int = 0x3f
VOLUME_LABEL: str = 'Espressif'
FILE_SYS_TYPE: str = 'FAT'
ROOT_ENTRIES_COUNT: int = 512 # number of entries in the root directory, recommended 512
MEDIA_TYPE: int = 0xf8
SIGNATURE_WORD: bytes = b'\x55\xAA'
# wear levelling defaults
VERSION: int = 2
TEMP_BUFFER_SIZE: int = 32
UPDATE_RATE: int = 16
WR_SIZE: int = 16
# wear leveling metadata (config sector) contains always sector size 4096
WL_SECTOR_SIZE: int = 4096

@ -0,0 +1,245 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
from datetime import datetime
from typing import Any, List, Optional
from fatfs_utils.boot_sector import BootSector
from fatfs_utils.exceptions import NoFreeClusterException
from fatfs_utils.fat import FAT
from fatfs_utils.fatfs_state import FATFSState
from fatfs_utils.fs_object import Directory
from fatfs_utils.long_filename_utils import get_required_lfn_entries_count
from fatfs_utils.utils import (BYTES_PER_DIRECTORY_ENTRY, FATFS_INCEPTION, FATFS_MIN_ALLOC_UNIT,
RESERVED_CLUSTERS_COUNT, FATDefaults, get_args_for_partition_generator,
get_fat_sectors_count, get_non_data_sectors_cnt, read_filesystem,
required_clusters_count)
class FATFS:
"""
The class FATFS provides API for generating FAT file system.
It contains reference to the FAT table and to the root directory.
"""
def __init__(self,
binary_image_path: Optional[str] = None,
size: int = FATDefaults.SIZE,
reserved_sectors_cnt: int = FATDefaults.RESERVED_SECTORS_COUNT,
fat_tables_cnt: int = FATDefaults.FAT_TABLES_COUNT,
sectors_per_cluster: int = FATDefaults.SECTORS_PER_CLUSTER,
sector_size: int = FATDefaults.SECTOR_SIZE,
hidden_sectors: int = FATDefaults.HIDDEN_SECTORS,
long_names_enabled: bool = False,
use_default_datetime: bool = True,
num_heads: int = FATDefaults.NUM_HEADS,
oem_name: str = FATDefaults.OEM_NAME,
sec_per_track: int = FATDefaults.SEC_PER_TRACK,
volume_label: str = FATDefaults.VOLUME_LABEL,
file_sys_type: str = FATDefaults.FILE_SYS_TYPE,
root_entry_count: int = FATDefaults.ROOT_ENTRIES_COUNT,
explicit_fat_type: int = None,
media_type: int = FATDefaults.MEDIA_TYPE) -> None:
# root directory bytes should be aligned by sector size
assert (root_entry_count * BYTES_PER_DIRECTORY_ENTRY) % sector_size == 0
# number of bytes in the root dir must be even multiple of BPB_BytsPerSec
assert ((root_entry_count * BYTES_PER_DIRECTORY_ENTRY) // sector_size) % 2 == 0
root_dir_sectors_cnt: int = (root_entry_count * BYTES_PER_DIRECTORY_ENTRY) // sector_size
self.state: FATFSState = FATFSState(sector_size=sector_size,
explicit_fat_type=explicit_fat_type,
reserved_sectors_cnt=reserved_sectors_cnt,
root_dir_sectors_cnt=root_dir_sectors_cnt,
size=size,
file_sys_type=file_sys_type,
num_heads=num_heads,
fat_tables_cnt=fat_tables_cnt,
sectors_per_cluster=sectors_per_cluster,
media_type=media_type,
hidden_sectors=hidden_sectors,
sec_per_track=sec_per_track,
long_names_enabled=long_names_enabled,
volume_label=volume_label,
oem_name=oem_name,
use_default_datetime=use_default_datetime)
binary_image: bytes = bytearray(
read_filesystem(binary_image_path) if binary_image_path else self.create_empty_fatfs())
self.state.binary_image = binary_image
self.fat: FAT = FAT(boot_sector_state=self.state.boot_sector_state, init_=True)
root_dir_size = self.state.boot_sector_state.root_dir_sectors_cnt * self.state.boot_sector_state.sector_size
self.root_directory: Directory = Directory(name='A', # the name is not important, must be string
size=root_dir_size,
fat=self.fat,
cluster=self.fat.clusters[1],
fatfs_state=self.state)
self.root_directory.init_directory()
def create_file(self, name: str,
extension: str = '',
path_from_root: Optional[List[str]] = None,
object_timestamp_: datetime = FATFS_INCEPTION,
is_empty: bool = False) -> None:
"""
Root directory recursively finds the parent directory of the new file, allocates cluster,
entry and appends a new file into the parent directory.
When path_from_root is None the dir is root.
:param name: The name of the file.
:param extension: The extension of the file.
:param path_from_root: List of strings containing names of the ancestor directories in the given order.
:param object_timestamp_: is not None, this will be propagated to the file's entry
:param is_empty: True if there is no need to allocate any cluster, otherwise False
"""
self.root_directory.new_file(name=name,
extension=extension,
path_from_root=path_from_root,
object_timestamp_=object_timestamp_,
is_empty=is_empty)
def create_directory(self, name: str,
path_from_root: Optional[List[str]] = None,
object_timestamp_: datetime = FATFS_INCEPTION) -> None:
"""
Initially recursively finds a parent of the new directory
and then create a new directory inside the parent.
When path_from_root is None the parent dir is root.
:param name: The full name of the directory (excluding its path)
:param path_from_root: List of strings containing names of the ancestor directories in the given order.
:param object_timestamp_: in case the user preserves the timestamps, this will be propagated to the
metadata of the directory (to the corresponding entry)
:returns: None
"""
parent_dir = self.root_directory
if path_from_root:
parent_dir = self.root_directory.recursive_search(path_from_root, self.root_directory)
self.root_directory.new_directory(name=name,
parent=parent_dir,
path_from_root=path_from_root,
object_timestamp_=object_timestamp_)
def write_content(self, path_from_root: List[str], content: bytes) -> None:
"""
fat fs invokes root directory to recursively find the required file and writes the content
"""
self.root_directory.write_to_file(path_from_root, content)
def create_empty_fatfs(self) -> Any:
boot_sector_ = BootSector(boot_sector_state=self.state.boot_sector_state)
boot_sector_.generate_boot_sector()
return boot_sector_.binary_image
def write_filesystem(self, output_path: str) -> None:
with open(output_path, 'wb') as output:
output.write(bytearray(self.state.binary_image))
def _generate_partition_from_folder(self,
folder_relative_path: str,
folder_path: str = '',
is_dir: bool = False) -> None:
"""
Given path to folder and folder name recursively encodes folder into binary image.
Used by method generate.
"""
real_path: str = os.path.join(folder_path, folder_relative_path)
lower_path: str = folder_relative_path
folder_relative_path = folder_relative_path.upper()
normal_path = os.path.normpath(folder_relative_path)
split_path = normal_path.split(os.sep)
object_timestamp = datetime.fromtimestamp(os.path.getctime(real_path))
if os.path.isfile(real_path):
with open(real_path, 'rb') as file:
content = file.read()
file_name, extension = os.path.splitext(split_path[-1])
extension = extension[1:] # remove the dot from the extension
self.create_file(name=file_name,
extension=extension,
path_from_root=split_path[1:-1] or None,
object_timestamp_=object_timestamp,
is_empty=len(content) == 0)
self.write_content(split_path[1:], content)
elif os.path.isdir(real_path):
if not is_dir:
self.create_directory(name=split_path[-1],
path_from_root=split_path[1:-1],
object_timestamp_=object_timestamp)
# sorting files for better testability
dir_content = list(sorted(os.listdir(real_path)))
for path in dir_content:
self._generate_partition_from_folder(os.path.join(lower_path, path), folder_path=folder_path)
def generate(self, input_directory: str) -> None:
"""
Normalize path to folder and recursively encode folder to binary image
"""
path_to_folder, folder_name = os.path.split(input_directory)
self._generate_partition_from_folder(folder_name, folder_path=path_to_folder, is_dir=True)
def calculate_min_space(path: List[str],
fs_entity: str,
sector_size: int = 0x1000,
long_file_names: bool = False,
is_root: bool = False) -> int:
if os.path.isfile(os.path.join(*path, fs_entity)):
with open(os.path.join(*path, fs_entity), 'rb') as file_:
content = file_.read()
res: int = required_clusters_count(sector_size, content)
return res
buff: int = 0
dir_size = 2 * FATDefaults.ENTRY_SIZE # record for symlinks "." and ".."
for file in sorted(os.listdir(os.path.join(*path, fs_entity))):
if long_file_names and True:
# LFN entries + one short entry
dir_size += (get_required_lfn_entries_count(fs_entity) + 1) * FATDefaults.ENTRY_SIZE
else:
dir_size += FATDefaults.ENTRY_SIZE
buff += calculate_min_space(path + [fs_entity], file, sector_size, long_file_names, is_root=False)
if is_root and dir_size // FATDefaults.ENTRY_SIZE > FATDefaults.ROOT_ENTRIES_COUNT:
raise NoFreeClusterException('Not enough space in root!')
# roundup sectors, at least one is required
buff += (dir_size + sector_size - 1) // sector_size
return buff
def main() -> None:
args = get_args_for_partition_generator('Create a FAT filesystem and populate it with directory content', wl=False)
if args.partition_size == -1:
clusters = calculate_min_space([], args.input_directory, args.sector_size, long_file_names=True, is_root=True)
fats = get_fat_sectors_count(clusters, args.sector_size)
root_dir_sectors = (FATDefaults.ROOT_ENTRIES_COUNT * FATDefaults.ENTRY_SIZE) // args.sector_size
args.partition_size = max(FATFS_MIN_ALLOC_UNIT * args.sector_size,
(clusters + fats + get_non_data_sectors_cnt(RESERVED_CLUSTERS_COUNT,
fats,
root_dir_sectors)
) * args.sector_size
)
fatfs = FATFS(sector_size=args.sector_size,
sectors_per_cluster=args.sectors_per_cluster,
size=args.partition_size,
root_entry_count=args.root_entry_count,
explicit_fat_type=args.fat_type,
long_names_enabled=args.long_name_support,
use_default_datetime=args.use_default_datetime)
fatfs.generate(args.input_directory)
fatfs.write_filesystem(args.output_file)
if __name__ == '__main__':
main()

@ -0,0 +1,167 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import os
import construct
from fatfs_utils.boot_sector import BootSector
from fatfs_utils.entry import Entry
from fatfs_utils.fat import FAT
from fatfs_utils.fatfs_state import BootSectorState
from fatfs_utils.utils import FULL_BYTE, LONG_NAMES_ENCODING, PAD_CHAR, FATDefaults, lfn_checksum, read_filesystem
from wl_fatfsgen import remove_wl
def build_file_name(name1: bytes, name2: bytes, name3: bytes) -> str:
full_name_ = name1 + name2 + name3
# need to strip empty bytes and null-terminating char ('\x00')
return full_name_.rstrip(FULL_BYTE).decode(LONG_NAMES_ENCODING).rstrip('\x00')
def get_obj_name(obj_: dict, directory_bytes_: bytes, entry_position_: int, lfn_checksum_: int) -> str:
obj_ext_ = obj_['DIR_Name_ext'].rstrip(chr(PAD_CHAR))
ext_ = f'.{obj_ext_}' if len(obj_ext_) > 0 else ''
obj_name_: str = obj_['DIR_Name'].rstrip(chr(PAD_CHAR)) + ext_ # short entry name
# if LFN was detected, the record is considered as single SFN record only if DIR_NTRes == 0x18 (LDIR_DIR_NTRES)
# if LFN was not detected, the record cannot be part of the LFN, no matter the value of DIR_NTRes
if not args.long_name_support or obj_['DIR_NTRes'] == Entry.LDIR_DIR_NTRES:
return obj_name_
full_name = {}
for pos in range(entry_position_ - 1, -1, -1): # loop from the current entry back to the start
obj_address_: int = FATDefaults.ENTRY_SIZE * pos
entry_bytes_: bytes = directory_bytes_[obj_address_: obj_address_ + FATDefaults.ENTRY_SIZE]
struct_ = Entry.parse_entry_long(entry_bytes_, lfn_checksum_)
if len(struct_.items()) > 0:
full_name[struct_['order']] = build_file_name(struct_['name1'], struct_['name2'], struct_['name3'])
if struct_['is_last']:
break
return ''.join(map(lambda x: x[1], sorted(full_name.items()))) or obj_name_
def traverse_folder_tree(directory_bytes_: bytes,
name: str,
state_: BootSectorState,
fat_: FAT,
binary_array_: bytes) -> None:
os.makedirs(name)
assert len(directory_bytes_) % FATDefaults.ENTRY_SIZE == 0
entries_count_: int = len(directory_bytes_) // FATDefaults.ENTRY_SIZE
for i in range(entries_count_):
obj_address_: int = FATDefaults.ENTRY_SIZE * i
try:
obj_: dict = Entry.ENTRY_FORMAT_SHORT_NAME.parse(
directory_bytes_[obj_address_: obj_address_ + FATDefaults.ENTRY_SIZE])
except (construct.core.ConstError, UnicodeDecodeError):
args.long_name_support = True
continue
if obj_['DIR_Attr'] == 0: # empty entry
continue
obj_name_: str = get_obj_name(obj_,
directory_bytes_,
entry_position_=i,
lfn_checksum_=lfn_checksum(obj_['DIR_Name'] + obj_['DIR_Name_ext']))
if obj_['DIR_Attr'] == Entry.ATTR_ARCHIVE:
content_ = b''
if obj_['DIR_FileSize'] > 0:
content_ = fat_.get_chained_content(cluster_id_=Entry.get_cluster_id(obj_),
size=obj_['DIR_FileSize'])
with open(os.path.join(name, obj_name_), 'wb') as new_file:
new_file.write(content_)
elif obj_['DIR_Attr'] == Entry.ATTR_DIRECTORY:
# avoid creating symlinks to itself and parent folder
if obj_name_ in ('.', '..'):
continue
child_directory_bytes_ = fat_.get_chained_content(cluster_id_=obj_['DIR_FstClusLO'])
traverse_folder_tree(directory_bytes_=child_directory_bytes_,
name=os.path.join(name, obj_name_),
state_=state_,
fat_=fat_,
binary_array_=binary_array_)
def remove_wear_levelling_if_exists(fs_: bytes) -> bytes:
"""
Detection of the wear levelling layer is performed in two steps:
1) check if the first sector is a valid boot sector
2) check if the size defined in the boot sector is the same as the partition size:
- if it is, there is no wear levelling layer
- otherwise, we need to remove wl for further processing
"""
try:
boot_sector__ = BootSector()
boot_sector__.parse_boot_sector(fs_)
if boot_sector__.boot_sector_state.size == len(fs_):
return fs_
except construct.core.ConstError:
pass
plain_fs: bytes = remove_wl(fs_)
return plain_fs
if __name__ == '__main__':
desc = 'Tool for parsing fatfs image and extracting directory structure on host.'
argument_parser: argparse.ArgumentParser = argparse.ArgumentParser(description=desc)
argument_parser.add_argument('input_image',
help='Path to the image that will be parsed and extracted.')
argument_parser.add_argument('--long-name-support',
action='store_true',
help=argparse.SUPPRESS)
# ensures backward compatibility
argument_parser.add_argument('--wear-leveling',
action='store_true',
help=argparse.SUPPRESS)
argument_parser.add_argument('--wl-layer',
choices=['detect', 'enabled', 'disabled'],
default=None,
help="If detection doesn't work correctly, "
'you can force analyzer to or not to assume WL.')
args = argument_parser.parse_args()
# if wear levelling is detected or user explicitly sets the parameter `--wl_layer enabled`
# the partition with wear levelling is transformed to partition without WL for convenient parsing
# in some cases the partitions with and without wear levelling can be 100% equivalent
# and only user can break this tie by explicitly setting
# the parameter --wl-layer to enabled, respectively disabled
if args.wear_leveling and args.wl_layer:
raise NotImplementedError('Argument --wear-leveling cannot be combined with --wl-layer!')
if args.wear_leveling:
args.wl_layer = 'enabled'
args.wl_layer = args.wl_layer or 'detect'
fs = read_filesystem(args.input_image)
# An algorithm for removing wear levelling:
# 1. find an remove dummy sector:
# a) dummy sector is at the position defined by the number of records in the state sector
# b) dummy may not be placed in state nor cfg sectors
# c) first (boot) sector position (boot_s_pos) is calculated using value of move count
# boot_s_pos = - mc
# 2. remove state sectors (trivial)
# 3. remove cfg sector (trivial)
# 4. valid fs is then old_fs[-mc:] + old_fs[:-mc]
if args.wl_layer == 'enabled':
fs = remove_wl(fs)
elif args.wl_layer != 'disabled':
# wear levelling is removed to enable parsing using common algorithm
fs = remove_wear_levelling_if_exists(fs)
boot_sector_ = BootSector()
boot_sector_.parse_boot_sector(fs)
fat = FAT(boot_sector_.boot_sector_state, init_=False)
boot_dir_start_ = boot_sector_.boot_sector_state.root_directory_start
boot_dir_sectors = boot_sector_.boot_sector_state.root_dir_sectors_cnt
full_ = fs[boot_dir_start_: boot_dir_start_ + boot_dir_sectors * boot_sector_.boot_sector_state.sector_size]
traverse_folder_tree(full_,
boot_sector_.boot_sector_state.volume_label.rstrip(chr(PAD_CHAR)),
boot_sector_.boot_sector_state, fat, fs)

@ -0,0 +1,212 @@
/*------------------------------------------------------------------------*/
/* A Sample Code of User Provided OS Dependent Functions for FatFs */
/*------------------------------------------------------------------------*/
#include <string.h>
#include <stdlib.h>
#include "ff.h"
#include "sdkconfig.h"
#ifdef CONFIG_FATFS_ALLOC_PREFER_EXTRAM
#include "esp_heap_caps.h"
#endif
/*------------------------------------------------------------------------*/
/* Allocate/Free a Memory Block */
/*------------------------------------------------------------------------*/
void* ff_memalloc ( /* Returns pointer to the allocated memory block (null if not enough core) */
unsigned msize /* Number of bytes to allocate */
)
{
#ifdef CONFIG_FATFS_ALLOC_PREFER_EXTRAM
return heap_caps_malloc_prefer(msize, 2, MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM,
MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL);
#else
return malloc(msize);
#endif
}
void ff_memfree (
void* mblock /* Pointer to the memory block to free (no effect if null) */
)
{
free(mblock); /* Free the memory block with POSIX API */
}
#if FF_FS_REENTRANT /* Mutal exclusion */
/*------------------------------------------------------------------------*/
/* Definitions of Mutex */
/*------------------------------------------------------------------------*/
#define OS_TYPE 3 /* 0:Win32, 1:uITRON4.0, 2:uC/OS-II, 3:FreeRTOS, 4:CMSIS-RTOS */
#if OS_TYPE == 0 /* Win32 */
#include <windows.h>
static HANDLE Mutex[FF_VOLUMES + 1]; /* Table of mutex handle */
#elif OS_TYPE == 1 /* uITRON */
#include "itron.h"
#include "kernel.h"
static mtxid Mutex[FF_VOLUMES + 1]; /* Table of mutex ID */
#elif OS_TYPE == 2 /* uc/OS-II */
#include "includes.h"
static OS_EVENT *Mutex[FF_VOLUMES + 1]; /* Table of mutex pinter */
#elif OS_TYPE == 3 /* FreeRTOS */
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
static SemaphoreHandle_t Mutex[FF_VOLUMES + 1]; /* Table of mutex handle */
#elif OS_TYPE == 4 /* CMSIS-RTOS */
#include "cmsis_os.h"
static osMutexId Mutex[FF_VOLUMES + 1]; /* Table of mutex ID */
#endif
/*------------------------------------------------------------------------*/
/* Create a Mutex */
/*------------------------------------------------------------------------*/
/* This function is called in f_mount function to create a new mutex
/ or semaphore for the volume. When a 0 is returned, the f_mount function
/ fails with FR_INT_ERR.
*/
int ff_mutex_create ( /* Returns 1:Function succeeded or 0:Could not create the mutex */
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
Mutex[vol] = CreateMutex(NULL, FALSE, NULL);
return (int)(Mutex[vol] != INVALID_HANDLE_VALUE);
#elif OS_TYPE == 1 /* uITRON */
T_CMTX cmtx = {TA_TPRI,1};
Mutex[vol] = acre_mtx(&cmtx);
return (int)(Mutex[vol] > 0);
#elif OS_TYPE == 2 /* uC/OS-II */
OS_ERR err;
Mutex[vol] = OSMutexCreate(0, &err);
return (int)(err == OS_NO_ERR);
#elif OS_TYPE == 3 /* FreeRTOS */
Mutex[vol] = xSemaphoreCreateMutex();
return (int)(Mutex[vol] != NULL);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
osMutexDef(cmsis_os_mutex);
Mutex[vol] = osMutexCreate(osMutex(cmsis_os_mutex));
return (int)(Mutex[vol] != NULL);
#endif
}
/*------------------------------------------------------------------------*/
/* Delete a Mutex */
/*------------------------------------------------------------------------*/
/* This function is called in f_mount function to delete a mutex or
/ semaphore of the volume created with ff_mutex_create function.
*/
void ff_mutex_delete ( /* Returns 1:Function succeeded or 0:Could not delete due to an error */
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
CloseHandle(Mutex[vol]);
#elif OS_TYPE == 1 /* uITRON */
del_mtx(Mutex[vol]);
#elif OS_TYPE == 2 /* uC/OS-II */
OS_ERR err;
OSMutexDel(Mutex[vol], OS_DEL_ALWAYS, &err);
#elif OS_TYPE == 3 /* FreeRTOS */
vSemaphoreDelete(Mutex[vol]);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
osMutexDelete(Mutex[vol]);
#endif
}
/*------------------------------------------------------------------------*/
/* Request a Grant to Access the Volume */
/*------------------------------------------------------------------------*/
/* This function is called on enter file functions to lock the volume.
/ When a 0 is returned, the file function fails with FR_TIMEOUT.
*/
int ff_mutex_take ( /* Returns 1:Succeeded or 0:Timeout */
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
return (int)(WaitForSingleObject(Mutex[vol], FF_FS_TIMEOUT) == WAIT_OBJECT_0);
#elif OS_TYPE == 1 /* uITRON */
return (int)(tloc_mtx(Mutex[vol], FF_FS_TIMEOUT) == E_OK);
#elif OS_TYPE == 2 /* uC/OS-II */
OS_ERR err;
OSMutexPend(Mutex[vol], FF_FS_TIMEOUT, &err));
return (int)(err == OS_NO_ERR);
#elif OS_TYPE == 3 /* FreeRTOS */
return (int)(xSemaphoreTake(Mutex[vol], FF_FS_TIMEOUT) == pdTRUE);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
return (int)(osMutexWait(Mutex[vol], FF_FS_TIMEOUT) == osOK);
#endif
}
/*------------------------------------------------------------------------*/
/* Release a Grant to Access the Volume */
/*------------------------------------------------------------------------*/
/* This function is called on leave file functions to unlock the volume.
*/
void ff_mutex_give (
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
ReleaseMutex(Mutex[vol]);
#elif OS_TYPE == 1 /* uITRON */
unl_mtx(Mutex[vol]);
#elif OS_TYPE == 2 /* uC/OS-II */
OSMutexPost(Mutex[vol]);
#elif OS_TYPE == 3 /* FreeRTOS */
xSemaphoreGive(Mutex[vol]);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
osMutexRelease(Mutex[vol]);
#endif
}
#endif /* FF_FS_REENTRANT */

@ -0,0 +1,45 @@
/*------------------------------------------------------------------------*/
/* OS Dependent Functions for FatFs */
/* (C)ChaN, 2018 */
/*------------------------------------------------------------------------*/
#include "ff.h"
#include <stdlib.h>
/* This is the implementation for host-side testing on Linux.
* Host-side tests are single threaded, so lock functionality isn't needed.
*/
void* ff_memalloc(UINT msize)
{
return malloc(msize);
}
void ff_memfree(void* mblock)
{
free(mblock);
}
static int* Mutex[FF_VOLUMES + 1]; /* Table of mutex handle */
/* 1:Function succeeded, 0:Could not create the mutex */
int ff_mutex_create(int vol)
{
Mutex[vol] = NULL;
return 1;
}
void ff_mutex_delete(int vol)
{
}
/* 1:Function succeeded, 0:Could not acquire lock */
int ff_mutex_take(int vol)
{
return 1;
}
void ff_mutex_give(int vol)
{
}

@ -0,0 +1,119 @@
# fatfs_create_partition_image
#
# Create a fatfs image of the specified directory on the host during build and optionally
# have the created image flashed using `idf.py flash`
function(fatfs_create_partition_image partition base_dir)
set(options FLASH_IN_PROJECT WL_INIT PRESERVE_TIME)
cmake_parse_arguments(arg "${options}" "" "${multi}" "${ARGN}")
idf_build_get_property(idf_path IDF_PATH)
idf_build_get_property(python PYTHON)
if(arg_WL_INIT)
set(fatfsgen_py ${python} ${idf_path}/components/fatfs/wl_fatfsgen.py)
else()
set(fatfsgen_py ${python} ${idf_path}/components/fatfs/fatfsgen.py)
endif()
if(arg_PRESERVE_TIME)
set(default_datetime_option)
else()
set(default_datetime_option --use_default_datetime)
endif()
if("${CONFIG_FATFS_SECTOR_512}")
set(fatfs_sector_size 512)
elseif("${CONFIG_FATFS_SECTOR_1024}")
set(fatfs_sector_size 1024)
elseif("${CONFIG_FATFS_SECTOR_2048}")
set(fatfs_sector_size 2048)
else()
set(fatfs_sector_size 4096)
endif()
if("${CONFIG_FATFS_LFN_NONE}")
set(fatfs_long_names_option)
elseif("${CONFIG_FATFS_LFN_HEAP}")
set(fatfs_long_names_option --long_name_support)
elseif("${CONFIG_FATFS_LFN_STACK}")
set(fatfs_long_names_option --long_name_support)
endif()
get_filename_component(base_dir_full_path ${base_dir} ABSOLUTE)
partition_table_get_partition_info(size "--partition-name ${partition}" "size")
partition_table_get_partition_info(offset "--partition-name ${partition}" "offset")
if("${size}" AND "${offset}")
set(image_file ${CMAKE_BINARY_DIR}/${partition}.bin)
# Execute FATFS image generation; this always executes as there is no way to specify for CMake to watch for
# contents of the base dir changing.
add_custom_target(fatfs_${partition}_bin ALL
COMMAND ${fatfsgen_py} ${base_dir_full_path}
${fatfs_long_names_option}
${default_datetime_option}
--partition_size ${size}
--output_file ${image_file}
--sector_size "${fatfs_sector_size}"
)
set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY
ADDITIONAL_CLEAN_FILES
${image_file})
idf_component_get_property(main_args esptool_py FLASH_ARGS)
idf_component_get_property(sub_args esptool_py FLASH_SUB_ARGS)
# Last (optional) parameter is the encryption for the target. In our
# case, fatfs is not encrypt so pass FALSE to the function.
esptool_py_flash_target(${partition}-flash "${main_args}" "${sub_args}" ALWAYS_PLAINTEXT)
esptool_py_flash_to_partition(${partition}-flash "${partition}" "${image_file}")
add_dependencies(${partition}-flash fatfs_${partition}_bin)
if(arg_FLASH_IN_PROJECT)
esptool_py_flash_to_partition(flash "${partition}" "${image_file}")
add_dependencies(flash fatfs_${partition}_bin)
endif()
else()
set(message "Failed to create FATFS image for partition '${partition}'. "
"Check project configuration if using the correct partition table file.")
fail_at_build_time(fatfs_${partition}_bin "${message}")
endif()
endfunction()
function(fatfs_create_rawflash_image partition base_dir)
set(options FLASH_IN_PROJECT PRESERVE_TIME)
cmake_parse_arguments(arg "${options}" "" "${multi}" "${ARGN}")
if(arg_FLASH_IN_PROJECT)
if(arg_PRESERVE_TIME)
fatfs_create_partition_image(${partition} ${base_dir} FLASH_IN_PROJECT PRESERVE_TIME)
else()
fatfs_create_partition_image(${partition} ${base_dir} FLASH_IN_PROJECT)
endif()
else()
if(arg_PRESERVE_TIME)
fatfs_create_partition_image(${partition} ${base_dir} PRESERVE_TIME)
else()
fatfs_create_partition_image(${partition} ${base_dir})
endif()
endif()
endfunction()
function(fatfs_create_spiflash_image partition base_dir)
set(options FLASH_IN_PROJECT PRESERVE_TIME)
cmake_parse_arguments(arg "${options}" "" "${multi}" "${ARGN}")
if(arg_FLASH_IN_PROJECT)
if(arg_PRESERVE_TIME)
fatfs_create_partition_image(${partition} ${base_dir} FLASH_IN_PROJECT WL_INIT PRESERVE_TIME)
else()
fatfs_create_partition_image(${partition} ${base_dir} FLASH_IN_PROJECT WL_INIT)
endif()
else()
if(arg_PRESERVE_TIME)
fatfs_create_partition_image(${partition} ${base_dir} WL_INIT PRESERVE_TIME)
else()
fatfs_create_partition_image(${partition} ${base_dir} WL_INIT)
endif()
endif()
endfunction()

@ -0,0 +1,368 @@
----------------------------------------------------------------------------
Revision history of FatFs module
----------------------------------------------------------------------------
R0.00 (February 26, 2006)
Prototype.
R0.01 (April 29, 2006)
The first release.
R0.02 (June 01, 2006)
Added FAT12 support.
Removed unbuffered mode.
Fixed a problem on small (<32M) partition.
R0.02a (June 10, 2006)
Added a configuration option (_FS_MINIMUM).
R0.03 (September 22, 2006)
Added f_rename().
Changed option _FS_MINIMUM to _FS_MINIMIZE.
R0.03a (December 11, 2006)
Improved cluster scan algorithm to write files fast.
Fixed f_mkdir() creates incorrect directory on FAT32.
R0.04 (February 04, 2007)
Added f_mkfs().
Supported multiple drive system.
Changed some interfaces for multiple drive system.
Changed f_mountdrv() to f_mount().
R0.04a (April 01, 2007)
Supported multiple partitions on a physical drive.
Added a capability of extending file size to f_lseek().
Added minimization level 3.
Fixed an endian sensitive code in f_mkfs().
R0.04b (May 05, 2007)
Added a configuration option _USE_NTFLAG.
Added FSINFO support.
Fixed DBCS name can result FR_INVALID_NAME.
Fixed short seek (<= csize) collapses the file object.
R0.05 (August 25, 2007)
Changed arguments of f_read(), f_write() and f_mkfs().
Fixed f_mkfs() on FAT32 creates incorrect FSINFO.
Fixed f_mkdir() on FAT32 creates incorrect directory.
R0.05a (February 03, 2008)
Added f_truncate() and f_utime().
Fixed off by one error at FAT sub-type determination.
Fixed btr in f_read() can be mistruncated.
Fixed cached sector is not flushed when create and close without write.
R0.06 (April 01, 2008)
Added fputc(), fputs(), fprintf() and fgets().
Improved performance of f_lseek() on moving to the same or following cluster.
R0.07 (April 01, 2009)
Merged Tiny-FatFs as a configuration option. (_FS_TINY)
Added long file name feature. (_USE_LFN)
Added multiple code page feature. (_CODE_PAGE)
Added re-entrancy for multitask operation. (_FS_REENTRANT)
Added auto cluster size selection to f_mkfs().
Added rewind option to f_readdir().
Changed result code of critical errors.
Renamed string functions to avoid name collision.
R0.07a (April 14, 2009)
Septemberarated out OS dependent code on reentrant cfg.
Added multiple sector size feature.
R0.07c (June 21, 2009)
Fixed f_unlink() can return FR_OK on error.
Fixed wrong cache control in f_lseek().
Added relative path feature.
Added f_chdir() and f_chdrive().
Added proper case conversion to extended character.
R0.07e (November 03, 2009)
Septemberarated out configuration options from ff.h to ffconf.h.
Fixed f_unlink() fails to remove a sub-directory on _FS_RPATH.
Fixed name matching error on the 13 character boundary.
Added a configuration option, _LFN_UNICODE.
Changed f_readdir() to return the SFN with always upper case on non-LFN cfg.
R0.08 (May 15, 2010)
Added a memory configuration option. (_USE_LFN = 3)
Added file lock feature. (_FS_SHARE)
Added fast seek feature. (_USE_FASTSEEK)
Changed some types on the API, XCHAR->TCHAR.
Changed .fname in the FILINFO structure on Unicode cfg.
String functions support UTF-8 encoding files on Unicode cfg.
R0.08a (August 16, 2010)
Added f_getcwd(). (_FS_RPATH = 2)
Added sector erase feature. (_USE_ERASE)
Moved file lock semaphore table from fs object to the bss.
Fixed f_mkfs() creates wrong FAT32 volume.
R0.08b (January 15, 2011)
Fast seek feature is also applied to f_read() and f_write().
f_lseek() reports required table size on creating CLMP.
Extended format syntax of f_printf().
Ignores duplicated directory separators in given path name.
R0.09 (September 06, 2011)
f_mkfs() supports multiple partition to complete the multiple partition feature.
Added f_fdisk().
R0.09a (August 27, 2012)
Changed f_open() and f_opendir() reject null object pointer to avoid crash.
Changed option name _FS_SHARE to _FS_LOCK.
Fixed assertion failure due to OS/2 EA on FAT12/16 volume.
R0.09b (January 24, 2013)
Added f_setlabel() and f_getlabel().
R0.10 (October 02, 2013)
Added selection of character encoding on the file. (_STRF_ENCODE)
Added f_closedir().
Added forced full FAT scan for f_getfree(). (_FS_NOFSINFO)
Added forced mount feature with changes of f_mount().
Improved behavior of volume auto detection.
Improved write throughput of f_puts() and f_printf().
Changed argument of f_chdrive(), f_mkfs(), disk_read() and disk_write().
Fixed f_write() can be truncated when the file size is close to 4GB.
Fixed f_open(), f_mkdir() and f_setlabel() can return incorrect value on error.
R0.10a (January 15, 2014)
Added arbitrary strings as drive number in the path name. (_STR_VOLUME_ID)
Added a configuration option of minimum sector size. (_MIN_SS)
2nd argument of f_rename() can have a drive number and it will be ignored.
Fixed f_mount() with forced mount fails when drive number is >= 1. (appeared at R0.10)
Fixed f_close() invalidates the file object without volume lock.
Fixed f_closedir() returns but the volume lock is left acquired. (appeared at R0.10)
Fixed creation of an entry with LFN fails on too many SFN collisions. (appeared at R0.07)
R0.10b (May 19, 2014)
Fixed a hard error in the disk I/O layer can collapse the directory entry.
Fixed LFN entry is not deleted when delete/rename an object with lossy converted SFN. (appeared at R0.07)
R0.10c (November 09, 2014)
Added a configuration option for the platforms without RTC. (_FS_NORTC)
Changed option name _USE_ERASE to _USE_TRIM.
Fixed volume label created by Mac OS X cannot be retrieved with f_getlabel(). (appeared at R0.09b)
Fixed a potential problem of FAT access that can appear on disk error.
Fixed null pointer dereference on attempting to delete the root direcotry. (appeared at R0.08)
R0.11 (February 09, 2015)
Added f_findfirst(), f_findnext() and f_findclose(). (_USE_FIND)
Fixed f_unlink() does not remove cluster chain of the file. (appeared at R0.10c)
Fixed _FS_NORTC option does not work properly. (appeared at R0.10c)
R0.11a (September 05, 2015)
Fixed wrong media change can lead a deadlock at thread-safe configuration.
Added code page 771, 860, 861, 863, 864, 865 and 869. (_CODE_PAGE)
Removed some code pages actually not exist on the standard systems. (_CODE_PAGE)
Fixed errors in the case conversion teble of code page 437 and 850 (ff.c).
Fixed errors in the case conversion teble of Unicode (cc*.c).
R0.12 (April 12, 2016)
Added support for exFAT file system. (_FS_EXFAT)
Added f_expand(). (_USE_EXPAND)
Changed some members in FINFO structure and behavior of f_readdir().
Added an option _USE_CHMOD.
Removed an option _WORD_ACCESS.
Fixed errors in the case conversion table of Unicode (cc*.c).
R0.12a (July 10, 2016)
Added support for creating exFAT volume with some changes of f_mkfs().
Added a file open method FA_OPEN_APPEND. An f_lseek() following f_open() is no longer needed.
f_forward() is available regardless of _FS_TINY.
Fixed f_mkfs() creates wrong volume. (appeared at R0.12)
Fixed wrong memory read in create_name(). (appeared at R0.12)
Fixed compilation fails at some configurations, _USE_FASTSEEK and _USE_FORWARD.
R0.12b (September 04, 2016)
Made f_rename() be able to rename objects with the same name but case.
Fixed an error in the case conversion teble of code page 866. (ff.c)
Fixed writing data is truncated at the file offset 4GiB on the exFAT volume. (appeared at R0.12)
Fixed creating a file in the root directory of exFAT volume can fail. (appeared at R0.12)
Fixed f_mkfs() creating exFAT volume with too small cluster size can collapse unallocated memory. (appeared at R0.12)
Fixed wrong object name can be returned when read directory at Unicode cfg. (appeared at R0.12)
Fixed large file allocation/removing on the exFAT volume collapses allocation bitmap. (appeared at R0.12)
Fixed some internal errors in f_expand() and f_lseek(). (appeared at R0.12)
R0.12c (March 04, 2017)
Improved write throughput at the fragmented file on the exFAT volume.
Made memory usage for exFAT be able to be reduced as decreasing _MAX_LFN.
Fixed successive f_getfree() can return wrong count on the FAT12/16 volume. (appeared at R0.12)
Fixed configuration option _VOLUMES cannot be set 10. (appeared at R0.10c)
R0.13 (May 21, 2017)
Changed heading character of configuration keywords "_" to "FF_".
Removed ASCII-only configuration, FF_CODE_PAGE = 1. Use FF_CODE_PAGE = 437 instead.
Added f_setcp(), run-time code page configuration. (FF_CODE_PAGE = 0)
Improved cluster allocation time on stretch a deep buried cluster chain.
Improved processing time of f_mkdir() with large cluster size by using FF_USE_LFN = 3.
Improved NoFatChain flag of the fragmented file to be set after it is truncated and got contiguous.
Fixed archive attribute is left not set when a file on the exFAT volume is renamed. (appeared at R0.12)
Fixed exFAT FAT entry can be collapsed when write or lseek operation to the existing file is done. (appeared at R0.12c)
Fixed creating a file can fail when a new cluster allocation to the exFAT directory occures. (appeared at R0.12c)
R0.13a (October 14, 2017)
Added support for UTF-8 encoding on the API. (FF_LFN_UNICODE = 2)
Added options for file name output buffer. (FF_LFN_BUF, FF_SFN_BUF).
Added dynamic memory allocation option for working buffer of f_mkfs() and f_fdisk().
Fixed f_fdisk() and f_mkfs() create the partition table with wrong CHS parameters. (appeared at R0.09)
Fixed f_unlink() can cause lost clusters at fragmented file on the exFAT volume. (appeared at R0.12c)
Fixed f_setlabel() rejects some valid characters for exFAT volume. (appeared at R0.12)
R0.13b (April 07, 2018)
Added support for UTF-32 encoding on the API. (FF_LFN_UNICODE = 3)
Added support for Unix style volume ID. (FF_STR_VOLUME_ID = 2)
Fixed accesing any object on the exFAT root directory beyond the cluster boundary can fail. (appeared at R0.12c)
Fixed f_setlabel() does not reject some invalid characters. (appeared at R0.09b)
R0.13c (October 14, 2018)
Supported stdint.h for C99 and later. (integer.h was included in ff.h)
Fixed reading a directory gets infinite loop when the last directory entry is not empty. (appeared at R0.12)
Fixed creating a sub-directory in the fragmented sub-directory on the exFAT volume collapses FAT chain of the parent directory. (appeared at R0.12)
Fixed f_getcwd() cause output buffer overrun when the buffer has a valid drive number. (appeared at R0.13b)
R0.14 (October 14, 2019)
Added support for 64-bit LBA and GUID partition table (FF_LBA64 = 1)
Changed some API functions, f_mkfs() and f_fdisk().
Fixed f_open() function cannot find the file with file name in length of FF_MAX_LFN characters.
Fixed f_readdir() function cannot retrieve long file names in length of FF_MAX_LFN - 1 characters.
Fixed f_readdir() function returns file names with wrong case conversion. (appeared at R0.12)
Fixed f_mkfs() function can fail to create exFAT volume in the second partition. (appeared at R0.12)
R0.14a (December 5, 2020)
Limited number of recursive calls in f_findnext().
Fixed old floppy disks formatted with MS-DOS 2.x and 3.x cannot be mounted.
Fixed some compiler warnings.
R0.14b (April 17, 2021)
Made FatFs uses standard library <string.h> for copy, compare and search instead of built-in string functions.
Added support for long long integer and floating point to f_printf(). (FF_STRF_LLI and FF_STRF_FP)
Made path name parser ignore the terminating separator to allow "dir/".
Improved the compatibility in Unix style path name feature.
Fixed the file gets dead-locked when f_open() failed with some conditions. (appeared at R0.12a)
Fixed f_mkfs() can create wrong exFAT volume due to a timing dependent error. (appeared at R0.12)
Fixed code page 855 cannot be set by f_setcp().
Fixed some compiler warnings.
R0.15 (November 6, 2022)
Changed user provided synchronization functions in order to completely eliminate the platform dependency from FatFs code.
FF_SYNC_t is removed from the configuration options.
Fixed a potential error in f_mount when FF_FS_REENTRANT.
Fixed file lock control FF_FS_LOCK is not mutal excluded when FF_FS_REENTRANT && FF_VOLUMES > 1 is true.
Fixed f_mkfs() creates broken exFAT volume when the size of volume is >= 2^32 sectors.
Fixed string functions cannot write the unicode characters not in BMP when FF_LFN_UNICODE == 2 (UTF-8).
Fixed a compatibility issue in identification of GPT header.

@ -0,0 +1,20 @@
FatFs Module Source Files R0.15
FILES
00readme.txt This file.
00history.txt Revision history.
ff.c FatFs module.
ffconf.h Configuration file of FatFs module.
ff.h Common include file for FatFs and application module.
diskio.h Common include file for FatFs and disk I/O module.
diskio.c An example of glue function to attach existing disk I/O module to FatFs.
ffunicode.c Optional Unicode utility functions.
ffsystem.c An example of optional O/S related functions.
Low level disk I/O module is not included in this archive because the FatFs
module is only a generic file system layer and it does not depend on any specific
storage device. You need to provide a low level disk I/O module written to
control the storage device that attached to the target system.

@ -0,0 +1,228 @@
/*-----------------------------------------------------------------------*/
/* Low level disk I/O module SKELETON for FatFs (C)ChaN, 2019 */
/*-----------------------------------------------------------------------*/
/* If a working storage control module is available, it should be */
/* attached to the FatFs via a glue function rather than modifying it. */
/* This is an example of glue functions to attach various exsisting */
/* storage control modules to the FatFs module with a defined API. */
/*-----------------------------------------------------------------------*/
#include "ff.h" /* Obtains integer types */
#include "diskio.h" /* Declarations of disk functions */
/* Definitions of physical drive number for each drive */
#define DEV_RAM 0 /* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC 1 /* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB 2 /* Example: Map USB MSD to physical drive 2 */
/*-----------------------------------------------------------------------*/
/* Get Drive Status */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
int result;
switch (pdrv) {
case DEV_RAM :
result = RAM_disk_status();
// translate the reslut code here
return stat;
case DEV_MMC :
result = MMC_disk_status();
// translate the reslut code here
return stat;
case DEV_USB :
result = USB_disk_status();
// translate the reslut code here
return stat;
}
return STA_NOINIT;
}
/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
int result;
switch (pdrv) {
case DEV_RAM :
result = RAM_disk_initialize();
// translate the reslut code here
return stat;
case DEV_MMC :
result = MMC_disk_initialize();
// translate the reslut code here
return stat;
case DEV_USB :
result = USB_disk_initialize();
// translate the reslut code here
return stat;
}
return STA_NOINIT;
}
/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to read */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// translate the arguments here
result = RAM_disk_read(buff, sector, count);
// translate the reslut code here
return res;
case DEV_MMC :
// translate the arguments here
result = MMC_disk_read(buff, sector, count);
// translate the reslut code here
return res;
case DEV_USB :
// translate the arguments here
result = USB_disk_read(buff, sector, count);
// translate the reslut code here
return res;
}
return RES_PARERR;
}
/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/
#if FF_FS_READONLY == 0
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to write */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// translate the arguments here
result = RAM_disk_write(buff, sector, count);
// translate the reslut code here
return res;
case DEV_MMC :
// translate the arguments here
result = MMC_disk_write(buff, sector, count);
// translate the reslut code here
return res;
case DEV_USB :
// translate the arguments here
result = USB_disk_write(buff, sector, count);
// translate the reslut code here
return res;
}
return RES_PARERR;
}
#endif
/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// Process of the command for the RAM drive
return res;
case DEV_MMC :
// Process of the command for the MMC/SD card
return res;
case DEV_USB :
// Process of the command the USB drive
return res;
}
return RES_PARERR;
}

@ -0,0 +1,79 @@
/*-----------------------------------------------------------------------/
/ Low level disk interface modlue include file (C)ChaN, 2019 /
/-----------------------------------------------------------------------*/
#ifndef _DISKIO_DEFINED
#define _DISKIO_DEFINED
#ifdef __cplusplus
extern "C" {
#endif
#include "ff.h"
/* Status of Disk Functions */
typedef BYTE DSTATUS;
/* Results of Disk Functions */
typedef enum {
RES_OK = 0, /* 0: Successful */
RES_ERROR, /* 1: R/W Error */
RES_WRPRT, /* 2: Write Protected */
RES_NOTRDY, /* 3: Not Ready */
RES_PARERR /* 4: Invalid Parameter */
} DRESULT;
/*---------------------------------------*/
/* Prototypes for disk control functions */
DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count);
DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
/* Disk Status Bits (DSTATUS) */
#define STA_NOINIT 0x01 /* Drive not initialized */
#define STA_NODISK 0x02 /* No medium in the drive */
#define STA_PROTECT 0x04 /* Write protected */
/* Command code for disk_ioctrl fucntion */
/* Generic command (Used by FatFs) */
#define CTRL_SYNC 0 /* Complete pending write process (needed at FF_FS_READONLY == 0) */
#define GET_SECTOR_COUNT 1 /* Get media size (needed at FF_USE_MKFS == 1) */
#define GET_SECTOR_SIZE 2 /* Get sector size (needed at FF_MAX_SS != FF_MIN_SS) */
#define GET_BLOCK_SIZE 3 /* Get erase block size (needed at FF_USE_MKFS == 1) */
#define CTRL_TRIM 4 /* Inform device that the data on the block of sectors is no longer used (needed at FF_USE_TRIM == 1) */
/* Generic command (Not used by FatFs) */
#define CTRL_POWER 5 /* Get/Set power status */
#define CTRL_LOCK 6 /* Lock/Unlock media removal */
#define CTRL_EJECT 7 /* Eject media */
#define CTRL_FORMAT 8 /* Create physical format on the media */
/* MMC/SDC specific ioctl command */
#define MMC_GET_TYPE 10 /* Get card type */
#define MMC_GET_CSD 11 /* Get CSD */
#define MMC_GET_CID 12 /* Get CID */
#define MMC_GET_OCR 13 /* Get OCR */
#define MMC_GET_SDSTAT 14 /* Get SD status */
#define ISDIO_READ 55 /* Read data form SD iSDIO register */
#define ISDIO_WRITE 56 /* Write data to SD iSDIO register */
#define ISDIO_MRITE 57 /* Masked write data to SD iSDIO register */
/* ATA/CF specific ioctl command */
#define ATA_GET_REV 20 /* Get F/W revision */
#define ATA_GET_MODEL 21 /* Get model name */
#define ATA_GET_SN 22 /* Get serial number */
#ifdef __cplusplus
}
#endif
#endif

File diff suppressed because it is too large Load Diff

@ -0,0 +1,429 @@
/*----------------------------------------------------------------------------/
/ FatFs - Generic FAT Filesystem module R0.15 /
/-----------------------------------------------------------------------------/
/
/ Copyright (C) 2022, ChaN, all right reserved.
/
/ FatFs module is an open source software. Redistribution and use of FatFs in
/ source and binary forms, with or without modification, are permitted provided
/ that the following condition is met:
/ 1. Redistributions of source code must retain the above copyright notice,
/ this condition and the following disclaimer.
/
/ This software is provided by the copyright holder and contributors "AS IS"
/ and any warranties related to this software are DISCLAIMED.
/ The copyright owner or contributors be NOT LIABLE for any damages caused
/ by use of this software.
/
/----------------------------------------------------------------------------*/
#ifndef FF_DEFINED
#define FF_DEFINED 80286 /* Revision ID */
#ifdef __cplusplus
extern "C" {
#endif
#include "ffconf.h" /* FatFs configuration options */
#if FF_DEFINED != FFCONF_DEF
#error Wrong configuration file (ffconf.h).
#endif
/* Integer types used for FatFs API */
#if defined(_WIN32) /* Windows VC++ (for development only) */
#define FF_INTDEF 2
#include <windows.h>
typedef unsigned __int64 QWORD;
#include <float.h>
#define isnan(v) _isnan(v)
#define isinf(v) (!_finite(v))
#elif (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__cplusplus) /* C99 or later */
#define FF_INTDEF 2
#include <stdint.h>
typedef unsigned int UINT; /* int must be 16-bit or 32-bit */
typedef unsigned char BYTE; /* char must be 8-bit */
typedef uint16_t WORD; /* 16-bit unsigned integer */
typedef uint32_t DWORD; /* 32-bit unsigned integer */
typedef uint64_t QWORD; /* 64-bit unsigned integer */
typedef WORD WCHAR; /* UTF-16 character type */
#else /* Earlier than C99 */
#define FF_INTDEF 1
typedef unsigned int UINT; /* int must be 16-bit or 32-bit */
typedef unsigned char BYTE; /* char must be 8-bit */
typedef unsigned short WORD; /* 16-bit unsigned integer */
typedef unsigned long DWORD; /* 32-bit unsigned integer */
typedef WORD WCHAR; /* UTF-16 character type */
#endif
/* Type of file size and LBA variables */
#if FF_FS_EXFAT
#if FF_INTDEF != 2
#error exFAT feature wants C99 or later
#endif
typedef QWORD FSIZE_t;
#if FF_LBA64
typedef QWORD LBA_t;
#else
typedef DWORD LBA_t;
#endif
#else
#if FF_LBA64
#error exFAT needs to be enabled when enable 64-bit LBA
#endif
typedef DWORD FSIZE_t;
typedef DWORD LBA_t;
#endif
/* Type of path name strings on FatFs API (TCHAR) */
#if FF_USE_LFN && FF_LFN_UNICODE == 1 /* Unicode in UTF-16 encoding */
typedef WCHAR TCHAR;
#define _T(x) L ## x
#define _TEXT(x) L ## x
#elif FF_USE_LFN && FF_LFN_UNICODE == 2 /* Unicode in UTF-8 encoding */
typedef char TCHAR;
#define _T(x) u8 ## x
#define _TEXT(x) u8 ## x
#elif FF_USE_LFN && FF_LFN_UNICODE == 3 /* Unicode in UTF-32 encoding */
typedef DWORD TCHAR;
#define _T(x) U ## x
#define _TEXT(x) U ## x
#elif FF_USE_LFN && (FF_LFN_UNICODE < 0 || FF_LFN_UNICODE > 3)
#error Wrong FF_LFN_UNICODE setting
#else /* ANSI/OEM code in SBCS/DBCS */
typedef char TCHAR;
#define _T(x) x
#define _TEXT(x) x
#endif
/* Definitions of volume management */
#if FF_MULTI_PARTITION /* Multiple partition configuration */
typedef struct {
BYTE pd; /* Physical drive number */
BYTE pt; /* Partition: 0:Auto detect, 1-4:Forced partition) */
} PARTITION;
extern const PARTITION VolToPart[]; /* Volume - Partition mapping table */
#endif
#if FF_STR_VOLUME_ID
#ifndef FF_VOLUME_STRS
extern const char* VolumeStr[FF_VOLUMES]; /* User defied volume ID */
#endif
#endif
/* Filesystem object structure (FATFS) */
typedef struct {
BYTE fs_type; /* Filesystem type (0:not mounted) */
BYTE pdrv; /* Volume hosting physical drive */
BYTE ldrv; /* Logical drive number (used only when FF_FS_REENTRANT) */
BYTE n_fats; /* Number of FATs (1 or 2) */
BYTE wflag; /* win[] status (b0:dirty) */
BYTE fsi_flag; /* FSINFO status (b7:disabled, b0:dirty) */
WORD id; /* Volume mount ID */
WORD n_rootdir; /* Number of root directory entries (FAT12/16) */
WORD csize; /* Cluster size [sectors] */
#if FF_MAX_SS != FF_MIN_SS
WORD ssize; /* Sector size (512, 1024, 2048 or 4096) */
#endif
#if FF_USE_LFN
WCHAR* lfnbuf; /* LFN working buffer */
#endif
#if FF_FS_EXFAT
BYTE* dirbuf; /* Directory entry block scratchpad buffer for exFAT */
#endif
#if !FF_FS_READONLY
DWORD last_clst; /* Last allocated cluster */
DWORD free_clst; /* Number of free clusters */
#endif
#if FF_FS_RPATH
DWORD cdir; /* Current directory start cluster (0:root) */
#if FF_FS_EXFAT
DWORD cdc_scl; /* Containing directory start cluster (invalid when cdir is 0) */
DWORD cdc_size; /* b31-b8:Size of containing directory, b7-b0: Chain status */
DWORD cdc_ofs; /* Offset in the containing directory (invalid when cdir is 0) */
#endif
#endif
DWORD n_fatent; /* Number of FAT entries (number of clusters + 2) */
DWORD fsize; /* Number of sectors per FAT */
LBA_t volbase; /* Volume base sector */
LBA_t fatbase; /* FAT base sector */
LBA_t dirbase; /* Root directory base sector (FAT12/16) or cluster (FAT32/exFAT) */
LBA_t database; /* Data base sector */
#if FF_FS_EXFAT
LBA_t bitbase; /* Allocation bitmap base sector */
#endif
LBA_t winsect; /* Current sector appearing in the win[] */
BYTE win[FF_MAX_SS]; /* Disk access window for Directory, FAT (and file data at tiny cfg) */
} FATFS;
/* Object ID and allocation information (FFOBJID) */
typedef struct {
FATFS* fs; /* Pointer to the hosting volume of this object */
WORD id; /* Hosting volume's mount ID */
BYTE attr; /* Object attribute */
BYTE stat; /* Object chain status (b1-0: =0:not contiguous, =2:contiguous, =3:fragmented in this session, b2:sub-directory stretched) */
DWORD sclust; /* Object data start cluster (0:no cluster or root directory) */
FSIZE_t objsize; /* Object size (valid when sclust != 0) */
#if FF_FS_EXFAT
DWORD n_cont; /* Size of first fragment - 1 (valid when stat == 3) */
DWORD n_frag; /* Size of last fragment needs to be written to FAT (valid when not zero) */
DWORD c_scl; /* Containing directory start cluster (valid when sclust != 0) */
DWORD c_size; /* b31-b8:Size of containing directory, b7-b0: Chain status (valid when c_scl != 0) */
DWORD c_ofs; /* Offset in the containing directory (valid when file object and sclust != 0) */
#endif
#if FF_FS_LOCK
UINT lockid; /* File lock ID origin from 1 (index of file semaphore table Files[]) */
#endif
} FFOBJID;
/* File object structure (FIL) */
typedef struct {
FFOBJID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */
BYTE flag; /* File status flags */
BYTE err; /* Abort flag (error code) */
FSIZE_t fptr; /* File read/write pointer (Zeroed on file open) */
DWORD clust; /* Current cluster of fpter (invalid when fptr is 0) */
LBA_t sect; /* Sector number appearing in buf[] (0:invalid) */
#if !FF_FS_READONLY
LBA_t dir_sect; /* Sector number containing the directory entry (not used at exFAT) */
BYTE* dir_ptr; /* Pointer to the directory entry in the win[] (not used at exFAT) */
#endif
#if FF_USE_FASTSEEK
DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */
#endif
#if !FF_FS_TINY
BYTE buf[FF_MAX_SS]; /* File private data read/write window */
#endif
} FIL;
/* Directory object structure (FF_DIR) */
typedef struct {
FFOBJID obj; /* Object identifier */
DWORD dptr; /* Current read/write offset */
DWORD clust; /* Current cluster */
LBA_t sect; /* Current sector (0:Read operation has terminated) */
BYTE* dir; /* Pointer to the directory item in the win[] */
BYTE fn[12]; /* SFN (in/out) {body[8],ext[3],status[1]} */
#if FF_USE_LFN
DWORD blk_ofs; /* Offset of current entry block being processed (0xFFFFFFFF:Invalid) */
#endif
#if FF_USE_FIND
const TCHAR* pat; /* Pointer to the name matching pattern */
#endif
} FF_DIR;
/* File information structure (FILINFO) */
typedef struct {
FSIZE_t fsize; /* File size */
WORD fdate; /* Modified date */
WORD ftime; /* Modified time */
BYTE fattrib; /* File attribute */
#if FF_USE_LFN
TCHAR altname[FF_SFN_BUF + 1];/* Alternative file name */
TCHAR fname[FF_LFN_BUF + 1]; /* Primary file name */
#else
TCHAR fname[12 + 1]; /* File name */
#endif
} FILINFO;
/* Format parameter structure (MKFS_PARM) */
typedef struct {
BYTE fmt; /* Format option (FM_FAT, FM_FAT32, FM_EXFAT and FM_SFD) */
BYTE n_fat; /* Number of FATs */
UINT align; /* Data area alignment (sector) */
UINT n_root; /* Number of root directory entries */
DWORD au_size; /* Cluster size (byte) */
} MKFS_PARM;
/* File function return code (FRESULT) */
typedef enum {
FR_OK = 0, /* (0) Succeeded */
FR_DISK_ERR, /* (1) A hard error occurred in the low level disk I/O layer */
FR_INT_ERR, /* (2) Assertion failed */
FR_NOT_READY, /* (3) The physical drive cannot work */
FR_NO_FILE, /* (4) Could not find the file */
FR_NO_PATH, /* (5) Could not find the path */
FR_INVALID_NAME, /* (6) The path name format is invalid */
FR_DENIED, /* (7) Access denied due to prohibited access or directory full */
FR_EXIST, /* (8) Access denied due to prohibited access */
FR_INVALID_OBJECT, /* (9) The file/directory object is invalid */
FR_WRITE_PROTECTED, /* (10) The physical drive is write protected */
FR_INVALID_DRIVE, /* (11) The logical drive number is invalid */
FR_NOT_ENABLED, /* (12) The volume has no work area */
FR_NO_FILESYSTEM, /* (13) There is no valid FAT volume */
FR_MKFS_ABORTED, /* (14) The f_mkfs() aborted due to any problem */
FR_TIMEOUT, /* (15) Could not get a grant to access the volume within defined period */
FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy */
FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated */
FR_TOO_MANY_OPEN_FILES, /* (18) Number of open files > FF_FS_LOCK */
FR_INVALID_PARAMETER /* (19) Given parameter is invalid */
} FRESULT;
/*--------------------------------------------------------------*/
/* FatFs Module Application Interface */
/*--------------------------------------------------------------*/
FRESULT f_open (FIL* fp, const TCHAR* path, BYTE mode); /* Open or create a file */
FRESULT f_close (FIL* fp); /* Close an open file object */
FRESULT f_read (FIL* fp, void* buff, UINT btr, UINT* br); /* Read data from the file */
FRESULT f_write (FIL* fp, const void* buff, UINT btw, UINT* bw); /* Write data to the file */
FRESULT f_lseek (FIL* fp, FSIZE_t ofs); /* Move file pointer of the file object */
FRESULT f_truncate (FIL* fp); /* Truncate the file */
FRESULT f_sync (FIL* fp); /* Flush cached data of the writing file */
FRESULT f_opendir (FF_DIR* dp, const TCHAR* path); /* Open a directory */
FRESULT f_closedir (FF_DIR* dp); /* Close an open directory */
FRESULT f_readdir (FF_DIR* dp, FILINFO* fno); /* Read a directory item */
FRESULT f_findfirst (FF_DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern); /* Find first file */
FRESULT f_findnext (FF_DIR* dp, FILINFO* fno); /* Find next file */
FRESULT f_mkdir (const TCHAR* path); /* Create a sub directory */
FRESULT f_unlink (const TCHAR* path); /* Delete an existing file or directory */
FRESULT f_rename (const TCHAR* path_old, const TCHAR* path_new); /* Rename/Move a file or directory */
FRESULT f_stat (const TCHAR* path, FILINFO* fno); /* Get file status */
FRESULT f_chmod (const TCHAR* path, BYTE attr, BYTE mask); /* Change attribute of a file/dir */
FRESULT f_utime (const TCHAR* path, const FILINFO* fno); /* Change timestamp of a file/dir */
FRESULT f_chdir (const TCHAR* path); /* Change current directory */
FRESULT f_chdrive (const TCHAR* path); /* Change current drive */
FRESULT f_getcwd (TCHAR* buff, UINT len); /* Get current directory */
FRESULT f_getfree (const TCHAR* path, DWORD* nclst, FATFS** fatfs); /* Get number of free clusters on the drive */
FRESULT f_getlabel (const TCHAR* path, TCHAR* label, DWORD* vsn); /* Get volume label */
FRESULT f_setlabel (const TCHAR* label); /* Set volume label */
FRESULT f_forward (FIL* fp, UINT(*func)(const BYTE*,UINT), UINT btf, UINT* bf); /* Forward data to the stream */
FRESULT f_expand (FIL* fp, FSIZE_t fsz, BYTE opt); /* Allocate a contiguous block to the file */
FRESULT f_mount (FATFS* fs, const TCHAR* path, BYTE opt); /* Mount/Unmount a logical drive */
FRESULT f_mkfs (const TCHAR* path, const MKFS_PARM* opt, void* work, UINT len); /* Create a FAT volume */
FRESULT f_fdisk (BYTE pdrv, const LBA_t ptbl[], void* work); /* Divide a physical drive into some partitions */
FRESULT f_setcp (WORD cp); /* Set current code page */
int f_putc (TCHAR c, FIL* fp); /* Put a character to the file */
int f_puts (const TCHAR* str, FIL* cp); /* Put a string to the file */
int f_printf (FIL* fp, const TCHAR* str, ...); /* Put a formatted string to the file */
TCHAR* f_gets (TCHAR* buff, int len, FIL* fp); /* Get a string from the file */
/* Some API fucntions are implemented as macro */
#define f_eof(fp) ((int)((fp)->fptr == (fp)->obj.objsize))
#define f_error(fp) ((fp)->err)
#define f_tell(fp) ((fp)->fptr)
#define f_size(fp) ((fp)->obj.objsize)
#define f_rewind(fp) f_lseek((fp), 0)
#define f_rewinddir(dp) f_readdir((dp), 0)
#define f_rmdir(path) f_unlink(path)
#define f_unmount(path) f_mount(0, path, 0)
/*--------------------------------------------------------------*/
/* Additional Functions */
/*--------------------------------------------------------------*/
/* RTC function (provided by user) */
#if !FF_FS_READONLY && !FF_FS_NORTC
DWORD get_fattime (void); /* Get current time */
#endif
/* LFN support functions (defined in ffunicode.c) */
#if FF_USE_LFN >= 1
WCHAR ff_oem2uni (WCHAR oem, WORD cp); /* OEM code to Unicode conversion */
WCHAR ff_uni2oem (DWORD uni, WORD cp); /* Unicode to OEM code conversion */
DWORD ff_wtoupper (DWORD uni); /* Unicode upper-case conversion */
#endif
/* O/S dependent functions (samples available in ffsystem.c) */
#if FF_USE_LFN == 3 /* Dynamic memory allocation */
void* ff_memalloc (UINT msize); /* Allocate memory block */
void ff_memfree (void* mblock); /* Free memory block */
#endif
#if FF_FS_REENTRANT /* Sync functions */
int ff_mutex_create (int vol); /* Create a sync object */
void ff_mutex_delete (int vol); /* Delete a sync object */
int ff_mutex_take (int vol); /* Lock sync object */
void ff_mutex_give (int vol); /* Unlock sync object */
#endif
/*--------------------------------------------------------------*/
/* Flags and Offset Address */
/*--------------------------------------------------------------*/
/* File access mode and open method flags (3rd argument of f_open) */
#define FA_READ 0x01
#define FA_WRITE 0x02
#define FA_OPEN_EXISTING 0x00
#define FA_CREATE_NEW 0x04
#define FA_CREATE_ALWAYS 0x08
#define FA_OPEN_ALWAYS 0x10
#define FA_OPEN_APPEND 0x30
/* Fast seek controls (2nd argument of f_lseek) */
#define CREATE_LINKMAP ((FSIZE_t)0 - 1)
/* Format options (2nd argument of f_mkfs) */
#define FM_FAT 0x01
#define FM_FAT32 0x02
#define FM_EXFAT 0x04
#define FM_ANY 0x07
#define FM_SFD 0x08
/* Filesystem type (FATFS.fs_type) */
#define FS_FAT12 1
#define FS_FAT16 2
#define FS_FAT32 3
#define FS_EXFAT 4
/* File attribute bits for directory entry (FILINFO.fattrib) */
#define AM_RDO 0x01 /* Read only */
#define AM_HID 0x02 /* Hidden */
#define AM_SYS 0x04 /* System */
#define AM_DIR 0x10 /* Directory */
#define AM_ARC 0x20 /* Archive */
#ifdef __cplusplus
}
#endif
#endif /* FF_DEFINED */

@ -0,0 +1,332 @@
#include "sdkconfig.h"
/*---------------------------------------------------------------------------/
/ Configurations of FatFs Module
/---------------------------------------------------------------------------*/
#define FFCONF_DEF 80286 /* Revision ID */
/*---------------------------------------------------------------------------/
/ Function Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_READONLY 0
/* This option switches read-only configuration. (0:Read/Write or 1:Read-only)
/ Read-only configuration removes writing API functions, f_write(), f_sync(),
/ f_unlink(), f_mkdir(), f_chmod(), f_rename(), f_truncate(), f_getfree()
/ and optional writing functions as well. */
#define FF_FS_MINIMIZE 0
/* This option defines minimization level to remove some basic API functions.
/
/ 0: Basic functions are fully enabled.
/ 1: f_stat(), f_getfree(), f_unlink(), f_mkdir(), f_truncate() and f_rename()
/ are removed.
/ 2: f_opendir(), f_readdir() and f_closedir() are removed in addition to 1.
/ 3: f_lseek() function is removed in addition to 2. */
#define FF_USE_FIND 0
/* This option switches filtered directory read functions, f_findfirst() and
/ f_findnext(). (0:Disable, 1:Enable 2:Enable with matching altname[] too) */
#define FF_USE_MKFS 1
/* This option switches f_mkfs() function. (0:Disable or 1:Enable) */
#define FF_USE_FASTSEEK 1
/* This option switches fast seek function. (0:Disable or 1:Enable) */
#define FF_USE_EXPAND 0
/* This option switches f_expand function. (0:Disable or 1:Enable) */
#define FF_USE_CHMOD 1
/* This option switches attribute manipulation functions, f_chmod() and f_utime().
/ (0:Disable or 1:Enable) Also FF_FS_READONLY needs to be 0 to enable this option. */
#define FF_USE_LABEL 0
/* This option switches volume label functions, f_getlabel() and f_setlabel().
/ (0:Disable or 1:Enable) */
#define FF_USE_FORWARD 1
/* This option switches f_forward() function. (0:Disable or 1:Enable) */
#define FF_USE_STRFUNC 0
#define FF_PRINT_LLI 0
#define FF_PRINT_FLOAT 0
#define FF_STRF_ENCODE 3
/* FF_USE_STRFUNC switches string functions, f_gets(), f_putc(), f_puts() and
/ f_printf().
/
/ 0: Disable. FF_PRINT_LLI, FF_PRINT_FLOAT and FF_STRF_ENCODE have no effect.
/ 1: Enable without LF-CRLF conversion.
/ 2: Enable with LF-CRLF conversion.
/
/ FF_PRINT_LLI = 1 makes f_printf() support long long argument and FF_PRINT_FLOAT = 1/2
/ makes f_printf() support floating point argument. These features want C99 or later.
/ When FF_LFN_UNICODE >= 1 with LFN enabled, string functions convert the character
/ encoding in it. FF_STRF_ENCODE selects assumption of character encoding ON THE FILE
/ to be read/written via those functions.
/
/ 0: ANSI/OEM in current CP
/ 1: Unicode in UTF-16LE
/ 2: Unicode in UTF-16BE
/ 3: Unicode in UTF-8
*/
/*---------------------------------------------------------------------------/
/ Locale and Namespace Configurations
/---------------------------------------------------------------------------*/
#define FF_CODE_PAGE CONFIG_FATFS_CODEPAGE
/* This option specifies the OEM code page to be used on the target system.
/ Incorrect code page setting can cause a file open failure.
/
/ 437 - U.S.
/ 720 - Arabic
/ 737 - Greek
/ 771 - KBL
/ 775 - Baltic
/ 850 - Latin 1
/ 852 - Latin 2
/ 855 - Cyrillic
/ 857 - Turkish
/ 860 - Portuguese
/ 861 - Icelandic
/ 862 - Hebrew
/ 863 - Canadian French
/ 864 - Arabic
/ 865 - Nordic
/ 866 - Russian
/ 869 - Greek 2
/ 932 - Japanese (DBCS)
/ 936 - Simplified Chinese (DBCS)
/ 949 - Korean (DBCS)
/ 950 - Traditional Chinese (DBCS)
/ 0 - Include all code pages above and configured by f_setcp()
*/
#if defined(CONFIG_FATFS_LFN_STACK)
#define FF_USE_LFN 2
#elif defined(CONFIG_FATFS_LFN_HEAP)
#define FF_USE_LFN 3
#else /* CONFIG_FATFS_LFN_NONE */
#define FF_USE_LFN 0
#endif
#ifdef CONFIG_FATFS_MAX_LFN
#define FF_MAX_LFN CONFIG_FATFS_MAX_LFN
#endif
/* The FF_USE_LFN switches the support for LFN (long file name).
/
/ 0: Disable LFN. FF_MAX_LFN has no effect.
/ 1: Enable LFN with static working buffer on the BSS. Always NOT thread-safe.
/ 2: Enable LFN with dynamic working buffer on the STACK.
/ 3: Enable LFN with dynamic working buffer on the HEAP.
/
/ To enable the LFN, ffunicode.c needs to be added to the project. The LFN function
/ requiers certain internal working buffer occupies (FF_MAX_LFN + 1) * 2 bytes and
/ additional (FF_MAX_LFN + 44) / 15 * 32 bytes when exFAT is enabled.
/ The FF_MAX_LFN defines size of the working buffer in UTF-16 code unit and it can
/ be in range of 12 to 255. It is recommended to be set it 255 to fully support LFN
/ specification.
/ When use stack for the working buffer, take care on stack overflow. When use heap
/ memory for the working buffer, memory management functions, ff_memalloc() and
/ ff_memfree() exemplified in ffsystem.c, need to be added to the project. */
#ifdef CONFIG_FATFS_API_ENCODING_UTF_8
#define FF_LFN_UNICODE 2
#else /* CONFIG_FATFS_API_ENCODING_ANSI_OEM */
#define FF_LFN_UNICODE 0
#endif
/* This option switches the character encoding on the API when LFN is enabled.
/
/ 0: ANSI/OEM in current CP (TCHAR = char)
/ 1: Unicode in UTF-16 (TCHAR = WCHAR)
/ 2: Unicode in UTF-8 (TCHAR = char)
/ 3: Unicode in UTF-32 (TCHAR = DWORD)
/
/ Also behavior of string I/O functions will be affected by this option.
/ When LFN is not enabled, this option has no effect. */
#define FF_LFN_BUF 255
#define FF_SFN_BUF 12
/* This set of options defines size of file name members in the FILINFO structure
/ which is used to read out directory items. These values should be suffcient for
/ the file names to read. The maximum possible length of the read file name depends
/ on character encoding. When LFN is not enabled, these options have no effect. */
#define FF_FS_RPATH 0
/* This option configures support for relative path.
/
/ 0: Disable relative path and remove related functions.
/ 1: Enable relative path. f_chdir() and f_chdrive() are available.
/ 2: f_getcwd() function is available in addition to 1.
*/
/*---------------------------------------------------------------------------/
/ Drive/Volume Configurations
/---------------------------------------------------------------------------*/
#define FF_VOLUMES CONFIG_FATFS_VOLUME_COUNT
/* Number of volumes (logical drives) to be used. (1-10) */
#define FF_STR_VOLUME_ID 0
#define FF_VOLUME_STRS "RAM","NAND","CF","SD","SD2","USB","USB2","USB3"
/* FF_STR_VOLUME_ID switches support for volume ID in arbitrary strings.
/ When FF_STR_VOLUME_ID is set to 1 or 2, arbitrary strings can be used as drive
/ number in the path name. FF_VOLUME_STRS defines the volume ID strings for each
/ logical drives. Number of items must not be less than FF_VOLUMES. Valid
/ characters for the volume ID strings are A-Z, a-z and 0-9, however, they are
/ compared in case-insensitive. If FF_STR_VOLUME_ID >= 1 and FF_VOLUME_STRS is
/ not defined, a user defined volume string table is needed as:
/
/ const char* VolumeStr[FF_VOLUMES] = {"ram","flash","sd","usb",...
*/
#define FF_MULTI_PARTITION 1
/* This option switches support for multiple volumes on the physical drive.
/ By default (0), each logical drive number is bound to the same physical drive
/ number and only an FAT volume found on the physical drive will be mounted.
/ When this function is enabled (1), each logical drive number can be bound to
/ arbitrary physical drive and partition listed in the VolToPart[]. Also f_fdisk()
/ function will be available. */
/* SD card sector size */
#define FF_SS_SDCARD 512
/* wear_levelling library sector size */
#define FF_SS_WL CONFIG_WL_SECTOR_SIZE
#define FF_MIN_SS MIN(FF_SS_SDCARD, FF_SS_WL)
#define FF_MAX_SS MAX(FF_SS_SDCARD, FF_SS_WL)
/* This set of options configures the range of sector size to be supported. (512,
/ 1024, 2048 or 4096) Always set both 512 for most systems, generic memory card and
/ harddisk, but a larger value may be required for on-board flash memory and some
/ type of optical media. When FF_MAX_SS is larger than FF_MIN_SS, FatFs is configured
/ for variable sector size mode and disk_ioctl() function needs to implement
/ GET_SECTOR_SIZE command. */
#define FF_LBA64 1
/* This option switches support for 64-bit LBA. (0:Disable or 1:Enable)
/ To enable the 64-bit LBA, also exFAT needs to be enabled. (FF_FS_EXFAT == 1) */
#define FF_MIN_GPT 0x10000000
/* Minimum number of sectors to switch GPT as partitioning format in f_mkfs and
/ f_fdisk function. 0x100000000 max. This option has no effect when FF_LBA64 == 0. */
#define FF_USE_TRIM 1
/* This option switches support for ATA-TRIM. (0:Disable or 1:Enable)
/ To enable Trim function, also CTRL_TRIM command should be implemented to the
/ disk_ioctl() function. */
/*---------------------------------------------------------------------------/
/ System Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_TINY (!CONFIG_FATFS_PER_FILE_CACHE)
/* This option switches tiny buffer configuration. (0:Normal or 1:Tiny)
/ At the tiny configuration, size of file object (FIL) is shrinked FF_MAX_SS bytes.
/ Instead of private sector buffer eliminated from the file object, common sector
/ buffer in the filesystem object (FATFS) is used for the file data transfer. */
#define FF_FS_EXFAT 1
/* This option switches support for exFAT filesystem. (0:Disable or 1:Enable)
/ To enable exFAT, also LFN needs to be enabled. (FF_USE_LFN >= 1)
/ Note that enabling exFAT discards ANSI C (C89) compatibility. */
#define FF_FS_NORTC 0
#define FF_NORTC_MON 1
#define FF_NORTC_MDAY 1
#define FF_NORTC_YEAR 2022
/* The option FF_FS_NORTC switches timestamp feature. If the system does not have
/ an RTC or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable the
/ timestamp feature. Every object modified by FatFs will have a fixed timestamp
/ defined by FF_NORTC_MON, FF_NORTC_MDAY and FF_NORTC_YEAR in local time.
/ To enable timestamp function (FF_FS_NORTC = 0), get_fattime() function need to be
/ added to the project to read current time form real-time clock. FF_NORTC_MON,
/ FF_NORTC_MDAY and FF_NORTC_YEAR have no effect.
/ These options have no effect in read-only configuration (FF_FS_READONLY = 1). */
#define FF_FS_NOFSINFO 0
/* If you need to know correct free space on the FAT32 volume, set bit 0 of this
/ option, and f_getfree() function at the first time after volume mount will force
/ a full FAT scan. Bit 1 controls the use of last allocated cluster number.
/
/ bit0=0: Use free cluster count in the FSINFO if available.
/ bit0=1: Do not trust free cluster count in the FSINFO.
/ bit1=0: Use last allocated cluster number in the FSINFO if available.
/ bit1=1: Do not trust last allocated cluster number in the FSINFO.
*/
#define FF_FS_LOCK CONFIG_FATFS_FS_LOCK
/* The option FF_FS_LOCK switches file lock function to control duplicated file open
/ and illegal operation to open objects. This option must be 0 when FF_FS_READONLY
/ is 1.
/
/ 0: Disable file lock function. To avoid volume corruption, application program
/ should avoid illegal open, remove and rename to the open objects.
/ >0: Enable file lock function. The value defines how many files/sub-directories
/ can be opened simultaneously under file lock control. Note that the file
/ lock control is independent of re-entrancy. */
#define FF_FS_REENTRANT 1
#define FF_FS_TIMEOUT (portMAX_DELAY)
/* The option FF_FS_REENTRANT switches the re-entrancy (thread safe) of the FatFs
/ module itself. Note that regardless of this option, file access to different
/ volume is always re-entrant and volume control functions, f_mount(), f_mkfs()
/ and f_fdisk() function, are always not re-entrant. Only file/directory access
/ to the same volume is under control of this featuer.
/
/ 0: Disable re-entrancy. FF_FS_TIMEOUT have no effect.
/ 1: Enable re-entrancy. Also user provided synchronization handlers,
/ ff_mutex_create(), ff_mutex_delete(), ff_mutex_take() and ff_mutex_give()
/ function, must be added to the project. Samples are available in ffsystem.c.
/
/ The FF_FS_TIMEOUT defines timeout period in unit of O/S time tick.
*/
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
/* Some memory allocation functions are declared here in addition to ff.h, so that
they can be used also by external code when LFN feature is disabled.
*/
void* ff_memalloc (unsigned msize);
void ff_memfree(void*);
/*--- End of configuration options ---*/
/* Redefine names of disk IO functions to prevent name collisions */
#define disk_initialize ff_disk_initialize
#define disk_status ff_disk_status
#define disk_read ff_disk_read
#define disk_write ff_disk_write
#define disk_ioctl ff_disk_ioctl

@ -0,0 +1,208 @@
/*------------------------------------------------------------------------*/
/* A Sample Code of User Provided OS Dependent Functions for FatFs */
/*------------------------------------------------------------------------*/
#include "ff.h"
#if FF_USE_LFN == 3 /* Use dynamic memory allocation */
/*------------------------------------------------------------------------*/
/* Allocate/Free a Memory Block */
/*------------------------------------------------------------------------*/
#include <stdlib.h> /* with POSIX API */
void* ff_memalloc ( /* Returns pointer to the allocated memory block (null if not enough core) */
UINT msize /* Number of bytes to allocate */
)
{
return malloc((size_t)msize); /* Allocate a new memory block */
}
void ff_memfree (
void* mblock /* Pointer to the memory block to free (no effect if null) */
)
{
free(mblock); /* Free the memory block */
}
#endif
#if FF_FS_REENTRANT /* Mutal exclusion */
/*------------------------------------------------------------------------*/
/* Definitions of Mutex */
/*------------------------------------------------------------------------*/
#define OS_TYPE 0 /* 0:Win32, 1:uITRON4.0, 2:uC/OS-II, 3:FreeRTOS, 4:CMSIS-RTOS */
#if OS_TYPE == 0 /* Win32 */
#include <windows.h>
static HANDLE Mutex[FF_VOLUMES + 1]; /* Table of mutex handle */
#elif OS_TYPE == 1 /* uITRON */
#include "itron.h"
#include "kernel.h"
static mtxid Mutex[FF_VOLUMES + 1]; /* Table of mutex ID */
#elif OS_TYPE == 2 /* uc/OS-II */
#include "includes.h"
static OS_EVENT *Mutex[FF_VOLUMES + 1]; /* Table of mutex pinter */
#elif OS_TYPE == 3 /* FreeRTOS */
#include "FreeRTOS.h"
#include "semphr.h"
static SemaphoreHandle_t Mutex[FF_VOLUMES + 1]; /* Table of mutex handle */
#elif OS_TYPE == 4 /* CMSIS-RTOS */
#include "cmsis_os.h"
static osMutexId Mutex[FF_VOLUMES + 1]; /* Table of mutex ID */
#endif
/*------------------------------------------------------------------------*/
/* Create a Mutex */
/*------------------------------------------------------------------------*/
/* This function is called in f_mount function to create a new mutex
/ or semaphore for the volume. When a 0 is returned, the f_mount function
/ fails with FR_INT_ERR.
*/
int ff_mutex_create ( /* Returns 1:Function succeeded or 0:Could not create the mutex */
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
Mutex[vol] = CreateMutex(NULL, FALSE, NULL);
return (int)(Mutex[vol] != INVALID_HANDLE_VALUE);
#elif OS_TYPE == 1 /* uITRON */
T_CMTX cmtx = {TA_TPRI,1};
Mutex[vol] = acre_mtx(&cmtx);
return (int)(Mutex[vol] > 0);
#elif OS_TYPE == 2 /* uC/OS-II */
OS_ERR err;
Mutex[vol] = OSMutexCreate(0, &err);
return (int)(err == OS_NO_ERR);
#elif OS_TYPE == 3 /* FreeRTOS */
Mutex[vol] = xSemaphoreCreateMutex();
return (int)(Mutex[vol] != NULL);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
osMutexDef(cmsis_os_mutex);
Mutex[vol] = osMutexCreate(osMutex(cmsis_os_mutex));
return (int)(Mutex[vol] != NULL);
#endif
}
/*------------------------------------------------------------------------*/
/* Delete a Mutex */
/*------------------------------------------------------------------------*/
/* This function is called in f_mount function to delete a mutex or
/ semaphore of the volume created with ff_mutex_create function.
*/
void ff_mutex_delete ( /* Returns 1:Function succeeded or 0:Could not delete due to an error */
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
CloseHandle(Mutex[vol]);
#elif OS_TYPE == 1 /* uITRON */
del_mtx(Mutex[vol]);
#elif OS_TYPE == 2 /* uC/OS-II */
OS_ERR err;
OSMutexDel(Mutex[vol], OS_DEL_ALWAYS, &err);
#elif OS_TYPE == 3 /* FreeRTOS */
vSemaphoreDelete(Mutex[vol]);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
osMutexDelete(Mutex[vol]);
#endif
}
/*------------------------------------------------------------------------*/
/* Request a Grant to Access the Volume */
/*------------------------------------------------------------------------*/
/* This function is called on enter file functions to lock the volume.
/ When a 0 is returned, the file function fails with FR_TIMEOUT.
*/
int ff_mutex_take ( /* Returns 1:Succeeded or 0:Timeout */
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
return (int)(WaitForSingleObject(Mutex[vol], FF_FS_TIMEOUT) == WAIT_OBJECT_0);
#elif OS_TYPE == 1 /* uITRON */
return (int)(tloc_mtx(Mutex[vol], FF_FS_TIMEOUT) == E_OK);
#elif OS_TYPE == 2 /* uC/OS-II */
OS_ERR err;
OSMutexPend(Mutex[vol], FF_FS_TIMEOUT, &err));
return (int)(err == OS_NO_ERR);
#elif OS_TYPE == 3 /* FreeRTOS */
return (int)(xSemaphoreTake(Mutex[vol], FF_FS_TIMEOUT) == pdTRUE);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
return (int)(osMutexWait(Mutex[vol], FF_FS_TIMEOUT) == osOK);
#endif
}
/*------------------------------------------------------------------------*/
/* Release a Grant to Access the Volume */
/*------------------------------------------------------------------------*/
/* This function is called on leave file functions to unlock the volume.
*/
void ff_mutex_give (
int vol /* Mutex ID: Volume mutex (0 to FF_VOLUMES - 1) or system mutex (FF_VOLUMES) */
)
{
#if OS_TYPE == 0 /* Win32 */
ReleaseMutex(Mutex[vol]);
#elif OS_TYPE == 1 /* uITRON */
unl_mtx(Mutex[vol]);
#elif OS_TYPE == 2 /* uC/OS-II */
OSMutexPost(Mutex[vol]);
#elif OS_TYPE == 3 /* FreeRTOS */
xSemaphoreGive(Mutex[vol]);
#elif OS_TYPE == 4 /* CMSIS-RTOS */
osMutexRelease(Mutex[vol]);
#endif
}
#endif /* FF_FS_REENTRANT */

File diff suppressed because it is too large Load Diff

@ -0,0 +1,19 @@
# fatfs component target tests
This directory contains tests for `fatfs` component which are run on chip targets.
See also [test_fatfs_host](../test_fatfs_host) directory for the tests which run on a Linux host.
Fatfs tests can be executed with different `diskio` backends: `diskio_sdmmc` (SD cards over SD or SPI interface), `diskio_spiflash` (wear levelling in internal flash) and `diskio_rawflash` (read-only, no wear levelling, internal flash). There is one test app here for each of these backends:
- [sdcard](sdcard/) — runs fatfs tests with an SD card over SDMMC or SDSPI interface
- [flash_wl](flash_wl/) - runs fatfs test in a wear_levelling partition in SPI flash
- [flash_ro](flash_ro/) - runs fatfs test in a read-only (no wear levelling) partition in SPI flash
These test apps define:
- test functions
- setup/teardown routines
- build/test configurations
- pytest test runners
The actual test cases (many of which are common between the test apps) are defined in the [test_fatfs_common component](test_fatfs_common/).

@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.16)
set(COMPONENTS main)
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../test_fatfs_common")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(test_fatfs_flash_ro)

@ -0,0 +1,14 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
This test app runs a few FATFS test cases in a read-only FAT partition.
These tests should be possible to run on any ESP development board, not extra hardware is necessary.
The initial FAT image is generated during the build process in [main/CMakeLists.txt](main/CMakeLists.txt):
- `create_test_files` function creates a set of files expected by the test cases
- `fatfs_create_rawflash_image` generates a FAT image from the set of files (via `fatfsgen.py`)
The generated FAT image is flashed into `storage` partition when running `idf.py flash`.
See [../README.md](../README.md) for more information about FATFS test apps.

@ -0,0 +1,41 @@
idf_component_register(SRCS "test_fatfs_flash_ro.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity spi_flash fatfs vfs test_fatfs_common
WHOLE_ARCHIVE)
set(out_dir "${CMAKE_CURRENT_BINARY_DIR}/fatfs_image")
# This helper function creates a set of files expected by the test case.
# The files are then added into the FAT image by 'fatfs_create_rawflash_image' below.
function(create_test_files)
message(STATUS "Generating source files for test_fatfs_flash_ro in ${out_dir}...")
# used in "(raw) can read file"
file(WRITE "${out_dir}/hello.txt" "Hello, World!\n")
# used in "(raw) can open maximum number of files"
foreach(i RANGE 1 32)
file(WRITE "${out_dir}/f/${i}.txt")
endforeach()
# used in "(raw) opendir, readdir, rewinddir, seekdir work as expected"
file(WRITE "${out_dir}/dir/1.txt")
file(WRITE "${out_dir}/dir/2.txt")
file(WRITE "${out_dir}/dir/boo.bin")
file(WRITE "${out_dir}/dir/inner/3.txt")
# used in "(raw) multiple tasks can use same volume"
foreach(i RANGE 1 4)
string(REPEAT "${i}" 32000 file_content)
file(WRITE "${out_dir}/ccrnt/${i}.txt" ${file_content})
endforeach()
# used in "(raw) read speed test"
string(REPEAT "a" 262144 file_content)
file(WRITE "${out_dir}/256k.bin" ${file_content})
endfunction()
create_test_files()
fatfs_create_rawflash_image(storage ${out_dir} FLASH_IN_PROJECT PRESERVE_TIME)

@ -0,0 +1,311 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/unistd.h>
#include "unity.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "test_fatfs_common.h"
void app_main(void)
{
unity_run_menu();
}
static void test_setup(size_t max_files)
{
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = max_files
};
TEST_ESP_OK(esp_vfs_fat_spiflash_mount_ro("/spiflash", "storage", &mount_config));
}
static void test_teardown(void)
{
TEST_ESP_OK(esp_vfs_fat_spiflash_unmount_ro("/spiflash", "storage"));
}
TEST_CASE("(raw) can read file", "[fatfs]")
{
test_setup(5);
FILE* f = fopen("/spiflash/hello.txt", "r");
TEST_ASSERT_NOT_NULL(f);
char buf[32] = { 0 };
int cb = fread(buf, 1, sizeof(buf), f);
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str), cb);
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str, buf));
TEST_ASSERT_EQUAL(0, fclose(f));
test_teardown();
}
TEST_CASE("(raw) can open maximum number of files", "[fatfs]")
{
size_t max_files = FOPEN_MAX - 3; /* account for stdin, stdout, stderr */
test_setup(max_files);
FILE** files = calloc(max_files, sizeof(FILE*));
for (size_t i = 0; i < max_files; ++i) {
char name[32];
snprintf(name, sizeof(name), "/spiflash/f/%d.txt", i + 1);
files[i] = fopen(name, "r");
TEST_ASSERT_NOT_NULL(files[i]);
}
/* close everything and clean up */
for (size_t i = 0; i < max_files; ++i) {
fclose(files[i]);
}
free(files);
test_teardown();
}
TEST_CASE("(raw) can lseek", "[fatfs]")
{
test_setup(5);
FILE* f = fopen("/spiflash/hello.txt", "r");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL(0, fseek(f, 2, SEEK_CUR));
TEST_ASSERT_EQUAL('l', fgetc(f));
TEST_ASSERT_EQUAL(0, fseek(f, 4, SEEK_SET));
TEST_ASSERT_EQUAL('o', fgetc(f));
TEST_ASSERT_EQUAL(0, fseek(f, -5, SEEK_END));
TEST_ASSERT_EQUAL('r', fgetc(f));
TEST_ASSERT_EQUAL(0, fseek(f, 3, SEEK_END));
TEST_ASSERT_EQUAL(17, ftell(f));
TEST_ASSERT_EQUAL(0, fseek(f, 0, SEEK_END));
TEST_ASSERT_EQUAL(14, ftell(f));
TEST_ASSERT_EQUAL(0, fseek(f, 0, SEEK_SET));
test_teardown();
}
TEST_CASE("(raw) stat returns correct values", "[fatfs]")
{
test_setup(5);
struct tm tm;
tm.tm_year = 2018 - 1900;
tm.tm_mon = 5; // Note: month can be 0-11 & not 1-12
tm.tm_mday = 13;
tm.tm_hour = 11;
tm.tm_min = 2;
tm.tm_sec = 10;
time_t t = mktime(&tm);
printf("Reference time: %s", asctime(&tm));
struct stat st;
TEST_ASSERT_EQUAL(0, stat("/spiflash/hello.txt", &st));
time_t mtime = st.st_mtime;
struct tm mtm;
localtime_r(&mtime, &mtm);
printf("File time: %s", asctime(&mtm));
TEST_ASSERT(mtime > t); // Modification time should be in future wrt ref time
TEST_ASSERT(st.st_mode & S_IFREG);
TEST_ASSERT_FALSE(st.st_mode & S_IFDIR);
memset(&st, 0, sizeof(st));
TEST_ASSERT_EQUAL(0, stat("/spiflash", &st));
TEST_ASSERT(st.st_mode & S_IFDIR);
TEST_ASSERT_FALSE(st.st_mode & S_IFREG);
test_teardown();
}
TEST_CASE("(raw) can opendir root directory of FS", "[fatfs]")
{
test_setup(5);
DIR* dir = opendir("/spiflash");
TEST_ASSERT_NOT_NULL(dir);
bool found = false;
while (true) {
struct dirent* de = readdir(dir);
if (!de) {
break;
}
if (strcasecmp(de->d_name, "hello.txt") == 0) {
found = true;
break;
}
}
TEST_ASSERT_TRUE(found);
TEST_ASSERT_EQUAL(0, closedir(dir));
test_teardown();
}
TEST_CASE("(raw) opendir, readdir, rewinddir, seekdir work as expected", "[fatfs]")
{
test_setup(5);
DIR* dir = opendir("/spiflash/dir");
TEST_ASSERT_NOT_NULL(dir);
int count = 0;
const char* names[4];
while(count < 4) {
struct dirent* de = readdir(dir);
if (!de) {
break;
}
printf("found '%s'\n", de->d_name);
if (strcasecmp(de->d_name, "1.txt") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "1.txt";
++count;
} else if (strcasecmp(de->d_name, "2.txt") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "2.txt";
++count;
} else if (strcasecmp(de->d_name, "inner") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_DIR);
names[count] = "inner";
++count;
} else if (strcasecmp(de->d_name, "boo.bin") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "boo.bin";
++count;
} else {
TEST_FAIL_MESSAGE("unexpected directory entry");
}
}
TEST_ASSERT_EQUAL(count, 4);
rewinddir(dir);
struct dirent* de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[0]));
seekdir(dir, 3);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[3]));
seekdir(dir, 1);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[1]));
seekdir(dir, 2);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[2]));
TEST_ASSERT_EQUAL(0, closedir(dir));
test_teardown();
}
typedef struct {
const char* filename;
size_t word_count;
unsigned val;
SemaphoreHandle_t done;
esp_err_t result;
} read_test_arg_t;
#define READ_TEST_ARG_INIT(name, val_) \
{ \
.filename = name, \
.word_count = 8000, \
.val = val_, \
.done = xSemaphoreCreateBinary() \
}
static void read_task(void* param)
{
read_test_arg_t* args = (read_test_arg_t*) param;
FILE* f = fopen(args->filename, "rb");
if (f == NULL) {
args->result = ESP_ERR_NOT_FOUND;
goto done;
}
for (size_t i = 0; i < args->word_count; ++i) {
unsigned rval;
int cnt = fread(&rval, sizeof(rval), 1, f);
if (cnt != 1 || rval != args->val) {
printf("E(r): i=%d, cnt=%d rval=0x08%x val=0x%08x\n", i, cnt, rval, args->val);
args->result = ESP_FAIL;
goto close;
}
}
args->result = ESP_OK;
close:
fclose(f);
done:
xSemaphoreGive(args->done);
vTaskDelay(1);
vTaskDelete(NULL);
}
TEST_CASE("(raw) multiple tasks can use same volume", "[fatfs]")
{
test_setup(5);
char names[4][64];
for (size_t i = 0; i < 4; ++i) {
snprintf(names[i], sizeof(names[i]), "/spiflash/ccrnt/%d.txt", i + 1);
}
read_test_arg_t args1 = READ_TEST_ARG_INIT(names[0], 0x31313131);
read_test_arg_t args2 = READ_TEST_ARG_INIT(names[1], 0x32323232);
read_test_arg_t args3 = READ_TEST_ARG_INIT(names[2], 0x33333333);
read_test_arg_t args4 = READ_TEST_ARG_INIT(names[3], 0x34343434);
const int cpuid_0 = 0;
const int cpuid_1 = portNUM_PROCESSORS - 1;
const int stack_size = 4096;
printf("reading files 1.txt 2.txt 3.txt 4.txt \n");
xTaskCreatePinnedToCore(&read_task, "r1", stack_size, &args1, 3, NULL, cpuid_1);
xTaskCreatePinnedToCore(&read_task, "r2", stack_size, &args2, 3, NULL, cpuid_0);
xTaskCreatePinnedToCore(&read_task, "r3", stack_size, &args3, 3, NULL, cpuid_0);
xTaskCreatePinnedToCore(&read_task, "r4", stack_size, &args4, 3, NULL, cpuid_1);
xSemaphoreTake(args1.done, portMAX_DELAY);
printf("1.txt done\n");
TEST_ASSERT_EQUAL(ESP_OK, args1.result);
xSemaphoreTake(args2.done, portMAX_DELAY);
printf("2.txt done\n");
TEST_ASSERT_EQUAL(ESP_OK, args2.result);
xSemaphoreTake(args3.done, portMAX_DELAY);
printf("3.txt done\n");
TEST_ASSERT_EQUAL(ESP_OK, args3.result);
xSemaphoreTake(args4.done, portMAX_DELAY);
printf("4.txt done\n");
TEST_ASSERT_EQUAL(ESP_OK, args4.result);
vSemaphoreDelete(args1.done);
vSemaphoreDelete(args2.done);
vSemaphoreDelete(args3.done);
vSemaphoreDelete(args4.done);
test_teardown();
}
TEST_CASE("(raw) read speed test", "[fatfs][timeout=60]")
{
test_setup(5);
const size_t buf_size = 16 * 1024;
uint32_t* buf = (uint32_t*) calloc(1, buf_size);
const size_t file_size = 256 * 1024;
const char* file = "/spiflash/256k.bin";
test_fatfs_rw_speed(file, buf, 4 * 1024, file_size, false);
test_fatfs_rw_speed(file, buf, 8 * 1024, file_size, false);
test_fatfs_rw_speed(file, buf, 16 * 1024, file_size, false);
free(buf);
test_teardown();
}

@ -0,0 +1,3 @@
# Name, Type, SubType, Offset, Size, Flags
factory, app, factory, 0x10000, 1M,
storage, data, fat, , 528k,
1 # Name Type SubType Offset Size Flags
2 factory app factory 0x10000 1M
3 storage data fat 528k

@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
@pytest.mark.supported_targets
@pytest.mark.generic
def test_fatfs_flash_ro(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('')
dut.expect_exact('Enter test for running.')
dut.write('*')
dut.expect_unity_test_output()

@ -0,0 +1,14 @@
# General options for additional checks
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_COMPILER_WARN_WRITE_STRINGS=y
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
CONFIG_COMPILER_STACK_CHECK=y
# disable task watchdog since this app uses an interactive menu
CONFIG_ESP_TASK_WDT_INIT=n
# use custom partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"

@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.16)
set(COMPONENTS main)
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../test_fatfs_common")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(test_fatfs_flash_wl)

@ -0,0 +1,8 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
This test app runs a few FATFS test cases in a wear levelling FAT partition.
These tests should be possible to run on any ESP development board, not extra hardware is necessary.
See [../README.md](../README.md) for more information about FATFS test apps.

@ -0,0 +1,4 @@
idf_component_register(SRCS "test_fatfs_flash_wl.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity spi_flash fatfs vfs test_fatfs_common
WHOLE_ARCHIVE)

@ -0,0 +1,276 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/unistd.h>
#include "unity.h"
#include "esp_partition.h"
#include "esp_log.h"
#include "esp_random.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "test_fatfs_common.h"
#include "wear_levelling.h"
#include "esp_partition.h"
#include "esp_memory_utils.h"
void app_main(void)
{
unity_run_menu();
}
static wl_handle_t s_test_wl_handle;
static void test_setup(void)
{
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
};
TEST_ESP_OK(esp_vfs_fat_spiflash_mount_rw_wl("/spiflash", NULL, &mount_config, &s_test_wl_handle));
}
static void test_teardown(void)
{
TEST_ESP_OK(esp_vfs_fat_spiflash_unmount_rw_wl("/spiflash", s_test_wl_handle));
}
TEST_CASE("(WL) can format partition", "[fatfs][wear_levelling]")
{
TEST_ESP_OK(esp_vfs_fat_spiflash_format_rw_wl("/spiflash", NULL));
test_setup();
test_teardown();
}
TEST_CASE("(WL) can format when the FAT is mounted already", "[fatfs][wear_levelling]")
{
test_setup();
TEST_ESP_OK(esp_vfs_fat_spiflash_format_rw_wl("/spiflash", NULL));
test_fatfs_create_file_with_text("/spiflash/hello.txt", fatfs_test_hello_str);
test_fatfs_pread_file("/spiflash/hello.txt");
test_teardown();
}
TEST_CASE("(WL) can create and write file", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_create_file_with_text("/spiflash/hello.txt", fatfs_test_hello_str);
test_teardown();
}
TEST_CASE("(WL) can create and open file with O_CREAT flag", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_create_file_with_o_creat_flag("/spiflash/hello.txt");
test_fatfs_open_file_with_o_creat_flag("/spiflash/hello.txt");
test_teardown();
}
TEST_CASE("(WL) can read file", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_create_file_with_text("/spiflash/hello.txt", fatfs_test_hello_str);
test_fatfs_read_file("/spiflash/hello.txt");
test_teardown();
}
TEST_CASE("(WL) can read file with pread", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_create_file_with_text("/spiflash/hello.txt", fatfs_test_hello_str);
test_fatfs_pread_file("/spiflash/hello.txt");
test_teardown();
}
TEST_CASE("(WL) pwrite() works well", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_pwrite_file("/spiflash/hello.txt");
test_teardown();
}
TEST_CASE("(WL) can open maximum number of files", "[fatfs][wear_levelling]")
{
size_t max_files = FOPEN_MAX - 3; /* account for stdin, stdout, stderr */
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = max_files
};
TEST_ESP_OK(esp_vfs_fat_spiflash_mount_rw_wl("/spiflash", NULL, &mount_config, &s_test_wl_handle));
test_fatfs_open_max_files("/spiflash/f", max_files);
TEST_ESP_OK(esp_vfs_fat_spiflash_unmount_rw_wl("/spiflash", s_test_wl_handle));
}
TEST_CASE("(WL) overwrite and append file", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_overwrite_append("/spiflash/hello.txt");
test_teardown();
}
TEST_CASE("(WL) can lseek", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_lseek("/spiflash/seek.txt");
test_teardown();
}
TEST_CASE("(WL) can truncate", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_truncate_file("/spiflash/truncate.txt");
test_teardown();
}
TEST_CASE("(WL) stat returns correct values", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_stat("/spiflash/stat.txt", "/spiflash");
test_teardown();
}
TEST_CASE("(WL) stat returns correct mtime if DST is enabled", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_mtime_dst("/spiflash/statdst.txt", "/spiflash");
test_teardown();
}
TEST_CASE("(WL) utime sets modification time", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_utime("/spiflash/utime.txt", "/spiflash");
test_teardown();
}
TEST_CASE("(WL) unlink removes a file", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_unlink("/spiflash/unlink.txt");
test_teardown();
}
TEST_CASE("(WL) link copies a file, rename moves a file", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_link_rename("/spiflash/link");
test_teardown();
}
TEST_CASE("(WL) can create and remove directories", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_mkdir_rmdir("/spiflash/dir");
test_teardown();
}
TEST_CASE("(WL) can opendir root directory of FS", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_can_opendir("/spiflash");
test_teardown();
}
TEST_CASE("(WL) opendir, readdir, rewinddir, seekdir work as expected", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_opendir_readdir_rewinddir("/spiflash/dir");
test_teardown();
}
TEST_CASE("(WL) multiple tasks can use same volume", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_concurrent("/spiflash/f");
test_teardown();
}
TEST_CASE("(WL) fatfs does not ignore leading spaces", "[fatfs][wear_levelling]")
{
// the functionality of ignoring leading and trailing whitespaces is not implemented yet
// when the feature is implemented, this test will fail
// please, remove the test and implement the functionality in fatfsgen.py to preserve the consistency
test_setup();
test_leading_spaces();
test_teardown();
}
TEST_CASE("(WL) write/read speed test", "[fatfs][wear_levelling][timeout=60]")
{
/* Erase partition before running the test to get consistent results */
const esp_partition_t* part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_DATA_FAT, NULL);
esp_partition_erase_range(part, 0, part->size);
test_setup();
const size_t buf_size = 16 * 1024;
uint32_t* buf = (uint32_t*) calloc(1, buf_size);
esp_fill_random(buf, buf_size);
const size_t file_size = 256 * 1024;
const char* file = "/spiflash/256k.bin";
test_fatfs_rw_speed(file, buf, 4 * 1024, file_size, true);
test_fatfs_rw_speed(file, buf, 8 * 1024, file_size, true);
test_fatfs_rw_speed(file, buf, 16 * 1024, file_size, true);
test_fatfs_rw_speed(file, buf, 4 * 1024, file_size, false);
test_fatfs_rw_speed(file, buf, 8 * 1024, file_size, false);
test_fatfs_rw_speed(file, buf, 16 * 1024, file_size, false);
unlink(file);
free(buf);
test_teardown();
}
TEST_CASE("(WL) can get partition info", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_info("/spiflash", "/spiflash/test.txt");
test_teardown();
}
/*
* In FatFs menuconfig, set CONFIG_FATFS_API_ENCODING to UTF-8 and set the
* Codepage to CP936 (Simplified Chinese) in order to run the following tests.
* Ensure that the text editor is UTF-8 compatible when compiling these tests.
*/
#if defined(CONFIG_FATFS_API_ENCODING_UTF_8) && (CONFIG_FATFS_CODEPAGE == 936)
TEST_CASE("(WL) can read file with UTF-8 encoded strings", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_create_file_with_text("/spiflash/测试文件.txt", fatfs_test_hello_str_utf);
test_fatfs_read_file_utf_8("/spiflash/测试文件.txt");
test_teardown();
}
TEST_CASE("(WL) opendir, readdir, rewinddir, seekdir work as expected using UTF-8 encoded strings", "[fatfs][wear_levelling]")
{
test_setup();
test_fatfs_opendir_readdir_rewinddir_utf_8("/spiflash/目录");
test_teardown();
}
#endif //defined(CONFIG_FATFS_API_ENCODING_UTF_8) && (CONFIG_FATFS_CODEPAGE == 936)
#ifdef CONFIG_SPIRAM
TEST_CASE("FATFS prefers SPI RAM for allocations", "[fatfs]")
{
test_setup();
DIR* dir = opendir("/spiflash");
TEST_ASSERT_NOT_NULL(dir);
TEST_ASSERT(esp_ptr_external_ram(dir));
closedir(dir);
test_teardown();
}
#endif // CONFIG_SPIRAM

@ -0,0 +1,3 @@
# Name, Type, SubType, Offset, Size, Flags
factory, app, factory, 0x10000, 1M,
storage, data, fat, , 528k,
1 # Name Type SubType Offset Size Flags
2 factory app factory 0x10000 1M
3 storage data fat 528k

@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
@pytest.mark.supported_targets
@pytest.mark.generic
@pytest.mark.parametrize(
'config',
[
'default',
'release',
'fastseek',
]
)
def test_fatfs_flash_wl_generic(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('')
dut.expect_exact('Enter test for running.')
dut.write('*')
dut.expect_unity_test_output(timeout=120)
@pytest.mark.supported_targets
@pytest.mark.psram
@pytest.mark.parametrize(
'config',
[
'psram',
]
)
def test_fatfs_flash_wl_psram(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('')
dut.expect_exact('Enter test for running.')
dut.write('*')
dut.expect_unity_test_output(timeout=120)

@ -0,0 +1,2 @@
CONFIG_FATFS_USE_FASTSEEK=y
CONFIG_FATFS_FAST_SEEK_BUFFER_SIZE=64

@ -0,0 +1,3 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0
CONFIG_FATFS_ALLOC_PREFER_EXTRAM=y

@ -0,0 +1,2 @@
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y

@ -0,0 +1,18 @@
# General options for additional checks
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_COMPILER_WARN_WRITE_STRINGS=y
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
CONFIG_COMPILER_STACK_CHECK=y
# disable task watchdog since this app uses an interactive menu
CONFIG_ESP_TASK_WDT_INIT=n
# use custom partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# some tests verify file name encoding
CONFIG_FATFS_API_ENCODING_UTF_8=y
CONFIG_FATFS_CODEPAGE_936=y

@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.16)
set(COMPONENTS main)
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../test_fatfs_common")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(test_fatfs_sdcard)

@ -0,0 +1,14 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
This test app runs a few FATFS test cases in a FAT-formatted SD card.
These tests require a development board with an SD card slot:
* ESP32-WROVER-KIT
* ESP32-S2 USB_OTG
* ESP32-C3-DevKit-C with an SD card breakout board
The test cases are split between `[sdmmc]` and `[sdspi]`. Only a few tests are executed for sdspi, though. The app could be refactored to ensure that a similar set of tests runs for sdmmc and sdspi.
See [../README.md](../README.md) for more information about FATFS test apps.

@ -0,0 +1,8 @@
idf_component_register(SRCS "test_fatfs_sdcard_main.c" "test_fatfs_sdspi.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity fatfs vfs sdmmc driver test_fatfs_common
WHOLE_ARCHIVE)
if(CONFIG_SOC_SDMMC_HOST_SUPPORTED)
target_sources(${COMPONENT_LIB} PRIVATE "test_fatfs_sdmmc.c")
endif()

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "unity.h"
void app_main(void)
{
unity_run_menu();
}

@ -0,0 +1,364 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/unistd.h>
#include "unity.h"
#include "esp_log.h"
#include "esp_random.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/sdmmc_defs.h"
#include "sdmmc_cmd.h"
#include "ff.h"
#include "test_fatfs_common.h"
#include "soc/soc_caps.h"
#if CONFIG_IDF_TARGET_ESP32
#define SDSPI_MISO_PIN 2
#define SDSPI_MOSI_PIN 15
#define SDSPI_CLK_PIN 14
#define SDSPI_CS_PIN 13
#elif CONFIG_IDF_TARGET_ESP32S2
// Adapted for internal test board ESP-32-S3-USB-OTG-Ev-BOARD_V1.0 (with ESP32-S2-MINI-1 module)
#define SDSPI_MISO_PIN 37
#define SDSPI_MOSI_PIN 35
#define SDSPI_CLK_PIN 36
#define SDSPI_CS_PIN 34
#elif CONFIG_IDF_TARGET_ESP32C3
#define SDSPI_MISO_PIN 6
#define SDSPI_MOSI_PIN 4
#define SDSPI_CLK_PIN 5
#define SDSPI_CS_PIN 1
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
#endif //CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C3
#ifndef SPI_DMA_CHAN
#define SPI_DMA_CHAN 1
#endif //SPI_DMA_CHAN
#define SDSPI_HOST_ID SPI2_HOST
#if !TEMPORARY_DISABLED_FOR_TARGETS(ESP32S3)
// No runner
#include "driver/sdmmc_host.h"
static void test_setup_sdmmc(sdmmc_card_t **out_card)
{
sdmmc_card_t *card = NULL;
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
TEST_ESP_OK(esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card));
*out_card = card;
}
static void test_teardown_sdmmc(sdmmc_card_t *card)
{
TEST_ESP_OK(esp_vfs_fat_sdcard_unmount("/sdcard", card));
}
static const char* test_filename = "/sdcard/hello.txt";
TEST_CASE("Mount fails cleanly without card inserted", "[fatfs][ignore]")
{
size_t heap_size;
HEAP_SIZE_CAPTURE(heap_size);
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5
};
for (int i = 0; i < 3; ++i) {
printf("Initializing card, attempt %d\n", i);
esp_err_t err = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, NULL);
printf("err=%d\n", err);
TEST_ESP_ERR(ESP_ERR_TIMEOUT, err);
}
HEAP_SIZE_CHECK(heap_size, 0);
}
TEST_CASE("(SD) can format partition", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
TEST_ESP_OK(esp_vfs_fat_sdcard_format("/sdcard", card));
test_fatfs_create_file_with_text(test_filename, fatfs_test_hello_str);
test_fatfs_read_file(test_filename);
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can create and write file", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_create_file_with_text(test_filename, fatfs_test_hello_str);
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can read file", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_create_file_with_text(test_filename, fatfs_test_hello_str);
test_fatfs_read_file(test_filename);
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can read file with pread()", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_create_file_with_text(test_filename, fatfs_test_hello_str);
test_fatfs_pread_file(test_filename);
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) pwrite() works well", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_pwrite_file(test_filename);
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) overwrite and append file", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_overwrite_append(test_filename);
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can lseek", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_lseek("/sdcard/seek.txt");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can truncate", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_truncate_file("/sdcard/truncate.txt");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can ftruncate", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_ftruncate_file("/sdcard/ftrunc.txt");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) stat returns correct values", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_stat("/sdcard/stat.txt", "/sdcard");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) utime sets modification time", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_utime("/sdcard/utime.txt", "/sdcard");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) unlink removes a file", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_unlink("/sdcard/unlink.txt");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) link copies a file, rename moves a file", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_link_rename("/sdcard/link");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can create and remove directories", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_mkdir_rmdir("/sdcard/dir");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) can opendir root directory of FS", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_can_opendir("/sdcard");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) opendir, readdir, rewinddir, seekdir work as expected", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_opendir_readdir_rewinddir("/sdcard/dir");
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) multiple tasks can use same volume", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_concurrent("/sdcard/f");
test_teardown_sdmmc(card);
}
static void sdmmc_speed_test(void *buf, size_t buf_size, size_t file_size, bool write);
TEST_CASE("(SD) write/read speed test", "[fatfs][sdmmc]")
{
size_t heap_size;
HEAP_SIZE_CAPTURE(heap_size);
const size_t buf_size = 16 * 1024;
uint32_t* buf = (uint32_t*) calloc(1, buf_size);
esp_fill_random(buf, buf_size);
const size_t file_size = 1 * 1024 * 1024;
sdmmc_speed_test(buf, 4 * 1024, file_size, true);
sdmmc_speed_test(buf, 8 * 1024, file_size, true);
sdmmc_speed_test(buf, 16 * 1024, file_size, true);
sdmmc_speed_test(buf, 4 * 1024, file_size, false);
sdmmc_speed_test(buf, 8 * 1024, file_size, false);
sdmmc_speed_test(buf, 16 * 1024, file_size, false);
free(buf);
HEAP_SIZE_CHECK(heap_size, 0);
}
static void sdmmc_speed_test(void *buf, size_t buf_size, size_t file_size, bool write)
{
sdmmc_card_t *card = NULL;
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = write,
.max_files = 5,
.allocation_unit_size = 64 * 1024
};
TEST_ESP_OK(esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card));
test_fatfs_rw_speed("/sdcard/4mb.bin", buf, buf_size, file_size, write);
TEST_ESP_OK(esp_vfs_fat_sdcard_unmount("/sdcard", card));
}
TEST_CASE("(SD) mount two FAT partitions, SDMMC and WL, at the same time", "[fatfs][sdmmc]")
{
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5
};
const char* filename_sd = "/sdcard/sd.txt";
const char* filename_wl = "/spiflash/wl.txt";
const char* str_sd = "this is sd\n";
const char* str_wl = "this is spiflash\n";
/* Erase flash before the first use */
const esp_partition_t *test_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL);
TEST_ASSERT_NOT_NULL(test_partition);
esp_partition_erase_range(test_partition, 0, test_partition->size);
/* Mount FATFS in SD can WL at the same time. Create a file on each FS */
wl_handle_t wl_handle = WL_INVALID_HANDLE;
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
TEST_ESP_OK(esp_vfs_fat_spiflash_mount_rw_wl("/spiflash", NULL, &mount_config, &wl_handle));
unlink(filename_sd);
unlink(filename_wl);
test_fatfs_create_file_with_text(filename_sd, str_sd);
test_fatfs_create_file_with_text(filename_wl, str_wl);
TEST_ESP_OK(esp_vfs_fat_spiflash_unmount_rw_wl("/spiflash", wl_handle));
test_teardown_sdmmc(card);
/* Check that the file "sd.txt" was created on FS in SD, and has the right data */
test_setup_sdmmc(&card);
TEST_ASSERT_NULL(fopen(filename_wl, "r"));
FILE* f = fopen(filename_sd, "r");
TEST_ASSERT_NOT_NULL(f);
char buf[64];
TEST_ASSERT_NOT_NULL(fgets(buf, sizeof(buf) - 1, f));
TEST_ASSERT_EQUAL(0, strcmp(buf, str_sd));
fclose(f);
test_teardown_sdmmc(card);
/* Check that the file "wl.txt" was created on FS in WL, and has the right data */
TEST_ESP_OK(esp_vfs_fat_spiflash_mount_rw_wl("/spiflash", NULL, &mount_config, &wl_handle));
TEST_ASSERT_NULL(fopen(filename_sd, "r"));
f = fopen(filename_wl, "r");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_NOT_NULL(fgets(buf, sizeof(buf) - 1, f));
TEST_ASSERT_EQUAL(0, strcmp(buf, str_wl));
fclose(f);
TEST_ESP_OK(esp_vfs_fat_spiflash_unmount_rw_wl("/spiflash", wl_handle));
}
/*
* In FatFs menuconfig, set CONFIG_FATFS_API_ENCODING to UTF-8 and set the
* Codepage to CP936 (Simplified Chinese) in order to run the following tests.
* Ensure that the text editor is UTF-8 compatible when compiling these tests.
*/
#if defined(CONFIG_FATFS_API_ENCODING_UTF_8) && (CONFIG_FATFS_CODEPAGE == 936)
static const char* test_filename_utf_8 = "/sdcard/测试文件.txt";
TEST_CASE("(SD) can read file using UTF-8 encoded strings", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_create_file_with_text(test_filename_utf_8, fatfs_test_hello_str_utf);
test_fatfs_read_file_utf_8(test_filename_utf_8);
test_teardown_sdmmc(card);
}
TEST_CASE("(SD) opendir, readdir, rewinddir, seekdir work as expected using UTF-8 encoded strings", "[fatfs][ignore]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_opendir_readdir_rewinddir_utf_8("/sdcard/目录");
test_teardown_sdmmc(card);
}
#endif // CONFIG_FATFS_API_ENCODING_UTF_8 && CONFIG_FATFS_CODEPAGE == 936
TEST_CASE("(SD) can get partition info", "[fatfs][sdmmc]")
{
sdmmc_card_t *card = NULL;
test_setup_sdmmc(&card);
test_fatfs_info("/sdcard", "/sdcard/test.txt");
test_teardown_sdmmc(card);
}
#endif //!TEMPORARY_DISABLED_FOR_TARGETS(ESP32S3)

@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/unistd.h>
#include "unity.h"
#include "esp_log.h"
#include "esp_random.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/sdmmc_defs.h"
#include "sdmmc_cmd.h"
#include "ff.h"
#include "test_fatfs_common.h"
#include "soc/soc_caps.h"
#if CONFIG_IDF_TARGET_ESP32
#define SDSPI_MISO_PIN 2
#define SDSPI_MOSI_PIN 15
#define SDSPI_CLK_PIN 14
#define SDSPI_CS_PIN 13
#elif CONFIG_IDF_TARGET_ESP32S2
// Adapted for internal test board ESP-32-S3-USB-OTG-Ev-BOARD_V1.0 (with ESP32-S2-MINI-1 module)
#define SDSPI_MISO_PIN 37
#define SDSPI_MOSI_PIN 35
#define SDSPI_CLK_PIN 36
#define SDSPI_CS_PIN 34
#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C6
#define SDSPI_MISO_PIN 6
#define SDSPI_MOSI_PIN 4
#define SDSPI_CLK_PIN 5
#define SDSPI_CS_PIN 1
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
#elif CONFIG_IDF_TARGET_ESP32H2
#define SDSPI_MISO_PIN 0
#define SDSPI_MOSI_PIN 5
#define SDSPI_CLK_PIN 4
#define SDSPI_CS_PIN 1
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
#endif
#ifndef SPI_DMA_CHAN
#define SPI_DMA_CHAN 1
#endif //SPI_DMA_CHAN
#define SDSPI_HOST_ID SPI2_HOST
typedef struct sdspi_mem {
size_t heap_size;
uint32_t* buf;
} sdspi_mem_t;
static const char* s_test_filename = "/sdcard/hello.txt";
static void sdspi_speed_test(void *buf, size_t buf_size, size_t file_size, bool write);
static void test_setup_sdspi(sdspi_mem_t* mem)
{
HEAP_SIZE_CAPTURE(mem->heap_size);
const size_t buf_size = 16 * 1024;
mem->buf = (uint32_t*) calloc(1, buf_size);
esp_fill_random(mem->buf, buf_size);
spi_bus_config_t bus_cfg = {
.mosi_io_num = SDSPI_MOSI_PIN,
.miso_io_num = SDSPI_MISO_PIN,
.sclk_io_num = SDSPI_CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4000,
};
esp_err_t err = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CHAN);
TEST_ESP_OK(err);
}
static void test_teardown_sdspi(sdspi_mem_t* mem)
{
free(mem->buf);
spi_bus_free(SDSPI_HOST_ID);
HEAP_SIZE_CHECK(mem->heap_size, 0);
}
TEST_CASE("(SDSPI) write/read speed test", "[fatfs][sdspi]")
{
sdspi_mem_t mem;
size_t file_size = 1 * 1024 * 1024;
test_setup_sdspi(&mem);
sdspi_speed_test(mem.buf, 4 * 1024, file_size, true);
sdspi_speed_test(mem.buf, 8 * 1024, file_size, true);
sdspi_speed_test(mem.buf, 16 * 1024, file_size, true);
sdspi_speed_test(mem.buf, 4 * 1024, file_size, false);
sdspi_speed_test(mem.buf, 8 * 1024, file_size, false);
sdspi_speed_test(mem.buf, 16 * 1024, file_size, false);
test_teardown_sdspi(&mem);
}
static void sdspi_speed_test(void *buf, size_t buf_size, size_t file_size, bool write)
{
const char path[] = "/sdcard";
sdmmc_card_t *card;
card = NULL;
sdspi_device_config_t device_cfg = {
.gpio_cs = SDSPI_CS_PIN,
.host_id = SDSPI_HOST_ID,
.gpio_cd = SDSPI_SLOT_NO_CD,
.gpio_wp = SDSPI_SLOT_NO_WP,
.gpio_int = SDSPI_SLOT_NO_INT,
};
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.slot = SDSPI_HOST_ID;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = write,
.max_files = 5,
.allocation_unit_size = 64 * 1024
};
TEST_ESP_OK(esp_vfs_fat_sdspi_mount(path, &host, &device_cfg, &mount_config, &card));
test_fatfs_rw_speed("/sdcard/4mb.bin", buf, buf_size, file_size, write);
TEST_ESP_OK(esp_vfs_fat_sdcard_unmount(path, card));
}
TEST_CASE("(SDSPI) can get partition info", "[fatfs][sdspi]")
{
sdspi_mem_t mem;
test_setup_sdspi(&mem);
const char path[] = "/sdcard";
sdmmc_card_t *card;
card = NULL;
sdspi_device_config_t device_cfg = {
.gpio_cs = SDSPI_CS_PIN,
.host_id = SDSPI_HOST_ID,
.gpio_cd = SDSPI_SLOT_NO_CD,
.gpio_wp = SDSPI_SLOT_NO_WP,
.gpio_int = SDSPI_SLOT_NO_INT,
};
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.slot = SDSPI_HOST_ID;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = 64 * 1024
};
TEST_ESP_OK(esp_vfs_fat_sdspi_mount(path, &host, &device_cfg, &mount_config, &card));
test_fatfs_info("/sdcard", "/sdcard/test.txt");
TEST_ESP_OK(esp_vfs_fat_sdcard_unmount(path, card));
test_teardown_sdspi(&mem);
}
TEST_CASE("(SDSPI) can format card", "[fatfs][sdspi]")
{
sdspi_mem_t mem;
test_setup_sdspi(&mem);
const char path[] = "/sdcard";
sdmmc_card_t *card;
card = NULL;
sdspi_device_config_t device_cfg = {
.gpio_cs = SDSPI_CS_PIN,
.host_id = SDSPI_HOST_ID,
.gpio_cd = SDSPI_SLOT_NO_CD,
.gpio_wp = SDSPI_SLOT_NO_WP,
.gpio_int = SDSPI_SLOT_NO_INT,
};
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.slot = SDSPI_HOST_ID;
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = 64 * 1024
};
TEST_ESP_OK(esp_vfs_fat_sdspi_mount(path, &host, &device_cfg, &mount_config, &card));
TEST_ESP_OK(esp_vfs_fat_sdcard_format("/sdcard", card));
test_fatfs_create_file_with_text(s_test_filename, fatfs_test_hello_str);
test_fatfs_read_file(s_test_filename);
TEST_ESP_OK(esp_vfs_fat_sdcard_unmount(path, card));
test_teardown_sdspi(&mem);
}

@ -0,0 +1,3 @@
# Name, Type, SubType, Offset, Size, Flags
factory, app, factory, 0x10000, 1M,
storage, data, fat, , 528k,
1 # Name Type SubType Offset Size Flags
2 factory app factory 0x10000 1M
3 storage data fat 528k

@ -0,0 +1,75 @@
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
@pytest.mark.esp32
@pytest.mark.sdcard_sdmode
@pytest.mark.parametrize(
'config',
[
'default',
'release',
]
)
def test_fatfs_sdcard_generic_sdmmc(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('')
dut.expect_exact('Enter test for running.')
dut.write('[sdmmc]')
dut.expect_unity_test_output(timeout=120)
@pytest.mark.esp32
@pytest.mark.esp32s2
@pytest.mark.esp32c3
@pytest.mark.sdcard_spimode
@pytest.mark.parametrize(
'config',
[
'default',
'release',
]
)
def test_fatfs_sdcard_generic_sdspi(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('')
dut.expect_exact('Enter test for running.')
dut.write('[sdspi]')
dut.expect_unity_test_output(timeout=120)
@pytest.mark.esp32
@pytest.mark.sdcard_sdmode
@pytest.mark.psram
@pytest.mark.parametrize(
'config',
[
'psram',
]
)
def test_fatfs_sdcard_psram_sdmmc(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('')
dut.expect_exact('Enter test for running.')
dut.write('[sdmmc]')
dut.expect_unity_test_output(timeout=120)
@pytest.mark.esp32
@pytest.mark.sdcard_spimode
@pytest.mark.psram
@pytest.mark.parametrize(
'config',
[
'psram',
]
)
def test_fatfs_sdcard_psram_sdspi(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('')
dut.expect_exact('Enter test for running.')
dut.write('[sdspi]')
dut.expect_unity_test_output(timeout=120)

@ -0,0 +1,3 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0
CONFIG_FATFS_ALLOC_PREFER_EXTRAM=y

@ -0,0 +1,2 @@
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y

@ -0,0 +1,19 @@
# General options for additional checks
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_COMPILER_WARN_WRITE_STRINGS=y
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
CONFIG_COMPILER_STACK_CHECK=y
# disable task watchdog since this app uses an interactive menu
CONFIG_ESP_TASK_WDT_INIT=n
# some tests verify file name encoding
CONFIG_FATFS_API_ENCODING_UTF_8=y
CONFIG_FATFS_CODEPAGE_936=y
# some of the tests verify concurrent operation of FAT partitions in
# an SD card and in Flash, so need to use a custom partition table.
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"

@ -0,0 +1,3 @@
idf_component_register(SRCS "test_fatfs_common.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity fatfs vfs unity)

@ -0,0 +1,979 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include <utime.h>
#include "unity.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "test_fatfs_common.h"
const char* fatfs_test_hello_str = "Hello, World!\n";
const char* fatfs_test_hello_str_utf = "世界,你好!\n";
void test_fatfs_create_file_with_text(const char* name, const char* text)
{
FILE* f = fopen(name, "wb");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_TRUE(fputs(text, f) != EOF);
TEST_ASSERT_EQUAL(0, fclose(f));
}
void test_fatfs_create_file_with_o_creat_flag(const char* filename)
{
const int fd = open(filename, O_CREAT|O_WRONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
const int r = pwrite(fd, fatfs_test_hello_str, strlen(fatfs_test_hello_str), 0); //offset=0
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str), r);
TEST_ASSERT_EQUAL(0, close(fd));
}
void test_fatfs_open_file_with_o_creat_flag(const char* filename)
{
char buf[32] = { 0 };
const int fd = open(filename, O_CREAT|O_RDONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
int r = pread(fd, buf, sizeof(buf), 0); // it is a regular read() with offset==0
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str, buf));
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str), r);
TEST_ASSERT_EQUAL(0, close(fd));
}
void test_fatfs_overwrite_append(const char* filename)
{
/* Create new file with 'aaaa' */
test_fatfs_create_file_with_text(filename, "aaaa");
/* Append 'bbbb' to file */
FILE *f_a = fopen(filename, "a");
TEST_ASSERT_NOT_NULL(f_a);
TEST_ASSERT_NOT_EQUAL(EOF, fputs("bbbb", f_a));
TEST_ASSERT_EQUAL(0, fclose(f_a));
/* Read back 8 bytes from file, verify it's 'aaaabbbb' */
char buf[10] = { 0 };
FILE *f_r = fopen(filename, "r");
TEST_ASSERT_NOT_NULL(f_r);
TEST_ASSERT_EQUAL(8, fread(buf, 1, 8, f_r));
TEST_ASSERT_EQUAL_STRING_LEN("aaaabbbb", buf, 8);
/* Be sure we're at end of file */
TEST_ASSERT_EQUAL(0, fread(buf, 1, 8, f_r));
TEST_ASSERT_EQUAL(0, fclose(f_r));
/* Overwrite file with 'cccc' */
test_fatfs_create_file_with_text(filename, "cccc");
/* Verify file now only contains 'cccc' */
f_r = fopen(filename, "r");
TEST_ASSERT_NOT_NULL(f_r);
bzero(buf, sizeof(buf));
TEST_ASSERT_EQUAL(4, fread(buf, 1, 8, f_r)); // trying to read 8 bytes, only expecting 4
TEST_ASSERT_EQUAL_STRING_LEN("cccc", buf, 4);
TEST_ASSERT_EQUAL(0, fclose(f_r));
}
void test_fatfs_read_file(const char* filename)
{
FILE* f = fopen(filename, "r");
TEST_ASSERT_NOT_NULL(f);
char buf[32] = { 0 };
int cb = fread(buf, 1, sizeof(buf), f);
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str), cb);
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str, buf));
TEST_ASSERT_EQUAL(0, fclose(f));
}
void test_fatfs_read_file_utf_8(const char* filename)
{
FILE* f = fopen(filename, "r");
TEST_ASSERT_NOT_NULL(f);
char buf[64] = { 0 }; //Doubled buffer size to allow for longer UTF-8 strings
int cb = fread(buf, 1, sizeof(buf), f);
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str_utf), cb);
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str_utf, buf));
TEST_ASSERT_EQUAL(0, fclose(f));
}
void test_fatfs_pread_file(const char* filename)
{
char buf[32] = { 0 };
const int fd = open(filename, O_RDONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
int r = pread(fd, buf, sizeof(buf), 0); // it is a regular read() with offset==0
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str, buf));
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str), r);
memset(buf, 0, sizeof(buf));
r = pread(fd, buf, sizeof(buf), 1); // offset==1
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str + 1, buf));
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str) - 1, r);
memset(buf, 0, sizeof(buf));
r = pread(fd, buf, sizeof(buf), 5); // offset==5
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str + 5, buf));
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str) - 5, r);
// regular read() should work now because pread() should not affect the current position in file
memset(buf, 0, sizeof(buf));
r = read(fd, buf, sizeof(buf)); // note that this is read() and not pread()
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str, buf));
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str), r);
memset(buf, 0, sizeof(buf));
r = pread(fd, buf, sizeof(buf), 10); // offset==10
TEST_ASSERT_EQUAL(0, strcmp(fatfs_test_hello_str + 10, buf));
TEST_ASSERT_EQUAL(strlen(fatfs_test_hello_str) - 10, r);
memset(buf, 0, sizeof(buf));
r = pread(fd, buf, sizeof(buf), strlen(fatfs_test_hello_str) + 1); // offset to EOF
TEST_ASSERT_EQUAL(0, r);
TEST_ASSERT_EQUAL(0, close(fd));
}
static void test_pwrite(const char *filename, off_t offset, const char *msg)
{
const int fd = open(filename, O_WRONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
const off_t current_pos = lseek(fd, 0, SEEK_END); // O_APPEND is not the same - jumps to the end only before write()
const int r = pwrite(fd, msg, strlen(msg), offset);
TEST_ASSERT_EQUAL(strlen(msg), r);
TEST_ASSERT_EQUAL(current_pos, lseek(fd, 0, SEEK_CUR)); // pwrite should not move the pointer
TEST_ASSERT_EQUAL(0, close(fd));
}
static void test_file_content(const char *filename, const char *msg)
{
char buf[32] = { 0 };
const int fd = open(filename, O_RDONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
int r = read(fd, buf, sizeof(buf));
TEST_ASSERT_NOT_EQUAL(-1, r);
TEST_ASSERT_EQUAL(0, strcmp(msg, buf));
TEST_ASSERT_EQUAL(0, close(fd));
}
void test_fatfs_pwrite_file(const char *filename)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC);
TEST_ASSERT_NOT_EQUAL(-1, fd);
TEST_ASSERT_EQUAL(0, close(fd));
test_pwrite(filename, 0, "Hello");
test_file_content(filename, "Hello");
test_pwrite(filename, strlen("Hello"), ", world!");
test_file_content(filename, "Hello, world!");
test_pwrite(filename, strlen("Hello, "), "Dolly");
test_file_content(filename, "Hello, Dolly!");
}
void test_fatfs_open_max_files(const char* filename_prefix, size_t files_count)
{
FILE** files = calloc(files_count, sizeof(FILE*));
for (size_t i = 0; i < files_count; ++i) {
char name[32];
snprintf(name, sizeof(name), "%s_%d.txt", filename_prefix, i);
files[i] = fopen(name, "w");
TEST_ASSERT_NOT_NULL(files[i]);
}
/* close everything and clean up */
for (size_t i = 0; i < files_count; ++i) {
fclose(files[i]);
}
free(files);
}
void test_fatfs_lseek(const char* filename)
{
FILE* f = fopen(filename, "wb+");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL(11, fprintf(f, "0123456789\n"));
TEST_ASSERT_EQUAL(0, fseek(f, -2, SEEK_CUR));
TEST_ASSERT_EQUAL('9', fgetc(f));
TEST_ASSERT_EQUAL(0, fseek(f, 3, SEEK_SET));
TEST_ASSERT_EQUAL('3', fgetc(f));
TEST_ASSERT_EQUAL(0, fseek(f, -3, SEEK_END));
TEST_ASSERT_EQUAL('8', fgetc(f));
TEST_ASSERT_EQUAL(0, fseek(f, 3, SEEK_END));
TEST_ASSERT_EQUAL(14, ftell(f));
TEST_ASSERT_EQUAL(4, fprintf(f, "abc\n"));
TEST_ASSERT_EQUAL(0, fseek(f, 0, SEEK_END));
TEST_ASSERT_EQUAL(18, ftell(f));
TEST_ASSERT_EQUAL(0, fseek(f, 0, SEEK_SET));
char buf[20];
TEST_ASSERT_EQUAL(18, fread(buf, 1, sizeof(buf), f));
const char ref_buf[] = "0123456789\n\0\0\0abc\n";
TEST_ASSERT_EQUAL_INT8_ARRAY(ref_buf, buf, sizeof(ref_buf) - 1);
TEST_ASSERT_EQUAL(0, fclose(f));
#ifdef CONFIG_FATFS_USE_FASTSEEK
f = fopen(filename, "rb+");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL(0, fseek(f, 0, SEEK_END));
TEST_ASSERT_EQUAL(18, ftell(f));
TEST_ASSERT_EQUAL(0, fseek(f, -4, SEEK_CUR));
TEST_ASSERT_EQUAL(14, ftell(f));
TEST_ASSERT_EQUAL(0, fseek(f, -14, SEEK_CUR));
TEST_ASSERT_EQUAL(0, ftell(f));
TEST_ASSERT_EQUAL(18, fread(buf, 1, sizeof(buf), f));
TEST_ASSERT_EQUAL_INT8_ARRAY(ref_buf, buf, sizeof(ref_buf) - 1);
TEST_ASSERT_EQUAL(0, fclose(f));
#endif
}
void test_fatfs_truncate_file(const char* filename)
{
int read = 0;
int truncated_len = 0;
const char input[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char output[sizeof(input)];
FILE* f = fopen(filename, "wb");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL(strlen(input), fprintf(f, input));
TEST_ASSERT_EQUAL(0, fclose(f));
// Extending file beyond size is not supported
TEST_ASSERT_EQUAL(-1, truncate(filename, strlen(input) + 1));
TEST_ASSERT_EQUAL(errno, EPERM);
TEST_ASSERT_EQUAL(-1, truncate(filename, -1));
TEST_ASSERT_EQUAL(errno, EINVAL);
// Truncating should succeed
const char truncated_1[] = "ABCDEFGHIJ";
truncated_len = strlen(truncated_1);
TEST_ASSERT_EQUAL(0, truncate(filename, truncated_len));
f = fopen(filename, "rb");
TEST_ASSERT_NOT_NULL(f);
memset(output, 0, sizeof(output));
read = fread(output, 1, sizeof(output), f);
TEST_ASSERT_EQUAL(truncated_len, read);
TEST_ASSERT_EQUAL_STRING_LEN(truncated_1, output, truncated_len);
TEST_ASSERT_EQUAL(0, fclose(f));
// Once truncated, the new file size should be the basis
// whether truncation should succeed or not
TEST_ASSERT_EQUAL(-1, truncate(filename, truncated_len + 1));
TEST_ASSERT_EQUAL(EPERM, errno);
TEST_ASSERT_EQUAL(-1, truncate(filename, strlen(input)));
TEST_ASSERT_EQUAL(EPERM, errno);
TEST_ASSERT_EQUAL(-1, truncate(filename, strlen(input) + 1));
TEST_ASSERT_EQUAL(EPERM, errno);
TEST_ASSERT_EQUAL(-1, truncate(filename, -1));
TEST_ASSERT_EQUAL(EINVAL, errno);
// Truncating a truncated file should succeed
const char truncated_2[] = "ABCDE";
truncated_len = strlen(truncated_2);
TEST_ASSERT_EQUAL(0, truncate(filename, truncated_len));
f = fopen(filename, "rb");
TEST_ASSERT_NOT_NULL(f);
memset(output, 0, sizeof(output));
read = fread(output, 1, sizeof(output), f);
TEST_ASSERT_EQUAL(truncated_len, read);
TEST_ASSERT_EQUAL_STRING_LEN(truncated_2, output, truncated_len);
TEST_ASSERT_EQUAL(0, fclose(f));
}
void test_fatfs_ftruncate_file(const char* filename)
{
int truncated_len = 0;
const char input[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char output[sizeof(input)];
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC);
TEST_ASSERT_NOT_EQUAL(-1, fd);
TEST_ASSERT_EQUAL(strlen(input), write(fd, input, strlen(input)));
// Extending file beyond size is not supported
TEST_ASSERT_EQUAL(-1, ftruncate(fd, strlen(input) + 1));
TEST_ASSERT_EQUAL(errno, EPERM);
TEST_ASSERT_EQUAL(-1, ftruncate(fd, -1));
TEST_ASSERT_EQUAL(errno, EINVAL);
// Truncating should succeed
const char truncated_1[] = "ABCDEFGHIJ";
truncated_len = strlen(truncated_1);
TEST_ASSERT_EQUAL(0, ftruncate(fd, truncated_len));
TEST_ASSERT_EQUAL(0, close(fd));
// open file for reading and validate the content
fd = open(filename, O_RDONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
memset(output, 0, sizeof(output));
TEST_ASSERT_EQUAL(truncated_len, read(fd, output, sizeof(output)));
TEST_ASSERT_EQUAL_STRING_LEN(truncated_1, output, truncated_len);
TEST_ASSERT_EQUAL(0, close(fd));
// further truncate the file
fd = open(filename, O_WRONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
// Once truncated, the new file size should be the basis
// whether truncation should succeed or not
TEST_ASSERT_EQUAL(-1, ftruncate(fd, truncated_len + 1));
TEST_ASSERT_EQUAL(EPERM, errno);
TEST_ASSERT_EQUAL(-1, ftruncate(fd, strlen(input)));
TEST_ASSERT_EQUAL(EPERM, errno);
TEST_ASSERT_EQUAL(-1, ftruncate(fd, strlen(input) + 1));
TEST_ASSERT_EQUAL(EPERM, errno);
TEST_ASSERT_EQUAL(-1, ftruncate(fd, -1));
TEST_ASSERT_EQUAL(EINVAL, errno);
// Truncating a truncated file should succeed
const char truncated_2[] = "ABCDE";
truncated_len = strlen(truncated_2);
TEST_ASSERT_EQUAL(0, ftruncate(fd, truncated_len));
TEST_ASSERT_EQUAL(0, close(fd));
// open file for reading and validate the content
fd = open(filename, O_RDONLY);
TEST_ASSERT_NOT_EQUAL(-1, fd);
memset(output, 0, sizeof(output));
TEST_ASSERT_EQUAL(truncated_len, read(fd, output, sizeof(output)));
TEST_ASSERT_EQUAL_STRING_LEN(truncated_2, output, truncated_len);
TEST_ASSERT_EQUAL(0, close(fd));
}
void test_fatfs_stat(const char* filename, const char* root_dir)
{
struct tm tm = {
.tm_year = 2017 - 1900,
.tm_mon = 11,
.tm_mday = 8,
.tm_hour = 19,
.tm_min = 51,
.tm_sec = 10
};
time_t t = mktime(&tm);
printf("Setting time: %s", asctime(&tm));
struct timeval now = { .tv_sec = t };
settimeofday(&now, NULL);
test_fatfs_create_file_with_text(filename, "foo\n");
struct stat st;
TEST_ASSERT_EQUAL(0, stat(filename, &st));
time_t mtime = st.st_mtime;
struct tm mtm;
localtime_r(&mtime, &mtm);
printf("File time: %s", asctime(&mtm));
TEST_ASSERT(llabs(mtime - t) < 2); // fatfs library stores time with 2 second precision
TEST_ASSERT(st.st_mode & S_IFREG);
TEST_ASSERT_FALSE(st.st_mode & S_IFDIR);
memset(&st, 0, sizeof(st));
TEST_ASSERT_EQUAL(0, stat(root_dir, &st));
TEST_ASSERT(st.st_mode & S_IFDIR);
TEST_ASSERT_FALSE(st.st_mode & S_IFREG);
}
void test_fatfs_mtime_dst(const char* filename, const char* root_dir)
{
struct timeval tv = { 1653638041, 0 };
settimeofday(&tv, NULL);
setenv("TZ", "MST7MDT,M3.2.0,M11.1.0", 1);
tzset();
struct tm tm;
time_t sys_time = tv.tv_sec;
localtime_r(&sys_time, &tm);
printf("Setting time: %s", asctime(&tm));
test_fatfs_create_file_with_text(filename, "foo\n");
struct stat st;
TEST_ASSERT_EQUAL(0, stat(filename, &st));
time_t mtime = st.st_mtime;
struct tm mtm;
localtime_r(&mtime, &mtm);
printf("File time: %s", asctime(&mtm));
TEST_ASSERT(llabs(mtime - sys_time) < 2); // fatfs library stores time with 2 second precision
unsetenv("TZ");
tzset();
}
void test_fatfs_utime(const char* filename, const char* root_dir)
{
struct stat achieved_stat;
struct tm desired_tm;
struct utimbuf desired_time = {
.actime = 0, // access time is not supported
.modtime = 0,
};
time_t false_now = 0;
memset(&desired_tm, 0, sizeof(struct tm));
{
// Setting up a false actual time - used when the file is created and for modification with the current time
desired_tm.tm_mon = 10 - 1;
desired_tm.tm_mday = 31;
desired_tm.tm_year = 2018 - 1900;
desired_tm.tm_hour = 10;
desired_tm.tm_min = 35;
desired_tm.tm_sec = 23;
false_now = mktime(&desired_tm);
struct timeval now = { .tv_sec = false_now };
settimeofday(&now, NULL);
}
test_fatfs_create_file_with_text(filename, "");
// 00:00:00. January 1st, 1980 - FATFS cannot handle earlier dates
desired_tm.tm_mon = 1 - 1;
desired_tm.tm_mday = 1;
desired_tm.tm_year = 1980 - 1900;
desired_tm.tm_hour = 0;
desired_tm.tm_min = 0;
desired_tm.tm_sec = 0;
printf("Testing mod. time: %s", asctime(&desired_tm));
desired_time.modtime = mktime(&desired_tm);
TEST_ASSERT_EQUAL(0, utime(filename, &desired_time));
TEST_ASSERT_EQUAL(0, stat(filename, &achieved_stat));
TEST_ASSERT_EQUAL_UINT32(desired_time.modtime, achieved_stat.st_mtime);
// current time
TEST_ASSERT_EQUAL(0, utime(filename, NULL));
TEST_ASSERT_EQUAL(0, stat(filename, &achieved_stat));
printf("Mod. time changed to (false actual time): %s", ctime(&achieved_stat.st_mtime));
TEST_ASSERT_NOT_EQUAL(desired_time.modtime, achieved_stat.st_mtime);
TEST_ASSERT(false_now - achieved_stat.st_mtime <= 2); // two seconds of tolerance are given
// 23:59:08. December 31st, 2037
desired_tm.tm_mon = 12 - 1;
desired_tm.tm_mday = 31;
desired_tm.tm_year = 2037 - 1900;
desired_tm.tm_hour = 23;
desired_tm.tm_min = 59;
desired_tm.tm_sec = 8;
printf("Testing mod. time: %s", asctime(&desired_tm));
desired_time.modtime = mktime(&desired_tm);
TEST_ASSERT_EQUAL(0, utime(filename, &desired_time));
TEST_ASSERT_EQUAL(0, stat(filename, &achieved_stat));
TEST_ASSERT_EQUAL_UINT32(desired_time.modtime, achieved_stat.st_mtime);
//WARNING: it has the Unix Millenium bug (Y2K38)
// 00:00:00. January 1st, 1970 - FATFS cannot handle years before 1980
desired_tm.tm_mon = 1 - 1;
desired_tm.tm_mday = 1;
desired_tm.tm_year = 1970 - 1900;
desired_tm.tm_hour = 0;
desired_tm.tm_min = 0;
desired_tm.tm_sec = 0;
printf("Testing mod. time: %s", asctime(&desired_tm));
desired_time.modtime = mktime(&desired_tm);
TEST_ASSERT_EQUAL(-1, utime(filename, &desired_time));
TEST_ASSERT_EQUAL(EINVAL, errno);
}
void test_fatfs_unlink(const char* filename)
{
test_fatfs_create_file_with_text(filename, "unlink\n");
TEST_ASSERT_EQUAL(0, unlink(filename));
TEST_ASSERT_NULL(fopen(filename, "r"));
}
void test_fatfs_link_rename(const char* filename_prefix)
{
char name_copy[64];
char name_dst[64];
char name_src[64];
snprintf(name_copy, sizeof(name_copy), "%s_cpy.txt", filename_prefix);
snprintf(name_dst, sizeof(name_dst), "%s_dst.txt", filename_prefix);
snprintf(name_src, sizeof(name_src), "%s_src.txt", filename_prefix);
unlink(name_copy);
unlink(name_dst);
unlink(name_src);
FILE* f = fopen(name_src, "w+");
TEST_ASSERT_NOT_NULL(f);
const char* str = "0123456789";
for (int i = 0; i < 4000; ++i) {
TEST_ASSERT_NOT_EQUAL(EOF, fputs(str, f));
}
TEST_ASSERT_EQUAL(0, fclose(f));
TEST_ASSERT_EQUAL(0, link(name_src, name_copy));
FILE* fcopy = fopen(name_copy, "r");
TEST_ASSERT_NOT_NULL(fcopy);
TEST_ASSERT_EQUAL(0, fseek(fcopy, 0, SEEK_END));
TEST_ASSERT_EQUAL(40000, ftell(fcopy));
TEST_ASSERT_EQUAL(0, fclose(fcopy));
TEST_ASSERT_EQUAL(0, rename(name_copy, name_dst));
TEST_ASSERT_NULL(fopen(name_copy, "r"));
FILE* fdst = fopen(name_dst, "r");
TEST_ASSERT_NOT_NULL(fdst);
TEST_ASSERT_EQUAL(0, fseek(fdst, 0, SEEK_END));
TEST_ASSERT_EQUAL(40000, ftell(fdst));
TEST_ASSERT_EQUAL(0, fclose(fdst));
}
void test_fatfs_mkdir_rmdir(const char* filename_prefix)
{
char name_dir1[64];
char name_dir2[64];
char name_dir2_file[64];
snprintf(name_dir1, sizeof(name_dir1), "%s1", filename_prefix);
snprintf(name_dir2, sizeof(name_dir2), "%s2", filename_prefix);
snprintf(name_dir2_file, sizeof(name_dir2_file), "%s2/1.txt", filename_prefix);
TEST_ASSERT_EQUAL(0, mkdir(name_dir1, 0755));
struct stat st;
TEST_ASSERT_EQUAL(0, stat(name_dir1, &st));
TEST_ASSERT_TRUE(st.st_mode & S_IFDIR);
TEST_ASSERT_FALSE(st.st_mode & S_IFREG);
TEST_ASSERT_EQUAL(0, rmdir(name_dir1));
TEST_ASSERT_EQUAL(-1, stat(name_dir1, &st));
TEST_ASSERT_EQUAL(0, mkdir(name_dir2, 0755));
test_fatfs_create_file_with_text(name_dir2_file, "foo\n");
TEST_ASSERT_EQUAL(0, stat(name_dir2, &st));
TEST_ASSERT_TRUE(st.st_mode & S_IFDIR);
TEST_ASSERT_FALSE(st.st_mode & S_IFREG);
TEST_ASSERT_EQUAL(0, stat(name_dir2_file, &st));
TEST_ASSERT_FALSE(st.st_mode & S_IFDIR);
TEST_ASSERT_TRUE(st.st_mode & S_IFREG);
TEST_ASSERT_EQUAL(-1, rmdir(name_dir2));
TEST_ASSERT_EQUAL(0, unlink(name_dir2_file));
TEST_ASSERT_EQUAL(0, rmdir(name_dir2));
}
void test_fatfs_can_opendir(const char* path)
{
char name_dir_file[64];
const char * file_name = "test_opd.txt";
snprintf(name_dir_file, sizeof(name_dir_file), "%s/%s", path, file_name);
unlink(name_dir_file);
test_fatfs_create_file_with_text(name_dir_file, "test_opendir\n");
DIR* dir = opendir(path);
TEST_ASSERT_NOT_NULL(dir);
bool found = false;
while (true) {
struct dirent* de = readdir(dir);
if (!de) {
break;
}
if (strcasecmp(de->d_name, file_name) == 0) {
found = true;
break;
}
}
TEST_ASSERT_TRUE(found);
TEST_ASSERT_EQUAL(0, closedir(dir));
unlink(name_dir_file);
}
void test_fatfs_opendir_readdir_rewinddir(const char* dir_prefix)
{
char name_dir_inner_file[64];
char name_dir_inner[64];
char name_dir_file3[64];
char name_dir_file2[64];
char name_dir_file1[64];
snprintf(name_dir_inner_file, sizeof(name_dir_inner_file), "%s/inner/3.txt", dir_prefix);
snprintf(name_dir_inner, sizeof(name_dir_inner), "%s/inner", dir_prefix);
snprintf(name_dir_file3, sizeof(name_dir_file2), "%s/boo.bin", dir_prefix);
snprintf(name_dir_file2, sizeof(name_dir_file2), "%s/2.txt", dir_prefix);
snprintf(name_dir_file1, sizeof(name_dir_file1), "%s/1.txt", dir_prefix);
unlink(name_dir_inner_file);
rmdir(name_dir_inner);
unlink(name_dir_file1);
unlink(name_dir_file2);
unlink(name_dir_file3);
rmdir(dir_prefix);
TEST_ASSERT_EQUAL(0, mkdir(dir_prefix, 0755));
test_fatfs_create_file_with_text(name_dir_file1, "1\n");
test_fatfs_create_file_with_text(name_dir_file2, "2\n");
test_fatfs_create_file_with_text(name_dir_file3, "\01\02\03");
TEST_ASSERT_EQUAL(0, mkdir(name_dir_inner, 0755));
test_fatfs_create_file_with_text(name_dir_inner_file, "3\n");
DIR* dir = opendir(dir_prefix);
TEST_ASSERT_NOT_NULL(dir);
int count = 0;
const char* names[4];
while(count < 4) {
struct dirent* de = readdir(dir);
if (!de) {
break;
}
printf("found '%s'\n", de->d_name);
if (strcasecmp(de->d_name, "1.txt") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "1.txt";
++count;
} else if (strcasecmp(de->d_name, "2.txt") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "2.txt";
++count;
} else if (strcasecmp(de->d_name, "inner") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_DIR);
names[count] = "inner";
++count;
} else if (strcasecmp(de->d_name, "boo.bin") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "boo.bin";
++count;
} else {
TEST_FAIL_MESSAGE("unexpected directory entry");
}
}
TEST_ASSERT_EQUAL(count, 4);
rewinddir(dir);
struct dirent* de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[0]));
seekdir(dir, 3);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[3]));
seekdir(dir, 1);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[1]));
seekdir(dir, 2);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[2]));
TEST_ASSERT_EQUAL(0, closedir(dir));
}
void test_fatfs_opendir_readdir_rewinddir_utf_8(const char* dir_prefix)
{
char name_dir_inner_file[64];
char name_dir_inner[64];
char name_dir_file3[64];
char name_dir_file2[64];
char name_dir_file1[64];
snprintf(name_dir_inner_file, sizeof(name_dir_inner_file), "%s/内部目录/内部文件.txt", dir_prefix);
snprintf(name_dir_inner, sizeof(name_dir_inner), "%s/内部目录", dir_prefix);
snprintf(name_dir_file3, sizeof(name_dir_file3), "%s/文件三.bin", dir_prefix);
snprintf(name_dir_file2, sizeof(name_dir_file2), "%s/文件二.txt", dir_prefix);
snprintf(name_dir_file1, sizeof(name_dir_file1), "%s/文件一.txt", dir_prefix);
unlink(name_dir_inner_file);
rmdir(name_dir_inner);
unlink(name_dir_file1);
unlink(name_dir_file2);
unlink(name_dir_file3);
rmdir(dir_prefix);
TEST_ASSERT_EQUAL(0, mkdir(dir_prefix, 0755));
test_fatfs_create_file_with_text(name_dir_file1, "一号\n");
test_fatfs_create_file_with_text(name_dir_file2, "二号\n");
test_fatfs_create_file_with_text(name_dir_file3, "\0\0\0");
TEST_ASSERT_EQUAL(0, mkdir(name_dir_inner, 0755));
test_fatfs_create_file_with_text(name_dir_inner_file, "三号\n");
DIR* dir = opendir(dir_prefix);
TEST_ASSERT_NOT_NULL(dir);
int count = 0;
const char* names[4];
while(count < 4) {
struct dirent* de = readdir(dir);
if (!de) {
break;
}
printf("found '%s'\n", de->d_name);
if (strcasecmp(de->d_name, "文件一.txt") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "文件一.txt";
++count;
} else if (strcasecmp(de->d_name, "文件二.txt") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "文件二.txt";
++count;
} else if (strcasecmp(de->d_name, "内部目录") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_DIR);
names[count] = "内部目录";
++count;
} else if (strcasecmp(de->d_name, "文件三.bin") == 0) {
TEST_ASSERT_TRUE(de->d_type == DT_REG);
names[count] = "文件三.bin";
++count;
} else {
TEST_FAIL_MESSAGE("unexpected directory entry");
}
}
TEST_ASSERT_EQUAL(count, 4);
rewinddir(dir);
struct dirent* de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[0]));
seekdir(dir, 3);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[3]));
seekdir(dir, 1);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[1]));
seekdir(dir, 2);
de = readdir(dir);
TEST_ASSERT_NOT_NULL(de);
TEST_ASSERT_EQUAL(0, strcasecmp(de->d_name, names[2]));
TEST_ASSERT_EQUAL(0, closedir(dir));
}
typedef struct {
const char* filename;
bool write;
size_t word_count;
unsigned seed;
SemaphoreHandle_t done;
esp_err_t result;
} read_write_test_arg_t;
#define READ_WRITE_TEST_ARG_INIT(name, seed_) \
{ \
.filename = name, \
.seed = seed_, \
.word_count = 8192, \
.write = true, \
.done = xSemaphoreCreateBinary() \
}
static void read_write_task(void* param)
{
read_write_test_arg_t* args = (read_write_test_arg_t*) param;
FILE* f = fopen(args->filename, args->write ? "wb" : "rb");
if (f == NULL) {
args->result = ESP_ERR_NOT_FOUND;
goto done;
}
srand(args->seed);
for (size_t i = 0; i < args->word_count; ++i) {
unsigned val = rand();
if (args->write) {
int cnt = fwrite(&val, sizeof(val), 1, f);
if (cnt != 1) {
printf("E(w): i=%d, cnt=%d val=0x08%x\n", i, cnt, val);
args->result = ESP_FAIL;
goto close;
}
} else {
unsigned rval;
int cnt = fread(&rval, sizeof(rval), 1, f);
if (cnt != 1 || rval != val) {
printf("E(r): i=%d, cnt=%d rval=0x08%x val=0x08%x\n", i, cnt, rval, val);
args->result = ESP_FAIL;
goto close;
}
}
}
args->result = ESP_OK;
close:
fclose(f);
done:
xSemaphoreGive(args->done);
vTaskDelay(1);
vTaskDelete(NULL);
}
void test_fatfs_concurrent(const char* filename_prefix)
{
char names[4][64];
for (size_t i = 0; i < 4; ++i) {
snprintf(names[i], sizeof(names[i]), "%s%d", filename_prefix, i + 1);
unlink(names[i]);
}
read_write_test_arg_t args1 = READ_WRITE_TEST_ARG_INIT(names[0], 1);
read_write_test_arg_t args2 = READ_WRITE_TEST_ARG_INIT(names[1], 2);
printf("writing f1 and f2\n");
const int cpuid_0 = 0;
const int cpuid_1 = portNUM_PROCESSORS - 1;
const int stack_size = 4096;
xTaskCreatePinnedToCore(&read_write_task, "rw1", stack_size, &args1, 3, NULL, cpuid_0);
xTaskCreatePinnedToCore(&read_write_task, "rw2", stack_size, &args2, 3, NULL, cpuid_1);
xSemaphoreTake(args1.done, portMAX_DELAY);
printf("f1 done\n");
TEST_ASSERT_EQUAL(ESP_OK, args1.result);
xSemaphoreTake(args2.done, portMAX_DELAY);
printf("f2 done\n");
TEST_ASSERT_EQUAL(ESP_OK, args2.result);
args1.write = false;
args2.write = false;
read_write_test_arg_t args3 = READ_WRITE_TEST_ARG_INIT(names[2], 3);
read_write_test_arg_t args4 = READ_WRITE_TEST_ARG_INIT(names[3], 4);
printf("reading f1 and f2, writing f3 and f4\n");
xTaskCreatePinnedToCore(&read_write_task, "rw3", stack_size, &args3, 3, NULL, cpuid_1);
xTaskCreatePinnedToCore(&read_write_task, "rw4", stack_size, &args4, 3, NULL, cpuid_0);
xTaskCreatePinnedToCore(&read_write_task, "rw1", stack_size, &args1, 3, NULL, cpuid_0);
xTaskCreatePinnedToCore(&read_write_task, "rw2", stack_size, &args2, 3, NULL, cpuid_1);
xSemaphoreTake(args1.done, portMAX_DELAY);
printf("f1 done\n");
TEST_ASSERT_EQUAL(ESP_OK, args1.result);
xSemaphoreTake(args2.done, portMAX_DELAY);
printf("f2 done\n");
TEST_ASSERT_EQUAL(ESP_OK, args2.result);
xSemaphoreTake(args3.done, portMAX_DELAY);
printf("f3 done\n");
TEST_ASSERT_EQUAL(ESP_OK, args3.result);
xSemaphoreTake(args4.done, portMAX_DELAY);
printf("f4 done\n");
TEST_ASSERT_EQUAL(ESP_OK, args4.result);
vSemaphoreDelete(args1.done);
vSemaphoreDelete(args2.done);
vSemaphoreDelete(args3.done);
vSemaphoreDelete(args4.done);
}
void test_leading_spaces(void){
// fatfs should ignore leading and trailing whitespaces
// and files "/spiflash/ thelongfile.txt " and "/spiflash/thelongfile.txt" should be equivalent
// this feature is currently not implemented
FILE* f = fopen( "/spiflash/ thelongfile.txt ", "wb");
fclose(f);
TEST_ASSERT_NULL(fopen("/spiflash/thelongfile.txt", "r"));
}
void test_fatfs_rw_speed(const char* filename, void* buf, size_t buf_size, size_t file_size, bool is_write)
{
const size_t buf_count = file_size / buf_size;
FILE* f = fopen(filename, (is_write) ? "wb" : "rb");
TEST_ASSERT_NOT_NULL(f);
struct timeval tv_start;
gettimeofday(&tv_start, NULL);
for (size_t n = 0; n < buf_count; ++n) {
if (is_write) {
TEST_ASSERT_EQUAL(buf_size, write(fileno(f), buf, buf_size));
} else {
if (read(fileno(f), buf, buf_size) != buf_size) {
printf("reading at n=%d, eof=%d", n, feof(f));
TEST_FAIL();
}
}
}
struct timeval tv_end;
gettimeofday(&tv_end, NULL);
TEST_ASSERT_EQUAL(0, fclose(f));
float t_s = tv_end.tv_sec - tv_start.tv_sec + 1e-6f * (tv_end.tv_usec - tv_start.tv_usec);
printf("%s %d bytes (block size %d) in %.3fms (%.3f MB/s)\n",
(is_write)?"Wrote":"Read", file_size, buf_size, t_s * 1e3,
file_size / (1024.0f * 1024.0f * t_s));
}
void test_fatfs_info(const char* base_path, const char* filepath)
{
// Empty FS
uint64_t total_bytes = 0;
uint64_t free_bytes = 0;
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_fat_info(base_path, &total_bytes, &free_bytes));
ESP_LOGD("fatfs info", "total_bytes=%llu, free_bytes=%llu", total_bytes, free_bytes);
TEST_ASSERT_NOT_EQUAL(0, total_bytes);
// FS with a file
FILE* f = fopen(filepath, "wb");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_TRUE(fputs(fatfs_test_hello_str, f) != EOF);
TEST_ASSERT_EQUAL(0, fclose(f));
uint64_t free_bytes_new = 0;
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_fat_info(base_path, &total_bytes, &free_bytes_new));
ESP_LOGD("fatfs info", "total_bytes=%llu, free_bytes_new=%llu", total_bytes, free_bytes_new);
TEST_ASSERT_NOT_EQUAL(free_bytes, free_bytes_new);
// File removed
TEST_ASSERT_EQUAL(0, remove(filepath));
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_fat_info(base_path, &total_bytes, &free_bytes_new));
ESP_LOGD("fatfs info", "total_bytes=%llu, free_bytes_after_delete=%llu", total_bytes, free_bytes_new);
TEST_ASSERT_EQUAL(free_bytes, free_bytes_new);
}

@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
/**
* @file test_fatfs_common.h
* @brief Common routines for FAT-on-SDMMC and FAT-on-WL tests
*/
#define HEAP_SIZE_CAPTURE(heap_size) \
heap_size = esp_get_free_heap_size();
#define HEAP_SIZE_CHECK(heap_size, tolerance) \
do {\
size_t final_heap_size = esp_get_free_heap_size(); \
if (final_heap_size < heap_size - tolerance) { \
printf("Initial heap size: %d, final: %d, diff=%d\n", heap_size, final_heap_size, heap_size - final_heap_size); \
} \
} while(0)
extern const char* fatfs_test_hello_str;
extern const char* fatfs_test_hello_str_utf;
void test_fatfs_create_file_with_text(const char* name, const char* text);
void test_fatfs_create_file_with_o_creat_flag(const char* filename);
void test_fatfs_open_file_with_o_creat_flag(const char* filename);
void test_fatfs_overwrite_append(const char* filename);
void test_fatfs_read_file(const char* filename);
void test_fatfs_read_file_utf_8(const char* filename);
void test_fatfs_pread_file(const char* filename);
void test_fatfs_pwrite_file(const char* filename);
void test_fatfs_open_max_files(const char* filename_prefix, size_t files_count);
void test_fatfs_lseek(const char* filename);
void test_fatfs_truncate_file(const char* path);
void test_fatfs_ftruncate_file(const char* path);
void test_fatfs_stat(const char* filename, const char* root_dir);
void test_fatfs_mtime_dst(const char* filename, const char* root_dir);
void test_fatfs_utime(const char* filename, const char* root_dir);
void test_fatfs_unlink(const char* filename);
void test_fatfs_link_rename(const char* filename_prefix);
void test_fatfs_concurrent(const char* filename_prefix);
void test_fatfs_mkdir_rmdir(const char* filename_prefix);
void test_fatfs_can_opendir(const char* path);
void test_fatfs_opendir_readdir_rewinddir(const char* dir_prefix);
void test_fatfs_opendir_readdir_rewinddir_utf_8(const char* dir_prefix);
void test_leading_spaces(void);
void test_fatfs_rw_speed(const char* filename, void* buf, size_t buf_size, size_t file_size, bool write);
void test_fatfs_info(const char* base_path, const char* filepath);

@ -0,0 +1,106 @@
ifndef COMPONENT
COMPONENT := fatfs
endif
COMPONENT_LIB := lib$(COMPONENT).a
TEST_PROGRAM := test_$(COMPONENT)
STUBS_LIB_DIR := ../../../components/spi_flash/sim/stubs
STUBS_LIB_BUILD_DIR := $(STUBS_LIB_DIR)/build
STUBS_LIB := libstubs.a
SPI_FLASH_SIM_DIR := ../../../components/spi_flash/sim
SPI_FLASH_SIM_BUILD_DIR := $(SPI_FLASH_SIM_DIR)/build
SPI_FLASH_SIM_LIB := libspi_flash.a
WEAR_LEVELLING_DIR := ../../../components/wear_levelling/test_wl_host
WEAR_LEVELLING_BUILD_DIR := $(WEAR_LEVELLING_DIR)/build
WEAR_LEVELLING_LIB := libwl.a
include Makefile.files
all: test
ifndef SDKCONFIG
SDKCONFIG_DIR := $(dir $(realpath sdkconfig/sdkconfig.h))
SDKCONFIG := $(SDKCONFIG_DIR)sdkconfig.h
else
SDKCONFIG_DIR := $(dir $(realpath $(SDKCONFIG)))
endif
INCLUDE_FLAGS := $(addprefix -I, $(INCLUDE_DIRS) $(SDKCONFIG_DIR) ../../../tools/catch)
CPPFLAGS += $(INCLUDE_FLAGS) -g -m32
CXXFLAGS += $(INCLUDE_FLAGS) -std=c++11 -g -m32
# Build libraries that this component is dependent on
$(STUBS_LIB_BUILD_DIR)/$(STUBS_LIB): force
$(MAKE) -C $(STUBS_LIB_DIR) lib SDKCONFIG=$(SDKCONFIG)
$(SPI_FLASH_SIM_BUILD_DIR)/$(SPI_FLASH_SIM_LIB): force
$(MAKE) -C $(SPI_FLASH_SIM_DIR) lib SDKCONFIG=$(SDKCONFIG)
$(WEAR_LEVELLING_BUILD_DIR)/$(WEAR_LEVELLING_LIB): force
$(MAKE) -C $(WEAR_LEVELLING_DIR) lib SDKCONFIG=$(SDKCONFIG)
# Create target for building this component as a library
CFILES := $(filter %.c, $(SOURCE_FILES))
CPPFILES := $(filter %.cpp, $(SOURCE_FILES))
CTARGET = ${2}/$(patsubst %.c,%.o,$(notdir ${1}))
CPPTARGET = ${2}/$(patsubst %.cpp,%.o,$(notdir ${1}))
ifndef BUILD_DIR
BUILD_DIR := build
endif
OBJ_FILES := $(addprefix $(BUILD_DIR)/, $(filter %.o, $(notdir $(SOURCE_FILES:.cpp=.o) $(SOURCE_FILES:.c=.o))))
define COMPILE_C
$(call CTARGET, ${1}, $(BUILD_DIR)) : ${1} $(SDKCONFIG)
mkdir -p $(BUILD_DIR)
$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(call CTARGET, ${1}, $(BUILD_DIR)) ${1}
endef
define COMPILE_CPP
$(call CPPTARGET, ${1}, $(BUILD_DIR)) : ${1} $(SDKCONFIG)
mkdir -p $(BUILD_DIR)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $(call CPPTARGET, ${1}, $(BUILD_DIR)) ${1}
endef
$(BUILD_DIR)/$(COMPONENT_LIB): $(OBJ_FILES) $(SDKCONFIG)
mkdir -p $(BUILD_DIR)
$(AR) rcs $@ $^
lib: $(BUILD_DIR)/$(COMPONENT_LIB)
$(foreach cfile, $(CFILES), $(eval $(call COMPILE_C, $(cfile))))
$(foreach cxxfile, $(CPPFILES), $(eval $(call COMPILE_CPP, $(cxxfile))))
# Create target for building this component as a test
TEST_SOURCE_FILES = \
test_fatfs.cpp \
main.cpp \
TEST_OBJ_FILES = $(filter %.o, $(TEST_SOURCE_FILES:.cpp=.o) $(TEST_SOURCE_FILES:.c=.o))
$(TEST_PROGRAM): lib $(TEST_OBJ_FILES) $(WEAR_LEVELLING_BUILD_DIR)/$(WEAR_LEVELLING_LIB) $(SPI_FLASH_SIM_BUILD_DIR)/$(SPI_FLASH_SIM_LIB) $(STUBS_LIB_BUILD_DIR)/$(STUBS_LIB) partition_table.bin $(SDKCONFIG)
g++ $(LDFLAGS) $(CXXFLAGS) -o $@ $(TEST_OBJ_FILES) -L$(BUILD_DIR) -l:$(COMPONENT_LIB) -L$(WEAR_LEVELLING_BUILD_DIR) -l:$(WEAR_LEVELLING_LIB) -L$(SPI_FLASH_SIM_BUILD_DIR) -l:$(SPI_FLASH_SIM_LIB) -L$(STUBS_LIB_BUILD_DIR) -l:$(STUBS_LIB)
test: $(TEST_PROGRAM)
./$(TEST_PROGRAM)
# Create other necessary targets
partition_table.bin: partition_table.csv
python ../../../components/partition_table/gen_esp32part.py --verify $< $@
force:
# Create target to cleanup files
clean:
$(MAKE) -C $(STUBS_LIB_DIR) clean
$(MAKE) -C $(SPI_FLASH_SIM_DIR) clean
$(MAKE) -C $(WEAR_LEVELLING_DIR) clean
rm -f $(OBJ_FILES) $(TEST_OBJ_FILES) $(TEST_PROGRAM) $(COMPONENT_LIB) partition_table.bin
.PHONY: all lib test clean force

@ -0,0 +1,44 @@
SOURCE_FILES := \
$(addprefix ../src/, \
ff.c \
ffunicode.c \
) \
$(addprefix ../diskio/,\
diskio.c \
diskio_wl.c \
) \
../port/linux/ffsystem.c
INCLUDE_DIRS := \
. \
../diskio \
../src \
$(addprefix ../../spi_flash/sim/stubs/, \
app_update/include \
driver/include \
freertos/include \
newlib/include \
sdmmc/include \
vfs/include \
) \
$(addprefix ../../../components/, \
esp_rom/include \
esp_hw_support/include \
esp_hw_support/include/soc \
esp_system/include \
log/include \
xtensa/include \
xtensa/esp32/include \
soc/esp32/include \
heap/include \
soc/include \
esp32/include \
esp_common/include \
bootloader_support/include \
bootloader_support/bootloader_flash/include \
app_update/include \
hal/include \
spi_flash/include \
wear_levelling/include \
esp_partition/include \
)

@ -0,0 +1,17 @@
include $(COMPONENT_PATH)/Makefile.files
COMPONENT_OWNBUILDTARGET := 1
COMPONENT_OWNCLEANTARGET := 1
COMPONENT_ADD_INCLUDEDIRS := $(INCLUDE_DIRS)
.PHONY: build
build: $(SDKCONFIG_HEADER)
$(MAKE) -C $(COMPONENT_PATH) lib SDKCONFIG=$(SDKCONFIG_HEADER) BUILD_DIR=$(COMPONENT_BUILD_DIR) COMPONENT=$(COMPONENT_NAME)
CLEAN_FILES := component_project_vars.mk
.PHONY: clean
clean:
$(summary) RM $(CLEAN_FILES)
rm -f $(CLEAN_FILES)
$(MAKE) -C $(COMPONENT_PATH) clean SDKCONFIG=$(SDKCONFIG_HEADER) BUILD_DIR=$(COMPONENT_BUILD_DIR) COMPONENT=$(COMPONENT_NAME)

@ -0,0 +1,2 @@
#define CATCH_CONFIG_MAIN
#include "catch.hpp"

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
storage, data, fat, , 1M,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 1M,
6 storage, data, fat, , 1M,

@ -0,0 +1,94 @@
#include <stdio.h>
#include <string.h>
#include "ff.h"
#include "esp_partition.h"
#include "wear_levelling.h"
#include "diskio_impl.h"
#include "diskio_wl.h"
#include "catch.hpp"
extern "C" void _spi_flash_init(const char* chip_size, size_t block_size, size_t sector_size, size_t page_size, const char* partition_bin);
TEST_CASE("create volume, open file, write and read back data", "[fatfs]")
{
_spi_flash_init(CONFIG_ESPTOOLPY_FLASHSIZE, CONFIG_WL_SECTOR_SIZE * 16, CONFIG_WL_SECTOR_SIZE, CONFIG_WL_SECTOR_SIZE, "partition_table.bin");
FRESULT fr_result;
BYTE pdrv;
FATFS fs;
FIL file;
UINT bw;
esp_err_t esp_result;
const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, "storage");
// Mount wear-levelled partition
wl_handle_t wl_handle;
esp_result = wl_mount(partition, &wl_handle);
REQUIRE(esp_result == ESP_OK);
// Get a physical drive
esp_result = ff_diskio_get_drive(&pdrv);
REQUIRE(esp_result == ESP_OK);
// Register physical drive as wear-levelled partition
esp_result = ff_diskio_register_wl_partition(pdrv, wl_handle);
// Create FAT volume on the entire disk
LBA_t part_list[] = {100, 0, 0, 0};
BYTE work_area[FF_MAX_SS];
fr_result = f_fdisk(pdrv, part_list, work_area);
REQUIRE(fr_result == FR_OK);
const MKFS_PARM opt = {(BYTE)FM_ANY, 0, 0, 0, 0};
fr_result = f_mkfs("", &opt, work_area, sizeof(work_area)); // Use default volume
// Mount the volume
fr_result = f_mount(&fs, "", 0);
REQUIRE(fr_result == FR_OK);
// Open, write and read data
fr_result = f_open(&file, "test.txt", FA_OPEN_ALWAYS | FA_READ | FA_WRITE);
REQUIRE(fr_result == FR_OK);
// Generate data
uint32_t data_size = 100000;
char *data = (char*) malloc(data_size);
char *read = (char*) malloc(data_size);
for(uint32_t i = 0; i < data_size; i += sizeof(i))
{
*((uint32_t*)(data + i)) = i;
}
// Write generated data
fr_result = f_write(&file, data, data_size, &bw);
REQUIRE(fr_result == FR_OK);
REQUIRE(bw == data_size);
// Move to beginning of file
fr_result = f_lseek(&file, 0);
REQUIRE(fr_result == FR_OK);
// Read written data
fr_result = f_read(&file, read, data_size, &bw);
REQUIRE(fr_result == FR_OK);
REQUIRE(bw == data_size);
REQUIRE(memcmp(data, read, data_size) == 0);
// Close file
fr_result = f_close(&file);
REQUIRE(fr_result == FR_OK);
// Unmount default volume
fr_result = f_mount(0, "", 0);
REQUIRE(fr_result == FR_OK);
free(read);
free(data);
}

@ -0,0 +1,535 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
import shutil
import sys
import unittest
from subprocess import STDOUT, check_output
from test_utils import CFG, fill_sector, generate_test_dir_1, generate_test_dir_2
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import fatfsgen # noqa E402 # pylint: disable=C0413
from fatfs_utils.boot_sector import BootSector # noqa E402 # pylint: disable=C0413
from fatfs_utils.cluster import Cluster # noqa E402 # pylint: disable=C0413
from fatfs_utils.entry import Entry # noqa E402 # pylint: disable=C0413
from fatfs_utils.exceptions import InconsistentFATAttributes # noqa E402 # pylint: disable=C0413
from fatfs_utils.exceptions import NotInitialized # noqa E402 # pylint: disable=C0413
from fatfs_utils.exceptions import TooLongNameException # noqa E402 # pylint: disable=C0413
from fatfs_utils.exceptions import WriteDirectoryException # noqa E402 # pylint: disable=C0413
from fatfs_utils.exceptions import LowerCaseException, NoFreeClusterException # noqa E402 # pylint: disable=C0413
from fatfs_utils.utils import right_strip_string # noqa E402 # pylint: disable=C0413
from fatfs_utils.utils import FAT12, read_filesystem # noqa E402 # pylint: disable=C0413
class FatFSGen(unittest.TestCase):
def setUp(self) -> None:
os.makedirs('output_data')
os.makedirs('test_dir')
generate_test_dir_1()
generate_test_dir_2()
def tearDown(self) -> None:
shutil.rmtree('output_data')
shutil.rmtree('test_dir')
if os.path.exists('fatfs_image.img'):
os.remove('fatfs_image.img')
def test_empty_file_sn_fat12(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('TESTFILE')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000:0x200c], b'TESTFILE \x20') # check entry name and type
self.assertEqual(file_system[0x1000:0x1006], b'\xf8\xff\xff\xff\x0f\x00') # check fat
def test_directory_sn_fat12(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000:0x200c], b'TESTFOLD \x10') # check entry name and type
self.assertEqual(file_system[0x1000:0x1006], b'\xf8\xff\xff\xff\x0f\x00') # check fat
self.assertEqual(file_system[0x6000:0x600c], b'. \x10') # reference to itself
self.assertEqual(file_system[0x6020:0x602c], b'.. \x10') # reference to parent
def test_empty_file_with_extension_sn_fat12(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('TESTF', extension='TXT')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000:0x200c], b'TESTF TXT\x20') # check entry name and type
self.assertEqual(file_system[0x1000:0x1006], b'\xf8\xff\xff\xff\x0f\x00') # check fat
def test_write_to_file_with_extension_sn_fat12(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('WRITEF', extension='TXT')
fatfs.write_content(path_from_root=['WRITEF.TXT'], content=b'testcontent')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000:0x200c], b'WRITEF TXT\x20') # check entry name and type
self.assertEqual(file_system[0x201a:0x2020], b'\x02\x00\x0b\x00\x00\x00') # check size and cluster ref
self.assertEqual(file_system[0x1000:0x1006], b'\xf8\xff\xff\xff\x0f\x00') # check fat
self.assertEqual(file_system[0x6000:0x600f], b'testcontent\x00\x00\x00\x00') # check file content
def test_write_to_file_in_folder_sn_fat12(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
fatfs.create_file('WRITEF', extension='TXT', path_from_root=['TESTFOLD'])
fatfs.write_content(path_from_root=['TESTFOLD', 'WRITEF.TXT'], content=b'testcontent')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000:0x200c], b'TESTFOLD \x10')
self.assertEqual(
file_system[0x1000:0x1010],
b'\xf8\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6040:0x6050], b'WRITEF TXT\x20\x00\x00\x00\x00')
self.assertEqual(file_system[0x605a:0x6060], b'\x03\x00\x0b\x00\x00\x00')
self.assertEqual(file_system[0x7000:0x700b], b'testcontent') # check file content
def test_cluster_setting_values(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('TESTFIL1')
fatfs.create_file('TESTFIL2')
fatfs.create_file('TESTFIL3')
fatfs.create_file('TESTFIL4')
fatfs.create_file('TESTFIL5')
fatfs.fat.clusters[2].set_in_fat(1000)
fatfs.fat.clusters[3].set_in_fat(4)
fatfs.fat.clusters[4].set_in_fat(5)
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(
file_system[0x1000:0x1010],
b'\xf8\xff\xff\xe8\x43\x00\x05\xf0\xff\xff\x0f\x00\x00\x00\x00\x00')
def test_full_sector_file(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('WRITEF', extension='TXT')
fatfs.write_content(path_from_root=['WRITEF.TXT'], content=CFG['sector_size'] * b'a')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x1000: 0x100e], b'\xf8\xff\xff\xff\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x7000], CFG['sector_size'] * b'a')
def test_file_chaining(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('WRITEF', extension='TXT')
fatfs.write_content(path_from_root=['WRITEF.TXT'], content=CFG['sector_size'] * b'a' + b'a')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x1000: 0x100e], b'\xf8\xff\xff\x03\xf0\xff\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x7000: 0x8000], b'a' + (CFG['sector_size'] - 1) * b'\x00')
def test_full_sector_folder(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
fill_sector(fatfs)
fatfs.write_content(path_from_root=['TESTFOLD', 'A0'], content=b'first')
fatfs.write_content(path_from_root=['TESTFOLD', 'A126'], content=b'later')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x1000: 0x10d0],
b'\xf8\xff\xff\x82\xf0\xff' + 192 * b'\xff' + 10 * b'\x00')
self.assertEqual(file_system[0x85000:0x85005], b'later')
self.assertEqual(file_system[0x86000:0x86010], b'A126 \x00\x00\x00\x00')
self.assertEqual(file_system[0x86020:0x86030], b'A127 \x00\x00\x00\x00')
def test_write_to_folder_in_folder_sn_fat12(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
fatfs.create_directory('TESTFOLL', path_from_root=['TESTFOLD'])
self.assertRaises(WriteDirectoryException, fatfs.write_content, path_from_root=['TESTFOLD', 'TESTFOLL'],
content=b'testcontent')
def test_write_non_existing_file_in_folder_sn_fat12(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
self.assertRaises(FileNotFoundError, fatfs.write_content, path_from_root=['TESTFOLD', 'AHOJ'],
content=b'testcontent')
@staticmethod
def create_too_many_files() -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
for i in range(2 * CFG['sector_size'] // CFG['entry_size']):
fatfs.create_file(f'A{str(i).upper()}', path_from_root=['TESTFOLD'])
def test_too_many_files(self) -> None:
self.assertRaises(NoFreeClusterException, self.create_too_many_files)
def test_full_two_sectors_folder(self) -> None:
fatfs = fatfsgen.FATFS(size=2 * 1024 * 1024)
fatfs.create_directory('TESTFOLD')
for i in range(2 * CFG['sector_size'] // CFG['entry_size']):
fatfs.create_file(f'A{str(i).upper()}', path_from_root=['TESTFOLD'])
fatfs.write_content(path_from_root=['TESTFOLD', 'A253'], content=b'later')
fatfs.write_content(path_from_root=['TESTFOLD', 'A255'], content=b'last')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x105000:0x105010], b'later\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x108000:0x108010], b'last\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
def test_lower_case_dir_short_names(self) -> None:
fatfs = fatfsgen.FATFS()
self.assertRaises(LowerCaseException, fatfs.create_directory, 'testfold')
def test_lower_case_file_short_names(self) -> None:
fatfs = fatfsgen.FATFS()
self.assertRaises(LowerCaseException, fatfs.create_file, 'newfile')
def test_too_long_name_dir_short_names(self) -> None:
fatfs = fatfsgen.FATFS()
self.assertRaises(TooLongNameException, fatfs.create_directory, 'TOOLONGNAME')
def test_fatfs16_detection(self) -> None:
fatfs = fatfsgen.FATFS(size=16 * 1024 * 1024)
self.assertEqual(fatfs.state.boot_sector_state.fatfs_type, 16)
def test_fatfs32_detection(self) -> None:
self.assertRaises(NotImplementedError, fatfsgen.FATFS, size=256 * 1024 * 1024)
def test_deep_structure(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
fatfs.create_directory('TESTFOLL', path_from_root=['TESTFOLD'])
fatfs.create_directory('TESTFOLO', path_from_root=['TESTFOLD', 'TESTFOLL'])
fatfs.create_file('WRITEF', extension='TXT', path_from_root=['TESTFOLD', 'TESTFOLL', 'TESTFOLO'])
fatfs.write_content(path_from_root=['TESTFOLD', 'TESTFOLL', 'TESTFOLO', 'WRITEF.TXT'], content=b'later')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x9000:0x9010], b'later\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
def test_same_name_deep_structure(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_directory('TESTFOLD')
fatfs.create_directory('TESTFOLD', path_from_root=['TESTFOLD'])
fatfs.create_directory('TESTFOLD', path_from_root=['TESTFOLD', 'TESTFOLD'])
fatfs.create_file('WRITEF', extension='TXT', path_from_root=['TESTFOLD', 'TESTFOLD', 'TESTFOLD'])
fatfs.write_content(path_from_root=['TESTFOLD', 'TESTFOLD', 'TESTFOLD', 'WRITEF.TXT'], content=b'later')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000:0x2010], b'TESTFOLD \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x2010:0x2020], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6040:0x6050], b'TESTFOLD \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6040:0x6050], b'TESTFOLD \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x7040:0x7050], b'TESTFOLD \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x8040:0x8050], b'WRITEF TXT \x00\x00\x00\x00')
self.assertEqual(file_system[0x9000:0x9010], b'later\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
def test_e2e_deep_folder_into_image(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.generate(CFG['test_dir'])
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x6060:0x6070], b'TESTFIL2 \x00\x00\x00\x00')
self.assertEqual(file_system[0x6070:0x6080], b'!\x00!\x00\x00\x00\x00\x00!\x00\x05\x00\x0b\x00\x00\x00')
self.assertEqual(file_system[0x7040:0x7050], b'LASTFILE \x00\x00\x00\x00')
self.assertEqual(file_system[0x8000:0x8010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x9000:0x9010], b'thisistest\n\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xa000:0xa010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
def test_e2e_deep_folder_into_image_ext(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.generate(CFG['test_dir2'])
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2020:0x2030], b'TESTFILE \x00\x00\x00\x00')
self.assertEqual(file_system[0x6060:0x6070], b'TESTFIL2 \x00\x00\x00\x00')
self.assertEqual(file_system[0x7000:0x7010], b'. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x7040:0x7050], b'LASTFILETXT \x00\x00\x00\x00')
self.assertEqual(file_system[0x8000:0x8010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x9000:0x9010], b'thisistest\n\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xa000:0xa010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xb000:0xb009], b'\xff\xff\xff\xff\xff\xff\xff\xff\xff')
def test_empty_fat16(self) -> None:
fatfs = fatfsgen.FATFS(size=17 * 1024 * 1024)
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x1000:0x1007], b'\xf8\xff\xff\xff\x00\x00\x00')
def test_simple_fat16(self) -> None:
fatfs = fatfsgen.FATFS(size=17 * 1024 * 1024)
fatfs.create_directory('TESTFOLD')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x1000:0x1007], b'\xf8\xff\xff\xff\xff\xff\x00')
def test_chaining_fat16(self) -> None:
fatfs = fatfsgen.FATFS(size=17 * 1024 * 1024)
fatfs.create_file('WRITEF', extension='TXT')
fatfs.write_content(path_from_root=['WRITEF.TXT'], content=CFG['sector_size'] * b'a' + b'a')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x1000: 0x100e], b'\xf8\xff\xff\xff\x03\x00\xff\xff\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x9000: 0xa000], b'a' + (CFG['sector_size'] - 1) * b'\x00')
def test_full_sector_folder_fat16(self) -> None:
fatfs = fatfsgen.FATFS(size=17 * 1024 * 1024)
fatfs.create_directory('TESTFOLD')
fill_sector(fatfs)
fatfs.write_content(path_from_root=['TESTFOLD', 'A0'], content=b'first')
fatfs.write_content(path_from_root=['TESTFOLD', 'A126'], content=b'later')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x1000: 0x1110],
b'\xf8\xff\xff\xff\x82\x00' + 258 * b'\xff' + 8 * b'\x00')
self.assertEqual(file_system[0x87000:0x87005], b'later')
self.assertEqual(file_system[0x88000:0x88010], b'A126 \x00\x00\x00\x00')
self.assertEqual(file_system[0x88020:0x88030], b'A127 \x00\x00\x00\x00')
def test_empty_lfn_short_name(self) -> None:
fatfs = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_file('HELLO', extension='TXT')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2019], b'HELLO TXT \x18\x00\x00\x00!\x00!\x00\x00\x00\x00\x00!')
def test_lfn_short_name(self) -> None:
fatfs = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_file('HELLO', extension='TXT')
fatfs.write_content(path_from_root=['HELLO.TXT'], content=b'this is a test')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'HELLO TXT \x18\x00\x00\x00')
self.assertEqual(file_system[0x2010: 0x2020], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x0e\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x6010], b'this is a test\x00\x00')
def test_lfn_empty_name(self) -> None:
fatfs = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_file('HELLOHELLOHELLO', extension='TXT')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'Bl\x00o\x00.\x00t\x00x\x00\x0f\x00\xadt\x00')
self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
self.assertEqual(file_system[0x2020: 0x2030], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\xadh\x00')
self.assertEqual(file_system[0x2030: 0x2040], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00')
self.assertEqual(file_system[0x2040: 0x2050], b'HELLOH~\x01TXT \x00\x00\x00\x00')
self.assertEqual(file_system[0x2050: 0x2060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
def test_lfn_plain_name(self) -> None:
fatfs = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_file('HELLOHELLOHELLO', extension='TXT')
fatfs.write_content(path_from_root=['HELLOHELLOHELLO.TXT'], content=b'this is a test')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'Bl\x00o\x00.\x00t\x00x\x00\x0f\x00\xadt\x00')
self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
self.assertEqual(file_system[0x2020: 0x2030], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\xadh\x00')
self.assertEqual(file_system[0x2030: 0x2040], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00')
self.assertEqual(file_system[0x2040: 0x2050], b'HELLOH~\x01TXT \x00\x00\x00\x00')
self.assertEqual(file_system[0x2050: 0x2060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x0e\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x6010], b'this is a test\x00\x00')
def test_lfn_plain_name_no_ext(self) -> None:
fatfs = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_file('HELLOHELLOHELLO')
fatfs.write_content(path_from_root=['HELLOHELLOHELLO'], content=b'this is a test')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'Bl\x00o\x00\x00\x00\xff\xff\xff\xff\x0f\x00P\xff\xff')
self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
self.assertEqual(file_system[0x2020: 0x2030], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00Ph\x00')
self.assertEqual(file_system[0x2030: 0x2040], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00')
self.assertEqual(file_system[0x2040: 0x2050], b'HELLOH~\x01 \x00\x00\x00\x00')
self.assertEqual(file_system[0x2050: 0x2060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x0e\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x6010], b'this is a test\x00\x00')
def test_lfn_short_entry_exception(self) -> None:
fatfs = fatfsgen.FATFS(long_names_enabled=True)
self.assertRaises(LowerCaseException, fatfs.create_file, 'hello', extension='txt')
def test_lfn_nested_empty(self) -> None:
fatfs = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_directory('VERYLONGTESTFOLD')
fatfs.create_file('HELLO', extension='TXT', path_from_root=['VERYLONGTESTFOLD'])
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\xa0\xff\xff')
self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\xa0o\x00')
self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00')
self.assertEqual(file_system[0x2040: 0x2050], b'VERYLO~\x01 \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x2050: 0x2060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6012: 0x6020], b'!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6030: 0x6040], b'!\x00!\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6040: 0x6050], b'HELLO TXT \x18\x00\x00\x00')
self.assertEqual(file_system[0x6050: 0x6060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x03\x00\x00\x00\x00\x00')
def test_lfn_nested_long_empty(self) -> None:
fatfs: fatfsgen.FATFS = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_directory('verylongtestfold')
fatfs.create_file('hellohellohello', extension='txt', path_from_root=['verylongtestfold'])
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\n\xff\xff')
self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\no\x00')
self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00')
self.assertEqual(file_system[0x2040: 0x2050], b'verylo~\x01 \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x2050: 0x2060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6012: 0x6020], b'!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6030: 0x6040], b'!\x00!\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6040: 0x6050], b'Bl\x00o\x00.\x00t\x00x\x00\x0f\x00\xcft\x00')
self.assertEqual(file_system[0x6050: 0x6060],
b'\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
def test_lfn_nested_text(self) -> None:
fatfs: fatfsgen.FATFS = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_directory('VERYLONGTESTFOLD')
fatfs.create_file('HELLO', extension='TXT', path_from_root=['VERYLONGTESTFOLD'])
fatfs.write_content(path_from_root=['VERYLONGTESTFOLD', 'HELLO.TXT'], content=b'this is a test')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\xa0\xff\xff')
self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\xa0o\x00')
self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00')
self.assertEqual(file_system[0x2040: 0x2050], b'VERYLO~\x01 \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x2050: 0x2060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6012: 0x6020], b'!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6030: 0x6040], b'!\x00!\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6040: 0x6050], b'HELLO TXT \x18\x00\x00\x00')
self.assertEqual(file_system[0x6050: 0x6060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x03\x00\x0e\x00\x00\x00')
self.assertEqual(file_system[0x7000: 0x7010], b'this is a test\x00\x00')
def test_boundary_clusters12(self) -> None:
output: bytes = check_output(['python', '../fatfsgen.py', '--partition_size', '16732160', 'test_dir'],
stderr=STDOUT)
self.assertEqual(
output,
b'WARNING: It is not recommended to create FATFS with bounding count of clusters: 4085 or 65525\n')
def test_boundary_clusters16(self) -> None:
output: bytes = check_output(['python', '../fatfsgen.py', '--partition_size', '268390400', 'test_dir'],
stderr=STDOUT)
self.assertEqual(
output,
b'WARNING: It is not recommended to create FATFS with bounding count of clusters: 4085 or 65525\n')
def test_boundary_clusters_fat32(self) -> None:
self.assertRaises(NotImplementedError, fatfsgen.FATFS, size=268419193)
def test_inconsistent_fat12(self) -> None:
self.assertRaises(InconsistentFATAttributes, fatfsgen.FATFS, size=20480000, explicit_fat_type=FAT12)
def test_lfn_increasing(self) -> None:
fatfs: fatfsgen.FATFS = fatfsgen.FATFS(long_names_enabled=True)
fatfs.create_directory('VERYLONGTESTFOLD')
fatfs.create_file('HELLOHELLOHELLOOOOOOO', extension='TXT', path_from_root=['VERYLONGTESTFOLD'])
fatfs.create_file('HELLOHELLOHELLOOOOOOB', extension='TXT', path_from_root=['VERYLONGTESTFOLD'])
fatfs.write_content(path_from_root=['VERYLONGTESTFOLD', 'HELLOHELLOHELLOOOOOOO.TXT'],
content=b'this is a test A')
fatfs.write_content(path_from_root=['VERYLONGTESTFOLD', 'HELLOHELLOHELLOOOOOOB.TXT'],
content=b'this is a test B')
fatfs.write_filesystem(CFG['output_file'])
file_system = read_filesystem(CFG['output_file'])
self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\xa0\xff\xff')
self.assertEqual(file_system[0x2011: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff')
self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\xa0o\x00')
self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00')
self.assertEqual(file_system[0x2040: 0x2050], b'VERYLO~\x01 \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x2050: 0x2060], b'!\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6011: 0x6020], b'\x00!\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x6030: 0x6040], b'!\x00!\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x6040: 0x6050], b'Bl\x00o\x00o\x00o\x00o\x00\x0f\x00\xado\x00')
self.assertEqual(file_system[0x6050: 0x6060], b'o\x00o\x00.\x00t\x00x\x00\x00\x00t\x00\x00\x00')
self.assertEqual(file_system[0x6050: 0x6060], b'o\x00o\x00.\x00t\x00x\x00\x00\x00t\x00\x00\x00')
self.assertEqual(file_system[0x6060: 0x6070], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\xadh\x00')
self.assertEqual(file_system[0x6070: 0x6080], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00')
self.assertEqual(file_system[0x6080: 0x6090], b'HELLOH~\x01TXT \x00\x00\x00\x00')
self.assertEqual(file_system[0x6090: 0x60a0], b'!\x00!\x00\x00\x00\x00\x00!\x00\x03\x00\x10\x00\x00\x00')
self.assertEqual(file_system[0x60a0: 0x60b0], b'Bl\x00o\x00o\x00o\x00o\x00\x0f\x00\x8do\x00')
self.assertEqual(file_system[0x60b0: 0x60c0], b'o\x00b\x00.\x00t\x00x\x00\x00\x00t\x00\x00\x00')
self.assertEqual(file_system[0x60c0: 0x60d0], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\x8dh\x00')
self.assertEqual(file_system[0x60d0: 0x60e0], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00')
self.assertEqual(file_system[0x60e0: 0x60f0], b'HELLOH~\x02TXT \x00\x00\x00\x00')
def test_bs_not_initialized(self) -> None:
self.assertEqual(str(BootSector()), 'Boot sector is not initialized!')
self.assertRaises(NotInitialized, BootSector().generate_boot_sector)
self.assertRaises(NotInitialized, lambda: BootSector().binary_image) # encapsulate property to callable
def test_bs_str(self) -> None:
fatfs = fatfsgen.FATFS()
bs = BootSector(fatfs.state.boot_sector_state)
bs.generate_boot_sector()
bs.parse_boot_sector(bs.binary_image)
x = 'FATFS properties:,clusters: 252,data_region_start: 24576,data_sectors: ' \
'250,entries_root_count: 512,fat_table_start_address: 4096,fat_tables_cnt: 1,' \
'fatfs_type: 12,file_sys_type: FAT ,hidden_sectors: 0,media_type: 248,' \
'non_data_sectors: 6,num_heads: 255,oem_name: MSDOS5.0,reserved_sectors_cnt: 1,' \
'root_dir_sectors_cnt: 4,root_directory_start: 8192,sec_per_track: 63,sector_size: 4096,' \
'sectors_count: 256,sectors_per_cluster: 1,sectors_per_fat_cnt: 1,size: 1048576,' \
'volume_label: Espressif ,volume_uuid: 1144419653,'
self.assertEqual(x.split(',')[:-2], str(bs).split('\n')[:-2]) # except for volume id
def test_parsing_error(self) -> None:
self.assertRaises(NotInitialized, BootSector().parse_boot_sector, b'')
def test_not_implemented_fat32(self) -> None:
self.assertEqual(
Entry.get_cluster_id(
Entry.ENTRY_FORMAT_SHORT_NAME.parse(
bytearray(b'AHOJ \x18\x00\xb0[&U&U\x00\x00\xb0[&U\x02\x00\x08\x00\x00\x00'))),
2)
def test_get_cluster_value_from_fat(self) -> None:
fatfs = fatfsgen.FATFS()
self.assertEqual(fatfs.fat.get_cluster_value(1), 0xFFF)
def test_is_cluster_last(self) -> None:
fatfs = fatfsgen.FATFS()
self.assertEqual(fatfs.fat.is_cluster_last(2), False)
def test_chain_in_fat(self) -> None:
fatfs = fatfsgen.FATFS()
self.assertEqual(fatfs.fat.get_chained_content(1), b'\x00' * 0x1000)
def test_retrieve_file_chaining(self) -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('WRITEF', extension='TXT')
fatfs.write_content(path_from_root=['WRITEF.TXT'], content=CFG['sector_size'] * b'a' + b'a')
fatfs.write_filesystem(CFG['output_file'])
self.assertEqual(fatfs.fat.get_chained_content(1)[:15], b'WRITEF TXT \x00\x00\x00')
self.assertEqual(fatfs.fat.get_chained_content(2)[:15], b'aaaaaaaaaaaaaaa')
def test_lstrip(self) -> None:
self.assertEqual(right_strip_string('\x20\x20\x20thisistest\x20\x20\x20'), ' thisistest')
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,354 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
import shutil
import sys
import unittest
from subprocess import STDOUT, run
from test_utils import compare_folders, fill_sector, generate_local_folder_structure, generate_test_dir_2
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import fatfsgen # noqa E402 # pylint: disable=C0413
from fatfs_utils.entry import Entry # noqa E402 # pylint: disable=C0413
class FatFSGen(unittest.TestCase):
def setUp(self) -> None:
os.makedirs('output_data')
generate_test_dir_2()
def tearDown(self) -> None:
shutil.rmtree('output_data', ignore_errors=True)
shutil.rmtree('Espressif', ignore_errors=True)
shutil.rmtree('testf', ignore_errors=True)
shutil.rmtree('testf_wl', ignore_errors=True)
if os.path.exists('fatfs_image.img'):
os.remove('fatfs_image.img')
@staticmethod
def test_gen_parse() -> None:
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "fatfsgen.py")}',
'output_data/tst_str'
], stderr=STDOUT)
run(['python', '../fatfsgen.py', 'output_data/tst_str'], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert set(os.listdir('Espressif')) == {'TEST', 'TESTFILE'}
with open('Espressif/TESTFILE', 'rb') as in_:
assert in_.read() == b'ahoj\n'
assert set(os.listdir('Espressif/TEST')) == {'TEST', 'TESTFIL2'}
with open('Espressif/TEST/TESTFIL2', 'rb') as in_:
assert in_.read() == b'thisistest\n'
assert set(os.listdir('Espressif/TEST/TEST')) == {'LASTFILE.TXT'}
with open('Espressif/TEST/TEST/LASTFILE.TXT', 'rb') as in_:
assert in_.read() == b'deeptest\n'
@staticmethod
def test_file_chaining() -> None:
fatfs = fatfsgen.FATFS()
fatfs.create_file('WRITEF', extension='TXT')
fatfs.write_content(path_from_root=['WRITEF.TXT'], content=4096 * b'a' + b'a')
fatfs.write_filesystem('fatfs_image.img')
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
with open('Espressif/WRITEF.TXT', 'rb') as in_:
assert in_.read() == 4097 * b'a'
@staticmethod
def test_full_two_sectors_folder() -> None:
fatfs = fatfsgen.FATFS(size=2 * 1024 * 1024)
fatfs.create_directory('TESTFOLD')
for i in range((2 * 4096) // 32):
fatfs.create_file(f'A{str(i).upper()}', path_from_root=['TESTFOLD'])
fatfs.write_content(path_from_root=['TESTFOLD', 'A253'], content=b'later')
fatfs.write_content(path_from_root=['TESTFOLD', 'A255'], content=b'last')
fatfs.write_filesystem('fatfs_image.img')
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert set(os.listdir('Espressif')) == {'TESTFOLD'}
assert set(os.listdir('Espressif/TESTFOLD')) == {f'A{str(i).upper()}' for i in range(256)}
with open('Espressif/TESTFOLD/A253', 'rb') as in_:
assert in_.read() == b'later'
with open('Espressif/TESTFOLD/A255', 'rb') as in_:
assert in_.read() == b'last'
@staticmethod
def test_empty_fat16() -> None:
fatfs = fatfsgen.FATFS(size=17 * 1024 * 1024)
fatfs.write_filesystem('fatfs_image.img')
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
@staticmethod
def test_chaining_fat16() -> None:
fatfs = fatfsgen.FATFS(size=17 * 1024 * 1024)
fatfs.create_file('WRITEF', extension='TXT')
fatfs.write_content(path_from_root=['WRITEF.TXT'], content=4096 * b'a' + b'a')
fatfs.write_filesystem('fatfs_image.img')
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
with open('Espressif/WRITEF.TXT', 'rb') as in_:
assert in_.read() == 4097 * b'a'
@staticmethod
def test_full_sector_folder_fat16() -> None:
fatfs = fatfsgen.FATFS(size=17 * 1024 * 1024)
fatfs.create_directory('TESTFOLD')
fill_sector(fatfs)
fatfs.write_content(path_from_root=['TESTFOLD', 'A0'], content=b'first')
fatfs.write_content(path_from_root=['TESTFOLD', 'A126'], content=b'later')
fatfs.write_filesystem('fatfs_image.img')
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert set(os.listdir('Espressif')) == {'TESTFOLD'}
assert set(os.listdir('Espressif/TESTFOLD')) == {f'A{str(i).upper()}' for i in range(128)}
with open('Espressif/TESTFOLD/A0', 'rb') as in_:
assert in_.read() == b'first'
with open('Espressif/TESTFOLD/A126', 'rb') as in_:
assert in_.read() == b'later'
@staticmethod
def file_(x: str, content_: str = 'hey this is a test') -> dict:
return {
'type': 'file',
'name': x,
'content': content_
}
def test_e2e_file(self) -> None:
struct_: dict = {
'type': 'folder',
'name': 'testf',
'content': [self.file_('NEWF')]
}
generate_local_folder_structure(struct_, path_='.')
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "fatfsgen.py")}',
'testf'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
shutil.rmtree('Espressif', ignore_errors=True)
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "wl_fatfsgen.py")}',
'testf'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
def test_e2e_deeper(self) -> None:
folder_ = {
'type': 'folder',
'name': 'XYZ',
'content': [
self.file_('NEWFLE'),
self.file_('NEW.TXT'),
self.file_('NEWE.TXT'),
self.file_('NEW4.TXT'),
self.file_('NEW5.TXT'),
]
}
struct_: dict = {
'type': 'folder',
'name': 'testf',
'content': [
self.file_('MY_NEW'),
folder_
]
}
generate_local_folder_structure(struct_, path_='.')
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "fatfsgen.py")}',
'testf'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
shutil.rmtree('Espressif', ignore_errors=True)
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "wl_fatfsgen.py")}',
'testf'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
def test_e2e_deeper_large(self) -> None:
folder_ = {
'type': 'folder',
'name': 'XYZ',
'content': [
self.file_('NEWFLE', content_=4097 * 'a'),
self.file_('NEW.TXT', content_=2 * 4097 * 'a'),
self.file_('NEWE.TXT'),
self.file_('NEW4.TXT'),
self.file_('NEW5.TXT'),
]
}
folder2_ = {
'type': 'folder',
'name': 'XYZ3',
'content': [
self.file_('NEWFLE', content_=4097 * 'a'),
self.file_('NEW.TXT', content_=2 * 4097 * 'a'),
self.file_('NEWE.TXT'),
self.file_('NEW4.TXT'),
self.file_('NEW5.TXT'),
]
}
folder3_ = {
'type': 'folder',
'name': 'XYZ2',
'content': [self.file_(f'A{i}') for i in range(50)]
}
struct_: dict = {
'type': 'folder',
'name': 'testf',
'content': [
self.file_('MY_NEW'),
folder_,
folder2_,
folder3_
]
}
generate_local_folder_structure(struct_, path_='.')
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "fatfsgen.py")}',
'testf'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
shutil.rmtree('Espressif', ignore_errors=True)
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "wl_fatfsgen.py")}',
'testf'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
def test_e2e_very_deep(self) -> None:
folder_ = {
'type': 'folder',
'name': 'XYZ',
'content': [
self.file_('NEWFLE', content_=4097 * 'a'),
self.file_('NEW.TXT', content_=2 * 4097 * 'a'),
self.file_('NEWE.TXT'),
self.file_('NEW4.TXT'),
self.file_('NEW5.TXT'),
]
}
folder2_ = {
'type': 'folder',
'name': 'XYZ3',
'content': [
self.file_('NEWFLE', content_=4097 * 'a'),
self.file_('NEW.TXT', content_=2 * 4097 * 'a'),
self.file_('NEWE.TXT'),
self.file_('NEW4.TXT'),
self.file_('NEW5.TXT'),
folder_,
]
}
folder3_ = {
'type': 'folder',
'name': 'XYZ2',
'content': [self.file_(f'A{i}') for i in range(50)] + [folder2_]
}
struct_: dict = {
'type': 'folder',
'name': 'testf',
'content': [
self.file_('MY_NEW'),
folder_,
folder2_,
folder3_
]
}
generate_local_folder_structure(struct_, path_='.')
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "fatfsgen.py")}',
'testf'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
def test_e2e_very_deep_long(self) -> None:
folder_ = {
'type': 'folder',
'name': 'veryveryverylong111',
'content': [
self.file_('myndewveryverylongfile1.txt', content_=4097 * 'a'),
self.file_('mynewveryverylongfile22.txt', content_=2 * 4097 * 'a'),
self.file_('mynewveryverylongfile333.txt' * 8),
self.file_('mynewveryverylongfile4444.txt' * 8),
self.file_('mynewveryverylongfile5555.txt'),
self.file_('SHORT.TXT'),
]
}
struct_: dict = {
'type': 'folder',
'name': 'testf',
'content': [
self.file_('mynewveryverylongfile.txt' * 5),
folder_,
]
}
generate_local_folder_structure(struct_, path_='.')
run([
'python',
f'{os.path.join(os.path.dirname(__file__), "..", "fatfsgen.py")}',
'testf', '--long_name_support'
], stderr=STDOUT)
run(['python', '../fatfsparse.py', 'fatfs_image.img'], stderr=STDOUT)
assert compare_folders('testf', 'Espressif')
def test_parse_long_name(self) -> None:
self.assertEqual(
Entry.parse_entry_long(
b'\x01t\x00h\x00i\x00s\x00_\x00\x0f\x00\xfbi\x00s\x00_\x00l\x00o\x00n\x00\x00\x00g\x00_\x00', 251),
{
'order': 1,
'name1': b't\x00h\x00i\x00s\x00_\x00',
'name2': b'i\x00s\x00_\x00l\x00o\x00n\x00',
'name3': b'g\x00_\x00',
'is_last': False
}
)
self.assertEqual(
Entry.parse_entry_long(
b'\x01t\x00h\x00i\x00s\x00_\x00\x0f\x00\xfbi\x00s\x00_\x00l\x00o\x00n\x00\x00\x00g\x00_\x00', 252
),
{}
)
self.assertEqual(
Entry.parse_entry_long(
b'\x01t\x00h\x00i\x00s\x00_\x00\x0f\x01\xfbi\x00s\x00_\x00l\x00o\x00n\x00\x00\x00g\x00_\x00', 251
),
{}
)
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,67 @@
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
import sys
from typing import Any, Dict, Union
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import fatfsgen # noqa E402 # pylint: disable=C0413
CFG = dict(
sector_size=4096,
entry_size=32,
fat_start=0x1000,
data_start=0x7000,
root_start=0x2000,
output_file=os.path.join('output_data', 'tmp_file.img'),
test_dir=os.path.join('output_data', 'test'),
test_dir2=os.path.join('output_data', 'tst_str'),
) # type: Union[Dict[str, Any]]
def generate_test_dir_1() -> None:
os.makedirs(os.path.join(CFG['test_dir'], 'test', 'test'))
with open(os.path.join(CFG['test_dir'], 'test', 'test', 'lastfile'), 'w') as file:
file.write('deeptest\n')
with open(os.path.join(CFG['test_dir'], 'test', 'testfil2'), 'w') as file:
file.write('thisistest\n')
with open(os.path.join(CFG['test_dir'], 'testfile'), 'w') as file:
file.write('ahoj\n')
def generate_test_dir_2() -> None:
os.makedirs(os.path.join(CFG['test_dir2'], 'test', 'test'))
with open(os.path.join(CFG['test_dir2'], 'test', 'test', 'lastfile.txt'), 'w') as file:
file.write('deeptest\n')
with open(os.path.join(CFG['test_dir2'], 'test', 'testfil2'), 'w') as file:
file.write('thisistest\n')
with open(os.path.join(CFG['test_dir2'], 'testfile'), 'w') as file:
file.write('ahoj\n')
def fill_sector(fatfs: fatfsgen.FATFS, file_prefix: str = 'A') -> None:
for i in range(CFG['sector_size'] // CFG['entry_size']):
fatfs.create_file(f'{file_prefix}{str(i).upper()}', path_from_root=['TESTFOLD'])
def generate_local_folder_structure(structure_: dict, path_: str) -> None:
if structure_['type'] == 'folder':
new_path_ = os.path.join(path_, structure_['name'])
os.makedirs(new_path_)
for item_ in structure_['content']:
generate_local_folder_structure(item_, new_path_)
else:
new_path_ = os.path.join(path_, structure_['name'])
with open(new_path_, 'w') as f_:
f_.write(structure_['content'])
def compare_folders(fp1: str, fp2: str) -> bool:
if os.path.isdir(fp1) != os.path.isdir(fp2):
return False
if os.path.isdir(fp1):
if set(os.listdir(fp1)) != set(os.listdir(fp2)):
return False
return all([compare_folders(os.path.join(fp1, path_), os.path.join(fp2, path_)) for path_ in os.listdir(fp1)])
with open(fp1, 'rb') as f1_, open(fp2, 'rb') as f2_:
return f1_.read() == f2_.read()

@ -0,0 +1,142 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
import shutil
import sys
import unittest
from test_utils import CFG, generate_test_dir_1, generate_test_dir_2
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import wl_fatfsgen # noqa E402 # pylint: disable=C0413
from fatfs_utils.exceptions import WLNotInitialized # noqa E402 # pylint: disable=C0413
class WLFatFSGen(unittest.TestCase):
def setUp(self) -> None:
os.makedirs('output_data')
generate_test_dir_1()
generate_test_dir_2()
def tearDown(self) -> None:
shutil.rmtree('output_data')
def test_empty_file_sn_fat12(self) -> None:
fatfs = wl_fatfsgen.WLFATFS()
fatfs.plain_fatfs.create_file('TESTFILE')
fatfs.init_wl()
fatfs.wl_write_filesystem(CFG['output_file'])
with open(CFG['output_file'], 'rb') as fs_file:
file_system = fs_file.read()
self.assertEqual(file_system[0x3000:0x300c], b'TESTFILE \x20') # check entry name and type
self.assertEqual(file_system[0x2000:0x2006], b'\xf8\xff\xff\xff\x0f\x00') # check fat
def test_directory_sn_fat12(self) -> None:
fatfs = wl_fatfsgen.WLFATFS(device_id=3750448905)
fatfs.plain_fatfs.create_directory('TESTFOLD')
fatfs.init_wl()
fatfs.wl_write_filesystem(CFG['output_file'])
with open(CFG['output_file'], 'rb') as fs_file:
file_system = fs_file.read()
# boot sector
self.assertEqual(file_system[0x1000:0x1010], b'\xeb\xfe\x90MSDOS5.0\x00\x10\x01\x01\x00')
self.assertEqual(file_system[0x1010:0x1020], b'\x01\x00\x02\xfa\x00\xf8\x01\x00?\x00\xff\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x102b:0x1034], b'Espressif')
self.assertEqual(file_system[0x3000:0x300c], b'TESTFOLD \x10') # check entry name and type
self.assertEqual(file_system[0x2000:0x2006], b'\xf8\xff\xff\xff\x0f\x00') # check fat
self.assertEqual(file_system[0x7000:0x700c], b'. \x10') # reference to itself
self.assertEqual(file_system[0x7020:0x702c], b'.. \x10') # reference to parent
# check state1
self.assertEqual(file_system[0xfb000:0xfb00f], b'\x00\x00\x00\x00\xfb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xfb010:0xfb020], b'\x10\x00\x00\x00\x00\x10\x00\x00\x02\x00\x00\x00\tO\x8b\xdf')
self.assertEqual(file_system[0xfb020:0xfb02f], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xfb031:0xfb040], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\xa1\x94i')
# check state2
self.assertEqual(file_system[0xfd000:0xfd00f], b'\x00\x00\x00\x00\xfb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xfd010:0xfd020], b'\x10\x00\x00\x00\x00\x10\x00\x00\x02\x00\x00\x00\tO\x8b\xdf')
self.assertEqual(file_system[0xfd020:0xfd02f], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xfd031:0xfd040], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\xa1\x94i')
# check config
self.assertEqual(file_system[0xff001:0xff010], b'\x00\x00\x00\x00\x00\x10\x00\x00\x10\x00\x00\x00\x10\x00\x00')
self.assertEqual(file_system[0xff010:0xff01f], b'\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00 \x00\x00')
self.assertEqual(file_system[0xff020:0xff030], b'\xe0b\xb5O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xff030:0xff03f], b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff')
def test_directory_sn_fat122mb(self) -> None:
fatfs = wl_fatfsgen.WLFATFS(device_id=3750448905, size=2 * 1024 * 1024)
fatfs.plain_fatfs.create_directory('TESTFOLD')
fatfs.init_wl()
fatfs.wl_write_filesystem(CFG['output_file'])
with open(CFG['output_file'], 'rb') as fs_file:
file_system = fs_file.read()
# check state1
self.assertEqual(file_system[0x1f9000:0x1f900e], b'\x00\x00\x00\x00\xf9\x01\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x1f9010:0x1f9020],
b'\x10\x00\x00\x00\x00\x10\x00\x00\x02\x00\x00\x00\tO\x8b\xdf')
self.assertEqual(file_system[0x1f9020:0x1f902e], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x1f9030:0x1f9040], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j5\xbdp')
# check state2
self.assertEqual(file_system[0x1fc000:0x1fc00e], b'\x00\x00\x00\x00\xf9\x01\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x1fc010:0x1fc020],
b'\x10\x00\x00\x00\x00\x10\x00\x00\x02\x00\x00\x00\tO\x8b\xdf')
self.assertEqual(file_system[0x1fc020:0x1fc02e], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x1fc030:0x1fc040], b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j5\xbdp')
# check config
self.assertEqual(file_system[0x1ff000:0x1ff00f], b'\x00\x00\x00\x00\x00\x00 \x00\x00\x10\x00\x00\x00\x10\x00')
self.assertEqual(file_system[0x1ff010:0x1ff01f], b'\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00 \x00\x00')
self.assertEqual(file_system[0x1ff020:0x1ff030], b')\x892j\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0x1ff030:0x1ff03e], b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff')
def test_write_not_initialized_wlfatfs(self) -> None:
fatfs = wl_fatfsgen.WLFATFS()
fatfs.plain_fatfs.create_directory('TESTFOLD')
self.assertRaises(WLNotInitialized, fatfs.wl_write_filesystem, CFG['output_file'])
def test_e2e_deep_folder_into_image_ext(self) -> None:
fatfs = wl_fatfsgen.WLFATFS()
fatfs.plain_fatfs.generate(CFG['test_dir2'])
fatfs.init_wl()
fatfs.wl_write_filesystem(CFG['output_file'])
with open(CFG['output_file'], 'rb') as fs_file:
file_system = bytearray(fs_file.read())
self.assertEqual(file_system[0x3020:0x3030], b'TESTFILE \x00\x00\x00\x00')
self.assertEqual(file_system[0x7060:0x7070], b'TESTFIL2 \x00\x00\x00\x00')
self.assertEqual(file_system[0x8000:0x8010], b'. \x10\x00\x00\x00\x00')
self.assertEqual(file_system[0x8040:0x8050], b'LASTFILETXT \x00\x00\x00\x00')
self.assertEqual(file_system[0x9000:0x9010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xa000:0xa010], b'thisistest\n\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xb000:0xb010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xc000:0xc009], b'\xff\xff\xff\xff\xff\xff\xff\xff\xff')
def test_e2e_deep_folder_into_image(self) -> None:
fatfs = wl_fatfsgen.WLFATFS()
fatfs.plain_fatfs.generate(CFG['test_dir'])
fatfs.init_wl()
fatfs.wl_write_filesystem(CFG['output_file'])
with open(CFG['output_file'], 'rb') as fs_file:
file_system = bytearray(fs_file.read())
self.assertEqual(file_system[0x7060:0x7070], b'TESTFIL2 \x00\x00\x00\x00')
self.assertEqual(file_system[0x7070:0x7080], b'!\x00!\x00\x00\x00\x00\x00!\x00\x05\x00\x0b\x00\x00\x00')
self.assertEqual(file_system[0x8040:0x8050], b'LASTFILE \x00\x00\x00\x00')
self.assertEqual(file_system[0x9000:0x9010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xa000:0xa010], b'thisistest\n\x00\x00\x00\x00\x00')
self.assertEqual(file_system[0xb000:0xb010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,356 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stddef.h>
#include "esp_err.h"
#include "driver/gpio.h"
#include "driver/sdmmc_types.h"
#include "driver/sdspi_host.h"
#include "ff.h"
#include "wear_levelling.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Register FATFS with VFS component
*
* This function registers given FAT drive in VFS, at the specified base path.
* If only one drive is used, fat_drive argument can be an empty string.
* Refer to FATFS library documentation on how to specify FAT drive.
* This function also allocates FATFS structure which should be used for f_mount
* call.
*
* @note This function doesn't mount the drive into FATFS, it just connects
* POSIX and C standard library IO function with FATFS. You need to mount
* desired drive into FATFS separately.
*
* @param base_path path prefix where FATFS should be registered
* @param fat_drive FATFS drive specification; if only one drive is used, can be an empty string
* @param max_files maximum number of files which can be open at the same time
* @param[out] out_fs pointer to FATFS structure which can be used for FATFS f_mount call is returned via this argument.
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_register was already called
* - ESP_ERR_NO_MEM if not enough memory or too many VFSes already registered
*/
esp_err_t esp_vfs_fat_register(const char* base_path, const char* fat_drive,
size_t max_files, FATFS** out_fs);
/**
* @brief Un-register FATFS from VFS
*
* @note FATFS structure returned by esp_vfs_fat_register is destroyed after
* this call. Make sure to call f_mount function to unmount it before
* calling esp_vfs_fat_unregister_ctx.
* Difference between this function and the one above is that this one
* will release the correct drive, while the one above will release
* the last registered one
*
* @param base_path path prefix where FATFS is registered. This is the same
* used when esp_vfs_fat_register was called
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if FATFS is not registered in VFS
*/
esp_err_t esp_vfs_fat_unregister_path(const char* base_path);
/**
* @brief Configuration arguments for esp_vfs_fat_sdmmc_mount and esp_vfs_fat_spiflash_mount_rw_wl functions
*/
typedef struct {
/**
* If FAT partition can not be mounted, and this parameter is true,
* create partition table and format the filesystem.
*/
bool format_if_mount_failed;
int max_files; ///< Max number of open files
/**
* If format_if_mount_failed is set, and mount fails, format the card
* with given allocation unit size. Must be a power of 2, between sector
* size and 128 * sector size.
* For SD cards, sector size is always 512 bytes. For wear_levelling,
* sector size is determined by CONFIG_WL_SECTOR_SIZE option.
*
* Using larger allocation unit size will result in higher read/write
* performance and higher overhead when storing small files.
*
* Setting this field to 0 will result in allocation unit set to the
* sector size.
*/
size_t allocation_unit_size;
/**
* Enables real ff_disk_status function implementation for SD cards
* (ff_sdmmc_status). Possibly slows down IO performance.
*
* Try to enable if you need to handle situations when SD cards
* are not unmounted properly before physical removal
* or you are experiencing issues with SD cards.
*
* Doesn't do anything for other memory storage media.
*/
bool disk_status_check_enable;
} esp_vfs_fat_mount_config_t;
// Compatibility definition
typedef esp_vfs_fat_mount_config_t esp_vfs_fat_sdmmc_mount_config_t;
/**
* @brief Convenience function to get FAT filesystem on SD card registered in VFS
*
* This is an all-in-one function which does the following:
* - initializes SDMMC driver or SPI driver with configuration in host_config
* - initializes SD card with configuration in slot_config
* - mounts FAT partition on SD card using FATFS library, with configuration in mount_config
* - registers FATFS library with VFS, with prefix given by base_prefix variable
*
* This function is intended to make example code more compact.
* For real world applications, developers should implement the logic of
* probing SD card, locating and mounting partition, and registering FATFS in VFS,
* with proper error checking and handling of exceptional conditions.
*
* @note Use this API to mount a card through SDSPI is deprecated. Please call
* `esp_vfs_fat_sdspi_mount()` instead for that case.
*
* @param base_path path where partition should be registered (e.g. "/sdcard")
* @param host_config Pointer to structure describing SDMMC host. When using
* SDMMC peripheral, this structure can be initialized using
* SDMMC_HOST_DEFAULT() macro. When using SPI peripheral,
* this structure can be initialized using SDSPI_HOST_DEFAULT()
* macro.
* @param slot_config Pointer to structure with slot configuration.
* For SDMMC peripheral, pass a pointer to sdmmc_slot_config_t
* structure initialized using SDMMC_SLOT_CONFIG_DEFAULT.
* @param mount_config pointer to structure with extra parameters for mounting FATFS
* @param[out] out_card if not NULL, pointer to the card information structure will be returned via this argument
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_sdmmc_mount was already called
* - ESP_ERR_NO_MEM if memory can not be allocated
* - ESP_FAIL if partition can not be mounted
* - other error codes from SDMMC or SPI drivers, SDMMC protocol, or FATFS drivers
*/
esp_err_t esp_vfs_fat_sdmmc_mount(const char* base_path,
const sdmmc_host_t* host_config,
const void* slot_config,
const esp_vfs_fat_mount_config_t* mount_config,
sdmmc_card_t** out_card);
/**
* @brief Convenience function to get FAT filesystem on SD card registered in VFS
*
* This is an all-in-one function which does the following:
* - initializes an SPI Master device based on the SPI Master driver with configuration in
* slot_config, and attach it to an initialized SPI bus.
* - initializes SD card with configuration in host_config_input
* - mounts FAT partition on SD card using FATFS library, with configuration in mount_config
* - registers FATFS library with VFS, with prefix given by base_prefix variable
*
* This function is intended to make example code more compact.
* For real world applications, developers should implement the logic of
* probing SD card, locating and mounting partition, and registering FATFS in VFS,
* with proper error checking and handling of exceptional conditions.
*
* @note This function try to attach the new SD SPI device to the bus specified in host_config.
* Make sure the SPI bus specified in `host_config->slot` have been initialized by
* `spi_bus_initialize()` before.
*
* @param base_path path where partition should be registered (e.g. "/sdcard")
* @param host_config_input Pointer to structure describing SDMMC host. This structure can be
* initialized using SDSPI_HOST_DEFAULT() macro.
* @param slot_config Pointer to structure with slot configuration.
* For SPI peripheral, pass a pointer to sdspi_device_config_t
* structure initialized using SDSPI_DEVICE_CONFIG_DEFAULT().
* @param mount_config pointer to structure with extra parameters for mounting FATFS
* @param[out] out_card If not NULL, pointer to the card information structure will be returned via
* this argument. It is suggested to hold this handle and use it to unmount the card later if
* needed. Otherwise it's not suggested to use more than one card at the same time and unmount one
* of them in your application.
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_sdmmc_mount was already called
* - ESP_ERR_NO_MEM if memory can not be allocated
* - ESP_FAIL if partition can not be mounted
* - other error codes from SDMMC or SPI drivers, SDMMC protocol, or FATFS drivers
*/
esp_err_t esp_vfs_fat_sdspi_mount(const char* base_path,
const sdmmc_host_t* host_config_input,
const sdspi_device_config_t* slot_config,
const esp_vfs_fat_mount_config_t* mount_config,
sdmmc_card_t** out_card);
/**
* @brief Unmount FAT filesystem and release resources acquired using esp_vfs_fat_sdmmc_mount
*
* @deprecated Use `esp_vfs_fat_sdcard_unmount()` instead.
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_sdmmc_mount hasn't been called
*/
esp_err_t esp_vfs_fat_sdmmc_unmount(void) __attribute__((deprecated("Please use esp_vfs_fat_sdcard_unmount instead")));
/**
* @brief Unmount an SD card from the FAT filesystem and release resources acquired using
* `esp_vfs_fat_sdmmc_mount()` or `esp_vfs_fat_sdspi_mount()`
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if the card argument is unregistered
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_sdmmc_mount hasn't been called
*/
esp_err_t esp_vfs_fat_sdcard_unmount(const char* base_path, sdmmc_card_t *card);
/**
* @brief Format FAT filesystem
*
* @note
* This API should be only called when the FAT is already mounted.
*
* @param base_path Path where partition should be registered (e.g. "/sdcard")
* @param card Pointer to the card handle, which should be initialised by calling `esp_vfs_fat_sdspi_mount` first
*
* @return
* - ESP_OK
* - ESP_ERR_INVALID_STATE: FAT partition isn't mounted, call esp_vfs_fat_sdmmc_mount or esp_vfs_fat_sdspi_mount first
* - ESP_ERR_NO_MEM: if memory can not be allocated
* - ESP_FAIL: fail to format it, or fail to mount back
*/
esp_err_t esp_vfs_fat_sdcard_format(const char *base_path, sdmmc_card_t *card);
/**
* @brief Convenience function to initialize FAT filesystem in SPI flash and register it in VFS
*
* This is an all-in-one function which does the following:
*
* - finds the partition with defined partition_label. Partition label should be
* configured in the partition table.
* - initializes flash wear levelling library on top of the given partition
* - mounts FAT partition using FATFS library on top of flash wear levelling
* library
* - registers FATFS library with VFS, with prefix given by base_prefix variable
*
* This function is intended to make example code more compact.
*
* @param base_path path where FATFS partition should be mounted (e.g. "/spiflash")
* @param partition_label label of the partition which should be used
* @param mount_config pointer to structure with extra parameters for mounting FATFS
* @param[out] wl_handle wear levelling driver handle
* @return
* - ESP_OK on success
* - ESP_ERR_NOT_FOUND if the partition table does not contain FATFS partition with given label
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_spiflash_mount_rw_wl was already called
* - ESP_ERR_NO_MEM if memory can not be allocated
* - ESP_FAIL if partition can not be mounted
* - other error codes from wear levelling library, SPI flash driver, or FATFS drivers
*/
esp_err_t esp_vfs_fat_spiflash_mount_rw_wl(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config,
wl_handle_t* wl_handle);
/**
* @brief Unmount FAT filesystem and release resources acquired using esp_vfs_fat_spiflash_mount_rw_wl
*
* @param base_path path where partition should be registered (e.g. "/spiflash")
* @param wl_handle wear levelling driver handle returned by esp_vfs_fat_spiflash_mount_rw_wl
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_spiflash_mount_rw_wl hasn't been called
*/
esp_err_t esp_vfs_fat_spiflash_unmount_rw_wl(const char* base_path, wl_handle_t wl_handle);
/**
* @brief Format FAT filesystem
*
* @note
* This API can be called when the FAT is mounted / not mounted.
* If this API is called when the FAT isn't mounted (by calling esp_vfs_fat_spiflash_mount_rw_wl),
* this API will first mount the FAT then format it, then restore back to the original state.
*
* @param base_path Path where partition should be registered (e.g. "/spiflash")
* @param partition_label Label of the partition which should be used
*
* @return
* - ESP_OK
* - ESP_ERR_NO_MEM: if memory can not be allocated
* - Other errors from esp_vfs_fat_spiflash_mount_rw_wl
*/
esp_err_t esp_vfs_fat_spiflash_format_rw_wl(const char* base_path, const char* partition_label);
/**
* @brief Convenience function to initialize read-only FAT filesystem and register it in VFS
*
* This is an all-in-one function which does the following:
*
* - finds the partition with defined partition_label. Partition label should be
* configured in the partition table.
* - mounts FAT partition using FATFS library
* - registers FATFS library with VFS, with prefix given by base_prefix variable
*
* @note Wear levelling is not used when FAT is mounted in read-only mode using this function.
*
* @param base_path path where FATFS partition should be mounted (e.g. "/spiflash")
* @param partition_label label of the partition which should be used
* @param mount_config pointer to structure with extra parameters for mounting FATFS
* @return
* - ESP_OK on success
* - ESP_ERR_NOT_FOUND if the partition table does not contain FATFS partition with given label
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_spiflash_mount_ro was already called for the same partition
* - ESP_ERR_NO_MEM if memory can not be allocated
* - ESP_FAIL if partition can not be mounted
* - other error codes from SPI flash driver, or FATFS drivers
*/
esp_err_t esp_vfs_fat_spiflash_mount_ro(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config);
/**
* @brief Unmount FAT filesystem and release resources acquired using esp_vfs_fat_spiflash_mount_ro
*
* @param base_path path where partition should be registered (e.g. "/spiflash")
* @param partition_label label of partition to be unmounted
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if esp_vfs_fat_spiflash_mount_ro hasn't been called
*/
esp_err_t esp_vfs_fat_spiflash_unmount_ro(const char* base_path, const char* partition_label);
esp_err_t esp_vfs_fat_spiflash_mount(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config,
wl_handle_t* wl_handle)
__attribute__((deprecated("esp_vfs_fat_spiflash_mount is deprecated, please use esp_vfs_fat_spiflash_mount_rw_wl instead")));
esp_err_t esp_vfs_fat_spiflash_unmount(const char* base_path, wl_handle_t wl_handle)
__attribute__((deprecated("esp_vfs_fat_spiflash_unmount is deprecated, please use esp_vfs_fat_spiflash_unmount_rw_wl instead")));
esp_err_t esp_vfs_fat_rawflash_mount(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config)
__attribute__((deprecated("esp_vfs_fat_rawflash_mount is deprecated, please use esp_vfs_fat_spiflash_mount_ro instead")));
esp_err_t esp_vfs_fat_rawflash_unmount(const char* base_path, const char* partition_label)
__attribute__((deprecated("esp_vfs_fat_rawflash_unmount is deprecated, please use esp_vfs_fat_spiflash_unmount_ro instead")));
/**
* @brief Get information for FATFS partition
*
* @param base_path Path where partition should be registered (e.g. "/spiflash")
* @param[out] out_total_bytes Size of the file system
* @param[out] out_free_bytes Current used bytes in the file system
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if partition not found
* - ESP_FAIL if another FRESULT error (saved in errno)
*/
esp_err_t esp_vfs_fat_info(const char* base_path, uint64_t* out_total_bytes, uint64_t* out_free_bytes);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "esp_vfs_fat.h"
#include <sys/param.h>
#include <stddef.h>
static inline size_t esp_vfs_fat_get_allocation_unit_size(
size_t sector_size, size_t requested_size)
{
size_t alloc_unit_size = requested_size;
const size_t max_sectors_per_cylinder = 128;
const size_t max_size = sector_size * max_sectors_per_cylinder;
alloc_unit_size = MAX(alloc_unit_size, sector_size);
alloc_unit_size = MIN(alloc_unit_size, max_size);
return alloc_unit_size;
}

@ -0,0 +1,498 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "vfs_fat_internal.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "diskio_impl.h"
#include "diskio_sdmmc.h"
#include "soc/soc_caps.h"
#include "driver/sdmmc_defs.h"
#if SOC_SDMMC_HOST_SUPPORTED
#include "driver/sdmmc_host.h"
#endif
static const char* TAG = "vfs_fat_sdmmc";
#define CHECK_EXECUTE_RESULT(err, str) do { \
if ((err) !=ESP_OK) { \
ESP_LOGE(TAG, str" (0x%x).", err); \
goto cleanup; \
} \
} while(0)
typedef struct vfs_fat_sd_ctx_t {
BYTE pdrv; //Drive number that is mounted
esp_vfs_fat_mount_config_t mount_config; //Mount configuration
FATFS *fs; //FAT structure pointer that is registered
sdmmc_card_t *card; //Card info
char *base_path; //Path where partition is registered
} vfs_fat_sd_ctx_t;
static vfs_fat_sd_ctx_t *s_ctx[FF_VOLUMES] = {};
/**
* This `s_saved_ctx_id` is only used by `esp_vfs_fat_sdmmc_unmount`, which is deprecated.
* This variable together with `esp_vfs_fat_sdmmc_unmount` should be removed in next major version
*/
static uint32_t s_saved_ctx_id = FF_VOLUMES;
static void call_host_deinit(const sdmmc_host_t *host_config);
static esp_err_t partition_card(const esp_vfs_fat_mount_config_t *mount_config,
const char *drv, sdmmc_card_t *card, BYTE pdrv);
static bool s_get_context_id_by_card(const sdmmc_card_t *card, uint32_t *out_id)
{
vfs_fat_sd_ctx_t *p_ctx = NULL;
for (int i = 0; i < FF_VOLUMES; i++) {
p_ctx = s_ctx[i];
if (p_ctx) {
if (p_ctx->card == card) {
*out_id = i;
return true;
}
}
}
return false;
}
static uint32_t s_get_unused_context_id(void)
{
for (uint32_t i = 0; i < FF_VOLUMES; i++) {
if (!s_ctx[i]) {
return i;
}
}
return FF_VOLUMES;
}
static esp_err_t mount_prepare_mem(const char *base_path,
BYTE *out_pdrv,
char **out_dup_path,
sdmmc_card_t** out_card)
{
esp_err_t err = ESP_OK;
char* dup_path = NULL;
sdmmc_card_t* card = NULL;
// connect SDMMC driver to FATFS
BYTE pdrv = FF_DRV_NOT_USED;
if (ff_diskio_get_drive(&pdrv) != ESP_OK || pdrv == FF_DRV_NOT_USED) {
ESP_LOGD(TAG, "the maximum count of volumes is already mounted");
return ESP_ERR_NO_MEM;
}
// not using ff_memalloc here, as allocation in internal RAM is preferred
card = (sdmmc_card_t*)malloc(sizeof(sdmmc_card_t));
if (card == NULL) {
ESP_LOGD(TAG, "could not locate new sdmmc_card_t");
err = ESP_ERR_NO_MEM;
goto cleanup;
}
dup_path = strdup(base_path);
if(!dup_path){
ESP_LOGD(TAG, "could not copy base_path");
err = ESP_ERR_NO_MEM;
goto cleanup;
}
*out_card = card;
*out_pdrv = pdrv;
*out_dup_path = dup_path;
return ESP_OK;
cleanup:
free(card);
free(dup_path);
return err;
}
static esp_err_t s_f_mount(sdmmc_card_t *card, FATFS *fs, const char *drv, uint8_t pdrv, const esp_vfs_fat_mount_config_t *mount_config)
{
esp_err_t err = ESP_OK;
FRESULT res = f_mount(fs, drv, 1);
if (res != FR_OK) {
err = ESP_FAIL;
ESP_LOGW(TAG, "failed to mount card (%d)", res);
bool need_mount_again = (res == FR_NO_FILESYSTEM || res == FR_INT_ERR) && mount_config->format_if_mount_failed;
if (!need_mount_again) {
return ESP_FAIL;
}
err = partition_card(mount_config, drv, card, pdrv);
if (err != ESP_OK) {
return err;
}
ESP_LOGW(TAG, "mounting again");
res = f_mount(fs, drv, 0);
if (res != FR_OK) {
err = ESP_FAIL;
ESP_LOGD(TAG, "f_mount failed after formatting (%d)", res);
return err;
}
}
return ESP_OK;
}
static esp_err_t mount_to_vfs_fat(const esp_vfs_fat_mount_config_t *mount_config, sdmmc_card_t *card, uint8_t pdrv,
const char *base_path, FATFS **out_fs)
{
FATFS *fs = NULL;
esp_err_t err;
ff_diskio_register_sdmmc(pdrv, card);
ff_sdmmc_set_disk_status_check(pdrv, mount_config->disk_status_check_enable);
ESP_LOGD(TAG, "using pdrv=%i", pdrv);
char drv[3] = {(char)('0' + pdrv), ':', 0};
// connect FATFS to VFS
err = esp_vfs_fat_register(base_path, drv, mount_config->max_files, &fs);
*out_fs = fs;
if (err == ESP_ERR_INVALID_STATE) {
// it's okay, already registered with VFS
} else if (err != ESP_OK) {
ESP_LOGD(TAG, "esp_vfs_fat_register failed 0x(%x)", err);
goto fail;
}
// Try to mount partition
err = s_f_mount(card, fs, drv, pdrv, mount_config);
if (err != ESP_OK) {
goto fail;
}
return ESP_OK;
fail:
if (fs) {
f_mount(NULL, drv, 0);
}
esp_vfs_fat_unregister_path(base_path);
ff_diskio_unregister(pdrv);
return err;
}
static esp_err_t partition_card(const esp_vfs_fat_mount_config_t *mount_config,
const char *drv, sdmmc_card_t *card, BYTE pdrv)
{
FRESULT res = FR_OK;
esp_err_t err;
const size_t workbuf_size = 4096;
void* workbuf = NULL;
ESP_LOGW(TAG, "partitioning card");
workbuf = ff_memalloc(workbuf_size);
if (workbuf == NULL) {
return ESP_ERR_NO_MEM;
}
LBA_t plist[] = {100, 0, 0, 0};
res = f_fdisk(pdrv, plist, workbuf);
if (res != FR_OK) {
err = ESP_FAIL;
ESP_LOGD(TAG, "f_fdisk failed (%d)", res);
goto fail;
}
size_t alloc_unit_size = esp_vfs_fat_get_allocation_unit_size(
card->csd.sector_size,
mount_config->allocation_unit_size);
ESP_LOGW(TAG, "formatting card, allocation unit size=%d", alloc_unit_size);
const MKFS_PARM opt = {(BYTE)FM_ANY, 0, 0, 0, alloc_unit_size};
res = f_mkfs(drv, &opt, workbuf, workbuf_size);
if (res != FR_OK) {
err = ESP_FAIL;
ESP_LOGD(TAG, "f_mkfs failed (%d)", res);
goto fail;
}
free(workbuf);
return ESP_OK;
fail:
free(workbuf);
return err;
}
#if SOC_SDMMC_HOST_SUPPORTED
static esp_err_t init_sdmmc_host(int slot, const void *slot_config, int *out_slot)
{
*out_slot = slot;
return sdmmc_host_init_slot(slot, (const sdmmc_slot_config_t*) slot_config);
}
esp_err_t esp_vfs_fat_sdmmc_mount(const char* base_path,
const sdmmc_host_t* host_config,
const void* slot_config,
const esp_vfs_fat_mount_config_t* mount_config,
sdmmc_card_t** out_card)
{
esp_err_t err;
vfs_fat_sd_ctx_t *ctx = NULL;
uint32_t ctx_id = FF_VOLUMES;
FATFS *fs = NULL;
int card_handle = -1; //uninitialized
sdmmc_card_t* card = NULL;
BYTE pdrv = FF_DRV_NOT_USED;
char* dup_path = NULL;
bool host_inited = false;
err = mount_prepare_mem(base_path, &pdrv, &dup_path, &card);
if (err != ESP_OK) {
ESP_LOGE(TAG, "mount_prepare failed");
return err;
}
err = (*host_config->init)();
CHECK_EXECUTE_RESULT(err, "host init failed");
//deinit() needs to be called to revert the init
host_inited = true;
//If this failed (indicated by card_handle != -1), slot deinit needs to called()
//leave card_handle as is to indicate that (though slot deinit not implemented yet.
err = init_sdmmc_host(host_config->slot, slot_config, &card_handle);
CHECK_EXECUTE_RESULT(err, "slot init failed");
// probe and initialize card
err = sdmmc_card_init(host_config, card);
CHECK_EXECUTE_RESULT(err, "sdmmc_card_init failed");
err = mount_to_vfs_fat(mount_config, card, pdrv, dup_path, &fs);
CHECK_EXECUTE_RESULT(err, "mount_to_vfs failed");
if (out_card != NULL) {
*out_card = card;
}
//For deprecation backward compatibility
if (s_saved_ctx_id == FF_VOLUMES) {
s_saved_ctx_id = 0;
}
ctx = calloc(sizeof(vfs_fat_sd_ctx_t), 1);
if (!ctx) {
CHECK_EXECUTE_RESULT(ESP_ERR_NO_MEM, "no mem");
}
ctx->pdrv = pdrv;
memcpy(&ctx->mount_config, mount_config, sizeof(esp_vfs_fat_mount_config_t));
ctx->card = card;
ctx->base_path = dup_path;
ctx->fs = fs;
ctx_id = s_get_unused_context_id();
assert(ctx_id != FF_VOLUMES);
s_ctx[ctx_id] = ctx;
return ESP_OK;
cleanup:
if (host_inited) {
call_host_deinit(host_config);
}
free(card);
free(dup_path);
return err;
}
#endif
static esp_err_t init_sdspi_host(int slot, const void *slot_config, int *out_slot)
{
esp_err_t err = sdspi_host_init_device((const sdspi_device_config_t*)slot_config, out_slot);
if (err != ESP_OK) {
ESP_LOGE(TAG,
"Failed to attach sdspi device onto an SPI bus (rc=0x%x), please initialize the \
bus first and check the device parameters."
, err);
}
return err;
}
esp_err_t esp_vfs_fat_sdspi_mount(const char* base_path,
const sdmmc_host_t* host_config_input,
const sdspi_device_config_t* slot_config,
const esp_vfs_fat_mount_config_t* mount_config,
sdmmc_card_t** out_card)
{
const sdmmc_host_t* host_config = host_config_input;
esp_err_t err;
vfs_fat_sd_ctx_t *ctx = NULL;
uint32_t ctx_id = FF_VOLUMES;
FATFS *fs = NULL;
int card_handle = -1; //uninitialized
bool host_inited = false;
BYTE pdrv = FF_DRV_NOT_USED;
sdmmc_card_t* card = NULL;
char* dup_path = NULL;
err = mount_prepare_mem(base_path, &pdrv, &dup_path, &card);
if (err != ESP_OK) {
ESP_LOGE(TAG, "mount_prepare failed");
return err;
}
//the init() function is usually empty, doesn't require any deinit to revert it
err = (*host_config->init)();
CHECK_EXECUTE_RESULT(err, "host init failed");
err = init_sdspi_host(host_config->slot, slot_config, &card_handle);
CHECK_EXECUTE_RESULT(err, "slot init failed");
//Set `host_inited` to true to indicate that host_config->deinit() needs
//to be called to revert `init_sdspi_host`
host_inited = true;
/*
* The `slot` argument inside host_config should be replaced by the SD SPI handled returned
* above. But the input pointer is const, so create a new variable.
*/
sdmmc_host_t new_config;
if (card_handle != host_config->slot) {
new_config = *host_config_input;
host_config = &new_config;
new_config.slot = card_handle;
}
// probe and initialize card
err = sdmmc_card_init(host_config, card);
CHECK_EXECUTE_RESULT(err, "sdmmc_card_init failed");
err = mount_to_vfs_fat(mount_config, card, pdrv, dup_path, &fs);
CHECK_EXECUTE_RESULT(err, "mount_to_vfs failed");
if (out_card != NULL) {
*out_card = card;
}
//For deprecation backward compatibility
if (s_saved_ctx_id == FF_VOLUMES) {
s_saved_ctx_id = 0;
}
ctx = calloc(sizeof(vfs_fat_sd_ctx_t), 1);
if (!ctx) {
CHECK_EXECUTE_RESULT(ESP_ERR_NO_MEM, "no mem");
}
ctx->pdrv = pdrv;
memcpy(&ctx->mount_config, mount_config, sizeof(esp_vfs_fat_mount_config_t));
ctx->card = card;
ctx->base_path = dup_path;
ctx->fs = fs;
ctx_id = s_get_unused_context_id();
assert(ctx_id != FF_VOLUMES);
s_ctx[ctx_id] = ctx;
return ESP_OK;
cleanup:
if (host_inited) {
call_host_deinit(host_config);
}
free(card);
free(dup_path);
return err;
}
static void call_host_deinit(const sdmmc_host_t *host_config)
{
if (host_config->flags & SDMMC_HOST_FLAG_DEINIT_ARG) {
host_config->deinit_p(host_config->slot);
} else {
host_config->deinit();
}
}
static esp_err_t unmount_card_core(const char *base_path, sdmmc_card_t *card)
{
BYTE pdrv = ff_diskio_get_pdrv_card(card);
if (pdrv == 0xff) {
return ESP_ERR_INVALID_ARG;
}
// unmount
char drv[3] = {(char)('0' + pdrv), ':', 0};
f_mount(0, drv, 0);
// release SD driver
ff_diskio_unregister(pdrv);
call_host_deinit(&card->host);
free(card);
esp_err_t err = esp_vfs_fat_unregister_path(base_path);
return err;
}
esp_err_t esp_vfs_fat_sdmmc_unmount(void)
{
esp_err_t err = unmount_card_core(s_ctx[s_saved_ctx_id]->base_path, s_ctx[s_saved_ctx_id]->card);
free(s_ctx[s_saved_ctx_id]);
s_ctx[s_saved_ctx_id] = NULL;
s_saved_ctx_id = FF_VOLUMES;
return err;
}
esp_err_t esp_vfs_fat_sdcard_unmount(const char *base_path, sdmmc_card_t *card)
{
uint32_t id = FF_VOLUMES;
bool found = s_get_context_id_by_card(card, &id);
if (!found) {
return ESP_ERR_INVALID_ARG;
}
free(s_ctx[id]);
s_ctx[id] = NULL;
esp_err_t err = unmount_card_core(base_path, card);
return err;
}
esp_err_t esp_vfs_fat_sdcard_format(const char *base_path, sdmmc_card_t *card)
{
esp_err_t ret = ESP_OK;
if (!card) {
ESP_LOGE(TAG, "card not initialized");
return ESP_ERR_INVALID_STATE;
}
BYTE pdrv = ff_diskio_get_pdrv_card(card);
if (pdrv == 0xff) {
ESP_LOGE(TAG, "card driver not registered");
return ESP_ERR_INVALID_STATE;
}
const size_t workbuf_size = 4096;
void *workbuf = ff_memalloc(workbuf_size);
if (workbuf == NULL) {
return ESP_ERR_NO_MEM;
}
//unmount
char drv[3] = {(char)('0' + pdrv), ':', 0};
f_mount(0, drv, 0);
//format
uint32_t id = FF_VOLUMES;
bool found = s_get_context_id_by_card(card, &id);
assert(found);
size_t alloc_unit_size = esp_vfs_fat_get_allocation_unit_size(
card->csd.sector_size,
s_ctx[id]->mount_config.allocation_unit_size);
ESP_LOGI(TAG, "Formatting card, allocation unit size=%d", alloc_unit_size);
const MKFS_PARM opt = {(BYTE)FM_ANY, 0, 0, 0, alloc_unit_size};
FRESULT res = f_mkfs(drv, &opt, workbuf, workbuf_size);
free(workbuf);
if (res != FR_OK) {
ret = ESP_FAIL;
ESP_LOGD(TAG, "f_mkfs failed (%d)", res);
}
//mount back
esp_err_t err = s_f_mount(card, s_ctx[id]->fs, drv, pdrv, &s_ctx[id]->mount_config);
if (err != ESP_OK) {
unmount_card_core(base_path, card);
ESP_LOGE(TAG, "failed to format, resources recycled, please mount again");
}
return ret;
}

@ -0,0 +1,335 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include "esp_check.h"
#include "esp_log.h"
#include "esp_vfs.h"
#include "esp_vfs_fat.h"
#include "vfs_fat_internal.h"
#include "diskio_impl.h"
#include "diskio_rawflash.h"
#include "wear_levelling.h"
#include "diskio_wl.h"
static const char* TAG = "vfs_fat_spiflash";
typedef struct vfs_fat_spiflash_ctx_t {
const esp_partition_t *partition; //The partition where the FAT is located
bool by_label; //If the partition is mounted by lable or not
BYTE pdrv; //Drive number that is mounted
FATFS *fs; //FAT structure pointer that is registered
wl_handle_t wlhandle; //WL handle
esp_vfs_fat_mount_config_t mount_config; //Mount configuration
} vfs_fat_spiflash_ctx_t;
static vfs_fat_spiflash_ctx_t *s_ctx[FF_VOLUMES] = {};
static bool s_get_context_id_by_label(const char *label, uint32_t *out_id)
{
vfs_fat_spiflash_ctx_t *p_ctx = NULL;
for (int i = 0; i < FF_VOLUMES; i++) {
p_ctx = s_ctx[i];
if (p_ctx) {
if (!label && !p_ctx->by_label) {
*out_id = i;
return true;
}
if (label && p_ctx->by_label && strncmp(label, p_ctx->partition->label, 20) == 0) {
*out_id = i;
return true;
}
}
}
return false;
}
static bool s_get_context_id_by_wl_handle(wl_handle_t wlhandle, uint32_t *out_id)
{
vfs_fat_spiflash_ctx_t *p_ctx = NULL;
for (int i = 0; i < FF_VOLUMES; i++) {
p_ctx = s_ctx[i];
if (p_ctx) {
if (p_ctx->wlhandle == wlhandle) {
*out_id = i;
return true;
}
}
}
return false;
}
static uint32_t s_get_unused_context_id(void)
{
for (uint32_t i = 0; i < FF_VOLUMES; i++) {
if (!s_ctx[i]) {
return i;
}
}
return FF_VOLUMES;
}
static esp_err_t s_f_mount_rw(FATFS *fs, const char *drv, const esp_vfs_fat_mount_config_t *mount_config)
{
FRESULT fresult = f_mount(fs, drv, 1);
if (fresult != FR_OK) {
ESP_LOGW(TAG, "f_mount failed (%d)", fresult);
bool need_mount_again = (fresult == FR_NO_FILESYSTEM || fresult == FR_INT_ERR) && mount_config->format_if_mount_failed;
if (!need_mount_again) {
return ESP_FAIL;
}
const size_t workbuf_size = 4096;
void *workbuf = ff_memalloc(workbuf_size);
if (workbuf == NULL) {
return ESP_ERR_NO_MEM;
}
size_t alloc_unit_size = esp_vfs_fat_get_allocation_unit_size(CONFIG_WL_SECTOR_SIZE, mount_config->allocation_unit_size);
ESP_LOGI(TAG, "Formatting FATFS partition, allocation unit size=%d", alloc_unit_size);
const MKFS_PARM opt = {(BYTE)(FM_ANY | FM_SFD), 0, 0, 0, alloc_unit_size};
fresult = f_mkfs(drv, &opt, workbuf, workbuf_size);
free(workbuf);
workbuf = NULL;
ESP_RETURN_ON_FALSE(fresult == FR_OK, ESP_FAIL, TAG, "f_mkfs failed (%d)", fresult);
ESP_LOGI(TAG, "Mounting again");
fresult = f_mount(fs, drv, 0);
ESP_RETURN_ON_FALSE(fresult == FR_OK, ESP_FAIL, TAG, "f_mount failed after formatting (%d)", fresult);
}
return ESP_OK;
}
esp_err_t esp_vfs_fat_spiflash_mount_rw_wl(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config,
wl_handle_t* wl_handle)
{
esp_err_t ret = ESP_OK;
vfs_fat_spiflash_ctx_t *ctx = NULL;
uint32_t ctx_id = FF_VOLUMES;
esp_partition_subtype_t subtype = partition_label ?
ESP_PARTITION_SUBTYPE_ANY : ESP_PARTITION_SUBTYPE_DATA_FAT;
const esp_partition_t *data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
subtype, partition_label);
ESP_RETURN_ON_FALSE(data_partition, ESP_ERR_NOT_FOUND, TAG, "Failed to find FATFS partition (type='data', subtype='fat', partition_label='%s'). Check the partition table.", partition_label);
ESP_RETURN_ON_ERROR(wl_mount(data_partition, wl_handle), TAG, "failed to mount wear levelling layer. ret = %i", ret);
// connect driver to FATFS
BYTE pdrv = 0xFF;
if (ff_diskio_get_drive(&pdrv) != ESP_OK) {
ESP_LOGD(TAG, "the maximum count of volumes is already mounted");
return ESP_ERR_NO_MEM;
}
ESP_LOGD(TAG, "using pdrv=%i", pdrv);
char drv[3] = {(char)('0' + pdrv), ':', 0};
ESP_GOTO_ON_ERROR(ff_diskio_register_wl_partition(pdrv, *wl_handle), fail, TAG, "ff_diskio_register_wl_partition failed pdrv=%i, error - 0x(%x)", pdrv, ret);
FATFS *fs;
ret = esp_vfs_fat_register(base_path, drv, mount_config->max_files, &fs);
if (ret == ESP_ERR_INVALID_STATE) {
// it's okay, already registered with VFS
} else if (ret != ESP_OK) {
ESP_LOGD(TAG, "esp_vfs_fat_register failed 0x(%x)", ret);
goto fail;
}
// Try to mount partition
ret = s_f_mount_rw(fs, drv, mount_config);
if (ret != ESP_OK) {
goto fail;
}
ctx = calloc(sizeof(vfs_fat_spiflash_ctx_t), 1);
ESP_GOTO_ON_FALSE(ctx, ESP_ERR_NO_MEM, fail, TAG, "no mem");
ctx->partition = data_partition;
ctx->by_label = (partition_label != NULL);
ctx->pdrv = pdrv;
ctx->fs = fs;
ctx->wlhandle = *wl_handle;
memcpy(&ctx->mount_config, mount_config, sizeof(esp_vfs_fat_mount_config_t));
ctx_id = s_get_unused_context_id();
//At this stage, we should always get a free context, otherwise program should return already
assert(ctx_id != FF_VOLUMES);
s_ctx[ctx_id] = ctx;
return ESP_OK;
fail:
esp_vfs_fat_unregister_path(base_path);
ff_diskio_unregister(pdrv);
free(ctx);
return ret;
}
esp_err_t esp_vfs_fat_spiflash_unmount_rw_wl(const char* base_path, wl_handle_t wl_handle)
{
BYTE pdrv = ff_diskio_get_pdrv_wl(wl_handle);
ESP_RETURN_ON_FALSE(pdrv != 0xff, ESP_ERR_INVALID_STATE, TAG, "partition isn't registered, call esp_vfs_fat_spiflash_mount_rw_wl first");
uint32_t id = FF_VOLUMES;
ESP_RETURN_ON_FALSE(s_get_context_id_by_wl_handle(wl_handle, &id), ESP_ERR_INVALID_STATE, TAG, "partition isn't registered, call esp_vfs_fat_spiflash_mount_rw_wl first");
//At this stage, as the wl_handle is valid, we should always get its context id, otherwise program should return already
assert(id != FF_VOLUMES);
char drv[3] = {(char)('0' + pdrv), ':', 0};
f_mount(0, drv, 0);
ff_diskio_unregister(pdrv);
ff_diskio_clear_pdrv_wl(wl_handle);
// release partition driver
esp_err_t err_drv = wl_unmount(wl_handle);
esp_err_t err = esp_vfs_fat_unregister_path(base_path);
if (err == ESP_OK) {
err = err_drv;
}
free(s_ctx[id]);
s_ctx[id] = NULL;
return err;
}
esp_err_t esp_vfs_fat_spiflash_format_rw_wl(const char* base_path, const char* partition_label)
{
esp_err_t ret = ESP_OK;
bool partition_was_mounted = false;
wl_handle_t temp_handle = WL_INVALID_HANDLE;
uint32_t id = FF_VOLUMES;
char drv[3] = {0, ':', 0};
bool found = s_get_context_id_by_label(partition_label, &id);
if (!found) {
const esp_vfs_fat_mount_config_t mount_config = {
.max_files = 1,
.format_if_mount_failed = true,
};
ESP_RETURN_ON_ERROR(esp_vfs_fat_spiflash_mount_rw_wl(base_path, partition_label, &mount_config, &temp_handle), TAG, "Failed to mount");
found = s_get_context_id_by_label(partition_label, &id);
assert(found);
} else {
partition_was_mounted = true;
}
//unmount
drv[1] = (char)('0' + s_ctx[id]->pdrv);
f_mount(0, drv, 0);
const size_t workbuf_size = 4096;
void *workbuf = ff_memalloc(workbuf_size);
if (workbuf == NULL) {
ret = ESP_ERR_NO_MEM;
goto mount_back;
}
size_t alloc_unit_size = esp_vfs_fat_get_allocation_unit_size(CONFIG_WL_SECTOR_SIZE, s_ctx[id]->mount_config.allocation_unit_size);
ESP_LOGI(TAG, "Formatting FATFS partition, allocation unit size=%d", alloc_unit_size);
const MKFS_PARM opt = {(BYTE)(FM_ANY | FM_SFD), 0, 0, 0, alloc_unit_size};
FRESULT fresult = f_mkfs(drv, &opt, workbuf, workbuf_size);
free(workbuf);
workbuf = NULL;
ESP_GOTO_ON_FALSE(fresult == FR_OK, ESP_FAIL, mount_back, TAG, "f_mkfs failed (%d)", fresult);
mount_back:
if (partition_was_mounted) {
esp_err_t err = s_f_mount_rw(s_ctx[id]->fs, drv, &s_ctx[id]->mount_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "failed to mount back, go to recycle");
goto recycle;
}
} else {
esp_vfs_fat_spiflash_unmount_rw_wl(base_path, s_ctx[id]->wlhandle);
}
return ret;
recycle:
ff_diskio_unregister(s_ctx[id]->pdrv);
ff_diskio_clear_pdrv_wl(s_ctx[id]->wlhandle);
wl_unmount(s_ctx[id]->wlhandle);
esp_vfs_fat_unregister_path(base_path);
free(s_ctx[id]);
s_ctx[id] = NULL;
ESP_LOGE(TAG, "failed to format, resources recycled, please mount again");
return ret;
}
esp_err_t esp_vfs_fat_spiflash_mount_ro(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config)
{
esp_err_t ret = ESP_OK;
const esp_partition_t *data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_DATA_FAT, partition_label);
ESP_RETURN_ON_FALSE(data_partition, ESP_ERR_NOT_FOUND, TAG, "Failed to find FATFS partition (type='data', subtype='fat', partition_label='%s'). Check the partition table.", partition_label);
// connect driver to FATFS
BYTE pdrv = 0xFF;
if (ff_diskio_get_drive(&pdrv) != ESP_OK) {
ESP_LOGD(TAG, "the maximum count of volumes is already mounted");
return ESP_ERR_NO_MEM;
}
ESP_LOGD(TAG, "using pdrv=%i", pdrv);
char drv[3] = {(char)('0' + pdrv), ':', 0};
ESP_GOTO_ON_ERROR(ff_diskio_register_raw_partition(pdrv, data_partition), fail, TAG, "ff_diskio_register_raw_partition failed pdrv=%i, error - 0x(%x)", pdrv, ret);
FATFS *fs;
ret = esp_vfs_fat_register(base_path, drv, mount_config->max_files, &fs);
if (ret == ESP_ERR_INVALID_STATE) {
// it's okay, already registered with VFS
} else if (ret != ESP_OK) {
ESP_LOGD(TAG, "esp_vfs_fat_register failed 0x(%x)", ret);
goto fail;
}
// Try to mount partition
FRESULT fresult = f_mount(fs, drv, 1);
if (fresult != FR_OK) {
ESP_LOGW(TAG, "f_mount failed (%d)", fresult);
ret = ESP_FAIL;
goto fail;
}
return ESP_OK;
fail:
esp_vfs_fat_unregister_path(base_path);
ff_diskio_unregister(pdrv);
return ret;
}
esp_err_t esp_vfs_fat_spiflash_unmount_ro(const char* base_path, const char* partition_label)
{
const esp_partition_t *data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_DATA_FAT, partition_label);
ESP_RETURN_ON_FALSE(data_partition, ESP_ERR_NOT_FOUND, TAG, "Failed to find FATFS partition (type='data', subtype='fat', partition_label='%s'). Check the partition table.", partition_label);
BYTE pdrv = ff_diskio_get_pdrv_raw(data_partition);
ESP_RETURN_ON_FALSE(pdrv != 0xff, ESP_ERR_INVALID_STATE, TAG, "partition isn't registered, call esp_vfs_fat_spiflash_mount_ro first");
char drv[3] = {(char)('0' + pdrv), ':', 0};
f_mount(0, drv, 0);
ff_diskio_unregister(pdrv);
esp_err_t err = esp_vfs_fat_unregister_path(base_path);
return err;
}
esp_err_t esp_vfs_fat_spiflash_mount(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config,
wl_handle_t* wl_handle)
__attribute__((alias("esp_vfs_fat_spiflash_mount_rw_wl")));
esp_err_t esp_vfs_fat_spiflash_unmount(const char* base_path, wl_handle_t wl_handle)
__attribute__((alias("esp_vfs_fat_spiflash_unmount_rw_wl")));
esp_err_t esp_vfs_fat_rawflash_mount(const char* base_path,
const char* partition_label,
const esp_vfs_fat_mount_config_t* mount_config)
__attribute__((alias("esp_vfs_fat_spiflash_mount_ro")));
esp_err_t esp_vfs_fat_rawflash_unmount(const char* base_path, const char* partition_label)
__attribute__((alias("esp_vfs_fat_spiflash_unmount_ro")));

@ -0,0 +1,215 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from construct import Const, Int32ul, Struct
from fatfs_utils.exceptions import WLNotInitialized
from fatfs_utils.utils import (FULL_BYTE, UINT32_MAX, FATDefaults, crc32, generate_4bytes_random,
get_args_for_partition_generator)
from fatfsgen import FATFS
def remove_wl(binary_image: bytes) -> bytes:
partition_size: int = len(binary_image)
total_sectors: int = partition_size // FATDefaults.WL_SECTOR_SIZE
wl_state_size: int = WLFATFS.WL_STATE_HEADER_SIZE + WLFATFS.WL_STATE_RECORD_SIZE * total_sectors
wl_state_sectors_cnt: int = (wl_state_size + FATDefaults.WL_SECTOR_SIZE - 1) // FATDefaults.WL_SECTOR_SIZE
wl_state_total_size: int = wl_state_sectors_cnt * FATDefaults.WL_SECTOR_SIZE
wl_sectors_size: int = (wl_state_sectors_cnt
* FATDefaults.WL_SECTOR_SIZE
* WLFATFS.WL_STATE_COPY_COUNT
+ FATDefaults.WL_SECTOR_SIZE)
correct_wl_configuration = binary_image[-wl_sectors_size:]
data_ = WLFATFS.WL_STATE_T_DATA.parse(correct_wl_configuration[:WLFATFS.WL_STATE_HEADER_SIZE])
total_records = 0
# iterating over records field of the first copy of the state sector
for i in range(WLFATFS.WL_STATE_HEADER_SIZE, wl_state_total_size, WLFATFS.WL_STATE_RECORD_SIZE):
if correct_wl_configuration[i:i + WLFATFS.WL_STATE_RECORD_SIZE] != WLFATFS.WL_STATE_RECORD_SIZE * b'\xff':
total_records += 1
else:
break
before_dummy = binary_image[:total_records * FATDefaults.WL_SECTOR_SIZE]
after_dummy = binary_image[total_records * FATDefaults.WL_SECTOR_SIZE + FATDefaults.WL_SECTOR_SIZE:]
new_image: bytes = before_dummy + after_dummy
# remove wl sectors
new_image = new_image[:len(new_image) - (FATDefaults.WL_SECTOR_SIZE + 2 * wl_state_total_size)]
# reorder to preserve original order
new_image = (new_image[-data_['move_count'] * FATDefaults.WL_SECTOR_SIZE:]
+ new_image[:-data_['move_count'] * FATDefaults.WL_SECTOR_SIZE])
return new_image
class WLFATFS:
# pylint: disable=too-many-instance-attributes
WL_CFG_SECTORS_COUNT = 1
WL_DUMMY_SECTORS_COUNT = 1
WL_CONFIG_HEADER_SIZE = 48
WL_STATE_RECORD_SIZE = 16
WL_STATE_HEADER_SIZE = 64
WL_STATE_COPY_COUNT = 2 # always 2 copies for power failure safety
WL_SECTOR_SIZE = 0x1000
WL_STATE_T_DATA = Struct(
'pos' / Int32ul,
'max_pos' / Int32ul,
'move_count' / Int32ul,
'access_count' / Int32ul,
'max_count' / Int32ul,
'block_size' / Int32ul,
'version' / Int32ul,
'device_id' / Int32ul,
'reserved' / Const(28 * b'\x00')
)
WL_CONFIG_T_DATA = Struct(
'start_addr' / Int32ul,
'full_mem_size' / Int32ul,
'page_size' / Int32ul,
'sector_size' / Int32ul, # always 4096 for the types of NOR flash supported by ESP-IDF!
'updaterate' / Int32ul,
'wr_size' / Int32ul,
'version' / Int32ul,
'temp_buff_size' / Int32ul
)
WL_CONFIG_T_HEADER_SIZE = 48
def __init__(self,
size: int = FATDefaults.SIZE,
sector_size: int = FATDefaults.SECTOR_SIZE,
reserved_sectors_cnt: int = FATDefaults.RESERVED_SECTORS_COUNT,
fat_tables_cnt: int = FATDefaults.FAT_TABLES_COUNT,
sectors_per_cluster: int = FATDefaults.SECTORS_PER_CLUSTER,
explicit_fat_type: int = None,
hidden_sectors: int = FATDefaults.HIDDEN_SECTORS,
long_names_enabled: bool = False,
num_heads: int = FATDefaults.NUM_HEADS,
oem_name: str = FATDefaults.OEM_NAME,
sec_per_track: int = FATDefaults.SEC_PER_TRACK,
volume_label: str = FATDefaults.VOLUME_LABEL,
file_sys_type: str = FATDefaults.FILE_SYS_TYPE,
use_default_datetime: bool = True,
version: int = FATDefaults.VERSION,
temp_buff_size: int = FATDefaults.TEMP_BUFFER_SIZE,
device_id: int = None,
root_entry_count: int = FATDefaults.ROOT_ENTRIES_COUNT,
media_type: int = FATDefaults.MEDIA_TYPE) -> None:
self._initialized = False
self._version = version
self._temp_buff_size = temp_buff_size
self._device_id = device_id
self.partition_size = size
self.total_sectors = self.partition_size // FATDefaults.WL_SECTOR_SIZE
self.wl_state_size = WLFATFS.WL_STATE_HEADER_SIZE + WLFATFS.WL_STATE_RECORD_SIZE * self.total_sectors
# determine the number of required sectors (roundup to sector size)
self.wl_state_sectors = (self.wl_state_size + FATDefaults.WL_SECTOR_SIZE - 1) // FATDefaults.WL_SECTOR_SIZE
self.boot_sector_start = FATDefaults.WL_SECTOR_SIZE # shift by one "dummy" sector
self.fat_table_start = self.boot_sector_start + reserved_sectors_cnt * FATDefaults.WL_SECTOR_SIZE
wl_sectors = (WLFATFS.WL_DUMMY_SECTORS_COUNT + WLFATFS.WL_CFG_SECTORS_COUNT +
self.wl_state_sectors * WLFATFS.WL_STATE_COPY_COUNT)
self.plain_fat_sectors = self.total_sectors - wl_sectors
self.plain_fatfs = FATFS(
explicit_fat_type=explicit_fat_type,
size=self.plain_fat_sectors * FATDefaults.WL_SECTOR_SIZE,
reserved_sectors_cnt=reserved_sectors_cnt,
fat_tables_cnt=fat_tables_cnt,
sectors_per_cluster=sectors_per_cluster,
sector_size=sector_size,
root_entry_count=root_entry_count,
hidden_sectors=hidden_sectors,
long_names_enabled=long_names_enabled,
num_heads=num_heads,
use_default_datetime=use_default_datetime,
oem_name=oem_name,
sec_per_track=sec_per_track,
volume_label=volume_label,
file_sys_type=file_sys_type,
media_type=media_type
)
self.fatfs_binary_image = self.plain_fatfs.state.binary_image
def init_wl(self) -> None:
self.fatfs_binary_image = self.plain_fatfs.state.binary_image
self._add_dummy_sector()
# config must be added after state, do not change the order of these two calls!
self._add_state_sectors()
self._add_config_sector()
self._initialized = True
def _add_dummy_sector(self) -> None:
self.fatfs_binary_image = FATDefaults.WL_SECTOR_SIZE * FULL_BYTE + self.fatfs_binary_image
def _add_config_sector(self) -> None:
wl_config_data = WLFATFS.WL_CONFIG_T_DATA.build(
dict(
start_addr=0,
full_mem_size=self.partition_size,
page_size=FATDefaults.WL_SECTOR_SIZE, # equal to sector size (always 4096)
sector_size=FATDefaults.WL_SECTOR_SIZE,
updaterate=FATDefaults.UPDATE_RATE,
wr_size=FATDefaults.WR_SIZE,
version=self._version,
temp_buff_size=self._temp_buff_size
)
)
crc = crc32(list(wl_config_data), UINT32_MAX)
wl_config_crc = Int32ul.build(crc)
# adding three 4 byte zeros to align the structure
wl_config = wl_config_data + wl_config_crc + Int32ul.build(0) + Int32ul.build(0) + Int32ul.build(0)
self.fatfs_binary_image += (
wl_config + (FATDefaults.WL_SECTOR_SIZE - WLFATFS.WL_CONFIG_HEADER_SIZE) * FULL_BYTE)
def _add_state_sectors(self) -> None:
wl_state_data = WLFATFS.WL_STATE_T_DATA.build(
dict(
pos=0,
max_pos=self.plain_fat_sectors + WLFATFS.WL_DUMMY_SECTORS_COUNT,
move_count=0,
access_count=0,
max_count=FATDefaults.UPDATE_RATE,
block_size=FATDefaults.WL_SECTOR_SIZE, # equal to page size, thus equal to wl sector size (4096)
version=self._version,
device_id=self._device_id or generate_4bytes_random(),
)
)
crc = crc32(list(wl_state_data), UINT32_MAX)
wl_state_crc = Int32ul.build(crc)
wl_state = wl_state_data + wl_state_crc
wl_state_sector_padding: bytes = (FATDefaults.WL_SECTOR_SIZE - WLFATFS.WL_STATE_HEADER_SIZE) * FULL_BYTE
wl_state_sector: bytes = (
wl_state + wl_state_sector_padding + (self.wl_state_sectors - 1) * FATDefaults.WL_SECTOR_SIZE * FULL_BYTE
)
self.fatfs_binary_image += (WLFATFS.WL_STATE_COPY_COUNT * wl_state_sector)
def wl_write_filesystem(self, output_path: str) -> None:
if not self._initialized:
raise WLNotInitialized('FATFS is not initialized with WL. First call method WLFATFS.init_wl!')
with open(output_path, 'wb') as output:
output.write(bytearray(self.fatfs_binary_image))
if __name__ == '__main__':
desc = 'Create a FAT filesystem with support for wear levelling and populate it with directory content'
args = get_args_for_partition_generator(desc, wl=True)
wl_fatfs = WLFATFS(sectors_per_cluster=args.sectors_per_cluster,
size=args.partition_size,
sector_size=args.sector_size,
root_entry_count=args.root_entry_count,
explicit_fat_type=args.fat_type,
long_names_enabled=args.long_name_support,
use_default_datetime=args.use_default_datetime)
wl_fatfs.plain_fatfs.generate(args.input_directory)
wl_fatfs.init_wl()
wl_fatfs.wl_write_filesystem(args.output_file)
Loading…
Cancel
Save