Skip to content

Commit e81dd4b

Browse files
committed
Refactor StringCare plugin architecture and enhance functionality: migrate domain models to domain.models, streamline execution result handling with structured responses, and improve XML parsing with SAX-first approach. Update CI workflow to include coverage verification and enhance README.md with refactor plan checklist. Remove obsolete model classes and improve task management for better performance and clarity.
1 parent 5cb10cb commit e81dd4b

46 files changed

Lines changed: 1232 additions & 478 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
run: chmod +x gradlew
2222

2323
- name: Plugin unit tests
24-
run: ./gradlew :plugin:test :plugin:jacocoTestReport :plugin:detekt --no-daemon
24+
run: ./gradlew :plugin:test :plugin:jacocoTestReport :plugin:jacocoTestCoverageVerification :plugin:detekt --no-daemon
2525

2626
- name: Plugin integration tests (optional)
2727
continue-on-error: true

.github/workflows/publish.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Manual / tag-driven publish to Maven Central (requires signing + Nexus credentials).
2+
name: Publish
3+
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
dry_run:
8+
description: "If true, only build plugin (no upload)"
9+
required: false
10+
default: "true"
11+
12+
jobs:
13+
publish:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: actions/setup-java@v4
19+
with:
20+
distribution: temurin
21+
java-version: 17
22+
23+
- name: Grant execute permission for gradlew
24+
run: chmod +x gradlew
25+
26+
- name: Build plugin JAR
27+
run: ./gradlew :plugin:jar --no-daemon
28+
29+
# Wire signing + publish when secrets are configured:
30+
# ./gradlew :plugin:publishPluginPublicationToSonatypeRepository -PnexusUsername=... -PnexusPassword=...

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,34 @@
22

33
## [Unreleased] — refactor / 6.x groundwork
44

5+
### Added
6+
7+
- **Docs**: [Refactor plan checklist](docs/refactor-plan-checklist.md) — seguimiento hecho/pendiente del roadmap del plugin.
8+
59
### Changed
610

11+
- **Architecture**: Domain models under `domain.models` (with `models` typealiases for compatibility); `infrastructure` packages for parsers, Gradle wiring, crypto, filesystem; thin `ObfuscateStringsUseCase` + `ResourceRepository`.
12+
- **XML**: SAX-first `strings.xml` parsing with DOM fallback for nested markup; `XmlAttributes` / `XmlParser` facade.
13+
- **Tasks**: Gradle `Property` inputs on obfuscate/restore/preview tasks; `@DisableCachingByDefault` (sources are mutated in place — not suitable for `@CacheableTask`).
14+
- **AGP**: `compileOnly` for the Android Gradle Plugin dependency (provided at runtime on Android projects).
15+
- **JAR size**: With `compileOnly` AGP, plugin JAR is ~180KB vs previous multi‑MB fat jar (verify locally with `./gradlew :plugin:jar`).
716
- **Gradle**: `StringCareBuildService` (shared build service) replaces mutable static state on `StringCarePlugin` for paths, temp dir, and variant applicationIds.
817
- **Dependencies**: Removed Guava and Gson; added `kotlinx-serialization-json` for task JSON list inputs.
9-
- **Execution**: Shell commands use `ProcessBuilder` with a 60s timeout and structured `ExecutionResult` (`Success` / `Failure`).
18+
- **Execution**: Shell commands use `ProcessBuilder` with a 60s timeout and structured `ExecutionResult` (`Success` / `Failure` / `Timeout`).
1019
- **Native host libs**: SHA-256 verification before `System.load`, retries, optional verbose logging tied to `debug` in tasks.
1120
- **XML / scan**: Faster attribute iteration in `parseXML`; `walkTopDown` skips `build/`, `.gradle/`, `.git/`, `node_modules/`; `mapNotNull` for resource/asset discovery; idempotent `StringCareConfiguration.normalize()`.
1221
- **Tooling**: Detekt + baseline, ktlint (non-failing), JaCoCo hook, Develocity build scan terms in root `settings.gradle.kts`.
22+
- **Backups**: Resource/asset backups use temp-file copy + atomic move when the filesystem supports it.
23+
- **Tests**: `ObfuscationServiceTest` (JNI roundtrip when loaded), `BackupServiceTest`, UTF-16 / malformed XML parser cases; CI runs `jacocoTestCoverageVerification` with a low interim line threshold.
24+
25+
### Breaking changes
26+
27+
- **Internal APIs**: Static mutable state on `StringCarePlugin` (paths, temp folder, variant map) was removed in favor of `StringCareBuildService`. Any build logic or tests reaching into those internals must use task inputs / the registered build service instead.
28+
- **Dependencies on the plugin JAR**: Guava and Gson are no longer bundled; list-style DSL fields are serialized with Kotlin serialization in task properties. Pure-Java consumers of internal packages are unsupported.
29+
30+
### Performance (roadmap vs previous generations)
31+
32+
- XML handling targets **SAX-first** parsing with DOM only when nested `<string>` markup requires it, and filesystem walks **prune** heavy directories (`build/`, `.gradle/`, `.git/`, `node_modules/`). End-to-end timings depend on project size; run `./gradlew :plugin:test` and your app’s obfuscation tasks locally to validate.
1333

