diff --git a/.github/workflows/build-and-test-macos.yaml b/.github/workflows/build-and-test-macos.yaml index 6738be264a..1d18a21030 100644 --- a/.github/workflows/build-and-test-macos.yaml +++ b/.github/workflows/build-and-test-macos.yaml @@ -254,6 +254,11 @@ jobs: run: | ./tests/test-structs + - name: "Test: test-term" + working-directory: build + run: | + ./tests/test-term + - name: "Test: test_etest.avm" timeout-minutes: 5 working-directory: build diff --git a/.github/workflows/build-and-test-on-freebsd.yaml b/.github/workflows/build-and-test-on-freebsd.yaml index f22718ecc2..d7a3b49407 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -157,6 +157,13 @@ jobs: cd build ./tests/test-structs + - name: "Test: test-term" + shell: freebsd {0} + run: | + cd $GITHUB_WORKSPACE; + cd build + ./tests/test-term + - name: "Test: test_etest.avm" shell: freebsd {0} run: | diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index f487ea0cf3..48de1aa824 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -875,6 +875,19 @@ jobs: ulimit -c unlimited ./tests/test-structs + - name: "Test: test-term with valgrind" + if: matrix.library-arch == '' + working-directory: build + run: | + ulimit -c unlimited + valgrind --error-exitcode=1 ./tests/test-term + + - name: "Test: test-term" + working-directory: build + run: | + ulimit -c unlimited + ./tests/test-term + - name: "Test: test_etest.avm with valgrind" if: matrix.library-arch == '' timeout-minutes: 5 diff --git a/.github/workflows/build-linux-artifacts.yaml b/.github/workflows/build-linux-artifacts.yaml index ff9018cc70..cd65b9cc87 100644 --- a/.github/workflows/build-linux-artifacts.yaml +++ b/.github/workflows/build-linux-artifacts.yaml @@ -236,6 +236,7 @@ jobs: make test-heap && make test-mailbox && make test-structs && + make test-term && file ./tests/test-erlang && ./tests/test-erlang -s prime_smp && file ./tests/test-enif && @@ -246,6 +247,8 @@ jobs: ./tests/test-mailbox && file ./tests/test-structs && ./tests/test-structs && + file ./tests/test-term && + ./tests/test-term && file ./src/AtomVM && ./src/AtomVM tests/libs/etest/test_etest.avm && ./src/AtomVM tests/libs/estdlib/test_estdlib.avm && diff --git a/CHANGELOG.md b/CHANGELOG.md index ecedd437fc..4c45240685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 sending to an alias reference - Added xtensa JIT backend for esp32 platform - Added support for configuring pins and width for sdmmc on ESP32 +- Added `esp:sdcard_open/2`, `esp:sdcard_read/2`, `esp:sdcard_write/3`, `esp:sdcard_info/1` and + `esp:sdcard_close/1` to the ESP32 `esp` module for low level SD card block (sector) access - Added support for map comprehensions - Added USB CDC port drivers for ESP32, RP2, and STM32 platforms - Added a Linux `gpio` driver for the generic_unix port (in `avm_unix`) using sysfs @@ -90,6 +92,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where negative or oversized segment sizes were not rejected in binary matching - Fixed the `network` mdns configuration to read the documented `host` key; the previously required, undocumented `hostname` key is still accepted +- Fixed `term_is_uint32` accepting big integers whose low 64 bits are within range on 32-bit + builds, which made `erlang:crc32/2`, `erlang:crc32_combine/3` and `crypto:pbkdf2_hmac/5` + silently truncate huge integer arguments instead of raising `badarg` ## [0.7.0-alpha.1] - 2026-04-06 diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index d3cc115b51..391a7c078e 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -1394,6 +1394,58 @@ These functions allow you to work with external storage devices or partitions on Remember to properly unmount any mounted filesystems before powering off or resetting the device to prevent data corruption. ``` +#### Raw SD card block access + +In addition to mounting a FAT filesystem, AtomVM can access an SD card at the block (sector) +level, without mounting any filesystem. This is useful for implementing custom filesystems, +inspecting partition tables, or reading and writing raw images. + +Open the card with [`esp:sdcard_open/2`](./apidocs/erlang/eavmlib/esp.md#sdcard_open-2). The +source string (`"sdmmc"` or `"sdspi"`) and the options are the same as for `esp:mount/4`: + +```erlang +{ok, SDCard} = esp:sdcard_open("sdmmc", []), +``` + +For an SPI attached card, first create a SPI instance, then pass the `spi_host` and `cs` +options, exactly as with `esp:mount/4`: + +```erlang +SPI = spi:open(SPIConfig), +{ok, SDCard} = esp:sdcard_open("sdspi", [{spi_host, SPI}, {cs, 5}]), +``` + +Use [`esp:sdcard_info/1`](./apidocs/erlang/eavmlib/esp.md#sdcard_info-1) to obtain the card +geometry, then read and write individual sectors with +[`esp:sdcard_read/2`](./apidocs/erlang/eavmlib/esp.md#sdcard_read-2) and +[`esp:sdcard_write/3`](./apidocs/erlang/eavmlib/esp.md#sdcard_write-3). The data passed to +`esp:sdcard_write/3` must be exactly one sector in size: + +```erlang +{ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), +{ok, Sector0} = esp:sdcard_read(SDCard, 0), +ok = esp:sdcard_write(SDCard, SectorCount - 1, <<0:(SectorSize * 8)>>), +``` + +When you are done, close the card with +[`esp:sdcard_close/1`](./apidocs/erlang/eavmlib/esp.md#sdcard_close-1): + +```erlang +ok = esp:sdcard_close(SDCard), +``` + +All `esp:sdcard_*` functions are synchronous: like `esp:mount/4`, they run on the scheduler +that executes the calling process and block it for the duration of the card transaction +(typically milliseconds). + +```{warning} +A given physical card should not be accessed through `esp:sdcard_open/2` and `esp:mount/4` at +the same time. Writing raw sectors bypasses any filesystem, so be careful not to corrupt a +filesystem you intend to keep using. A card opened with `"sdspi"` must be closed with +`esp:sdcard_close/1` before the SPI bus is closed with `spi:close/1`: closing the bus first +leaves the SPI host unusable until the device is restarted. +``` + ### Restart and Sleep You can use the [`esp:restart/0`](./apidocs/erlang/eavmlib/esp.md#restart0) function to immediately restart the ESP32 device. This function does not return a value. diff --git a/libs/avm_esp32/src/esp.erl b/libs/avm_esp32/src/esp.erl index fbc2468ec5..a393278d77 100644 --- a/libs/avm_esp32/src/esp.erl +++ b/libs/avm_esp32/src/esp.erl @@ -43,6 +43,11 @@ sleep_enable_timer_wakeup/1, mount/4, umount/1, + sdcard_open/2, + sdcard_read/2, + sdcard_write/3, + sdcard_info/1, + sdcard_close/1, nvs_fetch_binary/2, nvs_get_binary/1, nvs_get_binary/2, nvs_get_binary/3, nvs_set_binary/2, nvs_set_binary/3, @@ -148,6 +153,8 @@ | {cd, non_neg_integer()}. -type mount_options() :: [sdmmc_mount_option() | sdspi_mount_option()] | #{atom() => term()}. +-opaque sdcard() :: reference(). + -export_type( [ esp_reset_reason/0, @@ -160,6 +167,7 @@ esp_partition_props/0, mounted_fs/0, mount_options/0, + sdcard/0, task_wdt_config/0, task_wdt_user_handle/0 ] @@ -376,6 +384,86 @@ mount(_Source, _Target, _FS, _Opts) -> umount(_Target) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Source the device to open, either `"sdmmc"' or `"sdspi"' +%% @param Opts options for the device +%% @returns either a tuple having `ok' and the SD card resource, or an error tuple +%% @doc Open an SD card for raw block (sector) access, without mounting any +%% filesystem on it. The returned resource is used with +%% {@link sdcard_read/2}, {@link sdcard_write/3}, {@link sdcard_info/1} +%% and {@link sdcard_close/1}. The card is automatically closed when the +%% resource is garbage collected, but {@link sdcard_close/1} should be +%% preferred. +%% +%% The same options as {@link mount/4} are accepted. +%% +%% A given physical card should not be used through `sdcard_open/2' and +%% {@link mount/4} at the same time. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_open( + Source :: unicode:chardata(), + Opts :: mount_options() +) -> {ok, sdcard()} | {error, term()}. +sdcard_open(_Source, _Opts) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @param Sector the sector (block) number to read +%% @returns a tuple with `ok' and the sector data as a binary, or an error tuple +%% @doc Read a single sector from the SD card. The size of the returned +%% binary is the sector size reported by {@link sdcard_info/1} (usually +%% 512 bytes). +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_read(SDCard :: sdcard(), Sector :: non_neg_integer()) -> + {ok, binary()} | {error, term()}. +sdcard_read(_SDCard, _Sector) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @param Sector the sector (block) number to write +%% @param Data the sector data, its size must be exactly the sector size +%% @returns `ok' or an error tuple +%% @doc Write a single sector to the SD card. `Data' must be a binary whose +%% size is exactly the sector size reported by {@link sdcard_info/1}. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_write(SDCard :: sdcard(), Sector :: non_neg_integer(), Data :: binary()) -> + ok | {error, term()}. +sdcard_write(_SDCard, _Sector, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @returns a tuple with `ok' and a map describing the card, or an error tuple +%% @doc Return information about the SD card. The map contains the +%% `sector_size' (in bytes) and `sector_count' (the number of sectors) +%% keys. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_info(SDCard :: sdcard()) -> + {ok, #{sector_size := pos_integer(), sector_count := non_neg_integer()}} + | {error, term()}. +sdcard_info(_SDCard) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param SDCard the SD card resource returned by {@link sdcard_open/2} +%% @returns `ok' or an error tuple +%% @doc Close a previously opened SD card and release the underlying +%% peripheral. The resource must not be used after it has been closed. +%% +%% A card opened with `"sdspi"' must be closed before the SPI bus is +%% closed with `spi:close/1'. +%% @end +%%----------------------------------------------------------------------------- +-spec sdcard_close(SDCard :: sdcard()) -> ok | {error, term()}. +sdcard_close(_SDCard) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Namespace NVS namespace %% @param Key NVS key diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 40e943b415..46038f6a1e 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -1746,7 +1746,7 @@ static inline bool term_is_uint32(term t) #endif #if BOXED_TERMS_REQUIRED_FOR_INT != BOXED_TERMS_REQUIRED_FOR_INT64 - } else { + } else if (term_boxed_size(t) == BOXED_TERMS_REQUIRED_FOR_INT64) { avm_int64_t unboxed64 = term_unbox_int64(t); return unboxed64 <= UINT32_MAX; #endif diff --git a/src/platforms/esp32/components/avm_builtins/Kconfig b/src/platforms/esp32/components/avm_builtins/Kconfig index 41ad63aaba..0dffd36b6e 100644 --- a/src/platforms/esp32/components/avm_builtins/Kconfig +++ b/src/platforms/esp32/components/avm_builtins/Kconfig @@ -82,6 +82,10 @@ config AVM_ENABLE_STORAGE_NIFS bool "Enable Storage NIFs" default y +config AVM_ENABLE_RAW_SDCARD_NIFS + bool "Enable raw SD card block device NIFs" + default y + config AVM_ENABLE_GPIO_PORT_DRIVER bool "Enable GPIO port driver" default y diff --git a/src/platforms/esp32/components/avm_builtins/storage_nif.c b/src/platforms/esp32/components/avm_builtins/storage_nif.c index 2e57ec5a7a..d8fcdb5bd4 100644 --- a/src/platforms/esp32/components/avm_builtins/storage_nif.c +++ b/src/platforms/esp32/components/avm_builtins/storage_nif.c @@ -19,7 +19,7 @@ */ #include -#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS +#if defined(CONFIG_AVM_ENABLE_STORAGE_NIFS) || defined(CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS) #include #include @@ -38,7 +38,11 @@ #include #include #include +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS #include +#endif +#include +#include #include #include @@ -57,6 +61,7 @@ #define SMP_UNLOCK(mounted_fs) #endif +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS // TODO: allow ro option enum mount_type { @@ -86,6 +91,7 @@ const ErlNifResourceTypeInit mounted_fs_resource_type_init = { .members = 1, .dtor = mounted_fs_dtor }; +#endif static term make_esp_error_tuple(esp_err_t err, Context *ctx) { @@ -98,6 +104,7 @@ static term make_esp_error_tuple(esp_err_t err, Context *ctx) return result; } +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS static void opts_to_fatfs_mount_config(term opts_term, esp_vfs_fat_mount_config_t *mount_config) { mount_config->format_if_mount_failed = true; @@ -105,6 +112,7 @@ static void opts_to_fatfs_mount_config(term opts_term, esp_vfs_fat_mount_config_ mount_config->allocation_unit_size = 512; // TODO: make it configurable: disk_status_check_enable = false } +#endif #ifdef SDMMC_SLOT_CONFIG_DEFAULT #if defined(SOC_SDMMC_USE_GPIO_MATRIX) && SOC_SDMMC_USE_GPIO_MATRIX @@ -216,6 +224,101 @@ static bool storage_nif_configure_sdmmc_slot( } #endif +enum sdcard_interface +{ + SDCardSDMMC, + SDCardSDSPI +}; + +struct SDCardConfig +{ + enum sdcard_interface interface; + sdmmc_host_t host; + union + { +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + sdmmc_slot_config_t mmc_slot; +#endif + sdspi_device_config_t spi_dev; + } slot; +}; + +static bool sdcard_config_from_source( + const char *source, term opts_term, struct SDCardConfig *cfg, GlobalContext *glb) +{ +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + if (!strcmp(source, "sdmmc")) { + sdmmc_host_t host_config = SDMMC_HOST_DEFAULT(); + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + if (UNLIKELY(!storage_nif_configure_sdmmc_slot(opts_term, &slot_config, glb))) { + return false; + } + cfg->interface = SDCardSDMMC; + cfg->host = host_config; + cfg->slot.mmc_slot = slot_config; + return true; + } +#endif + +#ifdef CONFIG_AVM_ENABLE_SPI_PORT_DRIVER + if (!strcmp(source, "sdspi")) { + sdmmc_host_t host_config = SDSPI_HOST_DEFAULT(); + sdspi_device_config_t spi_slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); + + term spi_port = interop_kv_get_value(opts_term, ATOM_STR("\x8", "spi_host"), glb); + spi_host_device_t host_dev; + // spi_driver_get_peripheral already checks if spi_port is valid + if (!spi_driver_get_peripheral(spi_port, &host_dev, glb)) { + return false; + } + spi_slot_config.host_id = host_dev; + + term cs_term = interop_kv_get_value(opts_term, ATOM_STR("\x2", "cs"), glb); + if (UNLIKELY(!term_is_integer(cs_term))) { + return false; + } + spi_slot_config.gpio_cs = term_to_int(cs_term); + + term cd_term + = interop_kv_get_value_default(opts_term, ATOM_STR("\x2", "cd"), UNDEFINED_ATOM, glb); + if (cd_term != UNDEFINED_ATOM) { + if (UNLIKELY(!term_is_integer(cd_term))) { + return false; + } + spi_slot_config.gpio_cd = term_to_int(cd_term); + } + + cfg->interface = SDCardSDSPI; + cfg->host = host_config; + cfg->slot.spi_dev = spi_slot_config; + return true; + } +#endif + + return false; +} + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +struct SDCardBlockDevice +{ +#ifndef AVM_NO_SMP + Mutex *lock; +#endif + bool open; + uint32_t sector_size; + uint32_t sector_count; + sdmmc_card_t *card; +}; + +static void sdcard_dtor(ErlNifEnv *caller_env, void *obj); + +const ErlNifResourceTypeInit sdcard_resource_type_init = { + .members = 1, + .dtor = sdcard_dtor +}; +#endif + +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS static term nif_esp_mount(Context *ctx, int argc, term argv[]) { GlobalContext *glb = ctx->global; @@ -287,15 +390,11 @@ static term nif_esp_mount(Context *ctx, int argc, term argv[]) mount->base_path, source + part_by_name_len, &mount_config, &mount->handle.wl); #endif -// C3 doesn't support this -#ifdef SDMMC_SLOT_CONFIG_DEFAULT - } else if (!strcmp(source, "sdmmc")) { + } else if (!strcmp(source, "sdmmc") || !strcmp(source, "sdspi")) { mount_config.allocation_unit_size = 512; - sdmmc_host_t host_config = SDMMC_HOST_DEFAULT(); - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - - if (UNLIKELY(!storage_nif_configure_sdmmc_slot(opts_term, &slot_config, ctx->global))) { + struct SDCardConfig cfg; + if (!sdcard_config_from_source(source, opts_term, &cfg, ctx->global)) { free(source); free(target); RAISE_ERROR(BADARG_ATOM); @@ -310,61 +409,18 @@ static term nif_esp_mount(Context *ctx, int argc, term argv[]) SMP_LOCK_INIT(mount); mount->base_path = target; target = NULL; - mount->mount_type = FATSDMMC; - ret = esp_vfs_fat_sdmmc_mount( - mount->base_path, &host_config, &slot_config, &mount_config, &mount->handle.card); + if (cfg.interface == SDCardSDSPI) { + mount->mount_type = FATSDSPI; + ret = esp_vfs_fat_sdspi_mount( + mount->base_path, &cfg.host, &cfg.slot.spi_dev, &mount_config, &mount->handle.card); +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + } else { + mount->mount_type = FATSDMMC; + ret = esp_vfs_fat_sdmmc_mount(mount->base_path, &cfg.host, &cfg.slot.mmc_slot, + &mount_config, &mount->handle.card); #endif - - } else if (!strcmp(source, "sdspi")) { - mount_config.allocation_unit_size = 512; - - sdmmc_host_t host_config = SDSPI_HOST_DEFAULT(); - sdspi_device_config_t spi_slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - - term spi_port = interop_kv_get_value(opts_term, ATOM_STR("\x8", "spi_host"), ctx->global); - spi_host_device_t host_dev; - // spi_driver_get_peripheral already checks if spi_port is valid - bool ok = spi_driver_get_peripheral(spi_port, &host_dev, ctx->global); - if (!ok) { - free(source); - free(target); - RAISE_ERROR(BADARG_ATOM); } - spi_slot_config.host_id = host_dev; - - term cs_term = interop_kv_get_value(opts_term, ATOM_STR("\x2", "cs"), ctx->global); - if (UNLIKELY(!term_is_integer(cs_term))) { - free(source); - free(target); - RAISE_ERROR(BADARG_ATOM); - } - spi_slot_config.gpio_cs = term_to_int(cs_term); - - term cd_term = interop_kv_get_value_default( - opts_term, ATOM_STR("\x2", "cd"), UNDEFINED_ATOM, ctx->global); - if (cd_term != UNDEFINED_ATOM) { - if (UNLIKELY(!term_is_integer(cd_term))) { - free(source); - free(target); - RAISE_ERROR(BADARG_ATOM); - } - spi_slot_config.gpio_cd = term_to_int(cd_term); - } - - mount = enif_alloc_resource(platform->mounted_fs_resource_type, sizeof(struct MountedFS)); - if (IS_NULL_PTR(mount)) { - free(source); - free(target); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - SMP_LOCK_INIT(mount); - mount->base_path = target; - target = NULL; - mount->mount_type = FATSDSPI; - - ret = esp_vfs_fat_sdspi_mount( - mount->base_path, &host_config, &spi_slot_config, &mount_config, &mount->handle.card); } else { free(source); free(target); @@ -464,6 +520,389 @@ static void mounted_fs_dtor(ErlNifEnv *caller_env, void *obj) free(mounted_fs->base_path); } +#endif + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +// TODO: switch to the new ESP-IDF blockdev API once the API issues are fixed and it is +// proven working: https://github.com/espressif/esp-idf/issues/18875 +static esp_err_t sdcard_blockdev_read_sector( + struct SDCardBlockDevice *dev, void *dst, size_t sector) +{ + return sdmmc_read_sectors(dev->card, dst, sector, 1); +} + +static esp_err_t sdcard_blockdev_write_sector( + struct SDCardBlockDevice *dev, const void *src, size_t sector) +{ + return sdmmc_write_sectors(dev->card, src, sector, 1); +} + +// Mirrors call_host_deinit() in ESP-IDF's vfs_fat_sdmmc.c: per-device/per-slot +// teardown where the host provides it (IDF >= 5.3), forceful host deinit otherwise. +static esp_err_t sdcard_host_deinit(const sdmmc_host_t *host) +{ + if (host->flags & SDMMC_HOST_FLAG_DEINIT_ARG) { + return host->deinit_p(host->slot); + } + return host->deinit(); +} + +static esp_err_t sdcard_blockdev_close(struct SDCardBlockDevice *dev) +{ + esp_err_t ret = sdcard_host_deinit(&dev->card->host); + + free(dev->card); + dev->card = NULL; + + return ret; +} + +static esp_err_t sdcard_blockdev_init(struct SDCardConfig *cfg, struct SDCardBlockDevice *dev) +{ + esp_err_t err; + + dev->card = malloc(sizeof(sdmmc_card_t)); + if (IS_NULL_PTR(dev->card)) { + return ESP_ERR_NO_MEM; + } + + if (cfg->interface == SDCardSDSPI) { + err = sdspi_host_init(); + if (UNLIKELY(err != ESP_OK)) { + free(dev->card); + dev->card = NULL; + return err; + } + sdspi_dev_handle_t spi_handle; + err = sdspi_host_init_device(&cfg->slot.spi_dev, &spi_handle); + if (UNLIKELY(err != ESP_OK)) { + // No device was added and sdspi_host_init() is stateless: nothing to + // undo here, and sdspi_host_deinit() would destroy unrelated SDSPI + // slots, including any FAT-mounted card. + free(dev->card); + dev->card = NULL; + return err; + } + cfg->host.slot = spi_handle; + err = sdmmc_card_init(&cfg->host, dev->card); + if (UNLIKELY(err != ESP_OK)) { + sdcard_host_deinit(&cfg->host); + free(dev->card); + dev->card = NULL; + return err; + } +#ifdef SDMMC_SLOT_CONFIG_DEFAULT + } else { + err = (*cfg->host.init)(); + if (UNLIKELY(err != ESP_OK)) { + free(dev->card); + dev->card = NULL; + return err; + } + err = sdmmc_host_init_slot(cfg->host.slot, &cfg->slot.mmc_slot); + if (UNLIKELY(err != ESP_OK)) { + if (!(cfg->host.flags & SDMMC_HOST_FLAG_DEINIT_ARG)) { + // Pre-5.3 IDF: the exclusive sdmmc_host_init() succeeded, so this + // is the only host user and the host must be released. With + // per-slot refcounting (IDF >= 5.3) no slot reference was taken: + // a deinit here could tear down another slot's user, and the + // tolerant host init recovers on the next open. + cfg->host.deinit(); + } + free(dev->card); + dev->card = NULL; + return err; + } + err = sdmmc_card_init(&cfg->host, dev->card); + if (UNLIKELY(err != ESP_OK)) { + sdcard_host_deinit(&cfg->host); + free(dev->card); + dev->card = NULL; + return err; + } +#else + } else { + free(dev->card); + dev->card = NULL; + return ESP_ERR_NOT_SUPPORTED; +#endif + } + + dev->sector_size = dev->card->csd.sector_size; + dev->sector_count = dev->card->csd.capacity; + + return ESP_OK; +} + +static term nif_esp_sdcard_open(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + int str_ok; + char *source = interop_term_to_string(argv[0], &str_ok); + if (!str_ok) { + RAISE_ERROR(BADARG_ATOM); + } + + term opts_term = argv[1]; + if (!term_is_list(opts_term) && !term_is_map(opts_term)) { + free(source); + RAISE_ERROR(BADARG_ATOM); + } + + struct SDCardConfig cfg; + bool ok = sdcard_config_from_source(source, opts_term, &cfg, glb); + free(source); + if (!ok) { + RAISE_ERROR(BADARG_ATOM); + } + + struct SDCardBlockDevice *dev + = enif_alloc_resource(platform->sdcard_resource_type, sizeof(struct SDCardBlockDevice)); + if (IS_NULL_PTR(dev)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + dev->open = false; + dev->card = NULL; + dev->sector_size = 0; + dev->sector_count = 0; +#ifndef AVM_NO_SMP + dev->lock = smp_mutex_create(); + if (IS_NULL_PTR(dev->lock)) { + enif_release_resource(dev); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } +#endif + + esp_err_t err = sdcard_blockdev_init(&cfg, dev); + + term return_term; + if (UNLIKELY(err != ESP_OK)) { + return_term = make_esp_error_tuple(err, ctx); + } else { + dev->open = true; + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) + TERM_BOXED_RESOURCE_SIZE) + != MEMORY_GC_OK)) { + enif_release_resource(dev); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term dev_term = term_from_resource(dev, &ctx->heap); + return_term = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(return_term, 0, OK_ATOM); + term_put_tuple_element(return_term, 1, dev_term); + } + enif_release_resource(dev); + + return return_term; +} + +static term nif_esp_sdcard_read(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + if (UNLIKELY(!term_is_uint32(argv[1]))) { + RAISE_ERROR(BADARG_ATOM); + } + uint32_t sector = term_to_uint32(argv[1]); + + SMP_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(sector >= dev->sector_count)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + size_t sector_size = dev->sector_size; + + if (UNLIKELY(memory_ensure_free(ctx, term_binary_heap_size(sector_size) + TUPLE_SIZE(2)) + != MEMORY_GC_OK)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term binary = term_create_uninitialized_binary(sector_size, &ctx->heap, ctx->global); + // term_create_uninitialized_binary returns an invalid term if the refc binary + // allocation fails; memory_ensure_free only covers the boxed header words. + if (UNLIKELY(term_is_invalid_term(binary))) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + // sdmmc_read_sectors bounces non-DMA-capable / unaligned destinations (e.g. a + // binary in PSRAM) internally, so the binary is passed directly; it may fail + // with ESP_ERR_NO_MEM under DMA heap pressure. + esp_err_t err = sdcard_blockdev_read_sector( + dev, (void *) term_binary_data(binary), (size_t) sector); + SMP_MUTEX_UNLOCK(dev->lock); + + if (UNLIKELY(err != ESP_OK)) { + return make_esp_error_tuple(err, ctx); + } + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, binary); + + return result; +} + +static term nif_esp_sdcard_write(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + if (UNLIKELY(!term_is_uint32(argv[1]))) { + RAISE_ERROR(BADARG_ATOM); + } + uint32_t sector = term_to_uint32(argv[1]); + + VALIDATE_VALUE(argv[2], term_is_binary); + term data = argv[2]; + + SMP_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(term_binary_size(data) != dev->sector_size)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(sector >= dev->sector_count)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + + esp_err_t err = sdcard_blockdev_write_sector(dev, term_binary_data(data), (size_t) sector); + SMP_MUTEX_UNLOCK(dev->lock); + + if (UNLIKELY(err != ESP_OK)) { + return make_esp_error_tuple(err, ctx); + } + + return OK_ATOM; +} + +static term nif_esp_sdcard_info(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + SMP_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + uint32_t sector_size = dev->sector_size; + uint32_t sector_count = dev->sector_count; + SMP_MUTEX_UNLOCK(dev->lock); + + size_t needed = TUPLE_SIZE(2) + term_map_size_in_terms(2) + + term_boxed_integer_size(sector_size) + term_boxed_integer_size(sector_count); + if (UNLIKELY(memory_ensure_free(ctx, needed) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term sector_count_value = term_make_maybe_boxed_int64(sector_count, &ctx->heap); + term sector_size_value = term_make_maybe_boxed_int64(sector_size, &ctx->heap); + + term info = term_alloc_map(2, &ctx->heap); + term_set_map_assoc( + info, 0, globalcontext_make_atom(glb, ATOM_STR("\xC", "sector_count")), sector_count_value); + term_set_map_assoc( + info, 1, globalcontext_make_atom(glb, ATOM_STR("\xB", "sector_size")), sector_size_value); + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, info); + + return result; +} + +static term nif_esp_sdcard_close(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + GlobalContext *glb = ctx->global; + struct ESP32PlatformData *platform = glb->platform_data; + + void *dev_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], + platform->sdcard_resource_type, &dev_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) dev_obj_ptr; + + SMP_MUTEX_LOCK(dev->lock); + if (UNLIKELY(!dev->open)) { + SMP_MUTEX_UNLOCK(dev->lock); + RAISE_ERROR(BADARG_ATOM); + } + esp_err_t err = sdcard_blockdev_close(dev); + dev->open = false; + SMP_MUTEX_UNLOCK(dev->lock); + + if (UNLIKELY(err != ESP_OK)) { + return make_esp_error_tuple(err, ctx); + } + + return OK_ATOM; +} + +static void sdcard_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct SDCardBlockDevice *dev = (struct SDCardBlockDevice *) obj; + if (dev->open) { + esp_err_t err = sdcard_blockdev_close(dev); + dev->open = false; + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGW(TAG, + "Failed to close SD card block device in resource dtor. " + "Please use esp:sdcard_close/1."); + } + } + +#ifndef AVM_NO_SMP + if (dev->lock) { + smp_mutex_destroy(dev->lock); + dev->lock = NULL; + } +#endif +} +#endif void storage_nif_init(GlobalContext *global) { @@ -471,10 +910,17 @@ void storage_nif_init(GlobalContext *global) ErlNifEnv env; erl_nif_env_partial_init_from_globalcontext(&env, global); +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS platform->mounted_fs_resource_type = enif_init_resource_type( &env, "mounted_fs", &mounted_fs_resource_type_init, ERL_NIF_RT_CREATE, NULL); +#endif +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS + platform->sdcard_resource_type = enif_init_resource_type( + &env, "sdcard", &sdcard_resource_type_init, ERL_NIF_RT_CREATE, NULL); +#endif } +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS static const struct Nif esp_mount_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_esp_mount @@ -484,16 +930,69 @@ static const struct Nif esp_umount_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_esp_umount }; +#endif + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +static const struct Nif esp_sdcard_open_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_open +}; + +static const struct Nif esp_sdcard_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_read +}; + +static const struct Nif esp_sdcard_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_write +}; + +static const struct Nif esp_sdcard_info_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_info +}; + +static const struct Nif esp_sdcard_close_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_esp_sdcard_close +}; +#endif const struct Nif *storage_nif_get_nif(const char *nifname) { +#ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS if (strcmp("esp:mount/4", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &esp_mount_nif; - } else if (strcmp("esp:umount/1", nifname) == 0) { + } + if (strcmp("esp:umount/1", nifname) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &esp_umount_nif; } +#endif +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS + if (strcmp("esp:sdcard_open/2", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_open_nif; + } + if (strcmp("esp:sdcard_read/2", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_read_nif; + } + if (strcmp("esp:sdcard_write/3", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_write_nif; + } + if (strcmp("esp:sdcard_info/1", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_info_nif; + } + if (strcmp("esp:sdcard_close/1", nifname) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &esp_sdcard_close_nif; + } +#endif return NULL; } diff --git a/src/platforms/esp32/components/avm_sys/include/esp32_sys.h b/src/platforms/esp32/components/avm_sys/include/esp32_sys.h index e38368d3db..ccb3d99f7b 100644 --- a/src/platforms/esp32/components/avm_sys/include/esp32_sys.h +++ b/src/platforms/esp32/components/avm_sys/include/esp32_sys.h @@ -83,6 +83,9 @@ struct ESP32PlatformData #ifdef CONFIG_AVM_ENABLE_STORAGE_NIFS ErlNifResourceType *mounted_fs_resource_type; +#endif +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS + ErlNifResourceType *sdcard_resource_type; #endif ErlNifResourceType *partition_mmap_resource_type; }; diff --git a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt index a5b4106bf0..e99036fc56 100644 --- a/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt +++ b/src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt @@ -110,6 +110,7 @@ compile_erlang(test_md5) compile_erlang(test_crypto) compile_erlang(test_monotonic_time) compile_erlang(test_mount) +compile_erlang(test_sdcard) compile_erlang(test_net) compile_erlang(test_rtc_slow) compile_erlang(test_select) @@ -138,6 +139,7 @@ set(erlang_test_beams test_crypto.beam test_monotonic_time.beam test_mount.beam + test_sdcard.beam test_net.beam test_rtc_slow.beam test_select.beam diff --git a/src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl b/src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl new file mode 100644 index 0000000000..82d36947a7 --- /dev/null +++ b/src/platforms/esp32/test/main/test_erl_sources/test_sdcard.erl @@ -0,0 +1,105 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Davide Bettio +% +% 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. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_sdcard). +-export([start/0]). + +start() -> + {ok, SDCard} = esp:sdcard_open("sdmmc", []), + ok = test_info(SDCard), + ok = test_read(SDCard), + ok = test_write_roundtrip(SDCard), + ok = test_bad_args(SDCard), + ok = esp:sdcard_close(SDCard), + ok = test_use_after_close(SDCard), + ok = test_open_bad_opts(), + ok = test_reopen(), + ok. + +test_info(SDCard) -> + {ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), + true = is_integer(SectorSize) andalso SectorSize > 0, + true = is_integer(SectorCount) andalso SectorCount > 0, + ok. + +test_read(SDCard) -> + {ok, #{sector_size := SectorSize}} = esp:sdcard_info(SDCard), + {ok, Sector0} = esp:sdcard_read(SDCard, 0), + SectorSize = byte_size(Sector0), + ok. + +test_write_roundtrip(SDCard) -> + {ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), + LastSector = SectorCount - 1, + {ok, Original} = esp:sdcard_read(SDCard, LastSector), + Pattern = make_pattern(SectorSize), + try + ok = esp:sdcard_write(SDCard, LastSector, Pattern), + {ok, Pattern} = esp:sdcard_read(SDCard, LastSector) + after + restore_sector(SDCard, LastSector, Original) + end, + {ok, Original} = esp:sdcard_read(SDCard, LastSector), + ok. + +restore_sector(SDCard, Sector, Data) -> + try + esp:sdcard_write(SDCard, Sector, Data) + catch + _:_ -> ok + end. + +test_bad_args(SDCard) -> + {ok, #{sector_size := SectorSize, sector_count := SectorCount}} = esp:sdcard_info(SDCard), + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, 0, <<0>>) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, SectorCount) end), + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, SectorCount, make_pattern(SectorSize)) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, -1) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, 1 bsl 32) end), + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, 1 bsl 64) end), + ok = expect_badarg(fun() -> esp:sdcard_write(SDCard, 1 bsl 64, make_pattern(SectorSize)) end), + ok. + +test_use_after_close(SDCard) -> + ok = expect_badarg(fun() -> esp:sdcard_read(SDCard, 0) end), + ok. + +test_open_bad_opts() -> + %% sdspi without the required spi_host/cs options. + ok = expect_badarg(fun() -> esp:sdcard_open("sdspi", []) end), + ok. + +%% Close must fully release the host so that a new open works. +test_reopen() -> + {ok, SDCard} = esp:sdcard_open("sdmmc", []), + {ok, _Sector0} = esp:sdcard_read(SDCard, 0), + ok = esp:sdcard_close(SDCard), + ok. + +make_pattern(SectorSize) -> + list_to_binary([N rem 256 || N <- lists:seq(0, SectorSize - 1)]). + +expect_badarg(Fun) -> + try Fun() of + _ -> error + catch + error:badarg -> ok; + _:_ -> not_badarg + end. diff --git a/src/platforms/esp32/test/main/test_main.c b/src/platforms/esp32/test/main/test_main.c index 1fc46f33f0..af169a4258 100644 --- a/src/platforms/esp32/test/main/test_main.c +++ b/src/platforms/esp32/test/main/test_main.c @@ -391,6 +391,14 @@ TEST_CASE("test_mount", "[test_run]") term ret_value = avm_test_case("test_mount.beam"); TEST_ASSERT(ret_value == OK_ATOM); } + +#ifdef CONFIG_AVM_ENABLE_RAW_SDCARD_NIFS +TEST_CASE("test_sdcard", "[test_run]") +{ + term ret_value = avm_test_case("test_sdcard.beam"); + TEST_ASSERT(ret_value == OK_ATOM); +} +#endif #endif struct pipefs_global_ctx diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3c7f880c49..2ba0944bab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,7 @@ add_executable(test-heap test-heap.c) add_executable(test-jit_stream_flash test-jit_stream_flash.c ../src/libAtomVM/jit_stream_flash.c) add_executable(test-mailbox test-mailbox.c) add_executable(test-structs test-structs.c) +add_executable(test-term test-term.c) target_compile_features(test-erlang PUBLIC c_std_11) target_compile_features(test-enif PUBLIC c_std_11) @@ -34,6 +35,7 @@ target_compile_features(test-heap PUBLIC c_std_11) target_compile_features(test-jit_stream_flash PUBLIC c_std_11) target_compile_features(test-mailbox PUBLIC c_std_11) target_compile_features(test-structs PUBLIC c_std_11) +target_compile_features(test-term PUBLIC c_std_11) if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(test-erlang PUBLIC -Wall -pedantic -Wextra -ggdb) @@ -42,6 +44,7 @@ if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(test-jit_stream_flash PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-mailbox PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-structs PUBLIC -Wall -pedantic -Wextra -ggdb) + target_compile_options(test-term PUBLIC -Wall -pedantic -Wextra -ggdb) endif() if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") @@ -56,6 +59,7 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_link_libraries(test-jit_stream_flash PRIVATE ${LIBRT}) target_link_libraries(test-mailbox PRIVATE ${LIBRT}) target_link_libraries(test-structs PRIVATE ${LIBRT}) + target_link_libraries(test-term PRIVATE ${LIBRT}) else() # might also be in libc check_library_exists(c clock_gettime "" HAVE_CLOCK_GETTIME) @@ -70,6 +74,7 @@ if (MbedTLS_FOUND) target_link_libraries(test-jit_stream_flash PRIVATE MbedTLS::mbedtls) target_link_libraries(test-mailbox PRIVATE MbedTLS::mbedtls) target_link_libraries(test-structs PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-term PRIVATE MbedTLS::mbedtls) endif() set( @@ -87,6 +92,7 @@ if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR target_include_directories(test-jit_stream_flash PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-mailbox PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-structs PRIVATE ../src/platforms/generic_unix/lib) + target_include_directories(test-term PRIVATE ../src/platforms/generic_unix/lib) else() message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") endif() @@ -97,6 +103,7 @@ target_include_directories(test-heap PRIVATE ../src/libAtomVM) target_include_directories(test-jit_stream_flash PRIVATE ../src/libAtomVM ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(test-mailbox PRIVATE ../src/libAtomVM) target_include_directories(test-structs PRIVATE ../src/libAtomVM) +target_include_directories(test-term PRIVATE ../src/libAtomVM) target_link_libraries(test-erlang PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-enif PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-heap PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) @@ -108,6 +115,7 @@ endif() target_link_libraries(test-jit_stream_flash PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-mailbox PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-structs PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) +target_link_libraries(test-term PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) # Except for XCode, also compile beams if (NOT "${CMAKE_GENERATOR}" MATCHES "Xcode") @@ -133,12 +141,14 @@ if (COVERAGE) append_coverage_compiler_flags_to_target(test-jit_stream_flash) append_coverage_compiler_flags_to_target(test-mailbox) append_coverage_compiler_flags_to_target(test-structs) + append_coverage_compiler_flags_to_target(test-term) append_coverage_linker_flags_to_target(test-erlang) append_coverage_linker_flags_to_target(test-enif) append_coverage_linker_flags_to_target(test-heap) append_coverage_linker_flags_to_target(test-jit_stream_flash) append_coverage_linker_flags_to_target(test-mailbox) append_coverage_linker_flags_to_target(test-structs) + append_coverage_linker_flags_to_target(test-term) if (CMAKE_COMPILER_IS_GNUCC) setup_target_for_coverage_lcov(NAME coverage EXECUTABLE test-erlang DEPENDENCIES test-erlang) endif() diff --git a/tests/erlang_tests/test_crc32.erl b/tests/erlang_tests/test_crc32.erl index 4b973ea374..3841495f50 100644 --- a/tests/erlang_tests/test_crc32.erl +++ b/tests/erlang_tests/test_crc32.erl @@ -110,6 +110,15 @@ test_crc32_badarg() -> ok = expect_badarg(fun() -> erlang:crc32_combine(0, 0, ?MODULE:id(-1)) end), ok = expect_badarg(fun() -> erlang:crc32_combine(?MODULE:id(16#100000000), 0, 0) end), ok = expect_badarg(fun() -> erlang:crc32_combine(0, ?MODULE:id(16#100000000), 0) end), + + % big integers whose low 64 bits are small must not pass the uint32 check + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(1 bsl 64), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id((1 bsl 64) + 7), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(-(1 bsl 64)), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(1 bsl 130), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(?MODULE:id(1 bsl 64), 0, 0) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(0, ?MODULE:id((1 bsl 64) + 7), 0) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(0, 0, ?MODULE:id(1 bsl 64)) end), ok. test_crc32_boundary() -> diff --git a/tests/test-term.c b/tests/test-term.c new file mode 100644 index 0000000000..53d2021161 --- /dev/null +++ b/tests/test-term.c @@ -0,0 +1,272 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Davide Bettio + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include + +#include "context.h" +#include "globalcontext.h" +#include "intn.h" +#include "memory.h" +#include "term.h" +#include "utils.h" + +// Enough room for all terms created by a single test function, so that no GC +// runs between term creation and the checks +#define TEST_HEAP_SIZE 256 + +enum +{ + IsIntegerOn32Bit = 1 << 0, + IsIntegerOn64Bit = 1 << 1, + IsPositive = 1 << 2, + IsNegative = 1 << 3, + IsUint8 = 1 << 4, + IsInt32 = 1 << 5, + IsUint32 = 1 << 6, + IsInt64 = 1 << 7, + IsUint64 = 1 << 8, + IsBigint = 1 << 9 +}; + +static bool flags_is_immediate(unsigned int flags) +{ +#if TERM_BITS == 32 + return (flags & IsIntegerOn32Bit) != 0; +#elif TERM_BITS == 64 + return (flags & IsIntegerOn64Bit) != 0; +#else +#error "Unsupported TERM_BITS" +#endif +} + +static void assert_integer_predicates(term t, unsigned int flags) +{ + bool is_immediate = flags_is_immediate(flags); + bool is_positive = (flags & IsPositive) != 0; + bool is_negative = (flags & IsNegative) != 0; + + assert(term_is_any_integer(t)); + assert(term_is_number(t)); + assert(term_is_int(t) == is_immediate); + assert(term_is_boxed_integer(t) == !is_immediate); + + assert(term_is_non_neg_int(t) == (is_immediate && !is_negative)); + assert(term_is_pos_int(t) == (is_immediate && is_positive)); + assert(term_is_neg_int(t) == (is_immediate && is_negative)); + + assert(term_is_pos_boxed_integer(t) == (!is_immediate && !is_negative)); + assert(term_is_neg_boxed_integer(t) == (!is_immediate && is_negative)); + if (!is_immediate) { + assert(term_boxed_integer_sign(t) + == (is_negative ? TermNegativeInteger : TermPositiveInteger)); + } + + assert(term_is_any_non_neg_integer(t) == !is_negative); + assert(term_is_any_pos_integer(t) == is_positive); + assert(term_is_any_neg_integer(t) == is_negative); + + assert(term_is_bigint(t) == ((flags & IsBigint) != 0)); +} + +static term make_bigint_term( + Context *ctx, const intn_digit_t *digits, size_t len, term_integer_sign_t sign) +{ + size_t count = intn_count_digits(digits, len); + size_t intn_data_size; + size_t rounded_num_len; + term_bigint_size_requirements(count, &intn_data_size, &rounded_num_len); + + term t = term_create_uninitialized_bigint(intn_data_size, sign, &ctx->heap); + term_initialize_bigint(t, digits, count, rounded_num_len); + return t; +} + +static void test_int64_range_checks(void) +{ + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + enum MemoryGCResult res = memory_ensure_free(ctx, TEST_HEAP_SIZE); + assert(res == MEMORY_GC_OK); + + static const struct + { + int64_t value; + unsigned int flags; + } cases[] = { + { 0, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsUint8 | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + { 1, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { -1, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { 42, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { -42, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { 255, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { 256, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + // 2^27 - 1 and -2^27: immediate boundary on 32-bit builds + { 0x07FFFFFF, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + { 0x08000000, IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 | IsUint64 }, + { -0x08000000, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { -0x08000001, IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { INT32_MAX, IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 | IsUint64 }, + { INT32_MIN, IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { (int64_t) INT32_MAX + 1, IsIntegerOn64Bit | IsPositive | IsUint32 | IsInt64 | IsUint64 }, + { (int64_t) INT32_MIN - 1, IsIntegerOn64Bit | IsNegative | IsInt64 }, + { UINT32_MAX, IsIntegerOn64Bit | IsPositive | IsUint32 | IsInt64 | IsUint64 }, + { (int64_t) UINT32_MAX + 1, IsIntegerOn64Bit | IsPositive | IsInt64 | IsUint64 }, + // 2^59 - 1 and -2^59: immediate boundary on 64-bit builds + { 0x07FFFFFFFFFFFFFF, IsIntegerOn64Bit | IsPositive | IsInt64 | IsUint64 }, + { 0x0800000000000000, IsPositive | IsInt64 | IsUint64 }, + { -0x0800000000000000, IsIntegerOn64Bit | IsNegative | IsInt64 }, + { -0x0800000000000001, IsNegative | IsInt64 }, + { INT64_MAX, IsPositive | IsInt64 | IsUint64 }, + { INT64_MIN, IsNegative | IsInt64 }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int64_t value = cases[i].value; + unsigned int flags = cases[i].flags; + term t = term_make_maybe_boxed_int64(value, &ctx->heap); + + assert_integer_predicates(t, flags); + + if (flags_is_immediate(flags)) { + assert(term_is_integer(t) == true); + assert(term_to_int(t) == (avm_int_t) value); + } else { + assert(term_is_integer(t) == false); + } + if (flags & IsUint8) { + assert(term_is_uint8(t) == true); + assert(term_to_uint8(t) == (uint8_t) value); + } else { + assert(term_is_uint8(t) == false); + } + if (flags & IsInt32) { + assert(term_is_int32(t) == true); + assert(term_to_int32(t) == (int32_t) value); + } else { + assert(term_is_int32(t) == false); + } + if (flags & IsUint32) { + assert(term_is_uint32(t) == true); + assert(term_to_uint32(t) == (uint32_t) value); + } else { + assert(term_is_uint32(t) == false); + } + if (flags & IsInt64) { + assert(term_is_int64(t) == true); + assert(term_to_int64(t) == value); + } else { + assert(term_is_int64(t) == false); + } + if (flags & IsUint64) { + assert(term_is_uint64(t) == true); + assert(term_to_uint64(t) == (uint64_t) value); + } else { + assert(term_is_uint64(t) == false); + } + } + + context_destroy(ctx); + globalcontext_destroy(glb); +} + +static void test_bigint_range_checks(void) +{ + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + enum MemoryGCResult res = memory_ensure_free(ctx, TEST_HEAP_SIZE); + assert(res == MEMORY_GC_OK); + + static const struct + { + intn_digit_t digits[INTN_MAX_IN_LEN]; + size_t len; + unsigned int flags; + uint64_t uint64_value; + } cases[] = { + // 2^63 + { { 0x00000000, 0x80000000 }, 2, IsPositive | IsBigint | IsUint64, UINT64_C(1) << 63 }, + // 2^64 - 1 + { { 0xFFFFFFFF, 0xFFFFFFFF }, 2, IsPositive | IsBigint | IsUint64, UINT64_MAX }, + // 2^64: low 64 bits are 0 + { { 0x00000000, 0x00000000, 0x00000001 }, 3, IsPositive | IsBigint, 0 }, + // 2^64 + 7: low 64 bits are 7 + { { 0x00000007, 0x00000000, 0x00000001 }, 3, IsPositive | IsBigint, 0 }, + // 2^65 + 2^40 + { { 0x00000000, 0x00000100, 0x00000002 }, 3, IsPositive | IsBigint, 0 }, + // -(2^63 + 1) + { { 0x00000001, 0x80000000 }, 2, IsNegative | IsBigint, 0 }, + // -(2^64) + { { 0x00000000, 0x00000000, 0x00000001 }, 3, IsNegative | IsBigint, 0 }, + // 2^256 - 1: largest supported magnitude + { { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, + 0xFFFFFFFF }, + 8, IsPositive | IsBigint, 0 }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + unsigned int flags = cases[i].flags; + term_integer_sign_t sign = (flags & IsNegative) ? TermNegativeInteger : TermPositiveInteger; + term t = make_bigint_term(ctx, cases[i].digits, cases[i].len, sign); + + assert_integer_predicates(t, flags); + + assert(term_is_uint8(t) == false); + assert(term_is_int32(t) == false); + assert(term_is_uint32(t) == false); + assert(term_is_int64(t) == false); + if (flags & IsUint64) { + assert(term_is_uint64(t) == true); + assert(term_to_uint64(t) == cases[i].uint64_value); + } else { + assert(term_is_uint64(t) == false); + } + } + + context_destroy(ctx); + globalcontext_destroy(glb); +} + +int main(int argc, char **argv) +{ + UNUSED(argc); + UNUSED(argv); + + test_int64_range_checks(); + test_bigint_range_checks(); + + return EXIT_SUCCESS; +}