From fd1dcc072581439db1516920d420d48beb0139cb Mon Sep 17 00:00:00 2001 From: Alex Han Date: Tue, 25 Aug 2026 17:38:57 +0900 Subject: [PATCH] fix: synchronize quarantine buffer accounting --- CHANGELOG.md | 47 +++++++++++-------- app/build.gradle | 4 +- .../app/capture/BoundedFlowPayloadBuffer.kt | 11 ++++- .../capture/BoundedFlowPayloadBufferTest.kt | 16 +++++++ 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e293ca..8ae5fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to mobileGF2logger are documented here. +## 2.4.1 - 2026-08-25 + +### Changed + +- Require captured packets, roster imports, parsed-packet history, and backups + to enter an explicit identity-backed Platoon scope; legacy unscoped data is + no longer exposed as a selectable production profile. +- Refine the Home selector and capture card, use cohesive rounded dialogs, + compact packet-history settings, and prevent repeated transient messages + from accumulating in the interface. + +### Fixed + +- Keep profile registration, active selection, reset routing, backup restore, + and retained Activity state consistent across isolated Platoons. +- Remove metadata for non-parsed VPN flows on every close path and prevent a + delayed open callback from restoring metadata after a rejected close. +- Make packet-history option rows fully tappable and let weekly evidence badges + expand safely with the user's system font size. +- Cap the pre-identity quarantine at 32 decoded payloads per flow and 128 in + total, with synchronized accounting across parser and native-close threads. + ## 2.4.0 - 2026-08-25 ### Added @@ -21,40 +43,25 @@ All notable changes to mobileGF2logger are documented here. ### Changed -- Refine the Home capture card and active-Platoon selector, use cohesive - rounded dialogs, compact packet-history settings, and replace queued transient - messages so repeated actions do not leave stale feedback behind. - Quarantine up to 32 decoded payloads per TCP flow until both its supported Android client and valid Platoon identity are known; unverified flows never enter management storage. - Keep one-time-capture completion evidence isolated per detected Platoon so two clients cannot accidentally complete one checklist. -- Leave unscoped v2.3.x files untouched but remove them from selectable - production profiles; captured packets require a confirmed `21905` identity - before they can create or enter an immutable data scope. -- Require roster CSV imports to target an explicitly selected existing - profile or a newly user-declared client/server/name/ID profile, name that - destination in the preview, and explain that `21917` cannot identify or - verify a Platoon by itself. -- Scope recent and saved parsed-packet history to the admitted profile and - keep unconfirmed flow payloads out of history as well as management storage. -- Allow a scoped v3 backup to recreate its embedded Platoon profile on an empty - installation, while continuing to reject identity-free legacy archives. +- Preserve v2.3.x data as an unmoved legacy profile while new Platoons use + immutable private databases and retained-evidence directories. - Replace the arbitrary timezone list with the six supported server presets; an unconfigured client must be selected once before its first profile is admitted. ### Fixed -- Improve weekly evidence guidance and restore consistent spacing around weekly - notes and member-detail fields in both light and dark themes. - Resolve all six review findings from v2.3.3: clean flow metadata without a parser, order known same-day joins by instant, bound manual weekly notes, preserve captured member names, replace timezone-derived history atomically, and accept `21905` checklist evidence only after identity validation. -- Bound the profile registry, profile metadata, and pre-identity quarantine with - both per-flow and aggregate decoded-payload caps; reject invalid restores before - metadata changes, preserve the selected import scope through preview/apply, and - restore the matching client-region routing. +- Bound the profile registry, profile metadata, and pre-identity flow buffer; + reject invalid restores before metadata changes, preserve the selected import + scope through preview/apply, and restore the matching client-region routing. - Quarantine identity-changing or admission-failed flows, admit at most one new profile per client per user-started capture, and let users forget selector metadata without deleting the isolated Platoon data. diff --git a/app/build.gradle b/app/build.gradle index 233ed80..dec90c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,8 +154,8 @@ android { applicationId = 'dev.gf2log' minSdk = 26 targetSdk = 36 - versionCode = 20400 - versionName = '2.4.0' + versionCode = 20401 + versionName = '2.4.1' testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner' ndk { diff --git a/app/src/main/java/dev/gf2log/app/capture/BoundedFlowPayloadBuffer.kt b/app/src/main/java/dev/gf2log/app/capture/BoundedFlowPayloadBuffer.kt index e79dd47..0b2974e 100644 --- a/app/src/main/java/dev/gf2log/app/capture/BoundedFlowPayloadBuffer.kt +++ b/app/src/main/java/dev/gf2log/app/capture/BoundedFlowPayloadBuffer.kt @@ -1,6 +1,9 @@ package dev.gf2log.app.capture -/** Bounded pre-identity quarantine; overflow permanently rejects that flow until closure. */ +/** + * Bounded pre-identity quarantine; overflow permanently rejects that flow until closure. + * All state access is serialized because rejected-close cleanup can run off the parser executor. + */ internal class BoundedFlowPayloadBuffer( private val maxItemsPerFlow: Int, private val maxTotalItems: Int, @@ -14,6 +17,7 @@ internal class BoundedFlowPayloadBuffer( require(maxTotalItems > 0) } + @Synchronized fun offer(flowId: Long, item: T): OfferResult { if (flowId in rejected) return OfferResult.REJECTED val items = pending.getOrPut(flowId, ::ArrayDeque) @@ -26,24 +30,29 @@ internal class BoundedFlowPayloadBuffer( return OfferResult.ACCEPTED } + @Synchronized fun take(flowId: Long): List { val items = pending.remove(flowId) ?: return emptyList() totalItems -= items.size return items.toList() } + @Synchronized fun isRejected(flowId: Long): Boolean = flowId in rejected + @Synchronized fun reject(flowId: Long) { totalItems -= pending.remove(flowId)?.size ?: 0 rejected += flowId } + @Synchronized fun remove(flowId: Long) { totalItems -= pending.remove(flowId)?.size ?: 0 rejected.remove(flowId) } + @Synchronized fun clear() { pending.clear() rejected.clear() diff --git a/app/src/test/java/dev/gf2log/app/capture/BoundedFlowPayloadBufferTest.kt b/app/src/test/java/dev/gf2log/app/capture/BoundedFlowPayloadBufferTest.kt index 795417a..f0603a9 100644 --- a/app/src/test/java/dev/gf2log/app/capture/BoundedFlowPayloadBufferTest.kt +++ b/app/src/test/java/dev/gf2log/app/capture/BoundedFlowPayloadBufferTest.kt @@ -1,5 +1,6 @@ package dev.gf2log.app.capture +import java.lang.reflect.Modifier import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -60,4 +61,19 @@ class BoundedFlowPayloadBufferTest { assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(4, "four")) assertEquals(listOf("four"), buffer.take(4)) } + + @Test + fun everyStatefulOperationIsSerializedAcrossParserAndCloseThreads() { + val synchronizedMethods = setOf("offer", "take", "isRejected", "reject", "remove", "clear") + val methodsByName = BoundedFlowPayloadBuffer::class.java.declaredMethods + .filter { it.name in synchronizedMethods } + .associateBy { it.name } + + assertEquals(synchronizedMethods, methodsByName.keys) + synchronizedMethods.forEach { name -> + assertTrue("$name must synchronize quarantine bookkeeping", Modifier.isSynchronized( + requireNotNull(methodsByName[name]).modifiers, + )) + } + } }