1434
### Migration notes
1535

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ StringCare Android provides **compile-time obfuscation** for strings and assets
1313
- [Publishing](publishing.md) — Release workflow, secrets, and local publish (`publishToMavenLocal`).
1414
- [Migration](migration.md) — Upgrading from 4.x to 5.0 (groupId, plugin ID, package, Gradle/AGP).
1515
- [Architecture](architecture.md) — Mono-repo layout, library vs plugin, JNI, and Variant API.
16+
- [Refactor plan checklist](refactor-plan-checklist.md) — Estado del roadmap de refactor del plugin (hecho vs pendiente).
1617
- [Contributing](contributing.md) — Release workflow secrets and local publish steps.
1718
- [Troubleshooting](troubleshooting.md) — Submodule, signing, plugin not found, publish job, JNI/NDK.
1819
- [Verify obfuscation](verify-obfuscation.md) — Comandos `gradlew` para comprobar que strings/assets se ofuscan (nativas del host, `syncPluginNativeLibraries`).

docs/refactor-plan-checklist.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Refactor plan checklist (plugin / repo)
2+
3+
Estado al **2026-03-19**. Marca lo implementado en el código actual; úsalo para seguimiento del roadmap.
4+
5+
Leyenda: **`[x]`** hecho · **`[ ]`** pendiente / parcial · **`[~]`** hecho con matices (ver nota)
6+
7+
---
8+
9+
## Fase 0 — Setup y calidad base
10+
11+
- [x] Rama / flujo de trabajo acordado para cambios grandes del plugin
12+
- [x] Detekt en `plugin/build.gradle.kts` + `config/detekt/detekt.yml` + **baseline** (`baseline.xml`)
13+
- [x] Ktlint en el plugin (`ignoreFailures`, exclusión legacy `Vars.kt`)
14+
- [x] Develocity / build scan en `settings.gradle.kts` (publicación condicional a fallos)
15+
- [x] Tests JUnit 5 por defecto; tarea `:plugin:integrationTest` + `@Tag("integration")` para clones/nested Gradle
16+
17+
---
18+
19+
## Fase 1 — Seguridad y ejecución
20+
21+
- [x] Estado mutable del plugin migrado a **`StringCareBuildService`** + `StringCareSession` solo para tests
22+
- [x] Tareas críticas con **`usesService`** al build service
23+
- [x] Comandos shell vía **`ProcessBuilder`** (no shell inseguro directo en el path feliz)
24+
- [x] Timeout (~60s) y **`ExecutionResult.Timeout`**
25+
- [x] JNI del host: verificación **SHA-256** antes de cargar, reintentos, **`LoadResult`**, limpieza de temporales JNI (sin `deleteOnExit` como única estrategia)
26+
27+
---
28+
29+
## Fase 2 — Rendimiento y XML
30+
31+
- [x] Parser **SAX-first** para `strings.xml` + **fallback DOM** si hay anidamiento / detector falla
32+
- [x] Fachada `XmlParser` / `StringsXmlParser`; `XParser` delega
33+
- [x] Walk de recursos con **prune** de `build/`, `.gradle/`, `.git/`, `node_modules/`
34+
- [x] `StringCareConfiguration.normalize()` idempotente
35+
- [x] Copia de backup **temp + move atómico** (`BackupCopy` + `ResourceFile` / `AssetsFile`)
36+
- [ ] Módulo **JMH** u otro benchmark reproducible para el parser
37+
- [ ] Regresión de rendimiento automatizada en CI
38+
39+
---
40+
41+
## Fase 3 — APIs Gradle
42+
43+
- [x] Entradas de tareas como **`Property<>`** donde aplica
44+
- [x] **`@DisableCachingByDefault`** en tareas que mutan el árbol de fuentes (no `@CacheableTask`)
45+
- [x] Wiring con AGP: dependencias de merge/process/assets dentro de `onVariants {}` usando `tasks.matching { it.name == ... }.configureEach { ... }` (sin `afterEvaluate` ni `findByName`)
46+
- [x] Sustituido `afterEvaluate` por wiring lazy y seguro por nombre de tarea
47+
- [ ] Declarar **`@InputFiles` / outputs** fiables para tareas mutadoras (difícil mientras se editan fuentes in-place)
48+
49+
---
50+
51+
## Fase 4 — Arquitectura (capas)
52+
53+
- [x] Modelos en `domain.models` (`StringEntity`, `ResourceFile`, `AssetsFile`, `ExecutionResult`, `Backupable`, etc.)
54+
- [x] `infrastructure`: parsers, filesystem (`BackupService`), crypto (`ObfuscationService`), Gradle (`FileSystemResourceRepository`)
55+
- [x] Casos de uso / repositorios (`ObfuscateStringsUseCase`, `ResourceRepository`)
56+
- [x] AGP como **`compileOnly`** en el plugin
57+
- [x] JSON de listas con **kotlinx-serialization** (sin Gson)
58+
- [x] **`ExtractFingerprintUseCase`** en `domain.usecases` (tests pueden llamar `extract` con `log` por defecto; `extractFingerprint` delega desde `internal`)
59+
- [ ] Partir **`Extensions.kt`** en módulos más pequeños (Gradle vs FS vs XML)
60+
61+
---
62+
63+
## Fase 5 — Kotlin y robustez
64+
65+
- [x] Reducción de **`!!`** en fuentes principales del plugin (revisar periódicamente)
66+
- [x] Utilidades tipo `toReadableString` donde aplica
67+
- [~] API de ejecución: ahora **`execute(command: String)``Result<ExecutionResult>`** y existe overload con **`List<String>`**; queda como deuda dejar `List<String>` como API única
68+
69+
---
70+
71+
## Fase 6 — Tests y cobertura
72+
73+
- [x] Tests unitarios por defecto **sin** integración pesada (`:plugin:test` excluye tag `integration`)
74+
- [x] JaCoCo: `jacocoTestReport` + **`jacocoTestCoverageVerification`** (umbral **bajo interino** ~12% líneas; objetivo roadmap **>80%**)
75+
- [x] Tests de infraestructura XML (`XmlParserInfrastructureTest`: SAX, DOM implícito, **UTF-16**, XML mal formado)
76+
- [x] `ObfuscationServiceTest` (roundtrip JNI con **`assumeTrue(Stark.isNativeLibLoaded() is LoadResult.Loaded)`)**
77+
- [x] `BackupServiceTest` vía `BackupService` + `defaultConfig()`
78+
- [x] Otros tests de configuración / JSON / fingerprint según el árbol actual (`ConfigurationNormalizationTest`, `FingerprintExtractionTest`, `JsonListsTest`, etc.)
79+
- [ ] Fixtures **`test-projects/`** minimalistas adicionales (además de samples existentes)
80+
- [ ] Cobertura **>80%** como gate duro en CI
81+
82+
---
83+
84+
## Fase 7 — Mantenibilidad
85+
86+
- [x] Detekt con baseline (no “cero issues” sin baseline)
87+
- [ ] Subir poco a poco reglas / reducir baseline hasta acercarse a **Detekt sin baseline**
88+
- [x] Ktlint operativo; fallos no bloquean build (`ignoreFailures`)
89+
- [~] KDoc: presente en puntos clave; ampliar en APIs públicas internas del plugin
90+
91+
---
92+
93+
## Fase 8 — Documentación y CI/CD
94+
95+
- [x] `CHANGELOG.md` actualizado con hitos del refactor
96+
- [x] `MIGRATION.md` / docs de migración
97+
- [x] `SECURITY.md` y documentación de contribución / build
98+
- [x] `plugin/README.md` (requisitos, DSL avanzado, troubleshooting, rendimiento)
99+
- [x] **`.github/workflows/ci.yml`**: test, JaCoCo report + **verification**, detekt, integration opcional, `assembleDebug`
100+
- [x] **`.github/workflows/publish.yml`**: esqueleto (build JAR; publicación manual documentada)
101+
- [ ] **OWASP Dependency-Check** (o equivalente) en Gradle y/o CI del plugin
102+
- [ ] **Matriz CI** (p.ej. varias versiones AGP / Gradle) donde tenga sentido para el composite build
103+
104+
---
105+
106+
## Resumen rápido
107+
108+
| Área | Estado |
109+
|-------------------|--------|
110+
| BuildService / JNI / ProcessBuilder | Listo |
111+
| XML SAX + DOM | Listo |
112+
| Backup atómico | Listo (sin locks ni dirs UUID dedicados) |
113+
| AGP wiring | Estable con `onVariants` + `tasks.matching(...).configureEach` |
114+
| Tests + JaCoCo | Listo con umbral bajo; subir cobertura |
115+
| OWASP / JMH / matriz AGP | Pendiente |
116+
117+
---
118+
119+
*Si el plan original vive fuera del repo (p.ej. adjunto en Cursor), copia aquí los IDs `phase-*` y alinéalos con las secciones anteriores.*

