Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-02 11:51:03 +03:00
parent 8a83664e9a
commit 728187e890
11 changed files with 231 additions and 139 deletions

36
.gitattributes vendored Normal file
View file

@ -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

1
.gitignore vendored
View file

@ -48,4 +48,5 @@ find-latest-release-branch.output
# Claude
/.claude/worktrees/
/.claude/settings.local.json
CLAUDE.local.md

5
.worktreeinclude Normal file
View file

@ -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

View file

@ -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"

View file

@ -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');

View file

@ -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');

View file

@ -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;
}