chore(i18n): finish translating code comments and scripts to English
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s

Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
Lucas RayanandClaude Sonnet 5 committed 2026-10-30 20:43:02 -03:00
1 parent da212e6926
commit 6328220c60
129 files changed
+1961 -1834

No files matched your search

+14 -17
View File
@@ -1,35 +1,32 @@
#!/usr/bin/env bash
# Instala pacotes do apt no CI sem depender do espelho estar rápido: usa os .deb em cache quando
# existem e, se o apt travar ou falhar, tenta de novo com tempo limite por tentativa.
# Installs apt packages in CI without depending on fast mirrors: reuses cached .deb
# archives when present and retries with per-attempt timeouts if apt stalls or fails.
#
# Os passos de apt do CI já levaram de 13 a 28 minutos num dia de espelho lento, contra uns 35 s
# no normal. O cache evita o download; o tempo limite evita ficar preso.
#
# Uso: scripts/apt-install.sh <pasta-do-cache> <pacote>...
# Usage: scripts/apt-install.sh <cache-dir> <package>...
set -euo pipefail
[ $# -ge 2 ] || { echo "uso: $0 <pasta-do-cache> <pacote>..." >&2; exit 2; }
[ $# -ge 2 ] || { echo "usage: $0 <cache-dir> <package>..." >&2; exit 2; }
cache="$1"; shift
pkgs=("$@")
mkdir -p "$cache/partial"
# O apt roda como root e deixa arquivos (lock, .deb) que o usuário do runner não lê; sem devolver a
# posse, o actions/cache falha ao empacotar a pasta ("tar failed with exit code 2") e nunca grava.
# apt runs as root and leaves files (lock, .deb) unreadable by non-root runner users;
# restore ownership on exit so cache directories remain accessible.
trap 'sudo chown -R "$(id -u):$(id -g)" "$cache" 2>/dev/null || true; rm -f "$cache/lock"' EXIT
opts=(-o "Dir::Cache::archives=$cache" -y)
# 1) com .deb em cache e índice presente: instala direto, sem nem atualizar o índice do apt
# 1) With cached .deb files and package index present: install directly without updating apt lists
if [ -z "$(dpkg --print-foreign-architectures)" ] && ls "$cache"/*.deb >/dev/null 2>&1 && ls /var/lib/apt/lists/*Packages* >/dev/null 2>&1 && timeout 300 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
echo "instalado a partir do cache"
echo "installed from cache"
exit 0
fi
# 2) sem cache (ou multiarch / incompleto): atualiza e instala, até 3 tentativas com tempo limite
for tentativa in 1 2 3; do
# 2) Cache miss (or multiarch / incomplete): update and install, up to 3 attempts with timeout
for attempt in 1 2 3; do
if timeout 240 sudo apt-get update && { [ -z "$(dpkg --print-foreign-architectures)" ] || timeout 600 sudo apt-get "${opts[@]}" upgrade; } && timeout 600 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
echo "instalado (tentativa $tentativa)"
echo "installed (attempt $attempt)"
exit 0
fi
echo "apt falhou ou passou do tempo (tentativa $tentativa de 3)" >&2
sleep $((tentativa * 10))
echo "apt failed or timed out (attempt $attempt of 3)" >&2
sleep $((attempt * 10))
done
echo "não foi possível instalar: ${pkgs[*]}" >&2
echo "failed to install: ${pkgs[*]}" >&2
exit 1
+21 -22
View File
@@ -1,13 +1,12 @@
#!/usr/bin/env node
// Confere se o dicionário inglês cobre os textos da interface.
// Verifies that the English dictionary covers all user-facing interface strings.
//
// A varredura é estática e sem dependências: lê os arquivos da interface
// (JS e HTML), os rótulos que o núcleo em Rust envia para a tela (metadados,
// tipos de arquivo e saúde do disco) e falha quando encontra uma string que
// parece português e não tem tradução. Também recusa chaves duplicadas no
// dicionário (num objeto JS a última vence em silêncio).
// Static zero-dependency scanner: reads the UI files (JS and HTML) and the
// labels emitted by the Rust core (metadata, file types, disk health), failing
// whenever a string looks like Portuguese and lacks a translation. Also rejects
// duplicate keys in the dictionary (in a JS object literal the last key wins silently).
//
// Uso: node scripts/check-ui-i18n.mjs
// Usage: node scripts/check-ui-i18n.mjs
import { readFileSync, readdirSync } from 'node:fs';
import { join, relative } from 'node:path';
@@ -16,7 +15,7 @@ import { fileURLToPath } from 'node:url';
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
const rel = (p) => relative(root, p).replaceAll('\\', '/');
// ---------------------------------------------------------------- dicionário
// ---------------------------------------------------------------- dictionary
const dictPath = join(root, 'apps/gui/ui/i18n-en.js');
const dictSource = readFileSync(dictPath, 'utf8');
@@ -33,8 +32,8 @@ while ((m = keyRe.exec(dictSource))) {
}
if (duplicates.length) {
console.error('Chaves duplicadas em apps/gui/ui/i18n-en.js:');
for (const d of duplicates) console.error(` ${JSON.stringify(d.key)} (linhas ${d.first} e ${d.again})`);
console.error('Duplicate keys in apps/gui/ui/i18n-en.js:');
for (const d of duplicates) console.error(` ${JSON.stringify(d.key)} (lines ${d.first} and ${d.again})`);
process.exitCode = 1;
}
@@ -53,7 +52,7 @@ function translated(text) {
return patterns.some((re) => re.test(text));
}
// ---------------------------------------------------------------- heurística
// ---------------------------------------------------------------- heuristic
const ACCENT = /[áàâãäéèêëíìîïóòôõöúùûüçÁÀÂÃÄÉÈÊËÍÌÎÏÓÒÔÕÖÚÙÛÜÇ]/;
const WORDS = [
@@ -92,13 +91,13 @@ function suspicious(text) {
if (ALLOW.has(s) || ALLOW_EXACT.has(s)) return false;
if (ALLOW_PREFIX.some((p) => s.startsWith(p))) return false;
if (!isPortuguese(s)) return false;
// técnico: seletores, classes, extensões, expressões
// Technical tokens: CSS selectors, classes, extensions, expressions
if (/^[.#\w-]+(\s*[,>+~]\s*[.#\w-]+)+$/.test(s)) return false;
if (/^[a-z0-9_.:/@%{}$-]+$/i.test(s) && !ACCENT.test(s)) return false;
return true;
}
// ---------------------------------------------------------------- extratores
// ---------------------------------------------------------------- extractors
function jsStrings(source, file, baseLine = 1) {
const out = [];
@@ -212,14 +211,14 @@ function jsStrings(source, file, baseLine = 1) {
function htmlStrings(source, file, baseLine = 1) {
const out = [];
const lineAt = (idx) => baseLine + source.slice(0, idx).split('\n').length - 1;
// todo texto entre tags (inclusive começo e fim) — nós de texto reais
// Text between tags (including leading and trailing text nodes)
let last = 0;
for (const m of source.matchAll(/<[^>]*>/g)) {
out.push({ text: source.slice(last, m.index), file, line: lineAt(last) });
last = m.index + m[0].length;
}
out.push({ text: source.slice(last), file, line: lineAt(last) });
// atributos traduzíveis (com aspas simples ou duplas)
// Translatable attributes (single or double quotes)
for (const m of source.matchAll(/\b(placeholder|title|aria-label|alt|data-empty)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
out.push({ text: m[2] ?? m[3], file, line: lineAt(m.index) });
}
@@ -229,16 +228,16 @@ function htmlStrings(source, file, baseLine = 1) {
function rustLabels(source, file) {
const out = [];
const lineAt = (idx) => source.slice(0, idx).split('\n').length;
// m.add("Rótulo", ...) e m.add(\n "Rótulo", ...)
// m.add("Label", ...) and m.add(\n "Label", ...)
for (const m of source.matchAll(/\.add\(\s*"([^"]+)"/g)) out.push({ text: m[1], file, line: lineAt(m.index) });
// m.kind = "Tipo".into()
// m.kind = "Kind".into()
for (const m of source.matchAll(/\.kind\s*=\s*"([^"]+)"/g)) out.push({ text: m[1], file, line: lineAt(m.index) });
// rótulos de listas (saúde do disco, tipos de arquivo) dentro de arrays de tuplas
// List labels (disk health, file types) inside tuple arrays
for (const m of source.matchAll(/\(\s*"([^"]{2,60})"\s*,\s*"(?:[^"]*)"\s*\)/g)) out.push({ text: m[1], file, line: lineAt(m.index) });
return out;
}
// ---------------------------------------------------------------- coleta
// ---------------------------------------------------------------- collection
const uiFiles = readdirSync(join(root, 'apps/gui/ui'))
.filter((f) => f.endsWith('.js') && f !== 'i18n-en.js')
@@ -278,12 +277,12 @@ for (const { text, file, line } of found) {
}
if (missing.length) {
console.error('Textos em português sem tradução em apps/gui/ui/i18n-en.js:\n');
console.error('Untranslated Portuguese strings in apps/gui/ui/i18n-en.js:\n');
for (const x of missing) console.error(` ${x.file}:${x.line} ${JSON.stringify(x.text)}`);
console.error(`\n${missing.length} texto(s) sem tradução.`);
console.error(`\n${missing.length} untranslated string(s).`);
process.exitCode = 1;
}
if (!process.exitCode) {
console.log(`i18n ok: ${found.length} textos verificados, ${Object.keys(EN).length} chaves no dicionário.`);
console.log(`i18n ok: ${found.length} strings checked, ${Object.keys(EN).length} dictionary keys.`);
}
+41 -34
View File
@@ -1,68 +1,75 @@
#!/usr/bin/env bash
# Verificação local: roda o mesmo que o CI roda, antes de você dar push.
# Local verification: runs the same fast checks as CI before pushing.
#
# scripts/verificar.sh rápido (~1 min): formatação, clippy, tradução, scripts, workflows
# scripts/verificar.sh --completo inclui os testes do workspace e o cargo audit
# scripts/check.sh fast (~1 min): fmt, clippy, i18n, scripts, workflows, semver
# scripts/check.sh --full also runs workspace tests and cargo audit
#
# O CI leva de 5 a 30 minutos por execução e roda em Linux; quase tudo que o faz falhar
# aparece aqui em segundos. Se isto passa, o CI só deve revelar o que é próprio dele
# (imagens baixadas, apt do runner).
# CI takes 5 to 30 minutes per run; almost everything that breaks CI is caught
# here in seconds.
set -uo pipefail
cd "$(dirname "$0")/.."
completo=0
[ "${1:-}" = "--completo" ] && completo=1
falhas=()
passo() { # passo "nome" comando...
local nome="$1"; shift
printf '\n\033[1m== %s\033[0m\n' "$nome"
if "$@"; then printf '\033[32mok\033[0m %s\n' "$nome"; else printf '\033[31mFALHOU\033[0m %s\n' "$nome"; falhas+=("$nome"); fi
full=0
[ "${1:-}" = "--full" ] || [ "${1:-}" = "--completo" ] && full=1
failures=()
step() { # step "name" command...
local name="$1"; shift
printf '\n\033[1m== %s\033[0m\n' "$name"
if "$@"; then printf '\033[32mok\033[0m %s\n' "$name"; else printf '\033[31mFAILED\033[0m %s\n' "$name"; failures+=("$name"); fi
}
passo "formatação (cargo fmt)" cargo fmt --all -- --check
# as mesmas flags do CI: avisos viram erro, inclusive nos testes
passo "clippy (workspace, todos os alvos)" env RUSTFLAGS="-D warnings" cargo clippy --workspace --all-targets
passo "cobertura da tradução da interface" node scripts/check-ui-i18n.mjs
passo "teste do latest.json" python3 -B scripts/test_make_latest_json.py
passo "sintaxe dos scripts bash" bash -c 'for f in scripts/*.sh .githooks/*; do bash -n "$f" || exit 1; done'
passo "workflows e templates (YAML válido)" python3 -c "
step "formatting (cargo fmt)" cargo fmt --all -- --check
# Same flags as CI: warnings are treated as errors across all targets, including tests
step "clippy (workspace, all targets)" env RUSTFLAGS="-D warnings" cargo clippy --workspace --all-targets
step "UI translation coverage" node scripts/check-ui-i18n.mjs
step "latest.json unit tests" python3 -B scripts/test_make_latest_json.py
step "bash script syntax" bash -c 'for f in scripts/*.sh .githooks/*; do bash -n "$f" || exit 1; done'
step "workflows and templates (valid YAML)" python3 -c "
import glob, yaml
for f in glob.glob('.gitea/**/*.yml', recursive=True):
yaml.safe_load(open(f))
"
passo "versões consistentes" python3 -c "
step "consistent Semantic Versioning 2.0.0" python3 -c "
import json, re, sys
from pathlib import Path
semver_re = re.compile(
r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
r'(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?'
r'(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
)
def cargo_version(path):
m = re.search(r'^version\s*=\s*\"([^\"]+)\"', Path(path).read_text(), re.M)
if not m:
sys.exit(f'versão não encontrada em {path}')
sys.exit(f'version not found in {path}')
return m.group(1)
versoes = [
versions = [
cargo_version('Cargo.toml'),
cargo_version('apps/gui/src-tauri/Cargo.toml'),
json.load(open('apps/gui/src-tauri/tauri.conf.json'))['version'],
]
if len(set(versoes)) != 1:
sys.exit(f'versões divergentes: Cargo.toml={versoes[0]}, src-tauri={versoes[1]}, tauri.conf={versoes[2]}')
print(versoes[0])
if len(set(versions)) != 1:
sys.exit(f'version mismatch: Cargo.toml={versions[0]}, src-tauri={versions[1]}, tauri.conf={versions[2]}')
if not semver_re.match(versions[0]):
sys.exit(f'invalid SemVer 2.0.0 version: {versions[0]}')
print(versions[0])
"
if [ -f apps/gui/src-tauri/Cargo.toml ] && pkg-config --exists webkit2gtk-4.1 2>/dev/null; then
passo "app desktop (cargo check)" env RUSTFLAGS="" cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
step "desktop app (cargo check)" env RUSTFLAGS="" cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
else
printf '\n(app desktop: pulado, faltam as bibliotecas do WebKitGTK; o CI compila)\n'
printf '\n(desktop app: skipped, WebKitGTK development libraries not installed; built in CI)\n'
fi
if [ "$completo" = 1 ]; then
passo "testes (cargo test --workspace)" cargo test --workspace
passo "auditoria de dependências (cargo audit)" cargo audit
if [ "$full" = 1 ]; then
step "tests (cargo test --workspace)" cargo test --workspace
step "dependency audit (cargo audit)" cargo audit
fi
printf '\n'
if [ ${#falhas[@]} -eq 0 ]; then
printf '\033[32mtudo certo\033[0m: pode dar push.\n'
if [ ${#failures[@]} -eq 0 ]; then
printf '\033[32mall checks passed\033[0m: ready to push.\n'
else
printf '\033[31mfalhou:\033[0m %s\n' "${falhas[*]}"
printf '\033[31mfailed:\033[0m %s\n' "${failures[*]}"
exit 1
fi
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Baixa imagens de referência (BitLocker e FileVault2) do conjunto de testes do cryptsetup.
# Ficam em target/testdata (não versionado). Sem elas, os testes correspondentes são pulados.
# Downloads reference disk images (BitLocker, FileVault 2, dfvfs, AFF4) used by integration tests.
# Stored in target/testdata (untracked). When absent, the corresponding tests are skipped.
set -euo pipefail
dest="$(cd "$(dirname "$0")/.." && pwd)/target/testdata"
mkdir -p "$dest"
+30 -28
View File
@@ -1,47 +1,49 @@
#!/usr/bin/env python3
"""Gera, a partir da logo oficial, o ícone base usado pelo Tauri.
"""Generates the base Tauri icon from the official project logo.
A logo oficial é `logo.png` na raiz do repositório (RGBA, fundo transparente,
serve aos temas claro e escuro). Ela não é alterada aqui. Saída:
apps/gui/src-tauri/icons/source.png 1024 px, a arte sobre o fundo escuro com cantos arredondados;
o CI e `npx @tauri-apps/cli icon` geram o resto dos ícones.
The official logo is `logo.png` in the repository root (RGBA, transparent
background, suitable for both light and dark themes). It is not modified here.
Output:
apps/gui/src-tauri/icons/source.png 1024 px artwork centered over a rounded
dark background; CI and `npx @tauri-apps/cli icon`
derive all remaining icon formats from it.
Uso: python3 scripts/gerar-logo.py
Usage: python3 scripts/generate-logo.py
"""
import os
from PIL import Image, ImageDraw
RAIZ = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
ICONES = os.path.join(RAIZ, "apps/gui/src-tauri/icons")
FUNDO_ICONE = (10, 16, 28) # o azul-noite do fundo da arte original
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
ICONS_DIR = os.path.join(ROOT, "apps/gui/src-tauri/icons")
ICON_BG = (10, 16, 28) # dark navy background matching the original artwork
def recortar_quadrado(im, margem=0.04):
"""Recorta no que é visível (opacidade > 16, ignora resquícios) e centraliza num quadrado."""
def crop_square(im, margin=0.04):
"""Crops to visible pixels (opacity > 16, ignoring faint artifacts) and centers in a square."""
box = im.getchannel("A").point(lambda v: 255 if v > 16 else 0).getbbox()
im = im.crop(box)
w, h = im.size
lado = round(max(w, h) * (1 + 2 * margem))
quadro = Image.new("RGBA", (lado, lado), (0, 0, 0, 0))
quadro.paste(im, ((lado - w) // 2, (lado - h) // 2), im)
return quadro
side = round(max(w, h) * (1 + 2 * margin))
canvas = Image.new("RGBA", (side, side), (0, 0, 0, 0))
canvas.paste(im, ((side - w) // 2, (side - h) // 2), im)
return canvas
def main():
logo = recortar_quadrado(Image.open(os.path.join(RAIZ, "logo.png")).convert("RGBA"))
logo = crop_square(Image.open(os.path.join(ROOT, "logo.png")).convert("RGBA"))
lado = 1024
fundo = Image.new("RGBA", (lado, lado), FUNDO_ICONE + (255,))
arte = logo.resize((round(lado * 0.82),) * 2, Image.LANCZOS)
fundo.paste(arte, ((lado - arte.width) // 2, (lado - arte.height) // 2), arte)
mascara = Image.new("L", (lado, lado), 0)
ImageDraw.Draw(mascara).rounded_rectangle((0, 0, lado - 1, lado - 1), radius=round(lado * 0.2237), fill=255)
icone = Image.new("RGBA", (lado, lado), (0, 0, 0, 0))
icone.paste(fundo, (0, 0), mascara)
os.makedirs(ICONES, exist_ok=True)
destino = os.path.join(ICONES, "source.png")
icone.save(destino, optimize=True)
os.chmod(destino, 0o644)
side = 1024
bg = Image.new("RGBA", (side, side), ICON_BG + (255,))
art = logo.resize((round(side * 0.82),) * 2, Image.LANCZOS)
bg.paste(art, ((side - art.width) // 2, (side - art.height) // 2), art)
mask = Image.new("L", (side, side), 0)
ImageDraw.Draw(mask).rounded_rectangle((0, 0, side - 1, side - 1), radius=round(side * 0.2237), fill=255)
icon = Image.new("RGBA", (side, side), (0, 0, 0, 0))
icon.paste(bg, (0, 0), mask)
os.makedirs(ICONS_DIR, exist_ok=True)
dest = os.path.join(ICONS_DIR, "source.png")
icon.save(dest, optimize=True)
os.chmod(dest, 0o644)
print("icons/source.png")
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Liga o hook de pre-push do repositório (roda scripts/verificar.sh antes de cada push).
# Enables the repository Git hooks (.githooks/pre-push and .githooks/commit-msg).
set -euo pipefail
cd "$(dirname "$0")/.."
git config core.hooksPath .githooks
echo "hook ligado: git push passa a rodar scripts/verificar.sh (desligue com: git config --unset core.hooksPath)"
echo "hooks enabled: git commit validates Conventional Commits in English and git push runs scripts/check.sh (disable with: git config --unset core.hooksPath)"
+13 -13
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env bash
# Gera imagens de disco de teste (FAT32, exFAT, NTFS, ext4) com arquivos
# conhecidos, apaga alguns e grava os SHA-256 esperados em <saida>/expected-<fs>.txt.
# Generates test disk images (FAT32, exFAT, NTFS, ext4) populated with known files,
# deletes a subset of them, and writes expected SHA-256 digests to <out>/expected-<fs>.txt.
#
# Uso: scripts/make-fixtures.sh <diretório-de-saída> [fat32 exfat ntfs ext4]
# Requer (Linux): mtools, dosfstools, exfatprogs/exfat-fuse, ntfs-3g, e2fsprogs,
# imagemagick, zip, python3 e FUSE para exFAT/NTFS.
# Usage: scripts/make-fixtures.sh <output-dir> [fat32 exfat ntfs ext4]
# Requires (Linux): mtools, dosfstools, exfatprogs/exfat-fuse, ntfs-3g, e2fsprogs,
# imagemagick, zip, python3, and FUSE for exFAT/NTFS.
set -euo pipefail
OUT="$(realpath -m "${1:?uso: $0 <saida> [fs...]}")"
OUT="$(realpath -m "${1:?usage: $0 <output-dir> [fs...]}")"
shift || true
FSES=("${@:-fat32 exfat ntfs ext4}")
[ $# -eq 0 ] && FSES=(fat32 exfat ntfs ext4)
@@ -17,7 +17,7 @@ SIZE_MB=96
CONTENT="$OUT/content"
rm -rf "$CONTENT"; mkdir -p "$CONTENT/Documentos/Trabalho" "$CONTENT/Fotos"
# --- conteúdo de teste (mistura de formatos com e sem assinatura) ---
# --- test content (mix of formats with and without file signatures) ---
convert -size 1400x900 plasma:fractal -quality 92 "$CONTENT/Fotos/praia.jpg"
convert -size 900x700 plasma:red-blue "$CONTENT/Fotos/montanha.png"
convert -size 600x400 plasma:green-yellow "$CONTENT/Fotos/mantida.jpg"
@@ -26,7 +26,7 @@ head -c 3000000 /dev/urandom > "$CONTENT/Documentos/dados.bin"
python3 - "$CONTENT" <<'PY'
import sys, sqlite3, os
d = sys.argv[1]
pdf = b"%PDF-1.4\n" + b"".join(b"%% linha %d preenchimento %s\n" % (i, os.urandom(40).hex().encode()) for i in range(6000)) + b"trailer\n<< /Root 1 0 R >>\n%%EOF\n"
pdf = b"%PDF-1.4\n" + b"".join(b"%% line %d padding %s\n" % (i, os.urandom(40).hex().encode()) for i in range(6000)) + b"trailer\n<< /Root 1 0 R >>\n%%EOF\n"
open(d + "/Documentos/Trabalho/relatorio.pdf", "wb").write(pdf)
db = d + "/Documentos/contatos.db"
c = sqlite3.connect(db)
@@ -38,7 +38,7 @@ mkdir -p "$OUT/zip" && ( cd "$OUT/zip" && rm -rf * && mkdir -p word _rels && ech
&& head -c 400000 /dev/urandom | base64 > word/document.xml && zip -q -r "$CONTENT/Documentos/Trabalho/tese.docx" . )
rm -rf "$OUT/zip"
# Arquivos que serão APAGADOS (caminhos relativos a $CONTENT)
# Files that will be DELETED (paths relative to $CONTENT)
DELETE=(Fotos/praia.jpg Fotos/montanha.png Documentos/Trabalho/notas.txt Documentos/Trabalho/relatorio.pdf Documentos/Trabalho/tese.docx Documentos/contatos.db Documentos/dados.bin)
write_expected() { # $1=fs
@@ -60,13 +60,13 @@ for fs in "${FSES[@]}"; do
for f in "${DELETE[@]}"; do mdel -i "$img" "::/$f"; done
;;
exfat)
# Sem FUSE sem privilégio, só o sistema vazio é criado aqui; o teste em Rust
# injeta as entradas (veja crates/guara-core/tests/exfat.rs).
# Without unprivileged FUSE, only the empty filesystem is formatted here;
# the Rust test injects entries directly (see crates/guara-core/tests/exfat.rs).
mkfs.exfat -L GUARATEST "$img" >/dev/null
;;
ntfs)
# ntfscp só grava na raiz; "apagar" = limpar o bit "em uso" do registro da MFT,
# exatamente o que o NTFS faz (clusters e nome permanecem no disco).
# ntfscp writes to the root directory; "deleting" clears the in-use bit on the MFT
# record, matching standard NTFS deletion behavior (clusters and filename remain).
mkntfs -F -Q -L GUARATEST "$img" >/dev/null 2>&1
for f in $(cd "$CONTENT" && find . -type f | sed 's|^\./||'); do
ntfscp -f "$img" "$CONTENT/$f" "/$(basename "$f")" >/dev/null 2>&1
+13 -13
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
"""Monta o latest.json que o atualizador do app lê (Tauri updater), a partir dos pacotes e das
assinaturas `.sig` já enviados à release no Gitea.
"""Builds the latest.json manifest read by the Tauri updater from the release
bundles and `.sig` signatures already uploaded to the Gitea release.
Roda no job de publicação, depois que os jobs de empacotamento terminaram. Montar o arquivo
aqui, em um passo só, evita a corrida de quem tenta atualizá-lo em paralelo por plataforma.
Runs in the final publish job after all matrix packaging jobs complete. Building
the manifest in a single step avoids race conditions from parallel matrix jobs.
Uso: make-latest-json.py <tag> <notas.md> <saida.json>
Precisa de API (URL do repositório na API do Gitea) e TOKEN no ambiente.
Usage: make-latest-json.py <tag> <notes.md> <out.json>
Requires API (Gitea repository API URL) and TOKEN in the environment.
"""
import json
import os
@@ -15,8 +15,8 @@ import urllib.parse
import urllib.request
from datetime import datetime, timezone
# (chaves de plataforma, padrão do nome do pacote). {v} = versão sem o "v".
# Sem macOS: não há runner para compilar essa plataforma.
# (platform keys, package filename patterns). {v} = version without leading "v".
# No macOS: no Apple hardware/signing runner is available.
PLATFORMS = [
(["linux-x86_64", "linux-x86_64-appimage"], ("Guará_{v}_amd64.AppImage", "Guara_{v}_amd64.AppImage")),
(["linux-aarch64", "linux-aarch64-appimage"], ("Guará_{v}_aarch64.AppImage", "Guara_{v}_aarch64.AppImage")),
@@ -36,9 +36,9 @@ def api_get_text(url):
def build(version, notes, assets, pub_date):
"""`assets` = {nome_do_pacote: (url, assinatura)}. Devolve o dicionário do latest.json.
"""`assets` = {package_name: (url, signature)}. Returns the latest.json dict.
Plataforma sem pacote ou sem assinatura sai da lista; o chamador decide se isso é erro."""
Platforms missing a package or signature are omitted; the caller decides whether that is fatal."""
platforms = {}
for keys, patterns in PLATFORMS:
name = next((n for p in patterns if (n := p.format(v=version)) in assets), None)
@@ -66,12 +66,12 @@ def main():
assets[name] = (by_name[name]["browser_download_url"], sig)
data = build(version, open(notes_path, encoding="utf-8").read(), assets,
datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
# Exige os pacotes assinados de Linux (x86_64 e aarch64) e Windows.
# Require signed packages for Linux (x86_64 and aarch64) and Windows.
platforms = data["platforms"]
for must in ("linux-x86_64", "linux-aarch64", "windows-x86_64"):
if must not in platforms:
sys.exit(f"faltam pacotes ou assinaturas de {must} (achei {sorted(platforms)}); "
"confira se TAURI_SIGNING_PRIVATE_KEY está nos segredos")
sys.exit(f"missing package or signature for {must} (found {sorted(platforms)}); "
"verify that TAURI_SIGNING_PRIVATE_KEY is set in repository secrets")
with open(out, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
+6 -6
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""Gera uma imagem FAT32 com muitas fotos JPEG (EXIF de câmeras/datas/GPS) já apagadas.
"""Generates a FAT32 disk image containing deleted JPEG photos with camera/date/GPS EXIF tags.
Uso: make-photo-fixture.py <saida.img> [quantidade=300] [copias=1]
(`copias` repete cada foto N vezes com outro nome: é um jeito rápido de gerar milhares de arquivos.)
Serve para testar a navegação por data/câmera/pasta e as miniaturas.
Usage: make-photo-fixture.py <out.img> [count=300] [copies=1]
(`copies` duplicates each photo N times under a different filename to quickly generate thousands of files.)
Used to test date/camera/folder browsing and thumbnail generation.
"""
import os, random, shutil, struct, subprocess, sys, tempfile
@@ -80,7 +80,7 @@ for i in range(count):
subprocess.run(['convert', '-seed', str(i), '-size', f'{w}x{h}', 'plasma:fractal', '-gravity', 'center', '-pointsize', '34', '-fill', 'white',
'-stroke', 'black', '-strokewidth', '1', '-annotate', '0', f'{y}-{mo:02d}-{d:02d}\n#{i}', '-quality', '82', p], check=True, stderr=subprocess.DEVNULL)
raw = open(p, 'rb').read()
if i % 9 != 0: # alguns sem EXIF
if i % 9 != 0: # leave some images without EXIF metadata
gps = places[i % 4] if i % 3 == 0 else None
raw = add_exif(raw, make_exif(cam[0], cam[1], dt, gps, rnd.choice([1, 1, 1, 6, 8])))
open(p, 'wb').write(raw)
@@ -109,4 +109,4 @@ for folder, name, p in files:
for folder, name, p in files:
subprocess.run(['mdel', '-i', out, f'::/{folder}/{name}'], check=True)
shutil.rmtree(tmp)
print(f'ok: {out} ({len(files)} fotos apagadas)')
print(f'ok: {out} ({len(files)} deleted photos)')
+9 -9
View File
@@ -1,18 +1,18 @@
#!/usr/bin/env bash
# Cria uma imagem XFS com exclusão REAL (feita pelo kernel), para validar a recuperação de XFS
# sem simulação. Precisa de root, porque monta a imagem por loop: sudo scripts/make-xfs-real-deletion.sh
# Creates an XFS disk image with REAL kernel-performed file deletion to validate
# XFS recovery without simulation. Requires root (mounts via loop device):
# sudo scripts/make-xfs-real-deletion.sh
#
# Saída em target/testdata/xfs-real-delete/: disk.img e expected.tsv (nome, tamanho, sha256 e se
# o arquivo foi apagado). O teste em crates/guara-core/tests/xfs.rs usa esses dois arquivos e é
# pulado quando eles não existem.
# Output in target/testdata/xfs-real-delete/: disk.img and expected.tsv (path, size,
# sha256, and deletion status). Used by crates/guara-core/tests/xfs.rs when present.
set -euo pipefail
[ "$(id -u)" = 0 ] || { echo "rode com sudo: sudo $0" >&2; exit 1; }
[ "$(id -u)" = 0 ] || { echo "run with sudo: sudo $0" >&2; exit 1; }
root="$(cd "$(dirname "$0")/.." && pwd)"
out="${1:-$root/target/testdata/xfs-real-delete}"
mkdir -p "$out"
img="$out/disk.img"
rm -f "$img"
truncate -s 320M "$img" # o mkfs.xfs recente recusa menos de 300 MiB
truncate -s 320M "$img" # recent mkfs.xfs rejects volumes smaller than 300 MiB
mkfs.xfs -q -f "$img"
mnt="$(mktemp -d)"
cleanup() { mountpoint -q "$mnt" && umount "$mnt"; rmdir "$mnt" 2>/dev/null || true; }
@@ -27,14 +27,14 @@ def data(seed, n):
x ^= (x << 13) & 0xFFFFFFFFFFFFFFFF; x ^= x >> 7; x ^= (x << 17) & 0xFFFFFFFFFFFFFFFF
out.append((x >> 24) & 0xFF)
return bytes(out)
files = [ # (caminho, tamanho, semente, apagar?)
files = [ # (relative_path, size, seed, should_delete)
("docs/relatorio.dat", 150000, 5, True),
("docs/mantido.dat", 90000, 6, False),
("fotos/foto1.bin", 300000, 7, True),
("fotos/foto2.bin", 40000, 8, False),
("raiz-apagada.txt", 20000, 9, True),
]
# um arquivo com estrutura reconhecível (SQLite), para a recuperação por carving
# Include a file with a recognizable header (SQLite) to test carving recovery
real = open(sqlite, "rb").read()
rows = []
files.append(("dados/exemplo.sqlite", len(real), 0, True))
+5 -5
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Marca registros da MFT como apagados (limpa o bit 'em uso') para fixtures de teste.
"""Marks NTFS MFT records as deleted (clears the in-use bit) for test fixtures.
Uso: ntfs-mark-deleted.py imagem.img caminho/arquivo1 caminho/arquivo2 ...
Só o nome base é usado (ntfscp grava na raiz).
Usage: ntfs-mark-deleted.py image.img path/file1 path/file2 ...
Only the basename is matched (ntfscp writes files to the root directory).
"""
import os
import struct
@@ -17,7 +17,7 @@ mft_lcn = struct.unpack_from("<Q", boot, 48)[0]
rs = struct.unpack_from("<b", boot, 64)[0]
rec = rs * bps * spc if rs > 0 else 1 << -rs
f.seek(mft_lcn * bps * spc)
data = f.read(rec * 4096) # MFT inicial é contígua em imagens pequenas
data = f.read(rec * 4096) # Initial MFT is contiguous on small volumes
count = 0
for i in range(len(data) // rec):
r = bytearray(data[i * rec:(i + 1) * rec])
@@ -39,4 +39,4 @@ for i in range(len(data) // rec):
break
p += ln
f.flush()
print(f"marcados como apagados: {count}", file=sys.stderr)
print(f"marked as deleted: {count}", file=sys.stderr)
+8 -8
View File
@@ -1,19 +1,19 @@
#!/usr/bin/env bash
# Compila o CLI para uma plataforma e empacota: guara-<tag>-<plataforma>.tar.gz
# com um .sha256 ao lado.
# Builds the CLI for a target platform and packages it as:
# guara-<tag>-<platform>.tar.gz alongside a .sha256 checksum file.
#
# scripts/package-cli.sh v1.3.2 linux-x86_64 x86_64-unknown-linux-gnu
#
# Plataformas: linux-x86_64 e linux-aarch64. O alvo diferente da máquina de
# build usa o linker cruzado correspondente (gcc-x86-64-linux-gnu). Escreve os
# arquivos na raiz do repositório e imprime os nomes.
# Supported platforms: linux-x86_64 and linux-aarch64. When cross-compiling
# from a non-x86_64 host, uses gcc-x86-64-linux-gnu. Writes the archives to the
# repository root and prints their filenames.
set -euo pipefail
cd "$(dirname "$0")/.."
tag=${1:?uso: package-cli.sh <tag> <plataforma> <alvo-rust>}
platform=${2:?uso: package-cli.sh <tag> <plataforma> <alvo-rust>}
target=${3:?uso: package-cli.sh <tag> <plataforma> <alvo-rust>}
tag=${1:?usage: package-cli.sh <tag> <platform> <rust-target>}
platform=${2:?usage: package-cli.sh <tag> <platform> <rust-target>}
target=${3:?usage: package-cli.sh <tag> <platform> <rust-target>}
if [ "$target" = x86_64-unknown-linux-gnu ] && [ "$(uname -m)" != x86_64 ]; then
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
+4 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Imprime as notas de uma release: a seção do CHANGELOG.md daquela versão, ou o
# log de commits desde a tag anterior quando o changelog não tem a seção.
# Prints release notes for a given tag: extracts the matching CHANGELOG.md
# section, or falls back to the commit log since the previous tag.
#
# scripts/release-notes.sh v1.2.3
set -euo pipefail
tag=${1:?uso: release-notes.sh <tag>}
tag=${1:?usage: release-notes.sh <tag>}
version=${tag#v}
section=""
@@ -22,7 +22,7 @@ if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then
printf '%s\n' "$section"
else
previous=$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)
echo "## Mudanças${previous:+ desde $previous}"
echo "## Changes${previous:+ since $previous}"
echo
git log --no-merges --format='- %s (%h)' "${previous:+$previous..}$tag"
fi
+8 -8
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
# Anexa arquivos a uma release do Gitea, trocando os de mesmo nome (assim uma
# execução repetida do CI não falha por anexo duplicado).
# Uploads assets to a Gitea release, replacing any existing asset with the same
# filename so re-running a CI job never fails on duplicate attachments.
#
# API=https://git.exemplo.com/api/v1/repos/dono/repo TOKEN=... \
# scripts/release-upload.sh v1.2.3 arquivo1 arquivo2
# API=https://git.example.com/api/v1/repos/owner/repo TOKEN=... \
# scripts/release-upload.sh v1.2.3 file1 file2
set -euo pipefail
: "${API:?defina API (URL do repositório na API do Gitea)}"
: "${TOKEN:?defina TOKEN}"
tag=${1:?uso: release-upload.sh <tag> <arquivo>...}
: "${API:?set API (Gitea repository API URL)}"
: "${TOKEN:?set TOKEN}"
tag=${1:?usage: release-upload.sh <tag> <file>...}
shift
auth=(-H "Authorization: token $TOKEN")
@@ -21,5 +21,5 @@ for file in "$@"; do
curl -fsS -X DELETE "${auth[@]}" "$API/releases/$id/assets/$old"
done
curl -fsS -X POST "${auth[@]}" -F "attachment=@$file" "$API/releases/$id/assets?name=$encoded" >/dev/null
echo "enviado: $name"
echo "uploaded: $name"
done
+5 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Teste de scripts/make-latest-json.py: python3 scripts/test_make_latest_json.py"""
"""Unit tests for scripts/make-latest-json.py: python3 scripts/test_make_latest_json.py"""
import importlib.util
import os
import sys
@@ -16,19 +16,19 @@ BASE = "https://git.lucasrguerra.dev.br/lucasrguerra/guara/releases/download/v1.
def assets(version="1.3.2"):
names = [patterns[0].format(v=version) for _, patterns in m.PLATFORMS]
return {n: (BASE + n, f"assinatura-de-{n}\n") for n in names}
return {n: (BASE + n, f"sig-of-{n}\n") for n in names}
class LatestJson(unittest.TestCase):
def test_all_platforms_and_variants(self):
d = m.build("1.3.2", "### Novidades\n", assets(), "2026-10-30T12:00:00Z")
d = m.build("1.3.2", "### What's New\n", assets(), "2026-10-30T12:00:00Z")
self.assertEqual(d["version"], "1.3.2")
self.assertEqual(d["notes"], "### Novidades")
self.assertEqual(d["notes"], "### What's New")
p = d["platforms"]
for k in ("linux-x86_64", "linux-x86_64-appimage", "linux-aarch64", "windows-x86_64", "windows-x86_64-nsis"):
self.assertIn(k, p)
self.assertTrue(p["windows-x86_64"]["url"].endswith("x64-setup.exe"))
self.assertEqual(p["linux-x86_64"]["signature"], "assinatura-de-Guará_1.3.2_amd64.AppImage")
self.assertEqual(p["linux-x86_64"]["signature"], "sig-of-Guará_1.3.2_amd64.AppImage")
def test_missing_package_is_left_out(self):
a = assets()