plugin/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,39 @@ See repo root [CHANGELOG.md](../CHANGELOG.md), [MIGRATION.md](../MIGRATION.md),
1616
- Default `./gradlew :plugin:test` excludes `@Tag("integration")` (network + `git clone` KotlinSample + nested Gradle).
1717
- Full suite: `./gradlew :plugin:integrationTest` (may fail offline or if the sample repo changes).
1818

19+
### Advanced configuration
20+
21+
Apply in the **app** module `build.gradle.kts`:
22+
23+
```kotlin
24+
plugins {
25+
id("com.android.application")
26+
id("dev.vyp.stringcare.plugin")
27+
}
28+
29+
stringcare {
30+
debug = false
31+
skip = false
32+
mockedFingerprint = "" // optional: offline / CI
33+
srcFolders.add("src/main")
34+
stringFiles.add("strings.xml")
35+
assetsFiles.add("*.json")
36+
}
37+
```
38+
39+
### Troubleshooting
40+
41+
| Symptom | What to try |
42+
|--------|-------------|
43+
| `native library not available` | Run on a supported host/OS; ensure JNI prebuilts are packaged (see `preparePluginNativeLibraries`). |
44+
| Wrong or empty signing SHA1 | Run `./gradlew signingReport` for the same variant; check `mockedFingerprint` if offline. |
45+
| Tasks not ordered | Ensure `com.android.application` is applied before StringCare; use AGP 8.7+. |
46+
47+
### Performance tuning
48+
49+
- **Large projects**: SAX parsing is used for flat `<string>` entries; nested HTML strings fall back to DOM for that file.
50+
- **CI**: Use `./gradlew :plugin:test` (fast); defer `:plugin:integrationTest` to nightly or manual runs.
51+
- **Configuration cache**: Enabled at the root build; run `./gradlew :app:assembleDebug --configuration-cache` to validate.
1952

