fix(download): skip AC-4 repair for non-MP4 decrypt output

Decrypt can emit a raw FLAC stream; the AC-4 repair step fed it to the
MP4 box parser, which failed with a garbage atom type whose control
bytes then broke the unescaped Kotlin error JSON (FormatException in
Dart). Guard the repair call by extension, sniff the box type in Go as
a second layer, and build platform error JSON with JSONObject.quote.
This commit is contained in:
zarzet
2026-07-14 14:32:29 +07:00
parent c6e012b23d
commit 4605517d55
3 changed files with 40 additions and 13 deletions
@@ -2677,7 +2677,7 @@ class MainActivity: FlutterFragmentActivity() {
}
} catch (e: Exception) {
android.util.Log.e("SpotiFLAC", "readFileMetadata failed: ${e.message}", e)
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
@@ -2718,7 +2718,7 @@ class MainActivity: FlutterFragmentActivity() {
}
} catch (e: Exception) {
android.util.Log.e("SpotiFLAC", "editFileMetadata failed: ${e.message}", e)
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
@@ -2731,7 +2731,7 @@ class MainActivity: FlutterFragmentActivity() {
Gobackend.writeM4AFreeformTags(filePath, metadataJson)
} catch (e: Exception) {
android.util.Log.e("SpotiFLAC", "writeM4AFreeformTags failed: ${e.message}", e)
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
@@ -2744,7 +2744,7 @@ class MainActivity: FlutterFragmentActivity() {
Gobackend.ensureAC4Config(filePath, sourcePath)
} catch (e: Exception) {
android.util.Log.e("SpotiFLAC", "ensureAC4Config failed: ${e.message}", e)
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
@@ -2758,7 +2758,7 @@ class MainActivity: FlutterFragmentActivity() {
Gobackend.writeAC4Metadata(filePath, metadataJson, coverPath)
} catch (e: Exception) {
android.util.Log.e("SpotiFLAC", "writeAC4Metadata failed: ${e.message}", e)
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
@@ -2966,7 +2966,7 @@ class MainActivity: FlutterFragmentActivity() {
Gobackend.reEnrichFile(requestJson)
}
} catch (e: Exception) {
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
@@ -3685,7 +3685,7 @@ class MainActivity: FlutterFragmentActivity() {
Gobackend.readAudioMetadataJSON(filePath)
}
} catch (e: Exception) {
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
@@ -3759,7 +3759,7 @@ class MainActivity: FlutterFragmentActivity() {
Gobackend.parseCueSheet(cuePath, audioDir)
}
} catch (e: Exception) {
"""{"error":"${e.message?.replace("\"", "'")}"}"""
"""{"error":${org.json.JSONObject.quote(e.message ?: "unknown")}}"""
}
}
result.success(response)
+18
View File
@@ -3,6 +3,7 @@ package gobackend
import (
"encoding/binary"
"fmt"
"io"
"os"
)
@@ -295,6 +296,23 @@ func EnsureAC4ConfigBox(decryptedPath, sourcePath string) error {
f.Close()
return err
}
// A non-MP4 decrypt output (e.g. a raw FLAC stream) is not an AC-4 file;
// bail out before the box parser reports it as a corrupt MP4. A real
// ISO-BMFF box type is four printable ASCII bytes.
var head [8]byte
if _, err := f.ReadAt(head[:], 0); err != nil {
f.Close()
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil
}
return err
}
for _, c := range head[4:8] {
if c < 0x20 || c > 0x7e {
f.Close()
return nil
}
}
moovBuf, moovOffset, moovFound, err := loadTopLevelMP4Box(f, info.Size(), "moov")
if err != nil || !moovFound {
f.Close()
@@ -11,6 +11,13 @@ class _DecryptOutcome {
const _DecryptOutcome(this.path, {this.newFileName, this.failStage});
}
/// AC-4 repair only applies to MP4 containers; decrypt can also emit raw
/// FLAC, which the native MP4 box parser would reject as corrupt.
bool _isMp4Container(String path) {
final lower = path.toLowerCase();
return lower.endsWith('.m4a') || lower.endsWith('.mp4');
}
extension _DownloadQueueFinalization on DownloadQueueNotifier {
/// Builds the [DownloadHistoryItem] shared by the native-worker and inline
/// completion paths. Fields whose source/derivation legitimately differs
@@ -297,7 +304,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
return null;
}
addCleanup(decryptedTempPath);
if (repairAc4) {
if (repairAc4 && _isMp4Container(decryptedTempPath)) {
try {
await PlatformBridge.ensureAC4Config(decryptedTempPath, tempPath);
} catch (e) {
@@ -345,10 +352,12 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
failStage: DownloadQueueNotifier._decryptStageDecrypt,
);
}
try {
await PlatformBridge.ensureAC4Config(decryptedPath, filePath);
} catch (e) {
_log.w('AC-4 container repair skipped: $e');
if (_isMp4Container(decryptedPath)) {
try {
await PlatformBridge.ensureAC4Config(decryptedPath, filePath);
} catch (e) {
_log.w('AC-4 container repair skipped: $e');
}
}
try {
await deleteFile(filePath);