From 728187e890fd04459c6cef3e293a71e62184491a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Jul 2026 11:51:03 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .gitattributes | 36 +++++ .gitignore | 1 + .worktreeinclude | 5 + app/src/main/res/values-v24/bool.xml | 10 +- app/src/main/res/values/attrs.xml | 24 ++-- app/src/main/res/values/bool.xml | 10 +- core/ui/build.gradle.kts | 24 +++- core/ui/token-gen/build-icons.mjs | 14 +- core/ui/token-gen/build-tokens.mjs | 15 +- core/ui/token-gen/hash-util.mjs | 31 +++++ gradlew.bat | 200 +++++++++++++-------------- 11 files changed, 231 insertions(+), 139 deletions(-) create mode 100644 .gitattributes create mode 100644 .worktreeinclude create mode 100644 core/ui/token-gen/hash-util.mjs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..b7ee14c16c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Normalize all text files to LF in the repository and the working tree on +# every OS, regardless of the contributor's core.autocrlf setting. +* text=auto eol=lf + +# Windows script files must stay CRLF. +*.bat text eol=crlf +*.cmd text eol=crlf + +# Unix scripts and the Gradle wrapper must stay LF. +*.sh text eol=lf +gradlew text eol=lf + +# Binary files must be left untouched (never EOL-normalized or diffed as text). +# Images +*.png binary +*.webp binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +# Fonts +*.ttf binary +*.otf binary +# JVM / Android artifacts +*.jar binary +*.aar binary +*.so binary +# Signing keys & keystores +*.jks binary +*.keystore binary +# Archives & databases +*.zip binary +*.db binary diff --git a/.gitignore b/.gitignore index a0514e3436..37b3b8dc70 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,5 @@ find-latest-release-branch.output # Claude /.claude/worktrees/ +/.claude/settings.local.json CLAUDE.local.md diff --git a/.worktreeinclude b/.worktreeinclude new file mode 100644 index 0000000000..08a63f9a4f --- /dev/null +++ b/.worktreeinclude @@ -0,0 +1,5 @@ +# Gitignored files copied into new Claude Code worktrees. +# Syntax: .gitignore patterns. Only gitignored matches are copied. +.claude/settings.local.json +app/google-services.json +local.properties diff --git a/app/src/main/res/values-v24/bool.xml b/app/src/main/res/values-v24/bool.xml index d5677354f0..2f744f862f 100644 --- a/app/src/main/res/values-v24/bool.xml +++ b/app/src/main/res/values-v24/bool.xml @@ -1,5 +1,5 @@ - - - true - - + + + true + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 0917430c88..7ce840eb60 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,12 +1,12 @@ - - - - - - - - - + + + + + + + + + diff --git a/app/src/main/res/values/bool.xml b/app/src/main/res/values/bool.xml index d5677354f0..2f744f862f 100644 --- a/app/src/main/res/values/bool.xml +++ b/app/src/main/res/values/bool.xml @@ -1,5 +1,5 @@ - - - true - - + + + true + + diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index df7ac62a96..5863329189 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,3 +1,4 @@ +import java.io.ByteArrayOutputStream import java.security.MessageDigest plugins { @@ -61,15 +62,15 @@ abstract class VerifyDesignTokensTask : DefaultTask() { require(actual == expected) { "Design tokens are out of date!\n" + - " ds-tokens hash: $actual\n" + - " generated hash: $expected\n" + + " computed from ds-tokens: $actual\n" + + " committed .tokens-hash: $expected\n" + "Run the token generator: cd core/ui/token-gen && npm run build" } stampFile.get().asFile.writeText(actual) } - private fun hashTreeHex(root: java.io.File, extension: String): String { + private fun hashTreeHex(root: File, extension: String): String { val digest = MessageDigest.getInstance("SHA-256") val files = root.walkTopDown() .filter { it.isFile && it.extension == extension } @@ -79,12 +80,27 @@ abstract class VerifyDesignTokensTask : DefaultTask() { for (file in files) { digest.update(file.relativeTo(root).invariantSeparatorsPath.toByteArray()) digest.update(nul) - digest.update(file.readBytes()) + digest.update(file.readBytes().stripCr()) digest.update(nul) } return digest.digest() .joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') } } + + /** + * Strips CR (0x0D) bytes so the token hash ignores CRLF vs LF line endings. The ds-tokens + * submodule is not covered by this repo's .gitattributes, so its .json/.svg sources may be + * checked out with CRLF on some platforms; without this the verification is non-deterministic. + * Removes lone CRs too — safe for UTF-8 sources, where 0x0D never appears inside a + * multi-byte sequence. Must stay byte-for-byte identical to stripCr() in token-gen/hash-util.mjs. + */ + private fun ByteArray.stripCr(): ByteArray { + val cr: Byte = 0x0D + if (none { it == cr }) return this + val out = ByteArrayOutputStream(size) + for (b in this) if (b != cr) out.write(b.toInt()) + return out.toByteArray() + } } android { namespace = "com.tangem.core.ui" diff --git a/core/ui/token-gen/build-icons.mjs b/core/ui/token-gen/build-icons.mjs index 6313846665..3a8e28405f 100644 --- a/core/ui/token-gen/build-icons.mjs +++ b/core/ui/token-gen/build-icons.mjs @@ -2,6 +2,7 @@ import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { stripCr, compareCodeUnits } from './hash-util.mjs'; // ── Paths ────────────────────────────────────────────────────────────────────── const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -252,16 +253,17 @@ object Icons function computeIconsHash() { const files = [...walkSvgs(iconsDir)]; - files.sort((a, b) => { - const ra = path.relative(iconsDir, a).split(path.sep).join('/'); - const rb = path.relative(iconsDir, b).split(path.sep).join('/'); - return ra.localeCompare(rb); - }); + // Code-unit order (not localeCompare) to match the Kotlin verifier's invariantSeparatorsPath + // sort deterministically across locales/ICU versions — see hash-util.mjs. + files.sort((a, b) => compareCodeUnits( + path.relative(iconsDir, a).split(path.sep).join('/'), + path.relative(iconsDir, b).split(path.sep).join('/'), + )); const hash = crypto.createHash('sha256'); for (const file of files) { hash.update(path.relative(iconsDir, file).split(path.sep).join('/')); hash.update('\0'); - hash.update(fs.readFileSync(file)); + hash.update(stripCr(fs.readFileSync(file))); hash.update('\0'); } return hash.digest('hex'); diff --git a/core/ui/token-gen/build-tokens.mjs b/core/ui/token-gen/build-tokens.mjs index 5c185d8388..6f7c18527b 100644 --- a/core/ui/token-gen/build-tokens.mjs +++ b/core/ui/token-gen/build-tokens.mjs @@ -5,6 +5,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { buildIcons } from './build-icons.mjs'; +import { stripCr, compareCodeUnits } from './hash-util.mjs'; // ── Paths ────────────────────────────────────────────────────────────────────── const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -792,18 +793,18 @@ function computeTokensHash() { } } walk(tokensDir); - // Sort by relative path with forward slashes to match Gradle's invariantSeparatorsPath sorting - files.sort((a, b) => { - const ra = path.relative(tokensDir, a).split(path.sep).join('/'); - const rb = path.relative(tokensDir, b).split(path.sep).join('/'); - return ra.localeCompare(rb); - }); + // Sort by forward-slash relative path in UTF-16 code-unit order to match the Kotlin verifier's + // sortedBy { …invariantSeparatorsPath } (String.compareTo) deterministically — see hash-util.mjs. + files.sort((a, b) => compareCodeUnits( + path.relative(tokensDir, a).split(path.sep).join('/'), + path.relative(tokensDir, b).split(path.sep).join('/'), + )); const hash = crypto.createHash('sha256'); for (const file of files) { hash.update(path.relative(tokensDir, file).split(path.sep).join('/')); hash.update('\0'); - hash.update(fs.readFileSync(file)); + hash.update(stripCr(fs.readFileSync(file))); hash.update('\0'); } return hash.digest('hex'); diff --git a/core/ui/token-gen/hash-util.mjs b/core/ui/token-gen/hash-util.mjs new file mode 100644 index 0000000000..c52ac7b8d6 --- /dev/null +++ b/core/ui/token-gen/hash-util.mjs @@ -0,0 +1,31 @@ +// Shared hashing helpers for the design-token generators (build-tokens.mjs / build-icons.mjs). +// Kept in one place because the exact byte stream AND file ordering produced here must stay +// identical to the Kotlin verifier in core/ui/build.gradle.kts (VerifyDesignTokensTask): +// any drift silently breaks the token hash gate. + +/** + * Strip every CR (0x0D) byte so the hash ignores CRLF vs LF line endings. The ds-tokens submodule + * is not covered by the parent repo's .gitattributes, so its .json/.svg sources may be checked out + * with CRLF on some platforms. Lone CRs are dropped too — safe here because these sources are UTF-8 + * (0x0D never appears inside a multi-byte sequence) and carry no bare CR. + * Must stay byte-for-byte identical to ByteArray.stripCr() in core/ui/build.gradle.kts. + */ +export function stripCr(buf) { + const out = Buffer.allocUnsafe(buf.length); + let n = 0; + for (let i = 0; i < buf.length; i++) { + if (buf[i] !== 0x0d) out[n++] = buf[i]; + } + return out.subarray(0, n); +} + +/** + * Compare two forward-slash relative paths by UTF-16 code unit — identical to Kotlin's + * String.compareTo used by `sortedBy { …invariantSeparatorsPath }` in the verifier. + * Do NOT use String.prototype.localeCompare for file ordering: it is locale/ICU-version dependent + * (differs across machines) and case-insensitive at the primary level (disagrees with the Kotlin + * code-unit sort) — either would reorder files and change the hash. + */ +export function compareCodeUnits(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} diff --git a/gradlew.bat b/gradlew.bat index 24467a141f..9618d8d960 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,100 +1,100 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega