Skip to content

Commit f4d3cfa

Browse files
committed
Harden wav, flac, and aifc parsing
reject unsupported compressed WAV formats and validate extensible sub-formats to avoid incorrect duration/bitrate reporting add RF64 support by honoring ds64/data sentinel sizes and using unsigned chunk-size handling for safer bounds checks reject reserved FLAC metadata block type 127 and estimate bitrate from encoded audio bytes after metadata instead of total file size validate AIFC COMM compression type and accept only NONE/sowt for decode-safe probing add regression tests for WAV, FLAC, and AIFF parser edge cases and expand CI to run Maven verify on Java 17 and 21
1 parent a4f05c8 commit f4d3cfa

File tree

11 files changed

+424
-22
lines changed

11 files changed

+424
-22
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ on:
99

1010
jobs:
1111
build:
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
java: ['17', '21']
1216
runs-on: ubuntu-latest
1317

1418
steps:
1519
- name: Checkout
1620
uses: actions/checkout@v4
1721

18-
- name: Set up JDK 17
22+
- name: Set up JDK ${{ matrix.java }}
1923
uses: actions/setup-java@v4
2024
with:
2125
distribution: temurin
22-
java-version: '17'
26+
java-version: ${{ matrix.java }}
2327
cache: maven
2428

25-
- name: Build and test with Maven
26-
run: mvn -B clean test
29+
- name: Build and verify
30+
run: mvn -B clean verify
2731

.github/workflows/deep-tests.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Deep Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
workflow_dispatch:
9+
10+
jobs:
11+
deep-tests:
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
java: ['17', '21']
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Set up JDK ${{ matrix.java }}
23+
uses: actions/setup-java@v4
24+
with:
25+
distribution: temurin
26+
java-version: ${{ matrix.java }}
27+
cache: maven
28+
29+
- name: Compile and package gate
30+
run: mvn -B -DskipTests clean package
31+
32+
- name: Full project test suite
33+
run: mvn -B test
34+
35+
- name: Facade and integration-style tests
36+
run: |
37+
mvn -B -Dtest=CodecMediaFacadeTest test
38+
mvn -B -Dtest=CodecMediaRoundTripConversionTest test
39+
mvn -B -Dtest=CodecMediaPlayTest test
40+
41+
- name: Audio parser unit tests
42+
run: |
43+
mvn -B -Dtest=Mp3ParserTest test
44+
mvn -B -Dtest=OggParserTest test
45+
mvn -B -Dtest=WavParserTest test
46+
mvn -B -Dtest=FlacParserTest test
47+
mvn -B -Dtest=AiffParserTest test
48+
49+
- name: IO unit tests
50+
run: mvn -B -Dtest=ByteArrayReaderTest test
51+
52+
- name: Usage example smoke test
53+
run: mvn -B -Dtest=CodecMediaUsageExample test

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Strengthened OGG logical-stream parsing in [`OggParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/ogg/OggParser.java) with per-stream page-sequence validation and serial-scoped metrics for multiplexed files.
1515
- Refined Vorbis bitrate-mode classification in [`OggParser.detectVorbisBitrateMode()`](src/main/java/me/tamkungz/codecmedia/internal/audio/ogg/OggParser.java) to infer from observed bitrate variation instead of coarse nominal/page-count heuristics.
1616
- Replaced broad OGG payload string scanning with structured Vorbis/Opus comment-header parsing in [`OggParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/ogg/OggParser.java), and fixed sequence tracking to use `long` to avoid overflow.
17+
- Updated [`WavParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/wav/WavParser.java) to read/validate `audioFormat` from `fmt ` and reject unsupported compressed WAV formats instead of silently computing incorrect duration.
18+
- Added RF64-aware WAV parsing in [`WavParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/wav/WavParser.java), including unsigned chunk-size handling and `data` size sentinel (`0xFFFFFFFF`) resolution via `ds64`.
19+
- Updated [`FlacParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/flac/FlacParser.java) to reject reserved metadata block type `127` per FLAC spec.
20+
- Updated [`FlacParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/flac/FlacParser.java) bitrate estimation to use encoded audio payload region after metadata blocks (instead of whole file bytes), reducing artwork/metadata inflation.
21+
- Updated [`AiffParser`](src/main/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffParser.java) to validate AIFC `COMM` compression type and reject unsupported compressed variants.
1722