2053
License
2154
-------

plugin/build.gradle.kts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ repositories {
3838

3939
dependencies {
4040
implementation(gradleApi())
41-
implementation("com.android.tools.build:gradle:8.7.3")
41+
compileOnly("com.android.tools.build:gradle:8.7.3")
4242
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
4343
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
4444
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
@@ -89,6 +89,20 @@ tasks.jacocoTestReport {
8989
}
9090
}
9191

92+
/** Soft gate: raise minimum over time (target &gt;80% per roadmap). */
93+
tasks.jacocoTestCoverageVerification {
94+
dependsOn(tasks.test)
95+
violationRules {
96+
rule {
97+
limit {
98+
counter = "LINE"
99+
value = "COVEREDRATIO"
100+
minimum = "0.12".toBigDecimal()
101+
}
102+
}
103+
}
104+
}
105+
92106
detekt {
93107
buildUponDefaultConfig = true
94108
allRules = false
@@ -102,6 +116,7 @@ ktlint {
102116
ignoreFailures.set(true)
103117
filter {
104118
exclude("**/build/**")
119+
exclude("**/Vars.kt")
105120
}
106121
}
107122

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dev.vyp.stringcare.plugin.domain.models
2+
3+
import java.io.File
4+
5+
data class AssetsFile(
6+
val file: File,
7+
val sourceFolder: String,
8+
val module: String,
9+
) : Backupable {
10+
override fun backup(tempRoot: String): File {
11+
val cleanPath =
12+
"$tempRoot${File.separator}$module${File.separator}$sourceFolder${file.absolutePath.split(sourceFolder)[1]}"
13+
.replace("${File.separator}${File.separator}", File.separator)
14+
val backupFile = File(cleanPath)
15+
copyToBackupAtomically(file, backupFile)
16+
return backupFile
17+
}
18+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package dev.vyp.stringcare.plugin.domain.models
2+
3+
import java.io.File
4+
import java.nio.file.AtomicMoveNotSupportedException
5+
import java.nio.file.Files
6+
import java.nio.file.StandardCopyOption
7+
8+
/**
9+
* Copies [source] to [dest] via a temp file in the same directory, then replaces [dest].
10+
* Uses [StandardCopyOption.ATOMIC_MOVE] when supported so readers rarely see a half-written file.
11+
*/
12+
internal fun copyToBackupAtomically(
13+
source: File,
14+
dest: File,
15+
) {
16+
dest.parentFile?.mkdirs()
17+
val parent = dest.parentFile ?: error("Backup destination has no parent: ${dest.absolutePath}")
18+
val tmp = File.createTempFile("sc-backup-", ".tmp", parent)
19+
try {
20+
Files.copy(source.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING)
21+
try {
22+
Files.move(
23+
tmp.toPath(),
24+
dest.toPath(),
25+
StandardCopyOption.ATOMIC_MOVE,
26+
StandardCopyOption.REPLACE_EXISTING,
27+
)
28+
} catch (_: AtomicMoveNotSupportedException) {
29+
Files.move(tmp.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING)
30+
}
31+
} finally {
32+
if (tmp.exists()) {
33+
tmp.delete()
34+
}
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package dev.vyp.stringcare.plugin.domain.models
2+
3+
import java.io.File
4+
5+
/** Filesystem types that support backup into a StringCare temp root. */
6+
fun interface Backupable {
7+
fun backup(tempRoot: String): File
8+
}

0 commit comments

Comments
 (0)