From 8218fe32a86755c969b8efbac1c42eb48163625c Mon Sep 17 00:00:00 2001 From: niclo Date: Mon, 23 Feb 2026 14:00:01 +0100 Subject: [PATCH 1/4] build: update devenv --- devenv.lock | 48 +++++++++++++++++++++++++---------- devenv.nix | 73 +++++++++++++++++++---------------------------------- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/devenv.lock b/devenv.lock index 2dfd59b..3dda13a 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1760394278, + "lastModified": 1771672827, "owner": "cachix", "repo": "devenv", - "rev": "d786b5f55ad14a7078057779e7bf016e38311e66", + "rev": "6757a742f6393c7070f159eed9a59cbe20690aa3", "type": "github" }, "original": { @@ -19,14 +19,14 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1747046372, - "owner": "edolstra", + "lastModified": 1767039857, + "owner": "NixOS", "repo": "flake-compat", - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "type": "github" } @@ -40,10 +40,10 @@ ] }, "locked": { - "lastModified": 1760392170, + "lastModified": 1770726378, "owner": "cachix", "repo": "git-hooks.nix", - "rev": "46d55f0aeb1d567a78223e69729734f3dca25a85", + "rev": "5eaaedde414f6eb1aea8b8525c466dc37bba95ae", "type": "github" }, "original": { @@ -60,10 +60,10 @@ ] }, "locked": { - "lastModified": 1709087332, + "lastModified": 1762808025, "owner": "hercules-ci", "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", "type": "github" }, "original": { @@ -73,11 +73,14 @@ } }, "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, "locked": { - "lastModified": 1758532697, + "lastModified": 1770434727, "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f", + "rev": "8430f16a39c27bdeef236f1eeb56f0b51b33d348", "type": "github" }, "original": { @@ -87,6 +90,23 @@ "type": "github" } }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1769922788, + "narHash": "sha256-H3AfG4ObMDTkTJYkd8cz1/RbY9LatN5Mk4UF48VuSXc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "207d15f1a6603226e1e223dc79ac29c7846da32e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "devenv": "devenv", @@ -105,10 +125,10 @@ ] }, "locked": { - "lastModified": 1760409263, + "lastModified": 1771642886, "owner": "oxalica", "repo": "rust-overlay", - "rev": "5694018463c2134e2369996b38deed41b1b9afc1", + "rev": "85078369717bdbe1f266c9eaad5e66956fb6feea", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index 972d4a1..55b16f0 100644 --- a/devenv.nix +++ b/devenv.nix @@ -1,4 +1,7 @@ -{ pkgs, lib, config, inputs, ... }: +{ + pkgs, + ... +}: { # https://devenv.sh/basics/ @@ -11,55 +14,31 @@ ]; # https://devenv.sh/packages/ - packages = [ - pkgs.esp-idf-nvs-partition-gen - pkgs.git - pkgs.just - pkgs.actionlint - ]; + packages = with pkgs; [ + actionlint + cargo-edit + esp-idf-nvs-partition-gen + git + just + nixfmt + ]; languages.rust = { enable = true; channel = "stable"; - version = "1.90.0"; - components = [ "rustc" "rust-src" "cargo" "clippy" "rustfmt" "rust-analyzer" ]; - targets = [ "x86_64-unknown-linux-gnu" "riscv32imac-unknown-none-elf" "riscv32imc-unknown-none-elf" ]; + version = "1.93.1"; + components = [ + "rustc" + "rust-src" + "cargo" + "clippy" + "rustfmt" + "rust-analyzer" + ]; + targets = [ + "x86_64-unknown-linux-gnu" + "riscv32imac-unknown-none-elf" + "riscv32imc-unknown-none-elf" + ]; }; - - # https://devenv.sh/processes/ - # processes.cargo-watch.exec = "cargo-watch"; - - # https://devenv.sh/services/ - # services.postgres.enable = true; - - scripts.nvs_partition_gen.exec = '' - python3 -m esp_idf_nvs_partition_gen "$@" - ''; - - # https://devenv.sh/scripts/ -# scripts.hello.exec = '' -# echo hello from $GREET -# ''; -# -# enterShell = '' -# hello -# git --version -# ''; - - # https://devenv.sh/tasks/ - # tasks = { - # "myproj:setup".exec = "mytool build"; - # "devenv:enterShell".after = [ "myproj:setup" ]; - # }; - - # https://devenv.sh/tests/ - enterTest = '' - echo "Running tests" - git --version | grep --color=auto "${pkgs.git.version}" - ''; - - # https://devenv.sh/git-hooks/ - # git-hooks.hooks.shellcheck.enable = true; - - # See full reference at https://devenv.sh/reference/options/ } From 404c2617ed42efbfe0c7987ffbbc5046f6682a1b Mon Sep 17 00:00:00 2001 From: niclo Date: Mon, 23 Feb 2026 14:00:17 +0100 Subject: [PATCH 2/4] refactor: introduce workspace and rustfmt configuration --- .gitignore | 1 - Cargo.lock | 1358 +++++++++++++++++ Cargo.toml | 36 +- esp-nvs/Cargo.toml | 33 + esp-nvs/justfile | 21 + {src => esp-nvs/src}/error.rs | 14 +- {src => esp-nvs/src}/get.rs | 11 +- {src => esp-nvs/src}/internal.rs | 107 +- {src => esp-nvs/src}/lib.rs | 41 +- {src => esp-nvs/src}/platform.rs | 3 +- {src => esp-nvs/src}/raw.rs | 43 +- {src => esp-nvs/src}/set.rs | 6 +- {src => esp-nvs/src}/u24.rs | 5 +- .../tests}/assets/multi_page_blob.bin | Bin .../tests}/assets/test_nvs_data.bin | Bin .../tests}/assets/test_nvs_data.csv | 0 {tests => esp-nvs/tests}/benchmark.rs | 15 +- {tests => esp-nvs/tests}/common.rs | 9 +- {tests => esp-nvs/tests}/read.rs | 15 +- {tests => esp-nvs/tests}/write.rs | 64 +- justfile | 46 +- rustfmt.toml | 17 + 22 files changed, 1695 insertions(+), 150 deletions(-) create mode 100644 Cargo.lock create mode 100644 esp-nvs/Cargo.toml create mode 100644 esp-nvs/justfile rename {src => esp-nvs/src}/error.rs (90%) rename {src => esp-nvs/src}/get.rs (98%) rename {src => esp-nvs/src}/internal.rs (98%) rename {src => esp-nvs/src}/lib.rs (96%) rename {src => esp-nvs/src}/platform.rs (99%) rename {src => esp-nvs/src}/raw.rs (95%) rename {src => esp-nvs/src}/set.rs (98%) rename {src => esp-nvs/src}/u24.rs (92%) rename {tests => esp-nvs/tests}/assets/multi_page_blob.bin (100%) rename {tests => esp-nvs/tests}/assets/test_nvs_data.bin (100%) rename {tests => esp-nvs/tests}/assets/test_nvs_data.csv (100%) rename {tests => esp-nvs/tests}/benchmark.rs (90%) rename {tests => esp-nvs/tests}/common.rs (97%) rename {tests => esp-nvs/tests}/read.rs (97%) rename {tests => esp-nvs/tests}/write.rs (98%) create mode 100644 rustfmt.toml diff --git a/.gitignore b/.gitignore index f66e3aa..e199006 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /target -/Cargo.lock .idea/ /esp-nvs/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dfd03e8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1358 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitfield" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "embassy-embedded-hal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554e3e840696f54b4c9afcf28a0f24da431c927f4151040020416e7393d6d0d8" +dependencies = [ + "embassy-futures", + "embassy-hal-internal", + "embassy-sync 0.7.2", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", +] + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" + +[[package]] +name = "embassy-hal-internal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95285007a91b619dc9f26ea8f55452aa6c60f7115a4edc05085cd2bd3127cd7a" +dependencies = [ + "num-traits", +] + +[[package]] +name = "embassy-sync" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2c8cdff05a7a51ba0087489ea44b0b1d97a296ca6b1d6d1a33ea7423d34049" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async 0.6.1", + "futures-sink", + "futures-util", + "heapless", +] + +[[package]] +name = "embassy-sync" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async 0.6.1", + "futures-core", + "futures-sink", + "heapless", +] + +[[package]] +name = "embassy-usb-driver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a" +dependencies = [ + "embedded-io-async 0.6.1", +] + +[[package]] +name = "embassy-usb-synopsys-otg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288751f8eaa44a5cf2613f13cee0ca8e06e6638cb96e897e6834702c79084b23" +dependencies = [ + "critical-section", + "embassy-sync 0.7.2", + "embassy-usb-driver", +] + +[[package]] +name = "embedded-can" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io 0.6.1", +] + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "embedded-io 0.7.1", +] + +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + +[[package]] +name = "embedded-storage-async" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" +dependencies = [ + "embedded-storage", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "esp-config" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102871054f8dd98202177b9890cb4b71d0c6fe1f1413b7a379a8e0841fc2473c" +dependencies = [ + "document-features", + "esp-metadata-generated", + "serde", + "serde_yaml", + "somni-expr", +] + +[[package]] +name = "esp-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54786287c0a61ca0f78cb0c338a39427551d1be229103b4444591796c579e093" +dependencies = [ + "bitfield", + "bitflags 2.11.0", + "bytemuck", + "cfg-if", + "critical-section", + "delegate", + "digest", + "document-features", + "embassy-embedded-hal", + "embassy-futures", + "embassy-sync 0.7.2", + "embassy-usb-driver", + "embassy-usb-synopsys-otg", + "embedded-can", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-io 0.6.1", + "embedded-io 0.7.1", + "embedded-io-async 0.6.1", + "embedded-io-async 0.7.0", + "enumset", + "esp-config", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-riscv-rt", + "esp-rom-sys", + "esp-sync", + "esp-synopsys-usb-otg", + "esp32", + "esp32c2", + "esp32c3", + "esp32c6", + "esp32h2", + "esp32s2", + "esp32s3", + "fugit", + "instability", + "nb 1.1.0", + "paste", + "portable-atomic", + "rand_core 0.6.4", + "rand_core 0.9.5", + "riscv", + "sha1", + "sha2", + "strum", + "ufmt-write", + "xtensa-lx", + "xtensa-lx-rt", +] + +[[package]] +name = "esp-hal-procmacros" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e025a7a7a0affdb4ff913b5c4494aef96ee03d085bf83c27453ae3a71d50da6" +dependencies = [ + "document-features", + "object", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "termcolor", +] + +[[package]] +name = "esp-metadata-generated" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a93e39c8ad8d390d248dc7b9f4b59a873f313bf535218b8e2351356972399e3" + +[[package]] +name = "esp-nvs" +version = "0.3.0" +dependencies = [ + "defmt", + "embedded-storage", + "esp-hal", + "esp-storage", + "libz-sys", + "pretty_assertions", + "strum", + "thiserror", +] + +[[package]] +name = "esp-riscv-rt" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502744a5b1e7268d27fd2a4e56ad45efe42ead517d6c517a6961540de949b0ee" +dependencies = [ + "document-features", + "riscv", + "riscv-rt", +] + +[[package]] +name = "esp-rom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd66cccc6dd2d13e9f33668a57717ab14a6d217180ec112e6be533de93e7ecbf" +dependencies = [ + "cfg-if", + "document-features", + "esp-metadata-generated", +] + +[[package]] +name = "esp-storage" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1495fc1f5549bdd840b52d9ceb201746200e1620d2636f46958c11e765623b80" +dependencies = [ + "document-features", + "embedded-storage", + "esp-hal", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-rom-sys", + "esp-sync", +] + +[[package]] +name = "esp-sync" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44974639b4e88914f83fe60d2832c00276657d7d857628fdfc966cc7302e8a8" +dependencies = [ + "cfg-if", + "document-features", + "embassy-sync 0.6.2", + "embassy-sync 0.7.2", + "esp-metadata-generated", + "riscv", + "xtensa-lx", +] + +[[package]] +name = "esp-synopsys-usb-otg" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8938451cb19032f13365328ea66ab38c8d16deecdf322067442297110eb74468" +dependencies = [ + "critical-section", + "embedded-hal 0.2.7", + "ral-registers", + "usb-device", + "vcell", +] + +[[package]] +name = "esp32" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b76170a463d18f888a1ad258031901036fd827a9ef126733053ba5f8739fb0c8" +dependencies = [ + "critical-section", + "vcell", +] + +[[package]] +name = "esp32c2" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e62cf8932966b8d445b6f1832977b468178f0a84effb2e9fda89f60c24d45aa3" +dependencies = [ + "critical-section", + "vcell", +] + +[[package]] +name = "esp32c3" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356af3771d0d6536c735bf71136594f4d1cbb506abf6e0c51a6639e9bf4e7988" +dependencies = [ + "critical-section", + "vcell", +] + +[[package]] +name = "esp32c6" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5e511df672d79cd63365c92045135e01ba952b6bddd25b660baff5e1110f6b" +dependencies = [ + "critical-section", + "vcell", +] + +[[package]] +name = "esp32h2" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed4a50bbd1380931e095e0973b9b12f782a9c481f2edf1f7c42e7eb4ff736d6d" +dependencies = [ + "critical-section", + "vcell", +] + +[[package]] +name = "esp32s2" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98574d4c577fbe888fe3e6df7fc80d25a05624d9998f7d7de1500ae21fcca78f" +dependencies = [ + "critical-section", + "vcell", +] + +[[package]] +name = "esp32s3" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1810d8ee4845ef87542af981e38eb80ab531d0ef1061e1486014ab7af74c337a" +dependencies = [ + "critical-section", + "vcell", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fugit" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e639847d312d9a82d2e75b0edcc1e934efcc64e6cb7aa94f0b1fbec0bc231d6" +dependencies = [ + "gcd", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling 0.23.0", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ral-registers" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46b71a9d9206e8b46714c74255adcaea8b11e0350c1d8456165073c3f75fc81a" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", + "paste", + "riscv-macros", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "riscv-rt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d07b9f3a0eff773fc4df11f44ada4fa302e529bff4b7fe7e6a4b98a65ce9174" +dependencies = [ + "riscv", + "riscv-pac", + "riscv-rt-macros", + "riscv-target-parser", +] + +[[package]] +name = "riscv-rt-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def519ddeeb5e43c2b4fc3952c27b3a86782fc05192f322b2309125cd85b1fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "riscv-target-parser" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1376b15f3ff160e9b1e8ea564ce427f2f6fcf77528cc0a8bf405cb476f9cea7" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "somni-expr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed9b7648d5e8b2df6c5e49940c54bcdd2b4dd71eafc6e8f1c714eb4581b0f53" +dependencies = [ + "somni-parser", +] + +[[package]] +name = "somni-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f368519fc6c85fc1afdb769fb5a51123f6158013e143656e25a3485a0d401c" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ufmt-write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "usb-device" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" +dependencies = [ + "heapless", + "portable-atomic", +] + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "xtensa-lx" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e012d667b0aa6d2592ace8ef145a98bff3e76cca7a644f4181ecd7a916ed289b" +dependencies = [ + "critical-section", +] + +[[package]] +name = "xtensa-lx-rt" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8709f037fb123fe7ff146d2bce86f9dc0dfc53045c016bfd9d703317b6502845" +dependencies = [ + "document-features", + "xtensa-lx", + "xtensa-lx-rt-proc-macros", +] + +[[package]] +name = "xtensa-lx-rt-proc-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fb42cd29c42f8744c74276e9f5bee7b06685bbe5b88df891516d72cb320450" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index 662bed2..08ad902 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,6 @@ -[package] -name = "esp-nvs" -version = "0.3.0" -edition = "2024" -authors = ["Lars Hemala"] -repository = "https://github.com/lhemala/esp-nvs/" -license = "MIT OR Apache-2.0" -description = "ESP-IDF compatible, bare metal, non-volatile storage (NVS) library" -keywords = ["esp", "embedded", "nvs", "storage", "no-std"] -categories = ["embedded", "no-std", "hardware-support"] - -[features] -debug-logs = [] -defmt = ["dep:defmt"] -esp32 = ["dep:esp-storage", "esp-storage/esp32", "dep:esp-hal", "esp-hal/esp32"] -esp32s2 = ["dep:esp-storage", "esp-storage/esp32s2", "dep:esp-hal", "esp-hal/esp32s2"] -esp32s3 = ["dep:esp-storage", "esp-storage/esp32s3", "dep:esp-hal", "esp-hal/esp32s3"] -esp32c2 = ["dep:esp-storage", "esp-storage/esp32c2", "dep:esp-hal", "esp-hal/esp32c2"] -esp32c3 = ["dep:esp-storage", "esp-storage/esp32c3", "dep:esp-hal", "esp-hal/esp32c3"] -esp32c6 = ["dep:esp-storage", "esp-storage/esp32c6", "dep:esp-hal", "esp-hal/esp32c6"] -esp32h2 = ["dep:esp-storage", "esp-storage/esp32h2", "dep:esp-hal", "esp-hal/esp32h2"] - -[dependencies] -strum = { version = "0.27.1", default-features = false, features = ["derive"] } -embedded-storage = "0.3.1" -defmt = { version = "1.0.1", optional = true } -esp-storage = { version = "0.8.1", optional = true } -esp-hal = { version = "1.0.0", optional = true } -thiserror = { version = "2.0.0", default-features = false } - -[dev-dependencies] -libz-sys = "1.1.22" -pretty_assertions = "1.4.1" +[workspace] +members = ["esp-nvs"] +resolver = "2" [profile.dev.package.esp-storage] opt-level = 3 diff --git a/esp-nvs/Cargo.toml b/esp-nvs/Cargo.toml new file mode 100644 index 0000000..994f0a5 --- /dev/null +++ b/esp-nvs/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "esp-nvs" +version = "0.3.0" +edition = "2024" +authors = ["Lars Hemala"] +repository = "https://github.com/lhemala/esp-nvs/" +license = "MIT OR Apache-2.0" +description = "ESP-IDF compatible, bare metal, non-volatile storage (NVS) library" +keywords = ["esp", "embedded", "nvs", "storage", "no-std"] +categories = ["embedded", "no-std", "hardware-support"] + +[features] +debug-logs = [] +defmt = ["dep:defmt"] +esp32 = ["dep:esp-storage", "esp-storage/esp32", "dep:esp-hal", "esp-hal/esp32"] +esp32s2 = ["dep:esp-storage", "esp-storage/esp32s2", "dep:esp-hal", "esp-hal/esp32s2"] +esp32s3 = ["dep:esp-storage", "esp-storage/esp32s3", "dep:esp-hal", "esp-hal/esp32s3"] +esp32c2 = ["dep:esp-storage", "esp-storage/esp32c2", "dep:esp-hal", "esp-hal/esp32c2"] +esp32c3 = ["dep:esp-storage", "esp-storage/esp32c3", "dep:esp-hal", "esp-hal/esp32c3"] +esp32c6 = ["dep:esp-storage", "esp-storage/esp32c6", "dep:esp-hal", "esp-hal/esp32c6"] +esp32h2 = ["dep:esp-storage", "esp-storage/esp32h2", "dep:esp-hal", "esp-hal/esp32h2"] + +[dependencies] +strum = { version = "0.27.1", default-features = false, features = ["derive"] } +embedded-storage = "0.3.1" +defmt = { version = "1.0.1", optional = true } +esp-storage = { version = "0.8.1", optional = true } +esp-hal = { version = "1.0.0", optional = true } +thiserror = { version = "2.0.0", default-features = false } + +[dev-dependencies] +libz-sys = "1.1.22" +pretty_assertions = "1.4.1" diff --git a/esp-nvs/justfile b/esp-nvs/justfile new file mode 100644 index 0000000..9a2ddf4 --- /dev/null +++ b/esp-nvs/justfile @@ -0,0 +1,21 @@ +_default: + @just --list + +fix: + cargo clippy --fix --allow-dirty --allow-staged --release -p esp-nvs --features=defmt + +lint: + cargo clippy --release -p esp-nvs --features=defmt -- -D warnings + +update-changelog: + git-cliff --bump --include-path "esp-nvs/**" -o CHANGELOG.md + +publish-dry-run: + cargo publish --registry crates-io --dry-run + +publish: + cargo publish --registry crates-io + +[working-directory: 'tests/assets/'] +generate_test_nvs_bin: + esp-nvs-partition-tool generate test_nvs_data.csv test_nvs_data.bin --size 0x4000 diff --git a/src/error.rs b/esp-nvs/src/error.rs similarity index 90% rename from src/error.rs rename to esp-nvs/src/error.rs index 59d9c83..d6ea5c2 100644 --- a/src/error.rs +++ b/esp-nvs/src/error.rs @@ -1,11 +1,11 @@ -use crate::raw; +pub use raw::ItemType; use thiserror::Error; -pub use raw::ItemType; +use crate::raw; /// Errors that can occur during NVS operations. The list is likely to stay as is but marked as -/// non-exhaustive to allow for future additions without breaking the API. A caller would likely only -/// need to handle NamespaceNotFound and KeyNotFound as the other errors are static. +/// non-exhaustive to allow for future additions without breaking the API. A caller would likely +/// only need to handle NamespaceNotFound and KeyNotFound as the other errors are static. #[derive(Error, Debug, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] @@ -35,7 +35,8 @@ pub enum Error { #[error("namespace malformed")] NamespaceMalformed, - /// Strings are limited to `MAX_BLOB_DATA_PER_PAGE` while blobs can be up to `MAX_BLOB_SIZE` bytes + /// Strings are limited to `MAX_BLOB_DATA_PER_PAGE` while blobs can be up to `MAX_BLOB_SIZE` + /// bytes #[error("value too long")] ValueTooLong, @@ -47,7 +48,8 @@ pub enum Error { #[error("key too long")] KeyTooLong, - /// Key not found. Either the flash was corrupted and silently fixed on or no value has been written yet. + /// Key not found. Either the flash was corrupted and silently fixed on or no value has been + /// written yet. #[error("key not found")] KeyNotFound, diff --git a/src/get.rs b/esp-nvs/src/get.rs similarity index 98% rename from src/get.rs rename to esp-nvs/src/get.rs index 865e7e9..57c8396 100644 --- a/src/get.rs +++ b/esp-nvs/src/get.rs @@ -1,12 +1,17 @@ //! The `Get` trait and its implementation in this module allows providing a single generic, //! overloaded function `get()` for all supported types of the driver. -use crate::error::Error; -use crate::platform::Platform; -use crate::{Key, Nvs, raw}; use alloc::string::String; use alloc::vec::Vec; +use crate::error::Error; +use crate::platform::Platform; +use crate::{ + Key, + Nvs, + raw, +}; + pub trait Get { fn get(&mut self, namespace: &Key, key: &Key) -> Result; } diff --git a/src/internal.rs b/esp-nvs/src/internal.rs similarity index 98% rename from src/internal.rs rename to esp-nvs/src/internal.rs index a901e18..3ec25b9 100644 --- a/src/internal.rs +++ b/esp-nvs/src/internal.rs @@ -1,35 +1,71 @@ -use crate::Key; -use crate::error::Error; -#[cfg(feature = "debug-logs")] -use crate::raw::slice_with_nullbytes_to_str; -use crate::raw::{ - ENTRIES_PER_PAGE, ENTRY_STATE_BITMAP_SIZE, EntryMapState, FLASH_SECTOR_SIZE, Item, ItemData, - ItemDataBlobIndex, ItemType, MAX_BLOB_DATA_PER_PAGE, MAX_BLOB_SIZE, PageHeader, PageHeaderRaw, - PageState, RawItem, RawPage, write_aligned, +use alloc::collections::BTreeMap; +use alloc::string::{ + String, + ToString, }; -use crate::u24::u24; -use crate::{Nvs, raw}; -use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; -use core::cmp; use core::cmp::Ordering; #[cfg(feature = "debug-logs")] -use core::fmt::{Debug, Formatter}; -use core::mem::size_of; -use core::ops::Range; +use core::fmt::{ + Debug, + Formatter, +}; +use core::mem::{ + offset_of, + size_of, +}; +use core::ops::{ + Not, + Range, +}; +use core::{ + cmp, + mem, +}; -use crate::error::Error::{ItemTypeMismatch, KeyNotFound, PageFull}; -use crate::platform::{AlignedOps, Platform}; -use alloc::collections::BTreeMap; -use core::mem; -use core::mem::offset_of; -use core::ops::Not; #[cfg(feature = "defmt")] use defmt::trace; #[cfg(feature = "defmt")] use defmt::warn; +use crate::error::Error; +use crate::error::Error::{ + ItemTypeMismatch, + KeyNotFound, + PageFull, +}; +use crate::platform::{ + AlignedOps, + Platform, +}; +#[cfg(feature = "debug-logs")] +use crate::raw::slice_with_nullbytes_to_str; +use crate::raw::{ + ENTRIES_PER_PAGE, + ENTRY_STATE_BITMAP_SIZE, + EntryMapState, + FLASH_SECTOR_SIZE, + Item, + ItemData, + ItemDataBlobIndex, + ItemType, + MAX_BLOB_DATA_PER_PAGE, + MAX_BLOB_SIZE, + PageHeader, + PageHeaderRaw, + PageState, + RawItem, + RawPage, + write_aligned, +}; +use crate::u24::u24; +use crate::{ + Key, + Nvs, + raw, +}; + /// Maximum Key length is 15 bytes + 1 byte for the null terminator. /// Shorter keys need to be padded with null bytes. pub(crate) const MAX_KEY_LENGTH: usize = 15; @@ -413,7 +449,8 @@ impl ThinPage { let aligned_size = T::align_read(size); let mut buf = Vec::with_capacity(aligned_size); - // Safety: we just allocated the buffer with the exact size we need and we will override it the the call to hal.read() + // Safety: we just allocated the buffer with the exact size we need and we will override it + // the the call to hal.read() unsafe { Vec::set_len(&mut buf, aligned_size); } @@ -1292,9 +1329,9 @@ where )?; self.pages.push(page); - // Now that the new blob version has been successfully written, delete the old version if it exists - // _old_version is unused since it will be the first one that is bound to be found anyway as newer - // pages appear later in self.pages + // Now that the new blob version has been successfully written, delete the old version if it + // exists _old_version is unused since it will be the first one that is bound to be + // found anyway as newer pages appear later in self.pages if let Some(_old_version) = old_blob_version { self.delete_key(namespace_index, &key, ChunkIndex::BlobIndex)?; } @@ -1472,8 +1509,8 @@ where self.continue_free_page()?; - // After loading all pages, check for duplicate primitive/string entries and mark older ones as erased - // This handles cases where deletion failed after a successful write + // After loading all pages, check for duplicate primitive/string entries and mark older ones + // as erased This handles cases where deletion failed after a successful write self.cleanup_duplicate_entries()?; self.cleanup_dirty_blobs(blob_index)?; @@ -1836,9 +1873,9 @@ where #[cfg(feature = "defmt")] trace!("copy_items"); - // in case the operation was disturbed in the middle, target might already contain some parts - // of the source page, so we first get the last copied item so we can ignor it and everything - // before in our copy loop + // in case the operation was disturbed in the middle, target might already contain some + // parts of the source page, so we first get the last copied item so we can ignor it + // and everything before in our copy loop let mut last_copied_entry = match target.item_hash_list.iter().max_by_key(|it| it.index) { Some(hash_entry) => Some(target.load_item(&mut self.hal, hash_entry.index)?), None => None, @@ -1942,7 +1979,8 @@ where ))); } - // Safety: either we return directly CORRUPT/INVALID/EMPTY page or we check the crc afterwards + // Safety: either we return directly CORRUPT/INVALID/EMPTY page or we check the crc + // afterwards let raw_page: RawPage = unsafe { core::mem::transmute(buf) }; #[cfg(feature = "debug-logs")] @@ -1986,8 +2024,8 @@ where // Needed due to the desugaring below let mut namespaces: Vec = vec![]; - // This iterator desugaring is necessary to be able to skip entries, e.g. a BLOB or STR entries - // are followed by entries containing their raw value. + // This iterator desugaring is necessary to be able to skip entries, e.g. a BLOB or STR + // entries are followed by entries containing their raw value. let items = &raw_page.items; let mut item_iter = unsafe { items.entries.iter().zip(u8::MIN..u8::MAX) }; 'item_iter: while let Some((item, item_index)) = item_iter.next() { @@ -2029,7 +2067,8 @@ where } ItemType::Blob => { // TODO: should we just ignore this value or mark page corrupt? - // Alternatively, we could add support for BLOB_V1 and convert it here + // Alternatively, we could add support for BLOB_V1 and convert it + // here page.used_entry_count += 1; continue 'item_iter; } diff --git a/src/lib.rs b/esp-nvs/src/lib.rs similarity index 96% rename from src/lib.rs rename to esp-nvs/src/lib.rs index 2f68e92..c5c8a71 100644 --- a/src/lib.rs +++ b/esp-nvs/src/lib.rs @@ -1,10 +1,11 @@ -#![doc = include_str ! ("../README.md")] +#![doc = include_str ! ("../../README.md")] #![cfg_attr(not(target_arch = "x86_64"), no_std)] pub mod error; +pub mod platform; + mod get; mod internal; -pub mod platform; mod raw; mod set; mod u24; @@ -128,14 +129,28 @@ pub use set::Set; extern crate alloc; -use crate::error::Error; -use crate::internal::{ChunkIndex, IterPageItems, ThinPage, VersionOffset}; -use crate::platform::Platform; -use crate::raw::{ENTRIES_PER_PAGE, FLASH_SECTOR_SIZE, Item, ItemType}; -use alloc::collections::{BTreeMap, BinaryHeap}; +use alloc::collections::{ + BTreeMap, + BinaryHeap, +}; use alloc::vec::Vec; use core::fmt; +use crate::error::Error; +use crate::internal::{ + ChunkIndex, + IterPageItems, + ThinPage, + VersionOffset, +}; +use crate::platform::Platform; +use crate::raw::{ + ENTRIES_PER_PAGE, + FLASH_SECTOR_SIZE, + Item, + ItemType, +}; + #[derive(Debug, Clone, PartialEq)] pub struct NvsStatistics { pub pages: PageStatistics, @@ -181,7 +196,8 @@ impl Nvs { /// 3. Cleanup duplicate entries /// 4. Cleanup of duplicated blobs or orphaned blob data /// - /// Pages or entries with invalid CRC32 values are marked as corrupt and are erased when necessary + /// Pages or entries with invalid CRC32 values are marked as corrupt and are erased when + /// necessary pub fn new(partition_offset: usize, partition_size: usize, hal: T) -> Result, Error> { if !partition_offset.is_multiple_of(FLASH_SECTOR_SIZE) { return Err(Error::InvalidPartitionOffset); @@ -238,7 +254,8 @@ impl Nvs { /// Set a value and write it to the flash /// /// Type support: - /// * bool, singed and unsigned integers up to 64-bit width: saved as primitive value with 32 bytes + /// * bool, singed and unsigned integers up to 64-bit width: saved as primitive value with 32 + /// bytes /// * &str: Saved on a single page with a max size of 4000 bytes /// * &[u8]: May span multiple pages, max size ~500kB pub fn set(&mut self, namespace: &Key, key: &Key, value: R) -> Result<(), Error> @@ -400,7 +417,8 @@ impl<'a, T: Platform> Iterator for IterLoadedItems<'a, T> { // self.current is only None if there are no pages at all let current = self.current.as_mut()?; - // if the current page is exhausted, move to next page that has items (or until we run out of pages) + // if the current page is exhausted, move to next page that has items (or until we run out + // of pages) while current.is_empty() { let next_page = self.pages.split_off_first()?; @@ -443,7 +461,8 @@ impl<'a, T: Platform> Iterator for IterKeys<'a, T> { loop { return match self.items.next()? { Ok(item) => { - // Skip namespace entries (namespace_index == 0), and blobs (they are represented by their BlobData) + // Skip namespace entries (namespace_index == 0), and blobs (they are + // represented by their BlobData) if item.namespace_index == 0 || item.type_ == ItemType::Blob || item.type_ == ItemType::BlobIndex diff --git a/src/platform.rs b/esp-nvs/src/platform.rs similarity index 99% rename from src/platform.rs rename to esp-nvs/src/platform.rs index 5eff679..de00930 100644 --- a/src/platform.rs +++ b/esp-nvs/src/platform.rs @@ -61,9 +61,10 @@ impl AlignedOps for T {} feature = "esp32h2", ))] mod chip { - use crate::platform::Crc; use esp_storage::FlashStorage; + use crate::platform::Crc; + impl Crc for FlashStorage<'_> { fn crc32(init: u32, data: &[u8]) -> u32 { esp_hal::rom::crc::crc32_le(init, data) diff --git a/src/raw.rs b/esp-nvs/src/raw.rs similarity index 95% rename from src/raw.rs rename to esp-nvs/src/raw.rs index 5458fa5..eece632 100644 --- a/src/raw.rs +++ b/esp-nvs/src/raw.rs @@ -1,17 +1,33 @@ -use crate::Key; -use crate::error::Error; -use crate::internal::{ThinPageHeader, ThinPageState, VersionOffset}; -use crate::platform::{AlignedOps, FnCrc32, Platform}; -use crate::u24::u24; #[cfg(feature = "debug-logs")] use alloc::format; use alloc::vec; -use core::fmt::{Debug, Formatter}; -use core::mem::{size_of, transmute}; +use core::fmt::{ + Debug, + Formatter, +}; +use core::mem::{ + size_of, + transmute, +}; use core::slice::from_raw_parts; + #[cfg(feature = "defmt")] use defmt::trace; +use crate::Key; +use crate::error::Error; +use crate::internal::{ + ThinPageHeader, + ThinPageState, + VersionOffset, +}; +use crate::platform::{ + AlignedOps, + FnCrc32, + Platform, +}; +use crate::u24::u24; + // -1 is for the leading item of type BLOB_DATA or SZ (for str) pub(crate) const MAX_BLOB_DATA_PER_PAGE: usize = (ENTRIES_PER_PAGE - 1) * size_of::(); pub(crate) const MAX_BLOB_SIZE: usize = @@ -303,8 +319,8 @@ impl Item { Self::calculate_hash_ref(crc32, self.namespace_index, &self.key, self.chunk_index) } - /// `calculate_hash_ref` follows the details of the C++ implementation and accepts more collisions in - /// favor of memory efficiency + /// `calculate_hash_ref` follows the details of the C++ implementation and accepts more + /// collisions in favor of memory efficiency pub(crate) fn calculate_hash_ref( crc32: FnCrc32, namespace_index: u8, @@ -358,9 +374,9 @@ impl Debug for Item { } } -/// We know that keys and namespace names are saved in 16 byte arrays. Because they are originally C strings -/// they are followed by a null terminator in case they are shorter than 16 byte. We have to slice -/// before the null terminator if we want to transmute them to a str. +/// We know that keys and namespace names are saved in 16 byte arrays. Because they are originally C +/// strings they are followed by a null terminator in case they are shorter than 16 byte. We have to +/// slice before the null terminator if we want to transmute them to a str. #[cfg(feature = "debug-logs")] pub(crate) fn slice_with_nullbytes_to_str(raw: &[u8]) -> &str { let sliced = match raw.iter().position(|&e| e == 0x00) { @@ -389,7 +405,8 @@ pub(crate) fn write_aligned( hal.write(offset, header)?; } - // no need to write the trailer if remaining data is all ones - this the default state of the flash + // no need to write the trailer if remaining data is all ones - this the default state of + // the flash if bytes[pivot..].iter().any(|&e| e != 0xFF) { let mut buf = vec![0xFFu8; T::WRITE_SIZE]; buf[..trailer.len()].copy_from_slice(trailer); diff --git a/src/set.rs b/esp-nvs/src/set.rs similarity index 98% rename from src/set.rs rename to esp-nvs/src/set.rs index d245382..d3d1277 100644 --- a/src/set.rs +++ b/esp-nvs/src/set.rs @@ -1,6 +1,10 @@ use crate::error::Error; use crate::platform::Platform; -use crate::{Key, Nvs, raw}; +use crate::{ + Key, + Nvs, + raw, +}; pub trait Set { fn set(&mut self, namespace: &Key, key: &Key, value: T) -> Result<(), Error>; diff --git a/src/u24.rs b/esp-nvs/src/u24.rs similarity index 92% rename from src/u24.rs rename to esp-nvs/src/u24.rs index 63bd444..c54bc92 100644 --- a/src/u24.rs +++ b/esp-nvs/src/u24.rs @@ -1,4 +1,7 @@ -use core::fmt::{Debug, Formatter}; +use core::fmt::{ + Debug, + Formatter, +}; #[derive(Copy, Clone, PartialEq, Ord, PartialOrd, Eq)] #[allow(non_camel_case_types)] diff --git a/tests/assets/multi_page_blob.bin b/esp-nvs/tests/assets/multi_page_blob.bin similarity index 100% rename from tests/assets/multi_page_blob.bin rename to esp-nvs/tests/assets/multi_page_blob.bin diff --git a/tests/assets/test_nvs_data.bin b/esp-nvs/tests/assets/test_nvs_data.bin similarity index 100% rename from tests/assets/test_nvs_data.bin rename to esp-nvs/tests/assets/test_nvs_data.bin diff --git a/tests/assets/test_nvs_data.csv b/esp-nvs/tests/assets/test_nvs_data.csv similarity index 100% rename from tests/assets/test_nvs_data.csv rename to esp-nvs/tests/assets/test_nvs_data.csv diff --git a/tests/benchmark.rs b/esp-nvs/tests/benchmark.rs similarity index 90% rename from tests/benchmark.rs rename to esp-nvs/tests/benchmark.rs index 0c842c8..f4e4098 100644 --- a/tests/benchmark.rs +++ b/esp-nvs/tests/benchmark.rs @@ -1,8 +1,17 @@ -use crate::common::Operation::{Read, Write}; +use esp_nvs::Key; + +use crate::common::Operation::{ + Read, + Write, +}; use crate::common::{ - ENTRY_STATE_MAP_OFFSET, FLASH_SECTOR_SIZE, ITEM_OFFSET, ITEM_SIZE, PAGE_HEADER_SIZE, WORD_SIZE, + ENTRY_STATE_MAP_OFFSET, + FLASH_SECTOR_SIZE, + ITEM_OFFSET, + ITEM_SIZE, + PAGE_HEADER_SIZE, + WORD_SIZE, }; -use esp_nvs::Key; mod common; diff --git a/tests/common.rs b/esp-nvs/tests/common.rs similarity index 97% rename from tests/common.rs rename to esp-nvs/tests/common.rs index 2b4c216..f7b0c5d 100644 --- a/tests/common.rs +++ b/esp-nvs/tests/common.rs @@ -2,7 +2,11 @@ // filename according to https://doc.rust-lang.org/book/ch11-03-test-organization.html use embedded_storage::nor_flash::{ - ErrorType, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash, + ErrorType, + NorFlash, + NorFlashError, + NorFlashErrorKind, + ReadNorFlash, }; pub const FLASH_SECTOR_SIZE: usize = 4096; @@ -184,7 +188,8 @@ impl NorFlash for Flash { let offset = offset as usize; for (i, &val) in bytes.iter().enumerate() { // the esp flash we can only flip bits from 1 to 0 - // println!("0x[{:04x}] {} &= {val} = {}", offset+i,self.buf[offset + i], self.buf[offset + i] & val); + // println!("0x[{:04x}] {} &= {val} = {}", offset+i,self.buf[offset + i], + // self.buf[offset + i] & val); self.buf[offset + i] &= val; } Ok(()) diff --git a/tests/read.rs b/esp-nvs/tests/read.rs similarity index 97% rename from tests/read.rs rename to esp-nvs/tests/read.rs index 1694af4..6c85c3e 100644 --- a/tests/read.rs +++ b/esp-nvs/tests/read.rs @@ -1,5 +1,13 @@ -use esp_nvs::error::{Error, ItemType}; -use esp_nvs::{EntryStatistics, Key, NvsStatistics, PageStatistics}; +use esp_nvs::error::{ + Error, + ItemType, +}; +use esp_nvs::{ + EntryStatistics, + Key, + NvsStatistics, + PageStatistics, +}; use pretty_assertions::assert_eq; mod common; @@ -335,6 +343,7 @@ fn corrupt_entry() { // TODO: when reading a multi-page-blob and the bounds don't match, mark the entry as corrupt -// TODO: when reading a single-page-blob and the bounds don't match, mark the entry as corrupt (covers str as well) +// TODO: when reading a single-page-blob and the bounds don't match, mark the entry as corrupt +// (covers str as well) // TODO: when the CRC is invalid, mark the entry as corrupt diff --git a/tests/write.rs b/esp-nvs/tests/write.rs similarity index 98% rename from tests/write.rs rename to esp-nvs/tests/write.rs index 197a48f..0ba195b 100644 --- a/tests/write.rs +++ b/esp-nvs/tests/write.rs @@ -1,11 +1,12 @@ mod common; mod set { - use crate::common; use esp_nvs::Key; use esp_nvs::error::Error; use pretty_assertions::assert_eq; + use crate::common; + // TODO: test for writing namespace fails + cleanup #[test] @@ -538,11 +539,17 @@ mod set { } mod delete { - use crate::common; use esp_nvs::error::Error; - use esp_nvs::{EntryStatistics, Key, NvsStatistics, PageStatistics}; + use esp_nvs::{ + EntryStatistics, + Key, + NvsStatistics, + PageStatistics, + }; use pretty_assertions::assert_eq; + use crate::common; + #[test] fn primitive() { let mut flash = common::Flash::new(2); @@ -722,11 +729,20 @@ mod delete { } mod overwrite { - use crate::common; - use esp_nvs::error::Error::{FlashError, KeyNotFound}; - use esp_nvs::{EntryStatistics, Key, NvsStatistics, PageStatistics}; + use esp_nvs::error::Error::{ + FlashError, + KeyNotFound, + }; + use esp_nvs::{ + EntryStatistics, + Key, + NvsStatistics, + PageStatistics, + }; use pretty_assertions::assert_eq; + use crate::common; + #[test] fn primitive_overwrites_primitive() { let mut flash = common::Flash::new(2); @@ -1113,12 +1129,14 @@ mod overwrite { #[test] fn blob_overwrites_blob_atomicity_fail_to_delete_old() { - // fail_after_operations is the highest value that makes deleting the old, overwritten block fail. + // fail_after_operations is the highest value that makes deleting the old, overwritten block + // fail. let mut flash = common::Flash::new_with_fault(5, 39); // a page has 126 entries - // the first page contains the namespace, the header for the blob_data and the first 124*32 bytes - // the seconds page contains the blob_data header, 124*32 bytes of data and the blob_index entry + // the first page contains the namespace, the header for the blob_data and the first 124*32 + // bytes the seconds page contains the blob_data header, 124*32 bytes of data and + // the blob_index entry let blob_initial = (u8::MIN..u8::MAX) .cycle() .take(124 * 32 + 124 * 32) @@ -1166,12 +1184,14 @@ mod overwrite { #[test] fn blob_overwrites_blob_atomicity_fail_to_delete_old_twice() { - // fail_after_operations is the highest value that makes deleting the old, overwritten block fail. + // fail_after_operations is the highest value that makes deleting the old, overwritten block + // fail. let mut flash = common::Flash::new_with_fault(8, 60); // a page has 126 entries - // the first page contains the namespace, the header for the blob_data and the first 124*32 bytes - // the seconds page contains the blob_data header, 124*32 bytes of data and the blob_index entry + // the first page contains the namespace, the header for the blob_data and the first 124*32 + // bytes the seconds page contains the blob_data header, 124*32 bytes of data and + // the blob_index entry let blob_initial = (u8::MIN..u8::MAX) .cycle() .take(124 * 32 + 124 * 32) @@ -1228,12 +1248,18 @@ mod overwrite { // TODO overwrite small blob with fail to erase mod defrag { - use crate::common; - use crate::common::Operation; use esp_nvs::error::Error::FlashError; - use esp_nvs::{EntryStatistics, Key, NvsStatistics, PageStatistics}; + use esp_nvs::{ + EntryStatistics, + Key, + NvsStatistics, + PageStatistics, + }; use pretty_assertions::assert_eq; + use crate::common; + use crate::common::Operation; + #[test] fn defragmentation() { let mut flash = common::Flash::new(3); @@ -1723,8 +1749,9 @@ mod defrag { } // Inject fault after all entries copied but just before erase - // From test output: erase happens at operation #576 (196 operations after initial setup at #380) - // Inject fault at operation 195 to fail at operation 575 (just before erase at 576) + // From test output: erase happens at operation #576 (196 operations after initial setup at + // #380) Inject fault at operation 195 to fail at operation 575 (just before erase + // at 576) flash.fail_after_operation = flash.operations.len() + 195; { @@ -1807,5 +1834,6 @@ mod defrag { } // TODO: in case we we want to write a sized item to a page and it doesn't fit, before - // allocating an new empty page and defragmenting into it we can try to fill the still empty entries first + // allocating an new empty page and defragmenting into it we can try to fill the still empty + // entries first } diff --git a/justfile b/justfile index f8bb062..ad4b2e3 100644 --- a/justfile +++ b/justfile @@ -1,27 +1,33 @@ -default: - just --list +mod nvs 'esp-nvs/justfile' -fix: - cargo fmt - cargo clippy --fix --allow-dirty --allow-staged --release --features=defmt - cargo fmt +_default: + @just --list -lint: - cargo clippy --release --features=defmt -- -D warnings - cargo fmt --check +fix: nvs::fix -test: - cargo test +fmt-all: fmt + just --unstable --format + nixfmt devenv.nix + nixfmt .nix/esp-nvs-partition-tool.nix + +fmt: _nightly-fmt -publish-dry-run: - cargo publish --registry crates-io --dry-run +lint: _nightly-fmt-check nvs::lint + +test: + cargo test --all + cargo test --doc -publish: - cargo publish --registry crates-io +update-changelog: nvs::update-changelog -update-changelog: - git-cliff --bump -o CHANGELOG.md +_nightly-fmt: + devenv shell \ + --option languages.rust.version:string 2026-02-18 \ + --option languages.rust.channel:string nightly \ + cargo fmt --all -[working-directory: 'tests/assets/'] -generate_test_nvs_bin: - nvs_partition_gen generate test_nvs_data.csv test_nvs_data.bin 0x4000 +_nightly-fmt-check: + devenv shell \ + --option languages.rust.version:string 2026-02-18 \ + --option languages.rust.channel:string nightly \ + cargo fmt --all --check diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..79f458f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,17 @@ +# For more information see: https://rust-lang.github.io/rustfmt +edition = "2024" + +# As of this commit, we are using Rust nightly +# unstable_features = true + +# Comments +format_code_in_doc_comments = true +normalize_comments = true +wrap_comments = true +doc_comment_code_block_width = 100 +comment_width = 100 + +# Imports +group_imports = "StdExternalCrate" +imports_granularity = "Module" +imports_layout = "Vertical" From 46a6bc185ae81fb092ab7075b6395e7885bba262 Mon Sep 17 00:00:00 2001 From: niclo Date: Mon, 23 Feb 2026 14:17:49 +0100 Subject: [PATCH 3/4] ci: fix github workflows --- .github/workflows/check.yml | 30 +++++++++++++++--------------- .github/workflows/nostd.yml | 8 ++++---- .github/workflows/test.yml | 6 +++--- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ebb6449..b90ea77 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -11,18 +11,18 @@ name: check jobs: fmt: runs-on: ubuntu-latest - name: stable / fmt + name: nightly / fmt steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 with: submodules: true - - name: Install stable + - name: Install nightly uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e # branch=master with: - toolchain: stable + toolchain: nightly components: rustfmt - - name: cargo fmt --check - run: cargo fmt --check + - name: cargo fmt --all --check + run: cargo fmt --all --check clippy: runs-on: ubuntu-latest name: ${{ matrix.toolchain }} / clippy @@ -44,11 +44,10 @@ jobs: target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf,x86_64-unknown-linux-gnu toolchain: ${{ matrix.toolchain }} components: clippy - - name: cargo clippy - uses: giraffate/clippy-action@13b9d32482f25d29ead141b79e7e04e7900281e0 # tag=v1.0.1 - with: - reporter: 'github-pr-check' - github_token: ${{ secrets.GITHUB_TOKEN }} + - name: cargo clippy (esp-nvs) + run: cargo clippy --release --package esp-nvs --features=defmt -- --deny warnings + - name: cargo clippy (esp-nvs-partition-tool) + run: cargo clippy --release --package esp-nvs-partition-tool -- --deny warnings check: runs-on: ubuntu-latest name: stable / check @@ -60,9 +59,8 @@ jobs: uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e # branch=master with: toolchain: stable - components: rustfmt - name: cargo check - run: cargo check + run: cargo check --workspace --all-targets doc: # run docs generation on nightly rather than stable. This enables features like # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an @@ -79,8 +77,10 @@ jobs: toolchain: nightly - name: Install cargo-docs-rs uses: dtolnay/install@982daea0f5d846abc3c83e01a6a1d73c040047c1 # branch=cargo-docs-rs - - name: cargo docs-rs - run: cargo docs-rs + - name: cargo docs-rs (esp-nvs) + run: cargo docs-rs --package esp-nvs + - name: cargo docs-rs (esp-nvs-partition-tool) + run: cargo docs-rs --package esp-nvs-partition-tool msrv: runs-on: ubuntu-latest # we use a matrix here just because env can't be used in job names @@ -103,4 +103,4 @@ jobs: path: ~/.cargo key: ${{ runner.os }}-cargo - name: cargo +${{ matrix.msrv }} check - run: cargo check + run: cargo check --workspace diff --git a/.github/workflows/nostd.yml b/.github/workflows/nostd.yml index 4383ef7..c53de63 100644 --- a/.github/workflows/nostd.yml +++ b/.github/workflows/nostd.yml @@ -23,7 +23,7 @@ jobs: - name: rustup target add riscv32imc-unknown-none-elf run: rustup target add riscv32imc-unknown-none-elf - name: cargo check - run: cargo check --target riscv32imc-unknown-none-elf --no-default-features + run: cargo check --package esp-nvs --target riscv32imc-unknown-none-elf --no-default-features riscv32imac: runs-on: ubuntu-latest name: riscv32imac-unknown-none-elf @@ -38,12 +38,12 @@ jobs: - name: rustup target add riscv32imac-unknown-none-elf run: rustup target add riscv32imac-unknown-none-elf - name: cargo check - run: cargo check --target riscv32imac-unknown-none-elf --no-default-features + run: cargo check --package esp-nvs --target riscv32imac-unknown-none-elf --no-default-features xtensa: runs-on: ubuntu-latest name: xtensa-esp32-none-elf env: - GITHUB_TOKEN: ${{ secrets. GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 with: @@ -55,4 +55,4 @@ jobs: version: "latest" buildtargets: "esp32" - name: cargo check - run: cargo check --target xtensa-esp32-none-elf --no-default-features -Zbuild-std=core,alloc + run: cargo check --package esp-nvs --target xtensa-esp32-none-elf --no-default-features -Zbuild-std=core,alloc diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf4ab4f..86c34a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,10 +30,10 @@ jobs: if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile - name: cargo test --locked - run: cargo test --locked --all-targets + run: cargo test --locked --workspace --all-targets # https://github.com/rust-lang/cargo/issues/6669 - name: cargo test --doc - run: cargo test --locked --doc + run: cargo test --locked --workspace --doc os-check: # run cargo test on mac and windows runs-on: ${{ matrix.os }} @@ -56,4 +56,4 @@ jobs: if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile - name: cargo test - run: cargo test --locked --all-targets + run: cargo test --locked --workspace --all-targets From 23da6b62a2ecb3fa2e1c626a82438e1c63b971f9 Mon Sep 17 00:00:00 2001 From: niclo Date: Mon, 23 Feb 2026 14:00:21 +0100 Subject: [PATCH 4/4] feat: add esp nvs partition tool --- .nix/esp-idf-nvs-partition-gen.nix | 40 -- .nix/esp-nvs-partition-tool.nix | 35 ++ Cargo.lock | 467 +++++++++++++++- Cargo.toml | 2 +- devenv.lock | 12 +- devenv.nix | 4 +- esp-nvs-partition-tool/Cargo.toml | 40 ++ esp-nvs-partition-tool/README.md | 109 ++++ esp-nvs-partition-tool/devenv.lock | 123 +++++ esp-nvs-partition-tool/justfile | 17 + esp-nvs-partition-tool/src/bin/main.rs | 86 +++ esp-nvs-partition-tool/src/csv.rs | 4 + esp-nvs-partition-tool/src/csv/parser.rs | 108 ++++ esp-nvs-partition-tool/src/csv/row.rs | 58 ++ esp-nvs-partition-tool/src/csv/writer.rs | 65 +++ esp-nvs-partition-tool/src/error.rs | 42 ++ esp-nvs-partition-tool/src/lib.rs | 113 ++++ esp-nvs-partition-tool/src/partition.rs | 202 +++++++ .../src/partition/consts.rs | 36 ++ esp-nvs-partition-tool/src/partition/crc.rs | 39 ++ .../src/partition/generator.rs | 478 ++++++++++++++++ .../src/partition/parser.rs | 508 ++++++++++++++++++ .../tests/assets/hex2bin_test.csv | 3 + .../tests/assets/invalid_long_key.csv | 2 + .../tests/assets/large_string.csv | 3 + .../tests/assets/multiple_namespaces.csv | 7 + .../tests/assets/roundtrip_basic.csv | 5 + esp-nvs-partition-tool/tests/generate.rs | 92 ++++ esp-nvs-partition-tool/tests/parse.rs | 31 ++ esp-nvs-partition-tool/tests/roundtrip.rs | 426 +++++++++++++++ justfile | 8 +- rustfmt.toml | 3 - 32 files changed, 3112 insertions(+), 56 deletions(-) delete mode 100644 .nix/esp-idf-nvs-partition-gen.nix create mode 100644 .nix/esp-nvs-partition-tool.nix create mode 100644 esp-nvs-partition-tool/Cargo.toml create mode 100644 esp-nvs-partition-tool/README.md create mode 100644 esp-nvs-partition-tool/devenv.lock create mode 100644 esp-nvs-partition-tool/justfile create mode 100644 esp-nvs-partition-tool/src/bin/main.rs create mode 100644 esp-nvs-partition-tool/src/csv.rs create mode 100644 esp-nvs-partition-tool/src/csv/parser.rs create mode 100644 esp-nvs-partition-tool/src/csv/row.rs create mode 100644 esp-nvs-partition-tool/src/csv/writer.rs create mode 100644 esp-nvs-partition-tool/src/error.rs create mode 100644 esp-nvs-partition-tool/src/lib.rs create mode 100644 esp-nvs-partition-tool/src/partition.rs create mode 100644 esp-nvs-partition-tool/src/partition/consts.rs create mode 100644 esp-nvs-partition-tool/src/partition/crc.rs create mode 100644 esp-nvs-partition-tool/src/partition/generator.rs create mode 100644 esp-nvs-partition-tool/src/partition/parser.rs create mode 100644 esp-nvs-partition-tool/tests/assets/hex2bin_test.csv create mode 100644 esp-nvs-partition-tool/tests/assets/invalid_long_key.csv create mode 100644 esp-nvs-partition-tool/tests/assets/large_string.csv create mode 100644 esp-nvs-partition-tool/tests/assets/multiple_namespaces.csv create mode 100644 esp-nvs-partition-tool/tests/assets/roundtrip_basic.csv create mode 100644 esp-nvs-partition-tool/tests/generate.rs create mode 100644 esp-nvs-partition-tool/tests/parse.rs create mode 100644 esp-nvs-partition-tool/tests/roundtrip.rs diff --git a/.nix/esp-idf-nvs-partition-gen.nix b/.nix/esp-idf-nvs-partition-gen.nix deleted file mode 100644 index 82bcbd3..0000000 --- a/.nix/esp-idf-nvs-partition-gen.nix +++ /dev/null @@ -1,40 +0,0 @@ -{ pkgs ? import {} }: - -let - lib = pkgs.lib; - python = pkgs.python313; - pythonPackages = pkgs.python313Packages; -in - -pythonPackages.buildPythonPackage rec { - pname = "esp-idf-nvs-partition-gen"; - version = "0.2.0"; - pyproject = true; - - src = pkgs.fetchPypi { - pname = "esp_idf_nvs_partition_gen"; - version = version; - sha256 = "sha256-0fI86YdsBGnlB7NJkAEmbQ0XU4FYFRomEsxwuAcG3OA="; - }; - - build-system = [ - pythonPackages.setuptools - ]; - - dependencies = [ - pythonPackages.cryptography - pythonPackages.pyopenssl - ]; - - checkPhase = '' - ${python.interpreter} -c "import esp_idf_nvs_partition_gen as m; print('loaded', getattr(m,'__version__','no-version'))" - ''; - - meta = with lib; { - description = "Tool to generate ESP-IDF NVS partition images (nvs_partition_gen.py)"; - homepage = "https://pypi.org/project/esp-idf-nvs-partition-gen"; - license = licenses.mit; - maintainers = with pkgs.lib.maintainers; []; - mainProgram = "nvs_partition_gen.py"; - }; -} diff --git a/.nix/esp-nvs-partition-tool.nix b/.nix/esp-nvs-partition-tool.nix new file mode 100644 index 0000000..ce55a67 --- /dev/null +++ b/.nix/esp-nvs-partition-tool.nix @@ -0,0 +1,35 @@ +{ + lib, + rustPlatform, + pkg-config, +}: + +rustPlatform.buildRustPackage { + pname = "esp-nvs-partition-tool"; + version = "0.1.0"; + + src = ./..; + + cargoLock = { + lockFile = ../Cargo.lock; + }; + + # Only build the esp-nvs-partition-tool binary + cargoBuildFlags = [ + "--bin" + "esp-nvs-partition-tool" + ]; + + nativeBuildInputs = [ pkg-config ]; + + meta = with lib; { + description = "ESP-IDF compatible NVS partition table parser and generator"; + homepage = "https://github.com/lhemala/esp-nvs/"; + license = with licenses; [ + mit + asl20 + ]; + maintainers = [ ]; + mainProgram = "esp-nvs-partition-tool"; + }; +} diff --git a/Cargo.lock b/Cargo.lock index dfd03e8..41a5440 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,74 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitfield" version = "0.19.4" @@ -77,6 +139,52 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -102,6 +210,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "darling" version = "0.21.3" @@ -424,6 +553,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "esp-config" version = "0.6.1" @@ -529,6 +668,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "esp-nvs-partition-tool" +version = "0.1.0" +dependencies = [ + "base64", + "clap", + "csv", + "embedded-storage", + "esp-nvs", + "hex", + "libz-sys", + "serde", + "similar", + "tempfile", + "thiserror", +] + [[package]] name = "esp-riscv-rt" version = "0.13.0" @@ -664,6 +820,12 @@ dependencies = [ "vcell", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -676,6 +838,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fugit" version = "0.3.9" @@ -730,6 +898,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "hash32" version = "0.3.1" @@ -739,6 +920,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -761,6 +951,18 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -774,7 +976,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -799,12 +1003,24 @@ dependencies = [ "syn", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.182" @@ -823,12 +1039,24 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litrs" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.8.0" @@ -868,6 +1096,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "paste" version = "1.0.15" @@ -902,6 +1142,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -951,6 +1201,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "ral-registers" version = "0.1.3" @@ -1028,6 +1284,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1376b15f3ff160e9b1e8ea564ce427f2f6fcf77528cc0a8bf405cb476f9cea7" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1040,6 +1309,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1070,6 +1345,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1111,6 +1399,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "somni-expr" version = "0.2.0" @@ -1170,6 +1464,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1247,6 +1554,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1263,6 +1576,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcell" version = "0.1.3" @@ -1287,6 +1606,58 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -1320,6 +1691,94 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "xtensa-lx" version = "0.13.0" @@ -1356,3 +1815,9 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 08ad902..5d93ffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["esp-nvs"] +members = ["esp-nvs", "esp-nvs-partition-tool"] resolver = "2" [profile.dev.package.esp-storage] diff --git a/devenv.lock b/devenv.lock index 3dda13a..ea4ee9e 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1771672827, + "lastModified": 1771848767, "owner": "cachix", "repo": "devenv", - "rev": "6757a742f6393c7070f159eed9a59cbe20690aa3", + "rev": "8e42c095b51583993629134d8c0eb3bfdfb4a122", "type": "github" }, "original": { @@ -40,10 +40,10 @@ ] }, "locked": { - "lastModified": 1770726378, + "lastModified": 1771850327, "owner": "cachix", "repo": "git-hooks.nix", - "rev": "5eaaedde414f6eb1aea8b8525c466dc37bba95ae", + "rev": "fb048138383cf5c934bb2efa1ad5b2110f5d1d72", "type": "github" }, "original": { @@ -125,10 +125,10 @@ ] }, "locked": { - "lastModified": 1771642886, + "lastModified": 1771816254, "owner": "oxalica", "repo": "rust-overlay", - "rev": "85078369717bdbe1f266c9eaad5e66956fb6feea", + "rev": "085bdbf5dde5477538e4c87d1684b6c6df56c0ad", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index 55b16f0..409ad78 100644 --- a/devenv.nix +++ b/devenv.nix @@ -9,7 +9,7 @@ overlays = [ (final: prev: { - esp-idf-nvs-partition-gen = final.callPackage ./.nix/esp-idf-nvs-partition-gen.nix {}; + esp-nvs-partition-tool = final.callPackage ./.nix/esp-nvs-partition-tool.nix { }; }) ]; @@ -17,7 +17,7 @@ packages = with pkgs; [ actionlint cargo-edit - esp-idf-nvs-partition-gen + esp-nvs-partition-tool git just nixfmt diff --git a/esp-nvs-partition-tool/Cargo.toml b/esp-nvs-partition-tool/Cargo.toml new file mode 100644 index 0000000..25a0e95 --- /dev/null +++ b/esp-nvs-partition-tool/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "esp-nvs-partition-tool" +version = "0.1.0" +edition = "2021" +rust-version = "1.87" +authors = ["Lars Hemala"] +repository = "https://github.com/lhemala/esp-nvs/" +license = "MIT OR Apache-2.0" +description = "ESP-IDF compatible NVS partition table parser and generator" +keywords = ["esp", "embedded", "nvs", "partition", "esp-idf"] +categories = ["command-line-utilities", "embedded", "parsing"] + +[lib] +name = "esp_nvs_partition_tool" +path = "src/lib.rs" + +[[bin]] +name = "esp-nvs-partition-tool" +path = "src/bin/main.rs" +required-features = ["cli"] + +[features] +default = ["cli"] +cli = ["dep:clap"] + +[dependencies] +base64 = "0.22.1" +clap = { version = "4.5.60", features = ["derive"], optional = true } +csv = "1.4.0" +hex = "0.4.3" +serde = { version = "1.0.228", features = ["derive"] } +thiserror = "2.0.18" + + +[dev-dependencies] +embedded-storage = "0.3.1" +esp-nvs = { path = "../esp-nvs" } +libz-sys = "1.1.22" +similar = "2" +tempfile = "3.25.0" diff --git a/esp-nvs-partition-tool/README.md b/esp-nvs-partition-tool/README.md new file mode 100644 index 0000000..779020a --- /dev/null +++ b/esp-nvs-partition-tool/README.md @@ -0,0 +1,109 @@ +# esp-nvs-partition-tool + +ESP-IDF compatible NVS (Non-Volatile Storage) partition table parser and generator inspired by [esp-idf-nvs-partition-gen](https://github.com/espressif/esp-idf/tree/v5.5.3/components/nvs_flash/nvs_partition_tool). + +This library and CLI tool allows you to parse and generate NVS partition binary files from CSV files, following the [ESP-IDF NVS partition format specification](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c6/api-reference/storage/nvs_partition_gen.html#nvs-partition-generator-utility). + +## TODO + +- [ ] Encryption support (planned for future release) + +## CSV Format + +The CSV file must have exactly four columns: + +```csv +key,type,encoding,value +``` + +### Entry Types + +1. **namespace** - Defines a namespace + - Encoding and value must be empty + - Example: `my_namespace,namespace,,` + +2. **data** - Raw data entry + - Valid encodings: `u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u64`, `i64`, `string`, `hex2bin`, `base64` + - Example: `my_key,data,u32,12345` + +3. **file** - Read value from a file + - Valid encodings: `string`, `hex2bin`, `base64`, `binary` + - Value should be the file path (relative to CSV file) + - Example: `my_blob,file,binary,data.bin` + +### Example CSV + +```csv +key,type,encoding,value +namespace_one,namespace,, +example_u8,data,u8,100 +example_i8,data,i8,-100 +example_string,data,string,Hello World +example_blob,data,hex2bin,AABBCCDDEE +namespace_two,namespace,, +config,file,binary,config.bin +``` + +## CLI Usage + +### Generate NVS Partition Binary + +```bash +esp-nvs-partition-tool generate --size +``` + +The size can be specified in decimal or hexadecimal (with `0x` prefix): + +```bash +# Generate 16KB partition (decimal) +esp-nvs-partition-tool generate nvs_data.csv partition.bin --size 16384 + +# Generate 16KB partition (hexadecimal) +esp-nvs-partition-tool generate nvs_data.csv partition.bin --size 0x4000 +``` + +### Parse NVS Partition Binary to CSV + +```bash +esp-nvs-partition-tool parse +``` + +Example: + +```bash +# Parse a partition binary back to CSV +esp-nvs-partition-tool parse partition.bin recovered_data.csv +``` + +## Library Usage + +Add to your `Cargo.toml`: + +```toml +[dependencies] +esp-nvs-partition-tool = "0.1.0" +``` + +Example usage: + +```rust +use esp_nvs_partition_tool::NvsPartition; + +fn main() -> Result<(), Box> { + // Parse CSV file and generate binary + let partition = NvsPartition::from_csv_file("nvs_data.csv")?; + partition.generate_partition_file("output.bin", 16384)?; + + // Parse binary back to CSV + let recovered_partition = NvsPartition::parse_partition_file("output.bin")?; + recovered_partition.to_csv_file("recovered.csv")?; + + Ok(()) +} +``` + +## References + +- [ESP-IDF NVS Partition Generator Documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/nvs_partition_gen.html) +- [ESP-IDF NVS Flash Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/nvs_flash.html) +- [esp-idf-part](https://github.com/esp-rs/esp-idf-part) - Reference implementation for partition tables diff --git a/esp-nvs-partition-tool/devenv.lock b/esp-nvs-partition-tool/devenv.lock new file mode 100644 index 0000000..e283e8d --- /dev/null +++ b/esp-nvs-partition-tool/devenv.lock @@ -0,0 +1,123 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1771591269, + "owner": "cachix", + "repo": "devenv", + "rev": "cd2ec51a7bd74a9a9527c0145b8449c6110050d4", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770726378, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "5eaaedde414f6eb1aea8b8525c466dc37bba95ae", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1770434727, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "8430f16a39c27bdeef236f1eeb56f0b51b33d348", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1769922788, + "narHash": "sha256-H3AfG4ObMDTkTJYkd8cz1/RbY9LatN5Mk4UF48VuSXc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "207d15f1a6603226e1e223dc79ac29c7846da32e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/esp-nvs-partition-tool/justfile b/esp-nvs-partition-tool/justfile new file mode 100644 index 0000000..d15c851 --- /dev/null +++ b/esp-nvs-partition-tool/justfile @@ -0,0 +1,17 @@ +_default: + just --list + +fix: + cargo clippy --fix --allow-dirty --allow-staged --release -p esp-nvs-partition-tool + +lint: + cargo clippy --release -p esp-nvs-partition-tool -- -D warnings + +update-changelog: + git-cliff --bump --include-path "esp-nvs-partition-tool/**" -o CHANGELOG.md + +publish-dry-run: + cargo publish --registry crates-io --dry-run + +publish: + cargo publish --registry crates-io diff --git a/esp-nvs-partition-tool/src/bin/main.rs b/esp-nvs-partition-tool/src/bin/main.rs new file mode 100644 index 0000000..aab149d --- /dev/null +++ b/esp-nvs-partition-tool/src/bin/main.rs @@ -0,0 +1,86 @@ +use std::path::PathBuf; + +use clap::{ + Parser, + Subcommand, +}; +use esp_nvs_partition_tool::{ + NvsPartition, + FLASH_SECTOR_SIZE, +}; + +#[derive(Parser)] +#[command(name = "esp-nvs-partition-tool")] +#[command(about = "ESP NVS partition generator and parser", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Generate NVS partition binary from CSV file + Generate { + /// Input CSV file path + input: PathBuf, + + /// Output binary file path + output: PathBuf, + + /// Partition size in bytes (must be multiple of 4096) + #[arg(short, long, value_parser = parse_size)] + size: usize, + }, + /// Parse NVS partition binary to CSV file + Parse { + /// Input binary file path + input: PathBuf, + + /// Output CSV file path + output: PathBuf, + }, +} + +fn parse_size(s: &str) -> Result { + if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + usize::from_str_radix(hex, 16).map_err(|e| e.to_string()) + } else { + s.parse::().map_err(|e| e.to_string()) + } +} + +fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + match cli.command { + Commands::Generate { + input, + output, + size, + } => { + println!("Parsing CSV file: {}", input.display()); + let partition = NvsPartition::from_csv_file(&input)?; + println!("Found {} entries", partition.entries.len()); + + println!("Generating partition binary..."); + partition.generate_partition_file(&output, size)?; + + println!("Successfully generated NVS partition: {}", output.display()); + println!("Size: {} bytes ({} pages)", size, size / FLASH_SECTOR_SIZE); + + Ok(()) + } + Commands::Parse { input, output } => { + println!("Parsing binary file: {}", input.display()); + let partition = NvsPartition::parse_partition_file(&input)?; + println!("Found {} entries", partition.entries.len()); + + println!("Writing CSV file..."); + partition.to_csv_file(&output)?; + + println!("Successfully parsed NVS partition to: {}", output.display()); + + Ok(()) + } + } +} diff --git a/esp-nvs-partition-tool/src/csv.rs b/esp-nvs-partition-tool/src/csv.rs new file mode 100644 index 0000000..024ef6d --- /dev/null +++ b/esp-nvs-partition-tool/src/csv.rs @@ -0,0 +1,4 @@ +pub(crate) mod parser; +pub(crate) mod writer; + +mod row; diff --git a/esp-nvs-partition-tool/src/csv/parser.rs b/esp-nvs-partition-tool/src/csv/parser.rs new file mode 100644 index 0000000..21a7770 --- /dev/null +++ b/esp-nvs-partition-tool/src/csv/parser.rs @@ -0,0 +1,108 @@ +use std::path::Path; + +use base64::Engine; + +use crate::error::Error; +use crate::partition::{ + validate_key, + DataValue, + FileEncoding, + NvsEntry, +}; +use crate::NvsPartition; + +#[derive(Debug, serde::Deserialize)] +struct CsvRow { + key: String, + #[serde(rename = "type")] + entry_type: String, + encoding: String, + value: String, +} + +/// Parse NVS CSV content from a string into an [`NvsPartition`]. +pub(crate) fn parse_csv(content: &str) -> Result { + let mut partition = NvsPartition { entries: vec![] }; + let mut reader = csv::Reader::from_reader(content.as_bytes()); + let mut current_namespace: Option = None; + + for result in reader.deserialize() { + let row: CsvRow = result?; + + if row.entry_type == "namespace" { + validate_key(&row.key)?; + if !row.encoding.is_empty() || !row.value.is_empty() { + return Err(Error::InvalidValue( + "namespace entries must have empty encoding and value".to_string(), + )); + } + current_namespace = Some(row.key); + continue; + } + + let namespace = current_namespace.clone().ok_or(Error::MissingNamespace)?; + let entry = parse_row(row, namespace)?; + partition.entries.push(entry); + } + + Ok(partition) +} + +fn parse_row(row: CsvRow, namespace: String) -> Result { + validate_key(&row.key)?; + + match row.entry_type.as_str() { + "data" => { + if row.encoding.is_empty() { + return Err(Error::InvalidEncoding( + "data entries must have an encoding".to_string(), + )); + } + let value = parse_value(&row.value, &row.encoding)?; + Ok(NvsEntry::new_data(namespace, row.key, value)) + } + "file" => { + if row.value.is_empty() { + return Err(Error::InvalidValue( + "file entries must have a file path".to_string(), + )); + } + let encoding: FileEncoding = row.encoding.parse()?; + let file_path = Path::new(&row.value).to_path_buf(); + Ok(NvsEntry::new_file(namespace, row.key, encoding, file_path)) + } + _ => Err(Error::InvalidType(row.entry_type)), + } +} + +macro_rules! parse_numeric { + ($value:expr, $ty:ty, $variant:ident) => { + $value + .parse::<$ty>() + .map(DataValue::$variant) + .map_err(|e| Error::InvalidValue(format!("invalid {} value: {}", stringify!($ty), e))) + }; +} + +fn parse_value(value: &str, encoding: &str) -> Result { + match encoding { + "u8" => parse_numeric!(value, u8, U8), + "i8" => parse_numeric!(value, i8, I8), + "u16" => parse_numeric!(value, u16, U16), + "i16" => parse_numeric!(value, i16, I16), + "u32" => parse_numeric!(value, u32, U32), + "i32" => parse_numeric!(value, i32, I32), + "u64" => parse_numeric!(value, u64, U64), + "i64" => parse_numeric!(value, i64, I64), + "string" => Ok(DataValue::String(value.to_string())), + "hex2bin" => { + let bytes = hex::decode(value.trim())?; + Ok(DataValue::Binary(bytes)) + } + "base64" => { + let bytes = base64::engine::general_purpose::STANDARD.decode(value.trim())?; + Ok(DataValue::Binary(bytes)) + } + _ => Err(Error::InvalidEncoding(encoding.to_string())), + } +} diff --git a/esp-nvs-partition-tool/src/csv/row.rs b/esp-nvs-partition-tool/src/csv/row.rs new file mode 100644 index 0000000..ead7908 --- /dev/null +++ b/esp-nvs-partition-tool/src/csv/row.rs @@ -0,0 +1,58 @@ +#[derive(Debug, serde::Serialize)] +pub(crate) struct PartitionRow { + key: String, + r#type: Type, + encoding: String, + value: String, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "lowercase")] +enum Type { + Data, + File, +} + +impl PartitionRow { + fn new(key: String, r#type: Type, encoding: String, value: String) -> Self { + Self { + key, + r#type, + encoding, + value, + } + } +} + +impl From for PartitionRow { + fn from(entry: crate::NvsEntry) -> Self { + let r#type = Type::from(&entry.content); + + match entry.content { + crate::EntryContent::Data(value) => PartitionRow::new( + entry.key.to_owned(), + r#type, + value.encoding_str().to_string(), + value.to_string(), + ), + crate::EntryContent::File { + encoding, + file_path, + } => PartitionRow::new( + entry.key.to_owned(), + r#type, + encoding.as_str().to_owned(), + file_path.to_string_lossy().to_string(), + ), + } + } +} + +impl From<&crate::EntryContent> for Type { + fn from(content: &crate::EntryContent) -> Self { + match content { + crate::EntryContent::Data(_) => Self::Data, + crate::EntryContent::File { .. } => Self::File, + } + } +} diff --git a/esp-nvs-partition-tool/src/csv/writer.rs b/esp-nvs-partition-tool/src/csv/writer.rs new file mode 100644 index 0000000..b1c30b5 --- /dev/null +++ b/esp-nvs-partition-tool/src/csv/writer.rs @@ -0,0 +1,65 @@ +use std::path::Path; + +use csv::WriterBuilder; + +use crate::csv::row::PartitionRow; +use crate::error::Error; +use crate::NvsPartition; + +/// Serialize an NVS partition to a CSV file at the given `output_path`. +/// +/// Entries are written in their original insertion order. A namespace header +/// row is emitted whenever the namespace changes between consecutive entries. +/// +/// `Binary` data values are serialized as base64, matching the ESP-IDF +/// `nvs_partition_tool` convention. +pub(crate) fn write_csv>( + partition: NvsPartition, + output_path: P, +) -> Result<(), Error> { + let mut wtr = WriterBuilder::new() + .has_headers(false) + .from_path(output_path)?; + write_records(&mut wtr, partition) +} + +/// Serialize an NVS partition to CSV and return the content as a `String`. +/// +/// See [`write_csv`] for details on ordering and encoding behavior. +pub(crate) fn write_csv_content(partition: NvsPartition) -> Result { + let mut wtr = WriterBuilder::new() + .has_headers(false) + .from_writer(Vec::new()); + + write_records(&mut wtr, partition)?; + + let bytes = wtr + .into_inner() + .map_err(|e| Error::IoError(e.into_error()))?; + String::from_utf8(bytes) + .map_err(|e| Error::InvalidValue(format!("CSV output is not valid UTF-8: {}", e))) +} + +fn write_records( + wtr: &mut csv::Writer, + partition: NvsPartition, +) -> Result<(), Error> { + wtr.write_record(["key", "type", "encoding", "value"])?; + + // Emit namespace rows on demand, preserving the original entry order. + let mut current_namespace: Option = None; + + for entry in partition.entries { + // Emit a namespace row when the namespace changes + if current_namespace.as_deref() != Some(&entry.namespace) { + let namespace = entry.namespace.clone(); + wtr.write_record([&namespace, "namespace", "", ""])?; + current_namespace = Some(namespace); + } + + wtr.serialize(PartitionRow::from(entry))?; + } + + wtr.flush()?; + Ok(()) +} diff --git a/esp-nvs-partition-tool/src/error.rs b/esp-nvs-partition-tool/src/error.rs new file mode 100644 index 0000000..0d4b83f --- /dev/null +++ b/esp-nvs-partition-tool/src/error.rs @@ -0,0 +1,42 @@ +use thiserror::Error; + +/// Errors that can occur during CSV parsing, binary generation, or binary +/// parsing of NVS partitions. +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to parse CSV: {0}")] + CsvError(#[from] csv::Error), + + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + #[error("invalid entry type: {0}")] + InvalidType(String), + + #[error("invalid encoding: {0}")] + InvalidEncoding(String), + + #[error("invalid value: {0}")] + InvalidValue(String), + + #[error("hex decoding error: {0}")] + HexError(#[from] hex::FromHexError), + + #[error("base64 decoding error: {0}")] + Base64Error(#[from] base64::DecodeError), + + #[error("missing namespace")] + MissingNamespace, + + #[error("invalid key: {0}")] + InvalidKey(String), + + #[error("partition size {0} is too small")] + PartitionTooSmall(usize), + + #[error("invalid partition size {0}: must be a multiple of 4096 bytes")] + InvalidPartitionSize(usize), + + #[error("too many namespaces (max 255)")] + TooManyNamespaces, +} diff --git a/esp-nvs-partition-tool/src/lib.rs b/esp-nvs-partition-tool/src/lib.rs new file mode 100644 index 0000000..7a91233 --- /dev/null +++ b/esp-nvs-partition-tool/src/lib.rs @@ -0,0 +1,113 @@ +//! ESP-IDF compatible NVS (Non-Volatile Storage) partition table parser and +//! generator. + +pub mod error; +pub mod partition; + +mod csv; + +use std::fs; +use std::io::Write; +use std::path::Path; + +pub use error::Error; +pub use partition::{ + DataValue, + EntryContent, + FileEncoding, + NvsEntry, + FLASH_SECTOR_SIZE, + MAX_KEY_LENGTH, +}; + +/// A collection of NVS key-value entries, optionally spanning multiple +/// namespaces. +/// +/// This is the primary in-memory representation used by the CSV and binary +/// parsers/generators. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NvsPartition { + /// The ordered list of entries in this partition. + pub entries: Vec, +} + +impl NvsPartition { + /// Parse NVS CSV content from a string. + /// + /// File-type entries store the path exactly as written in the CSV. Use + /// [`NvsPartition::from_csv_file`] when parsing from a file on disk so + /// that relative paths are resolved automatically. + pub fn from_csv(content: &str) -> Result { + csv::parser::parse_csv(content) + } + + /// Parse an NVS CSV file at the given `path`. + /// + /// File-type entries in the CSV have their paths resolved relative to the + /// parent directory of the CSV file. + pub fn from_csv_file>(path: P) -> Result { + let content = fs::read_to_string(&path)?; + let mut partition = csv::parser::parse_csv(&content)?; + + // Resolve relative file paths against the CSV file's parent directory. + if let Some(base) = path.as_ref().parent() { + for entry in &mut partition.entries { + if let EntryContent::File { file_path, .. } = &mut entry.content { + if file_path.is_relative() { + *file_path = base.join(&file_path); + } + } + } + } + + Ok(partition) + } + + /// Serialize this partition to CSV and return the content as a `String`. + /// + /// See [`NvsPartition::to_csv_file`] for details on ordering and + /// encoding behavior. + pub fn to_csv(self) -> Result { + csv::writer::write_csv_content(self) + } + + /// Serialize this partition to a CSV file at the given `path`. + /// + /// Entries are written in their original insertion order. A namespace + /// header row is emitted whenever the namespace changes between + /// consecutive entries. `Encoding::Binary` values are serialized as + /// base64, matching the ESP-IDF `nvs_partition_tool` convention. + pub fn to_csv_file>(self, path: P) -> Result<(), Error> { + csv::writer::write_csv(self, path) + } + + /// Generate an NVS partition binary in memory. + /// + /// `size` must be a multiple of 4096 (the ESP-IDF flash sector size). + pub fn generate_partition(&self, size: usize) -> Result, Error> { + partition::generator::generate_partition_data(self, size) + } + + /// Generate an NVS partition binary and write it to `path`. + /// + /// `size` must be a multiple of 4096 (the ESP-IDF flash sector size). + pub fn generate_partition_file>( + &self, + path: P, + size: usize, + ) -> Result<(), Error> { + let data = self.generate_partition(size)?; + std::fs::File::create(path)?.write_all(&data)?; + Ok(()) + } + + /// Parse an NVS partition binary from an in-memory byte slice. + pub fn parse_partition(data: &[u8]) -> Result { + partition::parser::parse_binary_data(data) + } + + /// Parse an NVS partition binary file at the given `path`. + pub fn parse_partition_file>(path: P) -> Result { + partition::parser::parse_binary(path) + } +} diff --git a/esp-nvs-partition-tool/src/partition.rs b/esp-nvs-partition-tool/src/partition.rs new file mode 100644 index 0000000..8ed1ec4 --- /dev/null +++ b/esp-nvs-partition-tool/src/partition.rs @@ -0,0 +1,202 @@ +pub mod crc; + +pub(crate) mod consts; +pub(crate) mod generator; +pub(crate) mod parser; + +use std::path::PathBuf; + +pub use consts::FLASH_SECTOR_SIZE; + +use crate::error::Error; + +/// Maximum Key length is 15 bytes + 1 byte for the null terminator. +pub const MAX_KEY_LENGTH: usize = 15; + +/// A single NVS key-value entry belonging to a namespace. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NvsEntry { + /// The namespace this entry belongs to (max 15 bytes). + pub namespace: String, + /// The key identifying this entry within its namespace (max 15 bytes). + pub key: String, + /// The payload — either inline data or a reference to an external file. + pub content: EntryContent, +} + +/// The content of an NVS entry — either inline data or a file reference. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EntryContent { + /// Inline data whose encoding is determined by the [`DataValue`] variant. + Data(DataValue), + /// A reference to a file whose content will be read at generation time. + File { + /// How the file content is interpreted. + encoding: FileEncoding, + /// Path to the file (resolved relative to the CSV location). + file_path: PathBuf, + }, +} + +/// The encoding used to interpret file content for NVS file entries. +/// +/// `String` reads the file as UTF-8 text. `Hex2Bin` decodes hex-encoded +/// content. `Base64` decodes base64-encoded content. `Binary` uses the +/// raw bytes directly. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FileEncoding { + /// UTF-8 text. + String, + /// Hex-encoded binary data. + Hex2Bin, + /// Base64-encoded binary data. + Base64, + /// Raw binary data. + Binary, +} + +impl std::str::FromStr for FileEncoding { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "string" => Ok(Self::String), + "hex2bin" => Ok(Self::Hex2Bin), + "base64" => Ok(Self::Base64), + "binary" => Ok(Self::Binary), + _ => Err(Error::InvalidEncoding(s.to_string())), + } + } +} + +impl FileEncoding { + /// Return the encoding name as a static string slice. + pub fn as_str(&self) -> &'static str { + match self { + Self::String => "string", + Self::Hex2Bin => "hex2bin", + Self::Base64 => "base64", + Self::Binary => "binary", + } + } +} + +impl std::fmt::Display for FileEncoding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// A concrete data value stored in an NVS entry. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DataValue { + /// Unsigned 8-bit integer. + U8(u8), + /// Signed 8-bit integer. + I8(i8), + /// Unsigned 16-bit integer. + U16(u16), + /// Signed 16-bit integer. + I16(i16), + /// Unsigned 32-bit integer. + U32(u32), + /// Signed 32-bit integer. + I32(i32), + /// Unsigned 64-bit integer. + U64(u64), + /// Signed 64-bit integer. + I64(i64), + /// UTF-8 string (without null terminator). + String(String), + /// Opaque byte blob. + Binary(Vec), +} + +impl DataValue { + /// Return the CSV encoding column string for this value. + /// + /// `Binary` maps to `"base64"` because blobs parsed from a binary + /// partition have no original CSV encoding, and the ESP-IDF convention + /// is base64. + pub fn encoding_str(&self) -> &'static str { + match self { + Self::U8(_) => "u8", + Self::I8(_) => "i8", + Self::U16(_) => "u16", + Self::I16(_) => "i16", + Self::U32(_) => "u32", + Self::I32(_) => "i32", + Self::U64(_) => "u64", + Self::I64(_) => "i64", + Self::String(_) => "string", + Self::Binary(_) => "base64", + } + } +} + +impl std::fmt::Display for DataValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::U8(v) => write!(f, "{v}"), + Self::I8(v) => write!(f, "{v}"), + Self::U16(v) => write!(f, "{v}"), + Self::I16(v) => write!(f, "{v}"), + Self::U32(v) => write!(f, "{v}"), + Self::I32(v) => write!(f, "{v}"), + Self::U64(v) => write!(f, "{v}"), + Self::I64(v) => write!(f, "{v}"), + Self::String(s) => f.write_str(s), + Self::Binary(b) => { + use base64::Engine; + f.write_str(&base64::engine::general_purpose::STANDARD.encode(b)) + } + } + } +} + +impl NvsEntry { + /// Create a new entry with inline data. + /// + /// The encoding is derived automatically from the [`DataValue`] variant. + pub fn new_data(namespace: String, key: String, value: DataValue) -> Self { + Self { + namespace, + key, + content: EntryContent::Data(value), + } + } + + /// Create a new entry that references an external file. + /// + /// The file content will be read and converted according to `encoding` + /// at partition generation time. + pub fn new_file( + namespace: String, + key: String, + encoding: FileEncoding, + file_path: PathBuf, + ) -> Self { + Self { + namespace, + key, + content: EntryContent::File { + encoding, + file_path, + }, + } + } +} + +/// Validate that `key` is non-empty and within the NVS maximum key length. +pub(crate) fn validate_key(key: &str) -> Result<(), Error> { + if key.is_empty() { + return Err(Error::InvalidKey("key must not be empty".to_string())); + } + if key.len() > MAX_KEY_LENGTH { + return Err(Error::InvalidKey(format!( + "key '{}' is too long (max {} characters)", + key, MAX_KEY_LENGTH + ))); + } + Ok(()) +} diff --git a/esp-nvs-partition-tool/src/partition/consts.rs b/esp-nvs-partition-tool/src/partition/consts.rs new file mode 100644 index 0000000..7b74cd5 --- /dev/null +++ b/esp-nvs-partition-tool/src/partition/consts.rs @@ -0,0 +1,36 @@ +// NVS page layout +pub const FLASH_SECTOR_SIZE: usize = 4096; +pub const PAGE_HEADER_SIZE: usize = 32; +pub const ENTRY_STATE_BITMAP_SIZE: usize = 32; +pub const ENTRY_SIZE: usize = 32; +pub const ENTRIES_PER_PAGE: usize = 126; + +// Page states +pub const PAGE_STATE_ACTIVE: u32 = 0xFFFFFFFE; +pub const PAGE_STATE_FULL: u32 = 0xFFFFFFFC; +pub const PAGE_STATE_FREEING: u32 = 0xFFFFFFF8; +pub const PAGE_STATE_CORRUPT: u32 = 0x00000000; + +// Entry types from ESP-IDF +pub const ITEM_TYPE_U8: u8 = 0x01; +pub const ITEM_TYPE_I8: u8 = 0x11; +pub const ITEM_TYPE_U16: u8 = 0x02; +pub const ITEM_TYPE_I16: u8 = 0x12; +pub const ITEM_TYPE_U32: u8 = 0x04; +pub const ITEM_TYPE_I32: u8 = 0x14; +pub const ITEM_TYPE_U64: u8 = 0x08; +pub const ITEM_TYPE_I64: u8 = 0x18; +pub const ITEM_TYPE_SIZED: u8 = 0x21; +pub const ITEM_TYPE_BLOB: u8 = 0x41; // Legacy single-page blob (version 1 format) +pub const ITEM_TYPE_BLOB_INDEX: u8 = 0x48; +pub const ITEM_TYPE_BLOB_DATA: u8 = 0x42; + +// Reserved value for unused fields +pub const RESERVED_U16: u16 = 0xFFFF; + +// Entry states +pub const ENTRY_STATE_WRITTEN: u8 = 0b10; + +// Maximum data bytes per BLOB_DATA chunk. +// Each chunk uses one header entry + up to (ENTRIES_PER_PAGE - 1) data entries. +pub const MAX_DATA_PER_CHUNK: usize = (ENTRIES_PER_PAGE - 1) * ENTRY_SIZE; // 4000 bytes diff --git a/esp-nvs-partition-tool/src/partition/crc.rs b/esp-nvs-partition-tool/src/partition/crc.rs new file mode 100644 index 0000000..9714b10 --- /dev/null +++ b/esp-nvs-partition-tool/src/partition/crc.rs @@ -0,0 +1,39 @@ +/// Compute an NVS entry CRC over all bytes except the CRC field at offset 4..8. +/// +/// # Panics +/// Panics if `entry_data` is shorter than 32 bytes. +pub fn crc32_entry(entry_data: &[u8]) -> u32 { + assert!( + entry_data.len() >= 32, + "crc32_entry requires at least 32 bytes, got {}", + entry_data.len() + ); + let mut combined = [0u8; 28]; + combined[..4].copy_from_slice(&entry_data[0..4]); + combined[4..].copy_from_slice(&entry_data[8..32]); + crc32(&combined) +} + +/// CRC32 using the IEEE 802.3 polynomial (0xEDB88320, bit-reversed 0x04C11DB7). +/// +/// This matches the CRC32 algorithm used by ESP-IDF for NVS entry and page +/// header checksums. +/// +/// This function is intentionally public so that callers can verify or compute +/// CRCs over NVS data independently of the higher-level partition APIs. +pub fn crc32(data: &[u8]) -> u32 { + let mut crc: u32 = 0xFFFFFFFF; + + for &byte in data { + crc ^= byte as u32; + for _ in 0..8 { + if crc & 1 != 0 { + crc = (crc >> 1) ^ 0xEDB88320; + } else { + crc >>= 1; + } + } + } + + !crc +} diff --git a/esp-nvs-partition-tool/src/partition/generator.rs b/esp-nvs-partition-tool/src/partition/generator.rs new file mode 100644 index 0000000..f9ece02 --- /dev/null +++ b/esp-nvs-partition-tool/src/partition/generator.rs @@ -0,0 +1,478 @@ +use std::collections::HashMap; +use std::fs::read; + +use base64::Engine; + +use super::{ + validate_key, + DataValue, + EntryContent, + FileEncoding, +}; +use crate::error::Error; +use crate::partition::consts::*; +use crate::partition::crc::{ + crc32, + crc32_entry, +}; +use crate::NvsPartition; + +/// Generate an NVS partition binary in memory and return it as a `Vec`. +/// +/// `size` must be a multiple of 4096 (the ESP-IDF flash sector size). +pub(crate) fn generate_partition_data( + partition: &NvsPartition, + size: usize, +) -> Result, Error> { + if size < FLASH_SECTOR_SIZE { + return Err(Error::PartitionTooSmall(size)); + } else if !size.is_multiple_of(FLASH_SECTOR_SIZE) { + return Err(Error::InvalidPartitionSize(size)); + } + + let mut writer = PartitionWriter::new(size); + let mut namespace_map: HashMap = HashMap::new(); + let mut namespace_counter: u8 = 0; + + for entry in &partition.entries { + // Ensure the entry's namespace is registered in the binary + let ns_index = match namespace_map.get(&entry.namespace) { + Some(&id) => id, + None => { + // Register new namespace + namespace_counter = namespace_counter + .checked_add(1) + .ok_or(Error::TooManyNamespaces)?; + namespace_map.insert(entry.namespace.clone(), namespace_counter); + + // Write namespace entry to binary + if writer.current_entry >= ENTRIES_PER_PAGE { + writer.advance_page()?; + } + + writer.write_namespace_entry(&entry.namespace, namespace_counter)?; + + namespace_counter + } + }; + + // Resolve the value from the entry content. + // For file entries, read the file and convert to a DataValue at generation time. + let resolved_value; + let value = match &entry.content { + EntryContent::Data(val) => val, + EntryContent::File { + encoding, + file_path, + } => { + let content = read(file_path)?; + resolved_value = parse_file_content(&content, encoding)?; + &resolved_value + } + }; + + // Compute how many entries must fit on the current page. + // Primitives need 1; strings need header + data entries (all on one + // page); blobs only need the BLOB_INDEX entry here — BLOB_DATA + // handles page spanning internally. + let page_space_needed = match value { + DataValue::Binary(_) => 1, + DataValue::String(s) => 1 + (s.len() + 1).div_ceil(ENTRY_SIZE), + _ => 1, // primitives + }; + + if writer.current_entry + page_space_needed > ENTRIES_PER_PAGE { + writer.advance_page()?; + } + + writer.write_data_entry(ns_index, &entry.key, value)?; + } + + // Mark the last page as full only if it has no remaining free entries + if writer.current_entry >= ENTRIES_PER_PAGE { + write_page_header( + &mut writer.data, + writer.current_page, + page_seq(writer.current_page)?, + PAGE_STATE_FULL, + ); + } + + Ok(writer.data) +} + +struct PartitionWriter { + data: Vec, + current_page: usize, + current_entry: usize, + num_pages: usize, +} + +impl PartitionWriter { + fn new(size: usize) -> Self { + let num_pages = size / FLASH_SECTOR_SIZE; + let mut data = vec![0xFF; size]; + + // Initialize first page header + write_page_header(&mut data, 0, 0, PAGE_STATE_ACTIVE); + + Self { + data, + current_page: 0, + current_entry: 0, + num_pages, + } + } + + fn advance_page(&mut self) -> Result<(), Error> { + write_page_header( + &mut self.data, + self.current_page, + page_seq(self.current_page)?, + PAGE_STATE_FULL, + ); + + self.current_page += 1; + if self.current_page >= self.num_pages { + return Err(Error::PartitionTooSmall(self.num_pages * FLASH_SECTOR_SIZE)); + } + + write_page_header( + &mut self.data, + self.current_page, + page_seq(self.current_page)?, + PAGE_STATE_ACTIVE, + ); + + self.current_entry = 0; + + Ok(()) + } + + fn write_namespace_entry(&mut self, key: &str, namespace_index: u8) -> Result<(), Error> { + let mut data = [0xFF_u8; 8]; + data[0] = namespace_index; + self.write_entry_header(0, ITEM_TYPE_U8, 1, 0xFF, key, &data) + } + + /// Write a single 32-byte NVS entry. The caller provides the 8-byte data + /// field; this method handles the entry state bitmap, header bytes, key, + /// CRC, and entry-index advance. + fn write_entry_header( + &mut self, + namespace_index: u8, + item_type: u8, + span: u8, + chunk_index: u8, + key: &str, + data: &[u8; 8], + ) -> Result<(), Error> { + let offset = calc_entry_offset(self.current_page, self.current_entry); + + set_entry_state( + &mut self.data, + self.current_page, + self.current_entry, + ENTRY_STATE_WRITTEN, + ); + + self.data[offset] = namespace_index; + self.data[offset + 1] = item_type; + self.data[offset + 2] = span; + self.data[offset + 3] = chunk_index; + + write_key(&mut self.data[offset + 8..offset + 24], key)?; + self.data[offset + 24..offset + 32].copy_from_slice(data); + + let entry_crc = crc32_entry(&self.data[offset..offset + ENTRY_SIZE]); + self.data[offset + 4..offset + 8].copy_from_slice(&entry_crc.to_le_bytes()); + + self.current_entry += 1; + Ok(()) + } + + /// Write raw bytes across consecutive entry slots, marking each as written. + fn write_data_entries(&mut self, bytes: &[u8]) { + for (i, chunk) in bytes.chunks(ENTRY_SIZE).enumerate() { + let entry_idx = self.current_entry + i; + set_entry_state( + &mut self.data, + self.current_page, + entry_idx, + ENTRY_STATE_WRITTEN, + ); + let offset = calc_entry_offset(self.current_page, entry_idx); + self.data[offset..offset + chunk.len()].copy_from_slice(chunk); + } + self.current_entry += bytes.len().div_ceil(ENTRY_SIZE); + } + + fn write_data_entry( + &mut self, + namespace_index: u8, + key: &str, + value: &DataValue, + ) -> Result<(), Error> { + match value { + DataValue::U8(_) + | DataValue::I8(_) + | DataValue::U16(_) + | DataValue::I16(_) + | DataValue::U32(_) + | DataValue::I32(_) + | DataValue::U64(_) + | DataValue::I64(_) => { + self.write_primitive_entry(namespace_index, key, value)?; + } + DataValue::String(s) => { + let mut bytes = s.as_bytes().to_vec(); + + // ESP-IDF stores strings with a null terminator included in the size + bytes.push(0); + + // Strings always use SIZED type (0x21) and must fit on a single page + const MAX_STRING_SIZE: usize = (ENTRIES_PER_PAGE - 1) * ENTRY_SIZE; // 4000 bytes + if bytes.len() > MAX_STRING_SIZE { + return Err(Error::InvalidValue(format!( + "string for key '{}' is too large ({} bytes, max {})", + key, + bytes.len(), + MAX_STRING_SIZE + ))); + } + + self.write_sized_entry(namespace_index, key, &bytes)?; + } + DataValue::Binary(b) => { + self.write_blob_entries(namespace_index, key, b)?; + } + } + + Ok(()) + } + + fn write_primitive_entry( + &mut self, + namespace_index: u8, + key: &str, + value: &DataValue, + ) -> Result<(), Error> { + let item_type = match value { + DataValue::U8(_) => ITEM_TYPE_U8, + DataValue::I8(_) => ITEM_TYPE_I8, + DataValue::U16(_) => ITEM_TYPE_U16, + DataValue::I16(_) => ITEM_TYPE_I16, + DataValue::U32(_) => ITEM_TYPE_U32, + DataValue::I32(_) => ITEM_TYPE_I32, + DataValue::U64(_) => ITEM_TYPE_U64, + DataValue::I64(_) => ITEM_TYPE_I64, + _ => unreachable!("write_primitive_entry called with non-primitive DataValue"), + }; + + let mut data = [0xFF_u8; 8]; + match value { + DataValue::U8(v) => data[0] = *v, + DataValue::I8(v) => data[0] = *v as u8, + DataValue::U16(v) => data[..2].copy_from_slice(&v.to_le_bytes()), + DataValue::I16(v) => data[..2].copy_from_slice(&v.to_le_bytes()), + DataValue::U32(v) => data[..4].copy_from_slice(&v.to_le_bytes()), + DataValue::I32(v) => data[..4].copy_from_slice(&v.to_le_bytes()), + DataValue::U64(v) => data.copy_from_slice(&v.to_le_bytes()), + DataValue::I64(v) => data.copy_from_slice(&v.to_le_bytes()), + _ => unreachable!("write_primitive_entry called with non-primitive DataValue"), + } + + self.write_entry_header(namespace_index, item_type, 1, 0xFF, key, &data) + } + + fn write_sized_entry( + &mut self, + namespace_index: u8, + key: &str, + bytes: &[u8], + ) -> Result<(), Error> { + let num_data_entries = bytes.len().div_ceil(ENTRY_SIZE); + let span = u8::try_from(1 + num_data_entries).map_err(|_| { + Error::InvalidValue(format!( + "SIZED entry span {} exceeds u8 maximum", + 1 + num_data_entries + )) + })?; + + let data = build_sized_data_field(bytes)?; + self.write_entry_header(namespace_index, ITEM_TYPE_SIZED, span, 0xFF, key, &data)?; + self.write_data_entries(bytes); + Ok(()) + } + + fn write_blob_entries( + &mut self, + namespace_index: u8, + key: &str, + bytes: &[u8], + ) -> Result<(), Error> { + let chunk_count = bytes.len().div_ceil(MAX_DATA_PER_CHUNK); + let chunk_count_u8 = u8::try_from(chunk_count).map_err(|_| { + Error::InvalidValue(format!( + "blob for key '{}' requires {} chunks, exceeding the maximum of 255", + key, chunk_count + )) + })?; + + // Ensure BLOB_INDEX entry fits on current page + if self.current_entry >= ENTRIES_PER_PAGE { + self.advance_page()?; + } + + // Write BLOB_INDEX entry + let blob_size_u32 = u32::try_from(bytes.len()).map_err(|_| { + Error::InvalidValue(format!( + "blob for key '{}' is too large ({} bytes, max {})", + key, + bytes.len(), + u32::MAX + )) + })?; + let mut index_data = [0xFF_u8; 8]; + index_data[..4].copy_from_slice(&blob_size_u32.to_le_bytes()); + index_data[4] = chunk_count_u8; + index_data[5] = 0; // chunk_start + self.write_entry_header( + namespace_index, + ITEM_TYPE_BLOB_INDEX, + 1, + 0, + key, + &index_data, + )?; + + // Write BLOB_DATA chunks, spanning pages as needed + for (chunk_idx, chunk_data) in bytes.chunks(MAX_DATA_PER_CHUNK).enumerate() { + let num_data_entries = chunk_data.len().div_ceil(ENTRY_SIZE); + let chunk_span = 1 + num_data_entries; + + if self.current_entry + chunk_span > ENTRIES_PER_PAGE { + self.advance_page()?; + } + + let span = u8::try_from(chunk_span).map_err(|_| { + Error::InvalidValue(format!( + "BLOB_DATA chunk span {} for key '{}' exceeds u8 maximum", + chunk_span, key + )) + })?; + + let chunk_idx_u8 = u8::try_from(chunk_idx).map_err(|_| { + Error::InvalidValue(format!( + "BLOB_DATA chunk index {} for key '{}' exceeds u8 maximum", + chunk_idx, key + )) + })?; + + let data = build_sized_data_field(chunk_data)?; + self.write_entry_header( + namespace_index, + ITEM_TYPE_BLOB_DATA, + span, + chunk_idx_u8, + key, + &data, + )?; + self.write_data_entries(chunk_data); + } + + Ok(()) + } +} + +/// Build the 8-byte data field for SIZED and BLOB_DATA entries: +/// `[size:u16, reserved:u16, crc32:u32]`. +fn build_sized_data_field(bytes: &[u8]) -> Result<[u8; 8], Error> { + let size = u16::try_from(bytes.len()).map_err(|_| { + Error::InvalidValue(format!("data size {} exceeds u16 maximum", bytes.len())) + })?; + let mut data = [0u8; 8]; + data[..2].copy_from_slice(&size.to_le_bytes()); + data[2..4].copy_from_slice(&RESERVED_U16.to_le_bytes()); + let crc = crc32(bytes); + data[4..].copy_from_slice(&crc.to_le_bytes()); + Ok(data) +} + +fn page_seq(page_index: usize) -> Result { + u32::try_from(page_index) + .map_err(|_| Error::InvalidValue(format!("page index {} exceeds u32 range", page_index))) +} + +fn calc_entry_offset(page_index: usize, entry_index: usize) -> usize { + page_index * FLASH_SECTOR_SIZE + + PAGE_HEADER_SIZE + + ENTRY_STATE_BITMAP_SIZE + + (entry_index * ENTRY_SIZE) +} + +fn write_page_header(data: &mut [u8], page_index: usize, sequence: u32, state: u32) { + let offset = page_index * FLASH_SECTOR_SIZE; + + // Write state + data[offset..offset + 4].copy_from_slice(&state.to_le_bytes()); + + // Write sequence number + data[offset + 4..offset + 8].copy_from_slice(&sequence.to_le_bytes()); + + // Write version (0xFE for NVS format - used by ESP-IDF) + data[offset + 8] = 0xFE; + + // Reserved bytes (19 bytes) are already 0xFF + + // Calculate and write CRC32 + let crc = crc32(&data[offset + 4..offset + 28]); + data[offset + 28..offset + 32].copy_from_slice(&crc.to_le_bytes()); +} + +fn write_key(dest: &mut [u8], key: &str) -> Result<(), Error> { + validate_key(key)?; + + let key_bytes = key.as_bytes(); + dest[..key_bytes.len()].copy_from_slice(key_bytes); + // Null-terminate and zero-fill the rest (ESP-IDF format uses zeros, not 0xFF) + dest[key_bytes.len()..16].fill(0); + + Ok(()) +} + +fn set_entry_state(data: &mut [u8], page_index: usize, entry_index: usize, state: u8) { + let page_offset = page_index * FLASH_SECTOR_SIZE; + let bitmap_offset = page_offset + PAGE_HEADER_SIZE; + + let byte_index = entry_index / 4; + let bit_offset = (entry_index % 4) * 2; + + let mut byte = data[bitmap_offset + byte_index]; + byte &= !(0b11 << bit_offset); // Clear the 2 bits + byte |= state << bit_offset; // Set the state + data[bitmap_offset + byte_index] = byte; +} + +fn parse_file_content(content: &[u8], encoding: &FileEncoding) -> Result { + match encoding { + FileEncoding::String => { + let s = std::str::from_utf8(content) + .map_err(|e| Error::InvalidValue(format!("invalid UTF-8 in file: {}", e)))?; + Ok(DataValue::String(s.to_string())) + } + FileEncoding::Hex2Bin => { + let hex_str = std::str::from_utf8(content) + .map_err(|e| Error::InvalidValue(format!("invalid UTF-8 in hex file: {}", e)))?; + let bytes = hex::decode(hex_str.trim())?; + Ok(DataValue::Binary(bytes)) + } + FileEncoding::Base64 => { + let b64_str = std::str::from_utf8(content) + .map_err(|e| Error::InvalidValue(format!("invalid UTF-8 in base64 file: {}", e)))?; + let bytes = base64::engine::general_purpose::STANDARD.decode(b64_str.trim())?; + Ok(DataValue::Binary(bytes)) + } + FileEncoding::Binary => Ok(DataValue::Binary(content.to_vec())), + } +} diff --git a/esp-nvs-partition-tool/src/partition/parser.rs b/esp-nvs-partition-tool/src/partition/parser.rs new file mode 100644 index 0000000..8463197 --- /dev/null +++ b/esp-nvs-partition-tool/src/partition/parser.rs @@ -0,0 +1,508 @@ +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +use super::{ + DataValue, + NvsEntry, + MAX_KEY_LENGTH, +}; +use crate::error::Error; +use crate::partition::consts::*; +use crate::partition::crc::{ + crc32, + crc32_entry, +}; +use crate::NvsPartition; + +#[derive(Clone, PartialEq, Eq, Hash)] +struct BlobKey { + namespace_id: u8, + key: String, +} + +struct BlobChunk { + chunk_index: u8, + data: Vec, +} + +struct BlobInfo { + size: u32, + chunk_count: u8, +} + +/// Page-level context shared across entry-parsing helpers. +struct PageContext<'a> { + data: &'a [u8], + bitmap_offset: usize, + entries_offset: usize, + page_idx: usize, +} + +/// Parse an NVS partition binary file at the given `path`. +pub(crate) fn parse_binary>(path: P) -> Result { + let data = fs::read(path)?; + parse_binary_data(&data) +} + +/// Parse an NVS partition binary from an in-memory byte slice. +pub(crate) fn parse_binary_data(data: &[u8]) -> Result { + if data.is_empty() { + return Err(Error::InvalidValue( + "binary data is empty; an NVS partition requires at least one page (4096 bytes)" + .to_string(), + )); + } + + if !data.len().is_multiple_of(FLASH_SECTOR_SIZE) { + return Err(Error::InvalidValue(format!( + "binary size {} is not a multiple of page size {}", + data.len(), + FLASH_SECTOR_SIZE + ))); + } + + let mut partition = NvsPartition { entries: vec![] }; + let num_pages = data.len() / FLASH_SECTOR_SIZE; + + // Collect blob data: (namespace_id, key) -> Vec of (chunk_index, data) + let mut blob_data_chunks: HashMap> = HashMap::new(); + let mut blob_indices: HashMap = HashMap::new(); + let mut blob_positions: HashMap = HashMap::new(); + // Map namespace binary indices to their names + let mut namespace_names: HashMap = HashMap::new(); + + // First pass: collect all entries + for page_idx in 0..num_pages { + let page_offset = page_idx * FLASH_SECTOR_SIZE; + let page_data = &data[page_offset..page_offset + FLASH_SECTOR_SIZE]; + + // Parse page header + let state = read_u32(page_data, 0); + + // Skip uninitialized pages + if state == 0xFFFFFFFF { + continue; + } + + // Skip pages that are being freed (compaction in progress) + if state == PAGE_STATE_FREEING { + continue; + } + + if state == PAGE_STATE_CORRUPT { + return Err(Error::InvalidValue(format!( + "corrupt page detected at page {}", + page_idx + ))); + } + + // Only Active and Full pages contain valid data + if state != PAGE_STATE_ACTIVE && state != PAGE_STATE_FULL { + return Err(Error::InvalidValue(format!( + "unknown page state 0x{:08x} at page {}", + state, page_idx + ))); + } + + // Validate page version byte (must be 0xFE for NVS format version 2) + let version = page_data[8]; + if version != 0xFE { + return Err(Error::InvalidValue(format!( + "unsupported NVS page version 0x{:02x} at page {} (expected 0xFE)", + version, page_idx + ))); + } + + // Validate page header CRC (stored at offset 28, computed over bytes 4..28) + let stored_header_crc = read_u32(page_data, 28); + let computed_header_crc = crc32(&page_data[4..28]); + if stored_header_crc != computed_header_crc { + return Err(Error::InvalidValue(format!( + "page header CRC mismatch at page {}: stored 0x{:08x}, computed 0x{:08x}", + page_idx, stored_header_crc, computed_header_crc + ))); + } + + // Parse entries + let page = PageContext { + data: page_data, + bitmap_offset: PAGE_HEADER_SIZE, + entries_offset: PAGE_HEADER_SIZE + ENTRY_STATE_BITMAP_SIZE, + page_idx, + }; + + let mut entry_idx = 0; + while entry_idx < ENTRIES_PER_PAGE { + // Check if entry is written + let bitmap_byte_idx = entry_idx / 4; + let bitmap_bit_offset = (entry_idx % 4) * 2; + let bitmap_byte = page.data[page.bitmap_offset + bitmap_byte_idx]; + let entry_state = (bitmap_byte >> bitmap_bit_offset) & 0b11; + + if entry_state != ENTRY_STATE_WRITTEN { + entry_idx += 1; + continue; + } + + let entry_offset = page.entries_offset + (entry_idx * ENTRY_SIZE); + let entry_data = &page.data[entry_offset..entry_offset + ENTRY_SIZE]; + + let namespace_idx = entry_data[0]; + let item_type = entry_data[1]; + let span = entry_data[2]; + let chunk_index = entry_data[3]; + + // Extract key (16 bytes, null-terminated) + let key_bytes = &entry_data[8..24]; + let key = extract_key(key_bytes)?; + + // Extract data field (8 bytes) + let data_field = &entry_data[24..32]; + + // Validate entry CRC (stored at bytes 4..8, computed over 0..4 + 8..32) + let stored_entry_crc = read_u32(entry_data, 4); + let computed_entry_crc = crc32_entry(entry_data); + if stored_entry_crc != computed_entry_crc { + return Err(Error::InvalidValue(format!( + "entry CRC mismatch at page {}, entry {}: stored 0x{:08x}, computed 0x{:08x}", + page.page_idx, entry_idx, stored_entry_crc, computed_entry_crc + ))); + } + + match item_type { + ITEM_TYPE_U8 if namespace_idx == 0 => { + // This is a namespace entry — record the index-to-name mapping + let ns_id = data_field[0]; + if let Some(existing) = namespace_names.get(&ns_id) { + return Err(Error::InvalidValue(format!( + "duplicate namespace index {} at page {}, entry {}: '{}' conflicts with '{}'", + ns_id, page.page_idx, entry_idx, key, existing + ))); + } + namespace_names.insert(ns_id, key); + entry_idx += 1; + } + t @ (ITEM_TYPE_U8 | ITEM_TYPE_I8 | ITEM_TYPE_U16 | ITEM_TYPE_I16 + | ITEM_TYPE_U32 | ITEM_TYPE_I32 | ITEM_TYPE_U64 | ITEM_TYPE_I64) => { + let ns = resolve_namespace(&namespace_names, namespace_idx)?; + let value = decode_primitive(data_field, t); + partition.entries.push(NvsEntry::new_data(ns, key, value)); + entry_idx += 1; + } + ITEM_TYPE_SIZED => { + // ITEM_TYPE_SIZED (0x21) is always a null-terminated string + // (SZ type) in the ESP-IDF NVS format. + let ns = resolve_namespace(&namespace_names, namespace_idx)?; + let data = read_span_data(&page, entry_idx, span, data_field, &key, "SIZED")?; + + let s = std::str::from_utf8(&data).map_err(|e| { + Error::InvalidValue(format!( + "invalid UTF-8 in string entry '{}': {}", + key, e + )) + })?; + + partition.entries.push(NvsEntry::new_data( + ns, + key, + DataValue::String(s.trim_end_matches('\0').to_string()), + )); + + entry_idx += span as usize; + } + ITEM_TYPE_BLOB => { + // ITEM_TYPE_BLOB (0x41) is a legacy single-page blob + // (version 1 format). Same structure as SIZED but always + // contains binary data, not a string. + let ns = resolve_namespace(&namespace_names, namespace_idx)?; + let data = + read_span_data(&page, entry_idx, span, data_field, &key, "legacy BLOB")?; + + partition + .entries + .push(NvsEntry::new_data(ns, key, DataValue::Binary(data))); + + entry_idx += span as usize; + } + ITEM_TYPE_BLOB_INDEX => { + let ns = resolve_namespace(&namespace_names, namespace_idx)?; + + // BLOB_INDEX entries must always have span = 1 + if span != 1 { + return Err(Error::InvalidValue(format!( + "invalid span {} for BLOB_INDEX entry at page {}, entry {} (expected 1)", + span, page.page_idx, entry_idx + ))); + } + + // Record blob index information + let blob_size = read_u32(data_field, 0); + let chunk_count = data_field[4]; + + let blob_key = BlobKey { + namespace_id: namespace_idx, + key: key.clone(), + }; + if blob_indices.contains_key(&blob_key) { + return Err(Error::InvalidValue(format!( + "duplicate BLOB_INDEX for key '{}' at page {}, entry {}", + key, page.page_idx, entry_idx + ))); + } + blob_indices.insert( + blob_key.clone(), + BlobInfo { + size: blob_size, + chunk_count, + }, + ); + + // Insert a placeholder entry at this position. The second + // pass will replace it with the fully assembled blob data + // once all chunks have been collected across pages. + blob_positions.insert(blob_key, partition.entries.len()); + partition.entries.push(NvsEntry::new_data( + ns, + key, + DataValue::Binary(Vec::new()), + )); + + entry_idx += 1; + } + ITEM_TYPE_BLOB_DATA => { + // Collect blob data chunk + let blob_key = BlobKey { + namespace_id: namespace_idx, + key: key.clone(), + }; + let data = + read_span_data(&page, entry_idx, span, data_field, &key, "BLOB_DATA")?; + + blob_data_chunks + .entry(blob_key) + .or_default() + .push(BlobChunk { chunk_index, data }); + + entry_idx += span as usize; + } + _ => { + return Err(Error::InvalidValue(format!( + "unknown item type 0x{:02x} at page {}, entry {}", + item_type, page.page_idx, entry_idx + ))); + } + } + } + } + + // Second pass: assemble blob entries that have indices and replace placeholders + for (blob_key, info) in &blob_indices { + if let Some(mut chunks) = blob_data_chunks.remove(blob_key) { + // Verify the number of collected chunks matches the index header + if chunks.len() != info.chunk_count as usize { + return Err(Error::InvalidValue(format!( + "BLOB_INDEX for key '{}' expects {} chunks but {} were found", + blob_key.key, + info.chunk_count, + chunks.len() + ))); + } + + // Sort chunks by index + chunks.sort_by_key(|c| c.chunk_index); + + // Concatenate chunk data + let mut blob_data = Vec::new(); + for chunk in chunks { + blob_data.extend_from_slice(&chunk.data); + } + + // Trim to actual size + blob_data.truncate(info.size as usize); + + // Replace the placeholder entry at its original position + if let Some(&pos) = blob_positions.get(blob_key) { + let ns = resolve_namespace(&namespace_names, blob_key.namespace_id)?; + partition.entries[pos] = + NvsEntry::new_data(ns, blob_key.key.clone(), DataValue::Binary(blob_data)); + } + } else if info.size > 0 { + return Err(Error::InvalidValue(format!( + "BLOB_INDEX for key '{}' references {} bytes but no BLOB_DATA chunks were found", + blob_key.key, info.size + ))); + } + } + + // Check for orphaned BLOB_DATA chunks with no matching BLOB_INDEX + if !blob_data_chunks.is_empty() { + let orphaned_keys: Vec = blob_data_chunks.keys().map(|k| k.key.clone()).collect(); + return Err(Error::InvalidValue(format!( + "found BLOB_DATA chunks with no matching BLOB_INDEX for keys: {}", + orphaned_keys.join(", ") + ))); + } + + Ok(partition) +} + +fn resolve_namespace( + namespace_names: &HashMap, + namespace_idx: u8, +) -> Result { + namespace_names + .get(&namespace_idx) + .cloned() + .ok_or_else(|| Error::InvalidValue(format!("unknown namespace index {}", namespace_idx))) +} + +fn extract_key(key_bytes: &[u8]) -> Result { + // Find the null terminator + let key_len = key_bytes + .iter() + .position(|&b| b == 0) + .unwrap_or(key_bytes.len()); + + // ESP-IDF NVS keys must be 1-15 characters; an empty or overlong key indicates corruption. + // Reject keys that are empty, exceed MAX_KEY_LENGTH, or occupy the full field without a + // null terminator (the latter is covered by key_len > MAX_KEY_LENGTH, since the field + // size is MAX_KEY_LENGTH + 1). + if key_len == 0 || key_len > MAX_KEY_LENGTH { + return Err(Error::InvalidKey( + "entry has an invalid key length".to_string(), + )); + } + + let key_str = std::str::from_utf8(&key_bytes[..key_len]).map_err(|e| { + Error::InvalidValue(format!( + "invalid UTF-8 in key (bytes: {:?}): {}", + &key_bytes[..key_len.min(16)], + e + )) + })?; + + Ok(key_str.to_string()) +} + +/// Decode a primitive value from the 8-byte data field of an entry. +fn decode_primitive(data_field: &[u8], item_type: u8) -> DataValue { + match item_type { + ITEM_TYPE_U8 => DataValue::U8(data_field[0]), + ITEM_TYPE_I8 => DataValue::I8(data_field[0] as i8), + ITEM_TYPE_U16 => DataValue::U16(read_u16(data_field, 0)), + ITEM_TYPE_I16 => DataValue::I16(read_u16(data_field, 0) as i16), + ITEM_TYPE_U32 => DataValue::U32(read_u32(data_field, 0)), + ITEM_TYPE_I32 => DataValue::I32(read_u32(data_field, 0) as i32), + ITEM_TYPE_U64 => DataValue::U64(read_u64(data_field, 0)), + ITEM_TYPE_I64 => DataValue::I64(read_u64(data_field, 0) as i64), + _ => unreachable!("decode_primitive called with non-primitive item type 0x{item_type:02x}"), + } +} + +/// Read variable-length data from the sub-entries that follow a span header. +/// +/// SIZED, legacy BLOB, and BLOB_DATA entries all share the same on-disk +/// layout: a header entry whose 8-byte data field contains +/// `[size: u16, reserved: u16, crc32: u32]`, followed by `span - 1` +/// consecutive 32-byte entries holding the actual payload. This helper +/// validates the reserved field, span, bitmap states, CRC, and returns the +/// collected payload trimmed to `size`. +fn read_span_data( + page: &PageContext, + entry_idx: usize, + span: u8, + data_field: &[u8], + key: &str, + label: &str, +) -> Result, Error> { + let size = read_u16(data_field, 0) as usize; + + let reserved = read_u16(data_field, 2); + if reserved != RESERVED_U16 { + return Err(Error::InvalidValue(format!( + "unexpected reserved field 0x{:04x} in {} entry '{}' at page {}, entry {}", + reserved, label, key, page.page_idx, entry_idx + ))); + } + + if span == 0 { + return Err(Error::InvalidValue(format!( + "invalid span value 0 for {} entry at page {}, entry {}", + label, page.page_idx, entry_idx + ))); + } + + if entry_idx + span as usize > ENTRIES_PER_PAGE { + return Err(Error::InvalidValue(format!( + "{} entry span {} at page {}, entry {} exceeds page boundary", + label, span, page.page_idx, entry_idx + ))); + } + + validate_data_sub_entries(page, entry_idx, span, key, label)?; + + let num_data_entries = (span - 1) as usize; + let mut collected = Vec::with_capacity(num_data_entries * ENTRY_SIZE); + for i in 0..num_data_entries { + let data_entry_idx = entry_idx + 1 + i; + if data_entry_idx >= ENTRIES_PER_PAGE { + break; + } + let data_entry_offset = page.entries_offset + (data_entry_idx * ENTRY_SIZE); + collected.extend_from_slice(&page.data[data_entry_offset..data_entry_offset + ENTRY_SIZE]); + } + collected.truncate(size); + + let stored_crc = read_u32(data_field, 4); + let computed_crc = crc32(&collected); + if stored_crc != computed_crc { + return Err(Error::InvalidValue(format!( + "{} data CRC mismatch for key '{}': stored 0x{:08x}, computed 0x{:08x}", + label, key, stored_crc, computed_crc + ))); + } + + Ok(collected) +} + +/// Verify that every data sub-entry covered by `span` is marked Written in the +/// entry-state bitmap. Returns an error naming `label` and `key` on mismatch. +fn validate_data_sub_entries( + page: &PageContext, + entry_idx: usize, + span: u8, + key: &str, + label: &str, +) -> Result<(), Error> { + let num_data_entries = (span - 1) as usize; + for i in 0..num_data_entries { + let data_entry_idx = entry_idx + 1 + i; + if data_entry_idx >= ENTRIES_PER_PAGE { + break; + } + let sub_bitmap_byte_idx = data_entry_idx / 4; + let sub_bitmap_bit_offset = (data_entry_idx % 4) * 2; + let sub_bitmap_byte = page.data[page.bitmap_offset + sub_bitmap_byte_idx]; + let sub_entry_state = (sub_bitmap_byte >> sub_bitmap_bit_offset) & 0b11; + if sub_entry_state != ENTRY_STATE_WRITTEN { + return Err(Error::InvalidValue(format!( + "{} entry '{}' data sub-entry {} at page {} is not marked Written (state {})", + label, key, data_entry_idx, page.page_idx, sub_entry_state + ))); + } + } + Ok(()) +} + +fn read_u16(data: &[u8], offset: usize) -> u16 { + u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) +} + +fn read_u32(data: &[u8], offset: usize) -> u32 { + u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) +} + +fn read_u64(data: &[u8], offset: usize) -> u64 { + u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap()) +} diff --git a/esp-nvs-partition-tool/tests/assets/hex2bin_test.csv b/esp-nvs-partition-tool/tests/assets/hex2bin_test.csv new file mode 100644 index 0000000..d8b4d23 --- /dev/null +++ b/esp-nvs-partition-tool/tests/assets/hex2bin_test.csv @@ -0,0 +1,3 @@ +key,type,encoding,value +ns,namespace,, +data,data,hex2bin,00112233445566778899AABBCCDDEEFF diff --git a/esp-nvs-partition-tool/tests/assets/invalid_long_key.csv b/esp-nvs-partition-tool/tests/assets/invalid_long_key.csv new file mode 100644 index 0000000..1234637 --- /dev/null +++ b/esp-nvs-partition-tool/tests/assets/invalid_long_key.csv @@ -0,0 +1,2 @@ +key,type,encoding,value +verylongkeynamethatistoolongfortheformat,namespace,, diff --git a/esp-nvs-partition-tool/tests/assets/large_string.csv b/esp-nvs-partition-tool/tests/assets/large_string.csv new file mode 100644 index 0000000..64de4a8 --- /dev/null +++ b/esp-nvs-partition-tool/tests/assets/large_string.csv @@ -0,0 +1,3 @@ +key,type,encoding,value +ns,namespace,, +large,data,string,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/esp-nvs-partition-tool/tests/assets/multiple_namespaces.csv b/esp-nvs-partition-tool/tests/assets/multiple_namespaces.csv new file mode 100644 index 0000000..a6951db --- /dev/null +++ b/esp-nvs-partition-tool/tests/assets/multiple_namespaces.csv @@ -0,0 +1,7 @@ +key,type,encoding,value +ns1,namespace,, +key1,data,u8,1 +ns2,namespace,, +key2,data,u8,2 +ns1,namespace,, +key3,data,u8,3 diff --git a/esp-nvs-partition-tool/tests/assets/roundtrip_basic.csv b/esp-nvs-partition-tool/tests/assets/roundtrip_basic.csv new file mode 100644 index 0000000..02199ed --- /dev/null +++ b/esp-nvs-partition-tool/tests/assets/roundtrip_basic.csv @@ -0,0 +1,5 @@ +key,type,encoding,value +test_namespace,namespace,, +u8_val,data,u8,42 +i32_val,data,i32,-12345 +test_str,data,string,Hello World diff --git a/esp-nvs-partition-tool/tests/generate.rs b/esp-nvs-partition-tool/tests/generate.rs new file mode 100644 index 0000000..7093727 --- /dev/null +++ b/esp-nvs-partition-tool/tests/generate.rs @@ -0,0 +1,92 @@ +use std::fs; + +use esp_nvs_partition_tool::{ + DataValue, + NvsEntry, + NvsPartition, +}; +use tempfile::NamedTempFile; + +#[test] +fn test_csv_to_binary() { + let csv_path = "tests/assets/roundtrip_basic.csv"; + + let partition = NvsPartition::from_csv_file(csv_path).unwrap(); + assert_eq!(partition.entries.len(), 3); + assert_eq!(partition.entries[0].namespace, "test_namespace"); + assert_eq!(partition.entries[0].key, "u8_val"); + + let bin_file = NamedTempFile::new().unwrap(); + partition + .generate_partition_file(bin_file.path(), 16384) + .unwrap(); + + let metadata = fs::metadata(bin_file.path()).unwrap(); + assert_eq!(metadata.len(), 16384); +} + +#[test] +fn test_generate_from_api() { + let mut partition = NvsPartition { entries: vec![] }; + + partition.entries.push(NvsEntry::new_data( + "config".to_string(), + "version".to_string(), + DataValue::U8(1), + )); + partition.entries.push(NvsEntry::new_data( + "config".to_string(), + "count".to_string(), + DataValue::U32(12345), + )); + partition.entries.push(NvsEntry::new_data( + "config".to_string(), + "name".to_string(), + DataValue::String("Test Device".to_string()), + )); + + let bin_file = NamedTempFile::new().unwrap(); + let result = partition.generate_partition_file(bin_file.path(), 8192); + assert!(result.is_ok()); + + let metadata = fs::metadata(bin_file.path()).unwrap(); + assert_eq!(metadata.len(), 8192); +} + +#[test] +fn test_multiple_namespaces() { + let csv_path = "tests/assets/multiple_namespaces.csv"; + + let partition = NvsPartition::from_csv_file(csv_path).unwrap(); + assert_eq!(partition.entries.len(), 3); + + let bin_file = NamedTempFile::new().unwrap(); + let result = partition.generate_partition_file(bin_file.path(), 16384); + assert!(result.is_ok()); +} + +#[test] +fn test_large_string() { + let csv_path = "tests/assets/large_string.csv"; + + let partition = NvsPartition::from_csv_file(csv_path).unwrap(); + + let bin_file = NamedTempFile::new().unwrap(); + let result = partition.generate_partition_file(bin_file.path(), 16384); + assert!(result.is_ok()); +} + +#[test] +fn test_invalid_partition_size() { + let mut partition = NvsPartition { entries: vec![] }; + partition.entries.push(NvsEntry::new_data( + "test".to_string(), + "dummy".to_string(), + DataValue::U8(0), + )); + + let bin_file = NamedTempFile::new().unwrap(); + + let result = partition.generate_partition_file(bin_file.path(), 1024); + assert!(result.is_err()); +} diff --git a/esp-nvs-partition-tool/tests/parse.rs b/esp-nvs-partition-tool/tests/parse.rs new file mode 100644 index 0000000..06adf58 --- /dev/null +++ b/esp-nvs-partition-tool/tests/parse.rs @@ -0,0 +1,31 @@ +use esp_nvs_partition_tool::{ + DataValue, + EntryContent, + NvsPartition, +}; + +#[test] +fn test_hex2bin_encoding() { + let csv_path = "tests/assets/hex2bin_test.csv"; + + let partition = NvsPartition::from_csv_file(csv_path).unwrap(); + assert_eq!(partition.entries.len(), 1); + + match &partition.entries[0].content { + EntryContent::Data(DataValue::Binary(data)) => { + assert_eq!(data.len(), 16); + assert_eq!(data[0], 0x00); + assert_eq!(data[1], 0x11); + assert_eq!(data[15], 0xFF); + } + _ => panic!("Expected binary data"), + } +} + +#[test] +fn test_key_length_validation() { + let csv_path = "tests/assets/invalid_long_key.csv"; + + let result = NvsPartition::from_csv_file(csv_path); + assert!(result.is_err()); +} diff --git a/esp-nvs-partition-tool/tests/roundtrip.rs b/esp-nvs-partition-tool/tests/roundtrip.rs new file mode 100644 index 0000000..dbe9076 --- /dev/null +++ b/esp-nvs-partition-tool/tests/roundtrip.rs @@ -0,0 +1,426 @@ +use std::fs; + +use base64::Engine; +use esp_nvs_partition_tool::{ + DataValue, + EntryContent, + NvsEntry, + NvsPartition, +}; +use similar::TextDiff; +use tempfile::NamedTempFile; + +macro_rules! entry { + ($key:expr, $variant:ident, $val:expr) => { + NvsEntry::new_data( + "ns".to_string(), + $key.to_string(), + DataValue::$variant($val), + ) + }; +} + +/// Assert that the entry at `index` has the expected content. +fn assert_entry_content(partition: &NvsPartition, index: usize, expected: &EntryContent) { + assert_eq!( + &partition.entries[index].content, expected, + "entry {} ('{}') content mismatch", + index, partition.entries[index].key + ); +} + +/// Compare two CSV strings and, on mismatch, panic with a unified diff. +fn assert_csv_eq(expected: &str, actual: &str) { + if expected == actual { + return; + } + let diff = TextDiff::from_lines(expected, actual) + .unified_diff() + .header("expected", "actual") + .to_string(); + panic!("CSV content mismatch:\n{diff}"); +} + +/// Full end-to-end: CSV → binary → parse → CSV → parse → binary. +/// Verifies binary identity across the complete roundtrip. +#[test] +fn test_csv_binary_csv_roundtrip() { + let original_partition = + NvsPartition::from_csv_file("../esp-nvs/tests/assets/test_nvs_data.csv").unwrap(); + + // Generate binary + let bin_file = NamedTempFile::new().unwrap(); + original_partition + .generate_partition_file(bin_file.path(), 16384) + .unwrap(); + + // Parse binary back + let parsed_partition = NvsPartition::parse_partition_file(bin_file.path()).unwrap(); + + // Write to CSV + let csv_file = NamedTempFile::new().unwrap(); + parsed_partition + .clone() + .to_csv_file(csv_file.path()) + .unwrap(); + + // Parse the generated CSV and regenerate the binary + let reparsed_partition = NvsPartition::from_csv_file(csv_file.path()).unwrap(); + let bin_file2 = NamedTempFile::new().unwrap(); + reparsed_partition + .generate_partition_file(bin_file2.path(), 16384) + .unwrap(); + + // Verify we got all entries back + assert_eq!( + original_partition.entries.len(), + parsed_partition.entries.len() + ); + + // Verify that the binaries are identical + let bin1 = fs::read(bin_file.path()).unwrap(); + let bin2 = fs::read(bin_file2.path()).unwrap(); + assert_eq!( + bin1, bin2, + "CSV-binary-CSV-binary roundtrip should preserve the partition exactly" + ); +} + +/// Verify in-memory APIs (`from_csv` / `to_csv`, `generate_partition` / +/// `parse_partition`) produce the same results as their file-based +/// counterparts. +#[test] +fn test_in_memory_api_parity() { + // from_csv parses correctly + let csv_content = "key,type,encoding,value\ntest_ns,namespace,,\nval,data,u8,42\n"; + let partition = NvsPartition::from_csv(csv_content).unwrap(); + assert_eq!(partition.entries.len(), 1); + assert_eq!(partition.entries[0].namespace, "test_ns"); + assert_eq!(partition.entries[0].key, "val"); + + // to_csv produces valid re-parseable output + let csv_out = partition.clone().to_csv().unwrap(); + assert_csv_eq(csv_content, &csv_out); + + // generate_partition matches generate_partition_file + let data = partition.clone().generate_partition(8192).unwrap(); + assert_eq!(data.len(), 8192); + + let bin_file = NamedTempFile::new().unwrap(); + partition + .generate_partition_file(bin_file.path(), 8192) + .unwrap(); + let file_data = fs::read(bin_file.path()).unwrap(); + assert_eq!(data, file_data); + + // parse_partition matches parse_partition_file + let from_memory = NvsPartition::parse_partition(&data).unwrap(); + let from_file = NvsPartition::parse_partition_file(bin_file.path()).unwrap(); + assert_eq!(from_memory, from_file); +} + +/// Hand-crafted binary containing a legacy blob (type 0x41) parses correctly. +#[test] +fn test_parse_legacy_blob() { + use esp_nvs_partition_tool::partition::crc::crc32; + + let mut page = vec![0xFF_u8; 4096]; + + // Page header (32 bytes): state = ACTIVE + page[0..4].copy_from_slice(&0xFFFFFFFE_u32.to_le_bytes()); + page[4..8].copy_from_slice(&0_u32.to_le_bytes()); + page[8] = 0xFE; // version + let hdr_crc = crc32(&page[4..28]); + page[28..32].copy_from_slice(&hdr_crc.to_le_bytes()); + + // Entry-state bitmap: entries 0,1,2 = Written; rest = Empty + page[32] = 0xEA; + + let entries_base = 64; + + // Entry 0: Namespace "test_ns" → index 1 + let e0 = entries_base; + page[e0] = 0; + page[e0 + 1] = 0x01; + page[e0 + 2] = 1; + page[e0 + 3] = 0xFF; + let ns_key = b"test_ns\0\0\0\0\0\0\0\0\0"; + page[e0 + 8..e0 + 24].copy_from_slice(ns_key); + page[e0 + 24] = 1; + let e0_crc = esp_nvs_partition_tool::partition::crc::crc32_entry(&page[e0..e0 + 32]); + page[e0 + 4..e0 + 8].copy_from_slice(&e0_crc.to_le_bytes()); + + // Entry 1: Legacy blob header (type 0x41, span=2) + let e1 = entries_base + 32; + page[e1] = 1; + page[e1 + 1] = 0x41; + page[e1 + 2] = 2; + page[e1 + 3] = 0xFF; + let blob_key = b"my_blob\0\0\0\0\0\0\0\0\0"; + page[e1 + 8..e1 + 24].copy_from_slice(blob_key); + let payload: &[u8] = &[0xCA, 0xFE, 0xBA, 0xBE]; + let payload_size = payload.len() as u16; + page[e1 + 24..e1 + 26].copy_from_slice(&payload_size.to_le_bytes()); + page[e1 + 26..e1 + 28].copy_from_slice(&0xFFFF_u16.to_le_bytes()); + let payload_crc = crc32(payload); + page[e1 + 28..e1 + 32].copy_from_slice(&payload_crc.to_le_bytes()); + let e1_crc = esp_nvs_partition_tool::partition::crc::crc32_entry(&page[e1..e1 + 32]); + page[e1 + 4..e1 + 8].copy_from_slice(&e1_crc.to_le_bytes()); + + // Entry 2: Blob data payload + let e2 = entries_base + 64; + page[e2..e2 + payload.len()].copy_from_slice(payload); + + let parsed = NvsPartition::parse_partition(&page).unwrap(); + assert_eq!(parsed.entries.len(), 1); + assert_eq!(parsed.entries[0].namespace, "test_ns"); + assert_eq!(parsed.entries[0].key, "my_blob"); + + match &parsed.entries[0].content { + EntryContent::Data(DataValue::Binary(data)) => { + assert_eq!(data, &[0xCA, 0xFE, 0xBA, 0xBE]); + } + other => panic!("expected legacy binary blob, got {:?}", other), + } +} + +/// Roundtrip blobs of various sizes (empty, small, exact chunk boundary, +/// multi-chunk) and a near-max-size string, all in the same namespace. +#[test] +fn test_blob_and_string_roundtrip() { + let exact_boundary: Vec = (0..4000).map(|i| (i % 256) as u8).collect(); + let large_multi_chunk: Vec = (0..5000).map(|i| (i % 256) as u8).collect(); + let big_string = "x".repeat(3998); // 3998 chars + null terminator < 4000 + + let mut partition = NvsPartition { entries: vec![] }; + partition.entries.push(NvsEntry::new_data( + "ns".to_string(), + "empty".to_string(), + DataValue::Binary(vec![]), + )); + partition.entries.push(NvsEntry::new_data( + "ns".to_string(), + "small_a".to_string(), + DataValue::Binary(vec![1, 2, 3]), + )); + partition.entries.push(NvsEntry::new_data( + "ns".to_string(), + "small_b".to_string(), + DataValue::Binary(vec![4, 5, 6, 7]), + )); + partition.entries.push(NvsEntry::new_data( + "ns".to_string(), + "exact".to_string(), + DataValue::Binary(exact_boundary.clone()), + )); + partition.entries.push(NvsEntry::new_data( + "ns".to_string(), + "large".to_string(), + DataValue::Binary(large_multi_chunk.clone()), + )); + partition.entries.push(NvsEntry::new_data( + "ns".to_string(), + "big_str".to_string(), + DataValue::String(big_string.clone()), + )); + + let bin = partition.generate_partition(32768).unwrap(); + let parsed = NvsPartition::parse_partition(&bin).unwrap(); + assert_eq!(parsed.entries.len(), 6); + + assert_entry_content(&parsed, 0, &EntryContent::Data(DataValue::Binary(vec![]))); + assert_entry_content( + &parsed, + 1, + &EntryContent::Data(DataValue::Binary(vec![1, 2, 3])), + ); + assert_entry_content( + &parsed, + 2, + &EntryContent::Data(DataValue::Binary(vec![4, 5, 6, 7])), + ); + assert_entry_content( + &parsed, + 3, + &EntryContent::Data(DataValue::Binary(exact_boundary)), + ); + assert_entry_content( + &parsed, + 4, + &EntryContent::Data(DataValue::Binary(large_multi_chunk)), + ); + assert_entry_content( + &parsed, + 5, + &EntryContent::Data(DataValue::String(big_string)), + ); +} + +/// File entries (hex2bin, base64, string) resolve and roundtrip through binary. +#[test] +fn test_file_entry_roundtrip() { + use std::io::Write; + + let mut hex_file = NamedTempFile::new().unwrap(); + hex_file.write_all(b"DEADBEEF").unwrap(); + hex_file.flush().unwrap(); + + let mut b64_file = NamedTempFile::new().unwrap(); + let b64_content = base64::engine::general_purpose::STANDARD.encode(&[0xCA, 0xFE]); + b64_file.write_all(b64_content.as_bytes()).unwrap(); + b64_file.flush().unwrap(); + + let mut str_file = NamedTempFile::new().unwrap(); + str_file.write_all(b"hello from file").unwrap(); + str_file.flush().unwrap(); + + let csv = format!( + "key,type,encoding,value\ntest_ns,namespace,,\n\ + blob_hex,file,hex2bin,{}\n\ + blob_b64,file,base64,{}\n\ + greeting,file,string,{}\n", + hex_file.path().display(), + b64_file.path().display(), + str_file.path().display(), + ); + + let partition = NvsPartition::from_csv(&csv).unwrap(); + assert_eq!(partition.entries.len(), 3); + + let bin = partition.generate_partition(8192).unwrap(); + let parsed = NvsPartition::parse_partition(&bin).unwrap(); + assert_eq!(parsed.entries.len(), 3); + + assert_entry_content( + &parsed, + 0, + &EntryContent::Data(DataValue::Binary(vec![0xDE, 0xAD, 0xBE, 0xEF])), + ); + assert_entry_content( + &parsed, + 1, + &EntryContent::Data(DataValue::Binary(vec![0xCA, 0xFE])), + ); + assert_entry_content( + &parsed, + 2, + &EntryContent::Data(DataValue::String("hello from file".to_string())), + ); +} + +/// Roundtrip all primitive integer types at their boundary values, with enough +/// extra entries to exercise multi-page generation (>126 entries per page). +#[test] +fn test_primitive_roundtrip() { + let mut partition = NvsPartition { entries: vec![] }; + + // Boundary values for every integer type + partition.entries.push(entry!("u8_max", U8, u8::MAX)); + partition.entries.push(entry!("u8_min", U8, u8::MIN)); + partition.entries.push(entry!("i8_max", I8, i8::MAX)); + partition.entries.push(entry!("i8_min", I8, i8::MIN)); + partition.entries.push(entry!("u16_max", U16, u16::MAX)); + partition.entries.push(entry!("i16_min", I16, i16::MIN)); + partition.entries.push(entry!("u32_max", U32, u32::MAX)); + partition.entries.push(entry!("i32_min", I32, i32::MIN)); + partition.entries.push(entry!("u64_max", U64, u64::MAX)); + partition.entries.push(entry!("i64_min", I64, i64::MIN)); + + // Pad to >125 entries to force multi-page layout + for i in 0..120_u8 { + partition.entries.push(entry!(format!("k{i:03}"), U8, i)); + } + + let data = partition.generate_partition(8192).unwrap(); + let parsed = NvsPartition::parse_partition(&data).unwrap(); + assert_eq!(parsed.entries.len(), partition.entries.len()); + + for (orig, parsed_entry) in partition.entries.iter().zip(parsed.entries.iter()) { + assert_eq!( + orig.content, parsed_entry.content, + "mismatch for key '{}'", + orig.key + ); + } +} + +/// Max namespaces (255) roundtrips successfully, and interleaved namespaces +/// preserve entry order through CSV serialization. +#[test] +fn test_namespace_handling() { + // 255 namespaces is the maximum + let mut partition = NvsPartition { entries: vec![] }; + for i in 0..255_u8 { + partition.entries.push(NvsEntry::new_data( + format!("ns_{i:03}"), + "val".to_string(), + DataValue::U8(i), + )); + } + + let bin = partition.generate_partition(24576).unwrap(); + let parsed = NvsPartition::parse_partition(&bin).unwrap(); + assert_eq!(parsed.entries.len(), 255); + + // Interleaved namespaces preserve entry order through CSV + let mut interleaved = NvsPartition { entries: vec![] }; + interleaved.entries.push(NvsEntry::new_data( + "ns_a".to_string(), + "first".to_string(), + DataValue::U8(1), + )); + interleaved.entries.push(NvsEntry::new_data( + "ns_b".to_string(), + "second".to_string(), + DataValue::U8(2), + )); + interleaved.entries.push(NvsEntry::new_data( + "ns_a".to_string(), + "third".to_string(), + DataValue::U8(3), + )); + + let csv = interleaved.to_csv().unwrap(); + let reparsed = NvsPartition::from_csv(&csv).unwrap(); + + assert_eq!(reparsed.entries.len(), 3); + assert_eq!(reparsed.entries[0].key, "first"); + assert_eq!(reparsed.entries[0].namespace, "ns_a"); + assert_eq!(reparsed.entries[1].key, "second"); + assert_eq!(reparsed.entries[1].namespace, "ns_b"); + assert_eq!(reparsed.entries[2].key, "third"); + assert_eq!(reparsed.entries[2].namespace, "ns_a"); +} + +/// Invalid inputs are properly rejected: non-aligned partition size, bad +/// binary length, and namespace overflow. +#[test] +fn test_validation_errors() { + // Non-4096-aligned partition size + let partition = NvsPartition { entries: vec![] }; + let bin_file = NamedTempFile::new().unwrap(); + assert!( + partition + .generate_partition_file(bin_file.path(), 5000) + .is_err(), + "non-4096-aligned size should be rejected" + ); + + // Binary data whose length isn't a multiple of 4096 + let bad_data = vec![0xFF; 1000]; + assert!(NvsPartition::parse_partition(&bad_data).is_err()); + + // Too many namespaces (256 > 255 limit) + let mut partition = NvsPartition { entries: vec![] }; + for i in 0..256_u16 { + partition.entries.push(NvsEntry::new_data( + format!("ns_{i:03}"), + "val".to_string(), + DataValue::U8(0), + )); + } + assert!( + partition.generate_partition(32768).is_err(), + "256 namespaces should overflow" + ); +} diff --git a/justfile b/justfile index ad4b2e3..19a76fb 100644 --- a/justfile +++ b/justfile @@ -1,9 +1,11 @@ mod nvs 'esp-nvs/justfile' +mod partition_tool 'esp-nvs-partition-tool/justfile' _default: @just --list -fix: nvs::fix +fix: nvs::fix partition_tool::fix + cargo fmt fmt-all: fmt just --unstable --format @@ -12,13 +14,13 @@ fmt-all: fmt fmt: _nightly-fmt -lint: _nightly-fmt-check nvs::lint +lint: _nightly-fmt-check nvs::lint partition_tool::lint test: cargo test --all cargo test --doc -update-changelog: nvs::update-changelog +update-changelog: nvs::update-changelog partition_tool::update-changelog _nightly-fmt: devenv shell \ diff --git a/rustfmt.toml b/rustfmt.toml index 79f458f..acfc750 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,9 +1,6 @@ # For more information see: https://rust-lang.github.io/rustfmt edition = "2024" -# As of this commit, we are using Rust nightly -# unstable_features = true - # Comments format_code_in_doc_comments = true normalize_comments = true