1823
### Added
1924
- Added MP3 parser regression tests for Xing-priority duration, trailing ID3v1 handling, and unsupported Layer I/II diagnostics in [`Mp3ParserTest`](src/test/java/me/tamkungz/codecmedia/internal/audio/mp3/Mp3ParserTest.java).
2025
- Added OGG parser tests for Vorbis CBR/VBR mode inference, broken page-sequence detection, and multiplexed-stream metric isolation in [`OggParserTest`](src/test/java/me/tamkungz/codecmedia/internal/audio/ogg/OggParserTest.java).
26+
- Added WAV parser tests for unsupported compressed format rejection and RF64 `ds64`/`data` sentinel handling in [`WavParserTest`](src/test/java/me/tamkungz/codecmedia/internal/audio/wav/WavParserTest.java).
27+
- Added FLAC parser tests for reserved block type rejection and metadata-heavy bitrate estimation behavior in [`FlacParserTest`](src/test/java/me/tamkungz/codecmedia/internal/audio/flac/FlacParserTest.java).
28+
- Added explicit decode-only intent comment in [`FlacCodec`](src/main/java/me/tamkungz/codecmedia/internal/audio/flac/FlacCodec.java).
29+
- Added AIFF parser tests for supported AIFC `NONE` and unsupported compression-type rejection in [`AiffParserTest`](src/test/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffParserTest.java).
30+
- Added explicit decode-only intent comment in [`AiffCodec`](src/main/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffCodec.java).
2131

2232
### Verified
2333
- Confirmed MP3 parser updates with `mvn -Dtest=Mp3ParserTest test`.
2434
- Confirmed OGG parser updates with `mvn -Dtest=OggParserTest test`.
35+
- Confirmed WAV parser updates with `mvn -Dtest=WavParserTest test`.
36+
- Confirmed FLAC parser updates with `mvn -Dtest=FlacParserTest test`.
37+
- Confirmed AIFF parser updates with `mvn -Dtest=AiffParserTest test`.
2538

2639
## [1.1.0] - 2026-03-13
2740

src/main/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffCodec.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public static AiffProbeInfo decode(byte[] bytes, Path sourceRef) throws CodecMed
2626
return info;
2727
}
2828

