From 86a20fc97a9396d310c472cdb6f67fb296547235 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 18 Apr 2026 18:54:33 +0200 Subject: [PATCH] sync with dev-16 --- CHANGELOG_LATEST.md | 2 + Makefile | 4 +- cspell.json | 3 +- interface/package.json | 2 +- interface/pnpm-lock.yaml | 111 +++++++++-------- interface/progmem-generator.js | 26 ++-- interface/vite.config.ts | 54 +++++++- lib/semver/LICENSE | 22 ---- lib/semver/README.md | 7 -- lib/semver/Semver200_comparator.cpp | 121 ------------------ lib/semver/Semver200_parser.cpp | 187 ---------------------------- lib/semver/semver200.h | 53 -------- lib/semver/version.h | 149 ---------------------- lib/semver/version.inl | 132 -------------------- platformio.ini | 8 +- src/ESP32React/ArduinoJsonJWT.cpp | 16 ++- src/ESP32React/ESP32React.cpp | 113 ++++++++++------- src/ESP32React/ESP32React.h | 1 + src/core/EMSESP_Version.h | 124 ++++++++++++++++++ src/core/emsfactory.h | 1 + src/core/system.cpp | 55 ++++---- src/web/WebStatusService.cpp | 14 +-- src/web/WebStatusService.h | 2 +- 23 files changed, 380 insertions(+), 827 deletions(-) delete mode 100644 lib/semver/LICENSE delete mode 100644 lib/semver/README.md delete mode 100644 lib/semver/Semver200_comparator.cpp delete mode 100644 lib/semver/Semver200_parser.cpp delete mode 100644 lib/semver/semver200.h delete mode 100644 lib/semver/version.h delete mode 100644 lib/semver/version.inl create mode 100644 src/core/EMSESP_Version.h diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index ab7e9aad6..3f9d14590 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -14,6 +14,7 @@ For more details go to [emsesp.org](https://emsesp.org/). - heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933) - e-mail notification using ReadyMail Client - 2.nd freshwater module (dhw4, dhw5) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991) +- full system backup and restore ## Fixed @@ -34,3 +35,4 @@ For more details go to [emsesp.org](https://emsesp.org/). - secure mqtt uses ESP_SSLClient - fetch telegrams: set length to fetch [#3017](https://github.com/emsesp/EMS-ESP32/issues/3017) - move http client from stack to heap +- heap optimizations [#3021](https://github.com/emsesp/EMS-ESP32/discussions/3021) diff --git a/Makefile b/Makefile index 0728ef0f6..28c81c2f8 100644 --- a/Makefile +++ b/Makefile @@ -47,8 +47,8 @@ MAKEFLAGS += -j$(JOBS) -l$(shell echo $$(($(JOBS) * 2))) #---------------------------------------------------------------------- TARGET := emsesp BUILD := build -SOURCES := src/core src/devices src/web src/test lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton -INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/semver lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src +SOURCES := src/core src/devices src/web src/test lib_standalone lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton +INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src LIBRARIES := CPPCHECK = cppcheck diff --git a/cspell.json b/cspell.json index 075f7629f..4d4f9520b 100644 --- a/cspell.json +++ b/cspell.json @@ -38,6 +38,7 @@ "vite.config.ts", "lib/esp32-psram/**", "test/test_api/test_api.h", - "lib_standalone/**" + "lib_standalone/**", + "**/*.js" ] } \ No newline at end of file diff --git a/interface/package.json b/interface/package.json index f2a1dea0b..a0327fb0e 100644 --- a/interface/package.json +++ b/interface/package.json @@ -57,7 +57,7 @@ "@types/react-dom": "^19.2.3", "axe-core": "^4.11.3", "concurrently": "^9.2.1", - "eslint": "^10.2.0", + "eslint": "^10.2.1", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.3", "rollup-plugin-visualizer": "^7.0.1", diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 9b6c0b7c5..174a07821 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -80,7 +80,7 @@ importers: version: 7.29.0 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.2.0) + version: 10.0.1(eslint@10.2.1) '@preact/preset-vite': specifier: ^2.10.5 version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)) @@ -103,11 +103,11 @@ importers: specifier: ^9.2.1 version: 9.2.1 eslint: - specifier: ^10.2.0 - version: 10.2.0 + specifier: ^10.2.1 + version: 10.2.1 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.2.0) + version: 10.1.8(eslint@10.2.1) prettier: specifier: ^3.8.3 version: 3.8.3 @@ -119,7 +119,7 @@ importers: version: 5.46.1 typescript-eslint: specifier: ^8.58.2 - version: 8.58.2(eslint@10.2.0)(typescript@6.0.3) + version: 8.58.2(eslint@10.2.1)(typescript@6.0.3) vite: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1) @@ -498,12 +498,16 @@ packages: resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -1181,8 +1185,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.19: - resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1710,8 +1714,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.2.0: - resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2019,8 +2023,8 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} he@1.2.0: @@ -3693,9 +3697,9 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1)': dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3716,9 +3720,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.2.0)': + '@eslint/js@10.0.1(eslint@10.2.1)': optionalDependencies: - eslint: 10.2.0 + eslint: 10.2.1 '@eslint/object-schema@3.0.5': {} @@ -3727,13 +3731,18 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': + '@humanfs/core@0.19.2': dependencies: - '@humanfs/core': 0.19.1 + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 '@humanwhocodes/retry': 0.4.3 + '@humanfs/types@0.15.0': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.4.3': {} @@ -4166,15 +4175,15 @@ snapshots: dependencies: '@types/node': 25.6.0 - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.58.2(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 10.2.0 + eslint: 10.2.1 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -4182,14 +4191,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.58.2 '@typescript-eslint/types': 8.58.2 '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.58.2 debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4212,13 +4221,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.58.2(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.58.2 '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) debug: 4.4.3 - eslint: 10.2.0 + eslint: 10.2.1 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -4241,13 +4250,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.58.2(eslint@10.2.1)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@typescript-eslint/scope-manager': 8.58.2 '@typescript-eslint/types': 8.58.2 '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - eslint: 10.2.0 + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4325,7 +4334,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.19: {} + baseline-browser-mapping@2.10.20: {} bin-build@3.0.0: dependencies: @@ -4386,7 +4395,7 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.19 + baseline-browser-mapping: 2.10.20 caniuse-lite: 1.0.30001788 electron-to-chromium: 1.5.340 node-releases: 2.0.37 @@ -4898,9 +4907,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.2.0): + eslint-config-prettier@10.1.8(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-scope@9.1.2: dependencies: @@ -4913,15 +4922,15 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.2.0: + eslint@10.2.1: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.5.5 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.1 - '@humanfs/node': 0.16.7 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 @@ -5169,7 +5178,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.3 math-intrinsics: 1.1.0 get-proto@1.0.1: @@ -5302,7 +5311,7 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: + hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -5412,7 +5421,7 @@ snapshots: is-core-module@2.16.1: dependencies: - hasown: 2.0.2 + hasown: 2.0.3 is-cwebp-readable@3.0.0: dependencies: @@ -6390,13 +6399,13 @@ snapshots: dependencies: typescript: 6.0.3 - typescript-eslint@8.58.2(eslint@10.2.0)(typescript@6.0.3): + typescript-eslint@8.58.2(eslint@10.2.1)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.58.2(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) - eslint: 10.2.0 + '@typescript-eslint/utils': 8.58.2(eslint@10.2.1)(typescript@6.0.3) + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color diff --git a/interface/progmem-generator.js b/interface/progmem-generator.js index 30fc00094..be5cead21 100644 --- a/interface/progmem-generator.js +++ b/interface/progmem-generator.js @@ -24,20 +24,26 @@ let bundleStats = { other: { count: 0, uncompressed: 0, compressed: 0 } }; -const generateWWWClass = - () => `typedef std::function RouteRegistrationHandler; -// Bundle Statistics: +// AsyncWebHandler that performs the lookup. +const generateWWWClass = () => `// Bundle Statistics: // - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB // - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB // - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}% // - Generated on: ${new Date().toISOString()} -class WWWData { -${INDENT}public: -${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) { -${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, ${f.hash});`).join('\n')} -${INDENT.repeat(2)}} +struct WWWAsset { +${INDENT}const char * uri; +${INDENT}const char * contentType; +${INDENT}const uint8_t * content; +${INDENT}size_t len; +${INDENT}const char * etag; // already includes enclosing double quotes }; + +static const WWWAsset WWW_ASSETS[] = { +${fileInfo.map((f) => `${INDENT}{"${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "\\"${f.rawHash}\\""},`).join('\n')} +}; + +static constexpr size_t WWW_ASSETS_COUNT = sizeof(WWW_ASSETS) / sizeof(WWW_ASSETS[0]); `; const getFilesSync = (dir, files = []) => { @@ -72,6 +78,7 @@ const writeFile = (relativeFilePath, buffer) => { const zipBuffer = zlib.gzipSync(buffer, { level: 9 }); // const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex'); const hash = etag(zipBuffer); // use smaller md5 instead of sha256 + const rawHash = hash.replace(/^"|"$/g, ''); zipBuffer.forEach((b) => { if (!(size % bytesPerLine)) { @@ -94,7 +101,8 @@ const writeFile = (relativeFilePath, buffer) => { mimeType, variable, size, - hash + hash, + rawHash }); totalSize += size; diff --git a/interface/vite.config.ts b/interface/vite.config.ts index 0c16f6a7b..5c288677c 100644 --- a/interface/vite.config.ts +++ b/interface/vite.config.ts @@ -166,6 +166,51 @@ const createManualChunks = (detailed = false) => { }; }; +// Rolldown-native codeSplitting groups for the production build. +// See: https://rolldown.rs/reference/outputoptions.codesplitting +const createRolldownCodeSplitting = () => ({ + groups: [ + { name: '@preact', test: /[\\/]node_modules[\\/].*preact/, priority: 100 }, + { + name: '@react-router', + test: /[\\/]node_modules[\\/].*react-router/, + priority: 95 + }, + { + name: '@mui-material', + test: /[\\/]node_modules[\\/]@mui[\\/]material/, + priority: 95 + }, + { + name: '@mui-icons', + test: /[\\/]node_modules[\\/]@mui[\\/]icons-material/, + priority: 95 + }, + { name: '@alova', test: /[\\/]node_modules[\\/].*alova/, priority: 90 }, + { name: '@i18n', test: /[\\/]node_modules[\\/].*typesafe-i18n/, priority: 90 }, + { + name: '@toastify', + test: /[\\/]node_modules[\\/].*react-toastify/, + priority: 90 + }, + { + name: '@table-library', + test: /[\\/]node_modules[\\/]@table-library/, + priority: 90 + }, + { name: 'vendor', test: /[\\/]node_modules[\\/]/, priority: 10 }, + // Collapse the lazy-loaded route stubs + shared app/components/utils + // code into one chunk. This cuts firmware-side route count roughly in + // half and speeds up route-matching on every incoming HTTP request. + { + name: 'app', + test: /[\\/](app|components|utils)[\\/]/, + priority: 5 + }, + { name: 'api', test: /[\\/]api[\\/]/, priority: 5 } + ] +}); + // Common build base configuration const createBaseBuildConfig = () => ({ target: ES_TARGET, @@ -330,10 +375,17 @@ export default defineConfig( chunkFileNames: 'assets/[name]-[hash].js', entryFileNames: 'assets/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash].[ext]', + // Kept as a no-op for documentation/fallback. With Vite 8 manualChunks: createManualChunks(true), sourcemap: false } - } + }, + rolldownOptions: { + output: { + codeSplitting: createRolldownCodeSplitting() + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any } }; } diff --git a/lib/semver/LICENSE b/lib/semver/LICENSE deleted file mode 100644 index 53234651b..000000000 --- a/lib/semver/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Marko Živanović - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/lib/semver/README.md b/lib/semver/README.md deleted file mode 100644 index 82c86d7d3..000000000 --- a/lib/semver/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# About - -This project is MIT-licensed, C++14 implementation of [semantic versioning](http://semver.org) parser and comparator with support for modifying parsed version strings. Semantic versioning 2.0.0 specification is supported out-of-the-box and the code should be flexible-enough to support future revisions or other similar versioning schemes. - -Copyright (c) 2015 Marko Zivanovic - -Based on https://github.com/zmarko/semver diff --git a/lib/semver/Semver200_comparator.cpp b/lib/semver/Semver200_comparator.cpp deleted file mode 100644 index 1430bd14b..000000000 --- a/lib/semver/Semver200_comparator.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#include -#include -#include -#include "semver200.h" - -using namespace std; - -namespace version { - -namespace { - -// Compare normal version identifiers. -int compare_normal(const Version_data & l, const Version_data & r) { - if (l.major > r.major) - return 1; - if (l.major < r.major) - return -1; - if (l.minor > r.minor) - return 1; - if (l.minor < r.minor) - return -1; - if (l.patch > r.patch) - return 1; - if (l.patch < r.patch) - return -1; - return 0; -} - -// Compare alphanumeric prerelease identifiers. -inline int cmp_alnum_prerel_ids(const string & l, const string & r) { - auto cmp = l.compare(r); - if (cmp == 0) { - return cmp; - } else { - return cmp > 0 ? 1 : -1; - } -} - -// Compare numeric prerelease identifiers. -inline int cmp_num_prerel_ids(const string & l, const string & r) { - long long li = stoll(l); - long long ri = stoll(r); - if (li == ri) - return 0; - return li > ri ? 1 : -1; -} - -using Prerel_type_pair = pair; -using Prerel_id_comparator = function; -const map comparators = {{{Id_type::alnum, Id_type::alnum}, cmp_alnum_prerel_ids}, - {{Id_type::alnum, Id_type::num}, [](const string &, const string &) { return 1; }}, - {{Id_type::num, Id_type::alnum}, [](const string &, const string &) { return -1; }}, - {{Id_type::num, Id_type::num}, cmp_num_prerel_ids}}; - -// Compare prerelease identifiers based on their types. -inline int compare_prerel_identifiers(const Prerelease_identifier & l, const Prerelease_identifier & r) { - auto cmp = comparators.at({l.second, r.second}); - return cmp(l.first, r.first); -} - -inline int cmp_rel_prerel(const Prerelease_identifiers & l, const Prerelease_identifiers & r) { - if (l.empty() && !r.empty()) - return 1; - if (r.empty() && !l.empty()) - return -1; - return 0; -} -} // namespace - -int Semver200_comparator::compare(const Version_data & l, const Version_data & r) const { - // Compare normal version components. - int cmp = compare_normal(l, r); - if (cmp != 0) - return cmp; - - // Compare if one version is release and the other prerelease - release is always higher. - cmp = cmp_rel_prerel(l.prerelease_ids, r.prerelease_ids); - if (cmp != 0) - return cmp; - - // Compare prerelease by looking at each identifier: numeric ones are compared as numbers, - // alphanum as ASCII strings. - auto shorter = min(l.prerelease_ids.size(), r.prerelease_ids.size()); - for (size_t i = 0; i < shorter; i++) { - cmp = compare_prerel_identifiers(l.prerelease_ids[i], r.prerelease_ids[i]); - if (cmp != 0) - return cmp; - } - - // Prerelease identifiers are the same, to the length of the shorter version string; - // if they are the same length, then versions are equal, otherwise, longer one wins. - if (l.prerelease_ids.size() == r.prerelease_ids.size()) - return 0; - return l.prerelease_ids.size() > r.prerelease_ids.size() ? 1 : -1; -} - -} // namespace version diff --git a/lib/semver/Semver200_parser.cpp b/lib/semver/Semver200_parser.cpp deleted file mode 100644 index e5956be0e..000000000 --- a/lib/semver/Semver200_parser.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#include -#include -#include "semver200.h" - -using namespace std; - -namespace version { - -namespace { -enum class Parser_state { major, minor, patch, prerelease, build }; - -using Validator = function; -using State_transition_hook = function; -/// State transition is described by a character that triggers it, a state to transition to and -/// optional hook to be invoked on transition. -using Transition = tuple; -using Transitions = vector; -using State = tuple; -using State_machine = std::map; - -// Ranges of characters allowed in prerelease and build identifiers. -const vector> allowed_prerel_id_chars = {{'0', '9'}, {'A', 'Z'}, {'a', 'z'}, {'-', '-'}}; - -inline Transition mkx(const char c, Parser_state p, State_transition_hook pth) { - return make_tuple(c, p, pth); -} - -inline void Parse_error(const std::string & s) { - // EMSESP::logger().err("parse error: %s", s.c_str()); -} - -/// Advance parser state machine by a single step. -/** - Perform single step of parser state machine: if character matches one from transition tables - - trigger transition to next state; otherwise, validate if current token is in legal state - (throw Parse_error if not) and then add character to current token; State transition includes - preparing various vars for next state and invoking state transition hook (if specified) which is - where whole tokens are validated. - */ -inline void process_char(const char c, Parser_state & cstate, Parser_state & pstate, const Transitions & transitions, string & target, Validator validate) { - for (const auto & transition : transitions) { - if (c == get<0>(transition)) { - if (get<2>(transition)) - get<2>(transition)(target); - pstate = cstate; - cstate = get<1>(transition); - return; - } - } - validate(target, c); - target.push_back(c); -} - -/// Validate normal (major, minor, patch) version components. -inline void normal_version_validator(const string & tgt, const char c) { - if (c < '0' || c > '9') - Parse_error("invalid character encountered: " + string(1, c)); - if (tgt.compare(0, 1, "0") == 0) - Parse_error("leading 0 not allowed"); -} - -/// Validate that prerelease and build version identifiers are comprised of allowed chars only. -inline void prerelease_version_validator(const string &, const char c) { - bool res = false; - for (const auto & r : allowed_prerel_id_chars) { - res |= (c >= r.first && c <= r.second); - } - if (!res) - Parse_error("invalid character encountered: " + string(1, c)); -} - -inline bool is_identifier_numeric(const string & id) { - return id.find_first_not_of("0123456789") == string::npos; -} - -inline bool check_for_leading_0(const string & str) { - return str.length() > 1 && str[0] == '0'; -} - -/// Validate every individual prerelease identifier, determine it's type and add it to collection. -void prerelease_hook_impl(string & id, Prerelease_identifiers & prerelease) { - if (id.empty()) - Parse_error("version identifier cannot be empty"); - Id_type t = Id_type::alnum; - if (is_identifier_numeric(id)) { - t = Id_type::num; - if (check_for_leading_0(id)) { - Parse_error("numeric identifiers cannot have leading 0"); - } - } - prerelease.push_back(Prerelease_identifier(id, t)); - id.clear(); -} - -/// Validate every individual build identifier and add it to collection. -void build_hook_impl(string & id, Parser_state & pstate, Build_identifiers & build, std::string & prerelease_id, Prerelease_identifiers & prerelease) { - // process last token left from parsing prerelease data - if (pstate == Parser_state::prerelease) - prerelease_hook_impl(prerelease_id, prerelease); - if (id.empty()) - Parse_error("version identifier cannot be empty"); - build.push_back(id); - id.clear(); -} - -} // namespace - -/// Parse semver 2.0.0-compatible string to Version_data structure. -/** - Version text parser is implemented as a state machine. In each step one successive character from version - string is consumed and is either added to current token or triggers state transition. Hooks can be - injected into state transitions for validation/customization purposes. - */ -Version_data Semver200_parser::parse(const string & s) const { - string major; - string minor; - string patch; - string prerelease_id; - string build_id; - Prerelease_identifiers prerelease; - Build_identifiers build; - Parser_state cstate{Parser_state::major}; - Parser_state pstate; - - auto prerelease_hook = [&](string & id) { prerelease_hook_impl(id, prerelease); }; - - auto build_hook = [&](string & id) { build_hook_impl(id, pstate, build, prerelease_id, prerelease); }; - - // State transition tables - auto major_trans = {mkx('.', Parser_state::minor, {})}; - auto minor_trans = {mkx('.', Parser_state::patch, {})}; - auto patch_trans = {mkx('-', Parser_state::prerelease, {}), mkx('+', Parser_state::build, {})}; - auto prerelease_trans = {// When identifier separator (.) is found, stay in the same state but invoke hook - // in order to process each individual identifier separately. - mkx('.', Parser_state::prerelease, prerelease_hook), - mkx('+', Parser_state::build, {})}; - auto build_trans = {// Same stay-in-the-same-state-but-invoke-hook trick from above. - mkx('.', Parser_state::build, build_hook)}; - - State_machine state_machine = {{Parser_state::major, State{major_trans, major, normal_version_validator}}, - {Parser_state::minor, State{minor_trans, minor, normal_version_validator}}, - {Parser_state::patch, State{patch_trans, patch, normal_version_validator}}, - {Parser_state::prerelease, State{prerelease_trans, prerelease_id, prerelease_version_validator}}, - {Parser_state::build, State{build_trans, build_id, prerelease_version_validator}}}; - - // Main loop. - for (const auto & c : s) { - auto state = state_machine.at(cstate); - process_char(c, cstate, pstate, get<0>(state), get<1>(state), get<2>(state)); - } - - // Trigger appropriate hooks in order to process last token, because no state transition was - // triggered for it. - if (cstate == Parser_state::prerelease) { - prerelease_hook(prerelease_id); - } else if (cstate == Parser_state::build) { - build_hook(build_id); - } - - return Version_data{stoi(major), stoi(minor), stoi(patch), prerelease, build}; -} - -} // namespace version diff --git a/lib/semver/semver200.h b/lib/semver/semver200.h deleted file mode 100644 index 5c19f07ff..000000000 --- a/lib/semver/semver200.h +++ /dev/null @@ -1,53 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#pragma once - -#include "version.h" - -namespace version { - -/// Parse string into Version_data structure according to semantic versioning 2.0.0 rules. -struct Semver200_parser { - Version_data parse(const std::string &) const; -}; - -/// Compare Version_data to another using semantic versioning 2.0.0 rules. -struct Semver200_comparator { - int compare(const Version_data &, const Version_data &) const; -}; - -/// Concrete version class that binds all semver 2.0.0 functionality together. -class Semver200_version : public Basic_version { - public: - Semver200_version() - : Basic_version{Semver200_parser(), Semver200_comparator()} { - } - - Semver200_version(const std::string & v) - : Basic_version{v, Semver200_parser(), Semver200_comparator()} { - } -}; - -} // namespace version \ No newline at end of file diff --git a/lib/semver/version.h b/lib/semver/version.h deleted file mode 100644 index 517d6f435..000000000 --- a/lib/semver/version.h +++ /dev/null @@ -1,149 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#pragma once - -#include -#include - -namespace version { - -/// Type of prerelease identifier: alphanumeric or numeric. -/** - Type of identifier affects comparison: alphanumeric identifiers are compared as ASCII strings, while - numeric identifiers are compared as numbers. - */ -enum class Id_type { - alnum, ///< Identifier is alphanumerical - num ///< Identifier is numeric -}; - -/// Container for prerelease identifier value and it's type. -/** - Prerelease version string consist of an optional series of dot-separated identifiers. - These identifiers can be either numerical or alphanumerical. - This structure describes one such identifier. - */ -using Prerelease_identifier = std::pair; - -/// Container for all prerelease identifiers for a given version string. -using Prerelease_identifiers = std::vector; - -/// Build identifier is arbitrary string with no special meaning with regards to version precedence. -using Build_identifier = std::string; - -/// Container for all build identifiers of a given version string. -using Build_identifiers = std::vector; - -/// Description of version broken into parts, as per semantic versioning specification. -struct Version_data { - Version_data(const int M, const int m, const int p, const Prerelease_identifiers & pr, const Build_identifiers & b) - : major{M} - , minor{m} - , patch{p} - , prerelease_ids{pr} - , build_ids{b} { - } - - int major; ///< Major version, change only on incompatible API modifications. - int minor; ///< Minor version, change on backwards-compatible API modifications. - int patch; ///< Patch version, change only on bugfixes. - - /// Optional series of prerelease identifiers. - Prerelease_identifiers prerelease_ids; - - /// Optional series of build identifiers. - Build_identifiers build_ids; -}; - -// Forward declaration required for operators' template declarations. -template -class Basic_version; - -/// Test if left-hand version operand is of lower precedence than the right-hand version. -template -bool operator<(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand if of equal precedence as the right-hand version. -template -bool operator==(const Basic_version &, const Basic_version &); - -/// Test if left-hand version and right-hand version are of different precedence. -template -bool operator!=(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand is of higher precedence than the right-hand version. -template -bool operator>(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand is of higher or equal precedence as the right-hand version. -template -bool operator>=(const Basic_version &, const Basic_version &); - -/// Test if left-hand version operand is of lower or equal precedence as the right-hand version. -template -bool operator<=(const Basic_version &, const Basic_version &); - -/// Base class for various version parsing, precedence ordering and data manipulation schemes. -/** - Basic_version class describes general version object without prescribing parsing, - validation, comparison and modification rules. These rules are implemented by supplied Parser, Comparator - and Modifier objects. - */ -template -class Basic_version { - public: - /// Construct Basic_version object using Parser object to parse default ("0.0.0") version string, Comparator for comparison and Modifier for modification. - Basic_version(Parser, Comparator); - - /// Construct Basic_version object using Parser to parse supplied version string, Comparator for comparison and Modifier for modification. - Basic_version(const std::string &, Parser, Comparator); - - /// Construct Basic_version object using supplied Version_data, Parser, Comparator and Modifier objects. - Basic_version(const Version_data &, Parser, Comparator); - - /// Construct Basic_version by copying data from another one. - Basic_version(const Basic_version &); - - /// Copy version data from another Basic_version to this one. - Basic_version & operator=(const Basic_version &); - - int major() const; ///< Get major version. - int minor() const; ///< Get minor version. - int patch() const; ///< Get patch version. - const std::string prerelease() const; ///< Get prerelease version string. - const std::string build() const; ///< Get build version string. - - friend bool operator< <>(const Basic_version &, const Basic_version &); - friend bool operator==<>(const Basic_version &, const Basic_version &); - - private: - Parser parser_; - Comparator comparator_; - Version_data ver_; -}; - -} // namespace version - -#include "version.inl" diff --git a/lib/semver/version.inl b/lib/semver/version.inl deleted file mode 100644 index 8de18af61..000000000 --- a/lib/semver/version.inl +++ /dev/null @@ -1,132 +0,0 @@ -/* -The MIT License (MIT) - -Copyright (c) 2015 Marko Zivanovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#pragma once - -#include "version.h" - -namespace version { - -namespace { - -/// Utility function to splice all vector elements to output stream, using designated separator -/// between elements and function object for getting values from vector elements. -template -std::string & splice(std::string & ss, const std::vector & vec, const std::string & sep, F read) { - if (!vec.empty()) { - for (auto it = vec.cbegin(); it < vec.cend() - 1; ++it) { - ss += read(*it) + sep; - } - ss += read(*vec.crbegin()); - } - return ss; -} - -} // namespace - -template -Basic_version::Basic_version(Parser p, Comparator c) - : parser_(p) - , comparator_(c) - , ver_(parser_.parse("0.0.0")) { -} - -template -Basic_version::Basic_version(const std::string & v, Parser p, Comparator c) - : parser_(p) - , comparator_(c) - , ver_(parser_.parse(v)) { -} - -template -Basic_version::Basic_version(const Version_data & v, Parser p, Comparator c) - : parser_(p) - , comparator_(c) - , ver_(v) { -} - -template -Basic_version::Basic_version(const Basic_version &) = default; - -template -Basic_version & Basic_version::operator=(const Basic_version &) = default; - -template -int Basic_version::major() const { - return ver_.major; -} - -template -int Basic_version::minor() const { - return ver_.minor; -} - -template -int Basic_version::patch() const { - return ver_.patch; -} - -template -const std::string Basic_version::prerelease() const { - std::string ss; - return splice(ss, ver_.prerelease_ids, ".", [](const Prerelease_identifier & id) { return id.first; }); -} - -template -const std::string Basic_version::build() const { - std::string ss; - return splice(ss, ver_.build_ids, ".", [](const std::string & id) { return id; }); -} - -template -bool operator<(const Basic_version & l, const Basic_version & r) { - return l.comparator_.compare(l.ver_, r.ver_) == -1; -} - -template -bool operator==(const Basic_version & l, const Basic_version & r) { - return l.comparator_.compare(l.ver_, r.ver_) == 0; -} - -template -inline bool operator!=(const Basic_version & l, const Basic_version & r) { - return !(l == r); -} - -template -inline bool operator>(const Basic_version & l, const Basic_version & r) { - return r < l; -} - -template -inline bool operator>=(const Basic_version & l, const Basic_version & r) { - return !(l < r); -} - -template -inline bool operator<=(const Basic_version & l, const Basic_version & r) { - return !(l > r); -} - -} // namespace version diff --git a/platformio.ini b/platformio.ini index 1b2348bca..917051208 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,8 +21,8 @@ extra_configs = pio_local.ini [common] -core_build_flags = -std=c++17 -std=gnu++17 -O3 -Wno-type-limits -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers -core_unbuild_flags = -std=gnu++11 +core_build_flags = -std=gnu++20 -O3 -flto=auto -Wno-type-limits -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers +core_unbuild_flags = -std=gnu++11 -std=gnu++14 -std=gnu++17 -fno-lto my_build_flags = @@ -169,7 +169,7 @@ build_src_flags = -DEMSESP_STANDALONE -DEMSESP_TEST -DARDUINOJSON_ENABLE_ARDUINO_STRING=1 -DNO_TLS_SUPPORT - -std=gnu++17 -Og -ggdb + -std=gnu++20 -Og -ggdb -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces -Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare @@ -210,7 +210,7 @@ build_src_flags = -DARDUINOJSON_ENABLE_ARDUINO_STRING=1 -DNO_TLS_SUPPORT -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\" - -std=gnu++17 -Og -ggdb + -std=gnu++20 -Og -ggdb -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces -Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare diff --git a/src/ESP32React/ArduinoJsonJWT.cpp b/src/ESP32React/ArduinoJsonJWT.cpp index bbc2b11ac..7e7676af5 100644 --- a/src/ESP32React/ArduinoJsonJWT.cpp +++ b/src/ESP32React/ArduinoJsonJWT.cpp @@ -70,12 +70,16 @@ void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument & jsonDocument) { */ String ArduinoJsonJWT::sign(String & payload) { std::array hmacResult{}; - mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), - reinterpret_cast(_secret.c_str()), - _secret.length(), - reinterpret_cast(payload.c_str()), - payload.length(), - hmacResult.data()); + { + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); + mbedtls_md_hmac_starts(&ctx, reinterpret_cast(_secret.c_str()), _secret.length()); + mbedtls_md_hmac_update(&ctx, reinterpret_cast(payload.c_str()), payload.length()); + mbedtls_md_hmac_finish(&ctx, hmacResult.data()); + mbedtls_md_free(&ctx); + } return encode(reinterpret_cast(hmacResult.data()), hmacResult.size()); } diff --git a/src/ESP32React/ESP32React.cpp b/src/ESP32React/ESP32React.cpp index 9b533dfba..024dcd1ff 100644 --- a/src/ESP32React/ESP32React.cpp +++ b/src/ESP32React/ESP32React.cpp @@ -2,10 +2,75 @@ #include "WWWData.h" // include auto-generated static web resources +#include + static constexpr const char CACHE_CONTROL[] = "public,max-age=60"; +// Single static-content handler serving all assets embedded in WWWData.h. +class StaticContentHandler : public AsyncWebHandler { + public: + bool canHandle(AsyncWebServerRequest * request) const override { + const auto method = request->method(); + return method == HTTP_GET || method == HTTP_HEAD || method == HTTP_OPTIONS; + } + + void handleRequest(AsyncWebServerRequest * request) override { + // OPTIONS is handled generically - the server-level CORS headers are + // attached via DefaultHeaders in ESP32React::begin(). + if (request->method() == HTTP_OPTIONS) { + request->send(200); + return; + } + + const char * url = request->url().c_str(); + const WWWAsset * found = lookup(url); + const WWWAsset * asset = found ? found : index_asset(); + + if (asset == nullptr) { + request->send(404); + return; + } + + // If the client already has this exact ETag, respond 304 Not Modified without sending the body. + const String & inm = request->header(asyncsrv::T_INM); + if (inm.length() != 0 && strcmp(inm.c_str(), asset->etag) == 0) { + request->send(304); + return; + } + + AsyncWebServerResponse * response = request->beginResponse(200, asset->contentType, asset->content, asset->len); + response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false); + response->addHeader(asyncsrv::T_ETag, asset->etag, false); + response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false); + request->send(response); + } + + private: + // Exact-match lookup in the asset table + static const WWWAsset * lookup(const char * url) { + for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) { + if (strcmp(WWW_ASSETS[i].uri, url) == 0) { + return &WWW_ASSETS[i]; + } + } + return nullptr; + } + + // Returns the /index.html asset, used as the SPA fallback for any GET + // that didn't match an embedded asset (React Router handles routing on + // the client side). + static const WWWAsset * index_asset() { + static const WWWAsset * cached = nullptr; + if (cached == nullptr) { + cached = lookup("/index.html"); + } + return cached; + } +}; + ESP32React::ESP32React(AsyncWebServer * server, FS * fs) - : _securitySettingsService(server, fs) + : _server(server) + , _securitySettingsService(server, fs) , _networkSettingsService(server, fs, &_securitySettingsService) , _wifiScanner(server, &_securitySettingsService) , _networkStatus(server, &_securitySettingsService) @@ -17,50 +82,6 @@ ESP32React::ESP32React(AsyncWebServer * server, FS * fs) , _mqttSettingsService(server, fs, &_securitySettingsService) , _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService) , _authenticationService(server, &_securitySettingsService) { - // - // Serve static web resources - // - - ArRequestHandlerFunction indexHtmlHandler = nullptr; - - WWWData::registerRoutes([server, &indexHtmlHandler](const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash) { - String etag = "\"" + hash + "\""; // RFC9110: ETag must be enclosed in double quotes - - ArRequestHandlerFunction requestHandler = [contentType, content, len, etag](AsyncWebServerRequest * request) { - if (request->header(asyncsrv::T_INM) == etag) { - request->send(304); - return; - } - - AsyncWebServerResponse * response = request->beginResponse(200, contentType, content, len); - response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false); - response->addHeader(asyncsrv::T_ETag, etag, false); - response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false); - request->send(response); - }; - - server->on(uri, HTTP_GET, requestHandler); - - // Capture index.html handler to set onNotFound once after all routes are registered - if (strcmp(uri, "/index.html") == 0) { - indexHtmlHandler = requestHandler; - } - }); - - // Set onNotFound handler once after all routes are registered - // Serving non matching get requests with "/index.html" - // OPTIONS get a straight up 200 response - if (indexHtmlHandler != nullptr) { - server->onNotFound([indexHtmlHandler](AsyncWebServerRequest * request) { - if (request->method() == HTTP_GET) { - indexHtmlHandler(request); - } else if (request->method() == HTTP_OPTIONS) { - request->send(200); - } else { - request->send(404); // not found - } - }); - } } void ESP32React::begin() { @@ -78,11 +99,11 @@ void ESP32React::begin() { _ntpSettingsService.begin(); _mqttSettingsService.begin(); _securitySettingsService.begin(); + _server->addHandler(new StaticContentHandler()); } void ESP32React::loop() { _networkSettingsService.loop(); _apSettingsService.loop(); _mqttSettingsService.loop(); - _ntpSettingsService.loop(); } \ No newline at end of file diff --git a/src/ESP32React/ESP32React.h b/src/ESP32React/ESP32React.h index f5af50a8a..cbcea487e 100644 --- a/src/ESP32React/ESP32React.h +++ b/src/ESP32React/ESP32React.h @@ -68,6 +68,7 @@ class ESP32React { } private: + AsyncWebServer * _server; SecuritySettingsService _securitySettingsService; NetworkSettingsService _networkSettingsService; WiFiScanner _wifiScanner; diff --git a/src/core/EMSESP_Version.h b/src/core/EMSESP_Version.h new file mode 100644 index 000000000..c9be68f61 --- /dev/null +++ b/src/core/EMSESP_Version.h @@ -0,0 +1,124 @@ +/* + * EMS-ESP - https://github.com/emsesp/EMS-ESP + * Copyright 2020-2026 emsesp.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EMSESP_Version_H +#define EMSESP_Version_H + +#include +#include +#include + +// Drop-in lightweight replacement for the subset of the semver library actually used by EMS-ESP. +// The previous semver library (lib/semver) builds a std::map + std::function-based state machine on +// every parse, which fragments the internal heap on the ESP32. This replacement does no heap +// allocation beyond the std::string member for the prerelease tag, and matches the API surface +// we consume: construction from string, major()/minor()/patch()/prerelease(), and operator>/(const EMSESP_Version & a, const EMSESP_Version & b) { + return b < a; + } + + friend bool operator==(const EMSESP_Version & a, const EMSESP_Version & b) { + return a.major_ == b.major_ && a.minor_ == b.minor_ && a.patch_ == b.patch_; + } + + friend bool operator!=(const EMSESP_Version & a, const EMSESP_Version & b) { + return !(a == b); + } + + friend bool operator>=(const EMSESP_Version & a, const EMSESP_Version & b) { + return !(a < b); + } + + friend bool operator<=(const EMSESP_Version & a, const EMSESP_Version & b) { + return !(b < a); + } + + private: + int major_ = 0; + int minor_ = 0; + int patch_ = 0; + std::string prerelease_; + + void parse(const char * s) { + major_ = minor_ = patch_ = 0; + prerelease_.clear(); + if (s == nullptr || *s == '\0') { + return; + } + // parse numeric major.minor.patch; accept partial ("3", "3.9", "3.9.0") + sscanf(s, "%d.%d.%d", &major_, &minor_, &patch_); + // capture prerelease tag after '-' if present (stop at '+' which is build metadata) + const char * dash = strchr(s, '-'); + if (dash != nullptr) { + const char * plus = strchr(dash, '+'); + if (plus != nullptr) { + prerelease_.assign(dash + 1, plus - dash - 1); + } else { + prerelease_.assign(dash + 1); + } + } + } +}; + +} // namespace version + +#endif diff --git a/src/core/emsfactory.h b/src/core/emsfactory.h index 771aad4af..3be963be0 100644 --- a/src/core/emsfactory.h +++ b/src/core/emsfactory.h @@ -19,6 +19,7 @@ #ifndef EMSESP_EMSFACTORY_H_ #define EMSESP_EMSFACTORY_H_ +#include #include // for unique_ptr #include diff --git a/src/core/system.cpp b/src/core/system.cpp index 82fba4144..d1d39487c 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -26,13 +26,12 @@ #include #include "esp_efuse.h" #include -#include #endif #include #include -#include +#include "EMSESP_Version.h" #if defined(EMSESP_TEST) #include "../test/test.h" @@ -457,15 +456,16 @@ void System::get_partition_info() { strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); p_info.install_date = d > 1500000000L ? time_string : ""; - esp_image_metadata_t meta = {}; - esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; - if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { - p_info.size = meta.image_len / 1024; // actual firmware size in KB - } else { - p_info.size = 0; + if (!p_info.version.empty()) { + esp_image_metadata_t meta = {}; + esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; + if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { + p_info.size = meta.image_len / 1024; // actual firmware size in KB + } else { + p_info.size = 0; + } + partition_info_[part->label] = p_info; } - - partition_info_[part->label] = p_info; } it = esp_partition_next(it); // loop to next partition @@ -1613,8 +1613,8 @@ bool System::check_upgrade() { settingsVersion = "3.5.0"; // this was the last stable version without version info } - version::Semver200_version settings_version(settingsVersion); - version::Semver200_version this_version(EMSESP_APP_VERSION); + version::EMSESP_Version settings_version(settingsVersion); + version::EMSESP_Version this_version(EMSESP_APP_VERSION); std::string settings_version_type = settings_version.prerelease().empty() ? "" : ("-" + settings_version.prerelease()); std::string this_version_type = this_version.prerelease().empty() ? "" : ("-" + this_version.prerelease()); @@ -1765,18 +1765,17 @@ void System::exportSettings(const std::string & type, const char * filename, Jso File settingsFile = LittleFS.open(filename); if (settingsFile) { - JsonDocument jsonDocument; - DeserializationError error = deserializeJson(jsonDocument, settingsFile); - if (error == DeserializationError::Ok && jsonDocument.is()) { - JsonObject node = output[section].to(); - for (JsonPair kvp : jsonDocument.as()) { - node[kvp.key()] = kvp.value(); + { + JsonDocument jsonDocument; + DeserializationError error = deserializeJson(jsonDocument, settingsFile); + settingsFile.close(); // close early, we no longer need the file + if (error || !jsonDocument.is()) { + LOG_ERROR("Failed to deserialize settings file %s", filename); + return; } - } else { - LOG_ERROR("Failed to deserialize settings file %s", filename); + output[section].set(jsonDocument.as()); } LOG_DEBUG("Exported %s settings from file %s", section, filename); - settingsFile.close(); } else { LOG_ERROR("No settings file for %s found", filename); } @@ -1828,13 +1827,15 @@ void System::exportSystemBackup(JsonObject output) { if (file) { JsonDocument jsonDocument; DeserializationError error = deserializeJson(jsonDocument, file); - if (error == DeserializationError::Ok && jsonDocument.is()) { - JsonObject node = nodes.add(); - node["type"] = "customSupport"; - node["data"] = jsonDocument.as(); + file.close(); // close early, we no longer need the file + if (!error && jsonDocument.is()) { + JsonObject support_node = nodes.add(); + support_node["type"] = "customSupport"; + support_node["data"].set(jsonDocument.as()); + LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE); + } else { + LOG_ERROR("Failed to deserialize custom support file"); } - file.close(); - LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE); } // Backup NVS values diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index 23f4d3486..f360ff99e 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -253,7 +253,7 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) { // it's a filename with a .bin or .md extension, try and extract the version from it // e.g. EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin -> major=3 minor=8 patch=2 - version::Semver200_version latest_version; + version::EMSESP_Version latest_version; if ((version.find(".bin") != std::string::npos) || (version.find(".md") != std::string::npos)) { std::string filename = version; auto pos = filename.find("EMS-ESP-"); @@ -274,18 +274,18 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) { std::string major_version = filename.substr(pos, underscore1 - pos); std::string minor_version = filename.substr(underscore1 + 1, underscore2 - underscore1 - 1); std::string patch_version = filename.substr(underscore2 + 1, dash - underscore2 - 1); - latest_version = version::Semver200_version(major_version + "." + minor_version + "." + patch_version); + latest_version = version::EMSESP_Version(major_version + "." + minor_version + "." + patch_version); } else { // if it's .json file exit if (version.find(".json") != std::string::npos) { return 0; } else { // treat it like a version string like "3.9.0" - latest_version = version::Semver200_version(version); + latest_version = version::EMSESP_Version(version); } } - version::Semver200_version current_version(current_version_s); // get current version + version::EMSESP_Version current_version(current_version_s); // get current version if (latest_version > current_version && current_version.minor() < latest_version.minor()) { return 0; // if it's just a minor version upgrade return 0 @@ -306,9 +306,9 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) { // versions holds the latest development version and stable version in one string, comma separated bool WebStatusService::checkUpgrade(JsonObject root, std::string & version) { if (!version.empty()) { - version::Semver200_version current_version(current_version_s); - version::Semver200_version latest_dev_version(version.substr(0, version.find(','))); - version::Semver200_version latest_stable_version(version.substr(version.find(',') + 1)); + version::EMSESP_Version current_version(current_version_s); + version::EMSESP_Version latest_dev_version(version.substr(0, version.find(','))); + version::EMSESP_Version latest_stable_version(version.substr(version.find(',') + 1)); bool dev_upgradeable = latest_dev_version > current_version; bool stable_upgradeable = latest_stable_version > current_version; diff --git a/src/web/WebStatusService.h b/src/web/WebStatusService.h index 8d2c69bfd..6d3f59f1a 100644 --- a/src/web/WebStatusService.h +++ b/src/web/WebStatusService.h @@ -4,7 +4,7 @@ #define EMSESP_SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus" #define EMSESP_ACTION_SERVICE_PATH "/rest/action" -#include // for version checking +#include "../core/EMSESP_Version.h" #include "../emsesp_version.h" namespace emsesp {