29+
// Intentional: AIFF codec is currently decode/probe only in this library.
30+
// No encode API is exposed until a deterministic AIFF encoder path is introduced.
31+
2932
private static void validateDecodedProbe(AiffProbeInfo info, Path input) throws CodecMediaException {
3033
if (info.sampleRate() <= 0 || info.channels() <= 0 || info.bitrateKbps() <= 0) {
3134
throw new CodecMediaException("Decoded AIFF has invalid stream values: " + input);

src/main/java/me/tamkungz/codecmedia/internal/audio/aiff/AiffParser.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
public final class AiffParser {
77

8+
private static final String AIFC_COMPRESSION_NONE = "NONE";
9+
private static final String AIFC_COMPRESSION_SOWT = "sowt";
10+
811
private AiffParser() {
912
}
1013

@@ -13,6 +16,7 @@ public static AiffProbeInfo parse(byte[] bytes) throws CodecMediaException {
1316
throw new CodecMediaException("Not an AIFF file");
1417
}
1518

19+
boolean aifc = bytes[11] == 'C';
1620
int offset = 12;
1721
Integer channels = null;
1822
Integer bitsPerSample = null;
@@ -39,6 +43,14 @@ public static AiffProbeInfo parse(byte[] bytes) throws CodecMediaException {
3943
frameCount = readBeUInt32(bytes, chunkDataStart + 2);
4044
bitsPerSample = readBeShort(bytes, chunkDataStart + 6);
4145
sampleRate = decodeExtended80ToIntHz(bytes, chunkDataStart + 8);
46+
47+
if (aifc) {
48+
if (chunkSize < 22) {
49+
throw new CodecMediaException("AIFC COMM chunk missing compression type");
50+
}
51+
String compressionType = readAscii(bytes, chunkDataStart + 18, 4);
52+
validateAifcCompressionType(compressionType);
53+
}
4254
}
4355

4456
int padded = (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
@@ -59,6 +71,13 @@ public static AiffProbeInfo parse(byte[] bytes) throws CodecMediaException {
5971
return new AiffProbeInfo(durationMillis, bitrateKbps, sampleRate, channels, BitrateMode.CBR);
6072
}
6173

74+
private static void validateAifcCompressionType(String compressionType) throws CodecMediaException {
75+
if (AIFC_COMPRESSION_NONE.equals(compressionType) || AIFC_COMPRESSION_SOWT.equals(compressionType)) {
76+
return;
77+
}
78+
throw new CodecMediaException("Unsupported AIFC compression type: " + compressionType);
79+
}
80+
6281
public static boolean isLikelyAiff(byte[] bytes) {
6382
return bytes != null
6483
&& bytes.length >= 12

src/main/java/me/tamkungz/codecmedia/internal/audio/flac/FlacCodec.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public static FlacProbeInfo decode(byte[] bytes, Path sourceRef) throws CodecMed
2626
return info;
2727
}
2828

29+
// Intentional: FLAC codec is currently decode/probe only in this library.
30+
// No encode API is exposed until a deterministic FLAC encoder path is introduced.
31+
2932
private static void validateDecodedProbe(FlacProbeInfo info, Path input) throws CodecMediaException {
3033
if (info.sampleRate() <= 0 || info.channels() <= 0 || info.bitsPerSample() <= 0) {
3134
throw new CodecMediaException("Decoded FLAC has invalid stream values: " + input);

src/main/java/me/tamkungz/codecmedia/internal/audio/flac/FlacParser.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@ public static FlacProbeInfo parse(byte[] bytes) throws CodecMediaException {
1919
int channels = 0;
2020
int bitsPerSample = 0;
2121
long totalSamples = 0;
22+
int audioStartOffset = -1;
2223

2324
while (offset + 4 <= bytes.length) {
2425
int header = bytes[offset] & 0xFF;
2526
boolean last = (header & 0x80) != 0;
2627
int blockType = header & 0x7F;
28+
if (blockType == 0x7F) {
29+
throw new CodecMediaException("Invalid FLAC metadata block type: 127 is reserved");
30+
}
2731
int length = ((bytes[offset + 1] & 0xFF) << 16)
2832
| ((bytes[offset + 2] & 0xFF) << 8)
2933
| (bytes[offset + 3] & 0xFF);
3034
offset += 4;
3135

32-
if (length < 0 || offset + length > bytes.length) {
36+
if (offset + length > bytes.length) {
3337
throw new CodecMediaException("Invalid FLAC metadata block length");
3438
}
3539

@@ -47,6 +51,7 @@ public static FlacProbeInfo parse(byte[] bytes) throws CodecMediaException {
4751

4852
offset += length;
4953
if (last) {
54+
audioStartOffset = offset;
5055
break;
5156
}
5257
}
@@ -56,8 +61,11 @@ public static FlacProbeInfo parse(byte[] bytes) throws CodecMediaException {
5661
}
5762

5863
long durationMillis = totalSamples > 0 ? (totalSamples * 1000L) / sampleRate : 0;
64+
long encodedAudioBytes = (audioStartOffset >= 0 && audioStartOffset <= bytes.length)
65+
? (bytes.length - (long) audioStartOffset)
66+
: 0;
5967
int avgBitrateKbps = durationMillis > 0
60-
? (int) ((((long) bytes.length * 8L) * 1000L) / durationMillis / 1000L)
68+
? (int) ((encodedAudioBytes * 8L * 1000L) / durationMillis / 1000L)
6169
: 0;
6270
int pcmEquivalentKbps = (int) (((long) sampleRate * channels * bitsPerSample) / 1000L);
6371
int bitrateKbps = avgBitrateKbps > 0 ? avgBitrateKbps : pcmEquivalentKbps;

0 commit comments

Comments
 (0)