From b342baf608829fa50ee04196b0e7733b27dc3a7c Mon Sep 17 00:00:00 2001 From: torusrxxx Date: Wed, 25 Sep 2024 21:28:50 +0800 Subject: [PATCH 1/3] Add WAV content filter --- src/freenet/client/DefaultMIMETypes.java | 2 +- src/freenet/client/filter/ContentFilter.java | 10 +- src/freenet/client/filter/WAVFilter.java | 127 ++++++++++++++++++ src/freenet/l10n/freenet.l10n.en.properties | 2 + test/freenet/client/filter/WAVFilterTest.java | 45 +++++++ test/freenet/client/filter/wav/test.wav | Bin 0 -> 27804 bytes 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/freenet/client/filter/WAVFilter.java create mode 100644 test/freenet/client/filter/WAVFilterTest.java create mode 100644 test/freenet/client/filter/wav/test.wav diff --git a/src/freenet/client/DefaultMIMETypes.java b/src/freenet/client/DefaultMIMETypes.java index 6f754eab35..17524ef6b8 100644 --- a/src/freenet/client/DefaultMIMETypes.java +++ b/src/freenet/client/DefaultMIMETypes.java @@ -571,7 +571,7 @@ public synchronized static short byName(String s) { addMIMEType((short)439, "audio/x-realaudio", "ra"); addMIMEType((short)440, "audio/x-scpls", "pls"); addMIMEType((short)441, "audio/x-sd2", "sd2"); - addMIMEType((short)442, "audio/x-wav", "wav"); + addMIMEType((short)442, "audio/vnd.wave", "wav"); addMIMEType((short)443, "chemical/x-pdb", "pdb"); addMIMEType((short)444, "chemical/x-xyz", "xyz"); addMIMEType((short)445, "image/cgm"); diff --git a/src/freenet/client/filter/ContentFilter.java b/src/freenet/client/filter/ContentFilter.java index 3ae493ca55..590ed83cb1 100644 --- a/src/freenet/client/filter/ContentFilter.java +++ b/src/freenet/client/filter/ContentFilter.java @@ -56,6 +56,7 @@ public static void init() { l10n("textPlainReadAdvice"), true, "US-ASCII", null, false)); + // Images // GIF - has a filter register(new FilterMIMEType("image/gif", "gif", new String[0], new String[0], true, false, new GIFFilter(), false, false, false, false, false, false, @@ -74,7 +75,6 @@ true, false, new PNGFilter(true, true, true), false, false, false, false, true, l10n("imagePngReadAdvice"), false, null, null, false)); - // BMP - has a filter // Reference: http://filext.com/file-extension/BMP register(new FilterMIMEType("image/bmp", "bmp", new String[] { "image/x-bmp","image/x-bitmap","image/x-xbitmap","image/x-win-bitmap","image/x-windows-bmp","image/ms-bmp","image/x-ms-bmp","application/bmp","application/x-bmp","application/x-win-bitmap" }, new String[0], @@ -88,6 +88,7 @@ true, false, new WebPFilter(), false, false, false, false, true, false, l10n("imageWebPReadAdvice"), false, null, null, false)); + // Audio /* Ogg - has a filter * Xiph's container format. Contains one or more logical bitstreams. * Each type of bitstream will likely require additional processing, @@ -123,6 +124,11 @@ false, false, new M3UFilter(), false, false, false, false, false, false, register(new FilterMIMEType("audio/mpeg", "mp3", new String[] {"audio/mp3", "audio/x-mp3", "audio/x-mpeg", "audio/mpeg3", "audio/x-mpeg3", "audio/mpg", "audio/x-mpg", "audio/mpegaudio"}, new String[0], true, false, new MP3Filter(), true, true, false, true, false, false, l10n("audioMP3ReadAdvice"), false, null, null, false)); + + // WAV - has a filter + register(new FilterMIMEType("audio/vnd.wave", "mp3", new String[] {"audio/vnd.wave", "audio/x-wav", "audio/wav", "audio/wave"}, + new String[0], true, false, new WAVFilter(), true, true, false, true, false, false, + l10n("audioWAVReadAdvice"), false, null, null, false)); // ICO needs filtering. // Format is not the same as BMP iirc. @@ -546,6 +552,8 @@ public static String mimeTypeForSrc(String uriold) { subMimetype = "video/ogg"; } else if (uriPath.endsWith(".ogg")) { subMimetype = "application/ogg"; +} else if (uriPath.endsWith(".wav")) { + subMimetype = "audio/vnd.wave"; } else { // force mp3 for anything we do not know subMimetype = "audio/mpeg"; } diff --git a/src/freenet/client/filter/WAVFilter.java b/src/freenet/client/filter/WAVFilter.java new file mode 100644 index 0000000000..1c4a4f946b --- /dev/null +++ b/src/freenet/client/filter/WAVFilter.java @@ -0,0 +1,127 @@ +package freenet.client.filter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Map; + +import freenet.client.filter.WebPFilter.WebPFilterContext; +import freenet.l10n.NodeL10n; +import freenet.support.Logger; +import freenet.support.Logger.LogLevel; + +public class WAVFilter extends RIFFFilter { + // RFC 2361 + private final int WAVE_FORMAT_UNKNOWN = 0; + private final int WAVE_FORMAT_PCM = 1; + private final int WAVE_FORMAT_IEEE_FLOAT = 3; + private final int WAVE_FORMAT_ALAW = 6; + private final int WAVE_FORMAT_MULAW = 7; + + @Override + protected byte[] getChunkMagicNumber() { + return new byte[] {'W', 'A', 'V', 'E'}; + } + + class WAVFilterContext { + public boolean hasfmt = false; + public boolean hasdata = false; + public int nSamplesPerSec = 0; + public int nChannels = 0; + public int nBlockAlign = 0; + public int wBitsPerSample = 0; + public int format = 0; + } + + @Override + protected Object createContext() { + return new WAVFilterContext(); + } + + @Override + protected void readFilterChunk(byte[] ID, int size, Object context, DataInputStream input, DataOutputStream output, + String charset, Map otherParams, String schemeHostAndPort, FilterCallback cb) + throws DataFilterException, IOException { + WAVFilterContext ctx = (WAVFilterContext)context; + if(ID[0] == 'f' && ID[1] == 'm' && ID[2] == 't' && ID[3] == ' ') { + if(ctx.hasfmt) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected fmt chunk was encountered"); + } + if(size != 16 && size != 18 && size != 40) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fmt chunk size is invalid"); + } + ctx.format = Short.reverseBytes(input.readShort()); + if(ctx.format != WAVE_FORMAT_PCM && ctx.format != WAVE_FORMAT_IEEE_FLOAT && ctx.format != WAVE_FORMAT_ALAW && ctx.format != WAVE_FORMAT_MULAW) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "WAV file uses a not yet supported format"); + } + ctx.nChannels = Short.reverseBytes(input.readShort()); + output.write(ID); + writeLittleEndianInt(output, size); + output.writeInt((Short.reverseBytes((short) ctx.format) << 16) | Short.reverseBytes((short) ctx.nChannels)); + ctx.nSamplesPerSec = readLittleEndianInt(input); + writeLittleEndianInt(output, ctx.nSamplesPerSec); + int nAvgBytesPerSec = readLittleEndianInt(input); + writeLittleEndianInt(output, nAvgBytesPerSec); + ctx.nBlockAlign = Short.reverseBytes(input.readShort()); + ctx.wBitsPerSample = Short.reverseBytes(input.readShort()); + output.writeInt((Short.reverseBytes((short) ctx.nBlockAlign) << 16) | Short.reverseBytes((short) ctx.wBitsPerSample)); + ctx.hasfmt = true; + if(size > 16) { + short cbSize = Short.reverseBytes(input.readShort()); + if(cbSize + 18 != size) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fmt chunk size is invalid"); + } + output.writeShort(Short.reverseBytes(cbSize)); + } + if(size > 18) { + // wValidBitsPerSample, dwChannelMask, and SubFormat GUID + passthroughBytes(input, output, 22); + } + // Further checks + if((ctx.format == WAVE_FORMAT_ALAW || ctx.format == WAVE_FORMAT_MULAW) && ctx.wBitsPerSample != 8) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected bits per sample value"); + } + return; + } else if(!ctx.hasfmt) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected header chunk was encountered, instead of fmt chunk"); + } + if(ID[0] == 'd' && ID[1] == 'a' && ID[2] == 't' && ID[3] == 'a') { + if(ctx.format == WAVE_FORMAT_PCM || ctx.format == WAVE_FORMAT_IEEE_FLOAT || ctx.format == WAVE_FORMAT_ALAW || ctx.format == WAVE_FORMAT_MULAW) { + // Safe format, pass through + output.write(ID); + writeLittleEndianInt(output, size); + passthroughBytes(input, output, size); + if((size & 1) != 0) // Add padding if necessary + output.writeByte(input.readByte()); + ctx.hasdata = true; + } else { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Data format is not yet supported"); + } + } else if(ID[0] == 'f' && ID[1] == 'a' && ID[2] == 'c' && ID[3] == 't') { + if(size < 4) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fact chunk must contain at least 4 bytes"); + } + // Just dwSampleLength (Number of samples) here, pass through + output.write(ID); + writeLittleEndianInt(output, size); + passthroughBytes(input, output, size); + if((size & 1) != 0) // Add padding if necessary + output.writeByte(input.readByte()); + } else { + // Unknown block + writeJunkChunk(input, output, size); + } + } + + @Override + protected void EOFCheck(Object context) throws DataFilterException { + WAVFilterContext ctx = (WAVFilterContext)context; + if(!ctx.hasfmt || !ctx.hasdata) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "WAV file is missing fmt chunk or data chunk"); + } + } + + private static String l10n(String key) { + return NodeL10n.getBase().getString("WAVFilter."+key); + } +} diff --git a/src/freenet/l10n/freenet.l10n.en.properties b/src/freenet/l10n/freenet.l10n.en.properties index 9e3b11a52c..036cc20690 100644 --- a/src/freenet/l10n/freenet.l10n.en.properties +++ b/src/freenet/l10n/freenet.l10n.en.properties @@ -270,6 +270,7 @@ ContentDataFilter.warningUnknownCharsetTitle=Warning: Unknown character set (${c ContentFilter.applicationPdfReadAdvice=Adobe(R) PDF document - VERY DANGEROUS! ContentFilter.audioM3UReadAdvice=MP3 music/audio file - probably not dangerous but can contain metadata which might include URLs of unencrypted content; the filter will strip these out ContentFilter.audioMP3ReadAdvice=MP3 music/audio file - probably not dangerous but can contain metadata which might include URLs of unencrypted content; the filter will strip these out +ContentFilter.audioWAVReadAdvice=WAV music/audio file - probably not dangerous. ContentFilter.EOFMessage=Unexpected end of file ContentFilter.EOFDescription=The filter needed more data from the file you were accessing than was available. The file may be malformed or corrupted. ContentFilter.audioFLACReadAdvice=FLAC audio format - Dangerous. May contain off Freenet links to album art. If followed, these links can harm anonymity. @@ -2248,6 +2249,7 @@ UserAlertsToadlet.title=Status messages UserAlertsToadlet.noMessages=No messages VorbisBitstreamFilter.MalformedTitle=Malformed Vorbis Bitstream VorbisBitstreamFilter.MalformedMessage=The Vorbis bitstream is not correctly formatted, and could not be properly validated. +WAVFilter.invalidTitle=Invalid WAV file WebPFilter.animUnsupportedTitle=WebP animation is currently not supported WebPFilter.animUnsupported=WebP animation is currently not supported by the filter, because it could contain frames using the lossless encoding. WebP lossless format has known buffer overflow exploit. When viewed on unpatched browsers and applications, it can damage the security of the system. Therefore, the content filter cannot ensure the safety of this animation. WebPFilter.alphUnsupportedTitle=WebP alpha channel with lossless compression is currently not supported diff --git a/test/freenet/client/filter/WAVFilterTest.java b/test/freenet/client/filter/WAVFilterTest.java new file mode 100644 index 0000000000..cbd853baf8 --- /dev/null +++ b/test/freenet/client/filter/WAVFilterTest.java @@ -0,0 +1,45 @@ +package freenet.client.filter; + +import static freenet.client.filter.ResourceFileUtil.resourceToBucket; +import static org.junit.Assert.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; + +import org.junit.Test; + +import freenet.support.api.Bucket; +import freenet.support.io.ArrayBucket; +import freenet.support.io.BucketTools; + +/** + * Unit test for (parts of) {@link WAVFilter}. + */ +public class WAVFilterTest { + + @Test + public void testValidWAV() throws IOException { + Bucket input = resourceToBucket("./wav/test.wav"); + Bucket output = filterWAV(input); + + //Filter should return the original + assertEquals("Input and output should be the same length", input.size(), output.size()); + assertArrayEquals("Input and output are not identical", BucketTools.toByteArray(input), BucketTools.toByteArray(output)); + } + + private Bucket filterWAV(Bucket input) throws IOException { + WAVFilter objWAVFilter = new WAVFilter(); + Bucket output = new ArrayBucket(); + try ( + InputStream inStream = input.getInputStream(); + OutputStream outStream = output.getOutputStream() + ) { + objWAVFilter.readFilter(inStream, outStream, "", null, null, null); + } + return output; + } +} diff --git a/test/freenet/client/filter/wav/test.wav b/test/freenet/client/filter/wav/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..b3127d1e2412f0d4bf3ecb0d9937e3ec957bf7c5 GIT binary patch literal 27804 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⋙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 literal 0 HcmV?d00001 From c6114a572abdf34b58d1a9f4256292ba6f4042f4 Mon Sep 17 00:00:00 2001 From: torusrxxx Date: Wed, 2 Oct 2024 23:43:25 +0800 Subject: [PATCH 2/3] Add a few tests for WAV --- src/freenet/client/filter/WAVFilter.java | 3 - test/freenet/client/filter/WAVFilterTest.java | 58 ++++++++++++++++--- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/freenet/client/filter/WAVFilter.java b/src/freenet/client/filter/WAVFilter.java index 1c4a4f946b..998a62852c 100644 --- a/src/freenet/client/filter/WAVFilter.java +++ b/src/freenet/client/filter/WAVFilter.java @@ -5,10 +5,7 @@ import java.io.IOException; import java.util.Map; -import freenet.client.filter.WebPFilter.WebPFilterContext; import freenet.l10n.NodeL10n; -import freenet.support.Logger; -import freenet.support.Logger.LogLevel; public class WAVFilter extends RIFFFilter { // RFC 2361 diff --git a/test/freenet/client/filter/WAVFilterTest.java b/test/freenet/client/filter/WAVFilterTest.java index cbd853baf8..4ba51355d4 100644 --- a/test/freenet/client/filter/WAVFilterTest.java +++ b/test/freenet/client/filter/WAVFilterTest.java @@ -3,13 +3,11 @@ import static freenet.client.filter.ResourceFileUtil.resourceToBucket; import static org.junit.Assert.*; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.HashMap; - +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import org.junit.Test; import freenet.support.api.Bucket; @@ -24,21 +22,67 @@ public class WAVFilterTest { @Test public void testValidWAV() throws IOException { Bucket input = resourceToBucket("./wav/test.wav"); - Bucket output = filterWAV(input); + Bucket output = filterWAV(input, null); //Filter should return the original assertEquals("Input and output should be the same length", input.size(), output.size()); assertArrayEquals("Input and output are not identical", BucketTools.toByteArray(input), BucketTools.toByteArray(output)); } + + // This file is WebP, not WAV! + @Test + public void testAnotherFile() throws IOException { + Bucket input = resourceToBucket("./webp/test.webp"); + filterWAV(input, DataFilterException.class); + } + + // There is just a JUNK chunk in the file + @Test + public void testFileJustJUNK() throws IOException { + ByteBuffer buf = ByteBuffer.allocate(28) + .order(ByteOrder.LITTLE_ENDIAN) + .put(new byte[]{'R', 'I', 'F', 'F'}) + .putInt(20 /* file size */) + .put(new byte[]{'W', 'A', 'V', 'E'}) + .put(new byte[]{'J', 'U', 'N', 'K'}) + .putInt(7 /* chunk size */) + .putLong(0); + + Bucket input = new ArrayBucket(buf.array()); + filterWAV(input, DataFilterException.class); + } + + // There is just a fmt chunk in the file, but no audio data + @Test + public void testFileNoData() throws IOException { + ByteBuffer buf = ByteBuffer.allocate(36) + .order(ByteOrder.LITTLE_ENDIAN) + .put(new byte[]{'R', 'I', 'F', 'F'}) + .putInt(28 /* file size */) + .put(new byte[]{'W', 'A', 'V', 'E'}) + .put(new byte[]{'f', 'm', 't', ' '}) + .putInt(16 /* chunk size */) + .put(new byte[]{1, 0, 2, 0}) //format, nChannels + .putInt(44100) // nSamplesPerSec + .putInt(44100 * 4) // nAvgBytesPerSec + .put(new byte[]{4, 0, 16, 0}); // nBlockAlign, wBitsPerSample + + Bucket input = new ArrayBucket(buf.array()); + filterWAV(input, DataFilterException.class); + } - private Bucket filterWAV(Bucket input) throws IOException { + private Bucket filterWAV(Bucket input, Class expected) throws IOException { WAVFilter objWAVFilter = new WAVFilter(); Bucket output = new ArrayBucket(); try ( InputStream inStream = input.getInputStream(); OutputStream outStream = output.getOutputStream() ) { - objWAVFilter.readFilter(inStream, outStream, "", null, null, null); + if (expected != null) { + assertThrows(expected, () -> objWAVFilter.readFilter(inStream, outStream, "", null, null, null)); + } else { + objWAVFilter.readFilter(inStream, outStream, "", null, null, null); + } } return output; } From bd2bb4fa3875044cf77f2d08a9865ad7469f392c Mon Sep 17 00:00:00 2001 From: torusrxxx Date: Sun, 6 Oct 2024 21:15:01 +0800 Subject: [PATCH 3/3] fix coding styles --- src/freenet/client/DefaultMIMETypes.java | 3 - src/freenet/client/filter/ContentFilter.java | 42 +++++++------- src/freenet/client/filter/WAVFilter.java | 57 +++++++++++-------- test/freenet/client/filter/WAVFilterTest.java | 56 +++++++++--------- 4 files changed, 81 insertions(+), 77 deletions(-) diff --git a/src/freenet/client/DefaultMIMETypes.java b/src/freenet/client/DefaultMIMETypes.java index 17524ef6b8..0615915681 100644 --- a/src/freenet/client/DefaultMIMETypes.java +++ b/src/freenet/client/DefaultMIMETypes.java @@ -752,9 +752,6 @@ public synchronized static short byName(String s) { addMIMEType((short)620, "audio/ogg", "oga"); addMIMEType((short)621, "audio/flac", "flac"); addMIMEType((short)622, "image/webp", "webp"); - addMIMEType((short)623, "image/avif", "avif"); - addMIMEType((short)624, "image/heic", "heic"); - addMIMEType((short)625, "image/heif", "heif"); } /** Guess a MIME type from a filename. diff --git a/src/freenet/client/filter/ContentFilter.java b/src/freenet/client/filter/ContentFilter.java index 590ed83cb1..d41c165679 100644 --- a/src/freenet/client/filter/ContentFilter.java +++ b/src/freenet/client/filter/ContentFilter.java @@ -126,7 +126,7 @@ false, false, new M3UFilter(), false, false, false, false, false, false, l10n("audioMP3ReadAdvice"), false, null, null, false)); // WAV - has a filter - register(new FilterMIMEType("audio/vnd.wave", "mp3", new String[] {"audio/vnd.wave", "audio/x-wav", "audio/wav", "audio/wave"}, + register(new FilterMIMEType("audio/vnd.wave", "wav", new String[] {"audio/x-wav", "audio/wav", "audio/wave"}, new String[0], true, false, new WAVFilter(), true, true, false, true, false, false, l10n("audioWAVReadAdvice"), false, null, null, false)); @@ -538,26 +538,26 @@ public static boolean startsWith(byte[] data, byte[] cmp, int length) { } public static String mimeTypeForSrc(String uriold) { - String uriPath = uriold.contains("?") - ? uriold.split("\\?")[0] - : uriold; - String subMimetype; - if (uriPath.endsWith(".m3u") || uriPath.endsWith(".m3u8")) { - subMimetype = "audio/mpegurl"; -} else if (uriPath.endsWith(".flac")) { - subMimetype = "audio/flac"; -} else if (uriPath.endsWith(".oga")) { - subMimetype = "audio/ogg"; -} else if (uriPath.endsWith(".ogv")) { - subMimetype = "video/ogg"; -} else if (uriPath.endsWith(".ogg")) { - subMimetype = "application/ogg"; -} else if (uriPath.endsWith(".wav")) { - subMimetype = "audio/vnd.wave"; - } else { // force mp3 for anything we do not know - subMimetype = "audio/mpeg"; - } - return subMimetype; + String uriPath = uriold.contains("?") + ? uriold.split("\\?")[0] + : uriold; + String subMimetype; + if (uriPath.endsWith(".m3u") || uriPath.endsWith(".m3u8")) { + subMimetype = "audio/mpegurl"; + } else if (uriPath.endsWith(".flac")) { + subMimetype = "audio/flac"; + } else if (uriPath.endsWith(".oga")) { + subMimetype = "audio/ogg"; + } else if (uriPath.endsWith(".ogv")) { + subMimetype = "video/ogg"; + } else if (uriPath.endsWith(".ogg")) { + subMimetype = "application/ogg"; + } else if (uriPath.endsWith(".wav")) { + subMimetype = "audio/vnd.wave"; + } else { // force mp3 for anything we do not know + subMimetype = "audio/mpeg"; + } + return subMimetype; } public static class FilterStatus { diff --git a/src/freenet/client/filter/WAVFilter.java b/src/freenet/client/filter/WAVFilter.java index 998a62852c..c5d45dc21c 100644 --- a/src/freenet/client/filter/WAVFilter.java +++ b/src/freenet/client/filter/WAVFilter.java @@ -14,20 +14,27 @@ public class WAVFilter extends RIFFFilter { private final int WAVE_FORMAT_IEEE_FLOAT = 3; private final int WAVE_FORMAT_ALAW = 6; private final int WAVE_FORMAT_MULAW = 7; + // Header sizes (https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html) + // fmt header without cbSize field + private final int FMT_SIZE_BASIC = 16; + // fmt header with cbSize = 0 + private final int FMT_SIZE_cbSize = 18; + // fmt header with cbSize and extensions + private final int FMT_SIZE_cbSize_extension = 40; @Override protected byte[] getChunkMagicNumber() { return new byte[] {'W', 'A', 'V', 'E'}; } - class WAVFilterContext { - public boolean hasfmt = false; - public boolean hasdata = false; - public int nSamplesPerSec = 0; - public int nChannels = 0; - public int nBlockAlign = 0; - public int wBitsPerSample = 0; - public int format = 0; + private static final class WAVFilterContext { + boolean hasfmt = false; + boolean hasdata = false; + int nSamplesPerSec = 0; + int nChannels = 0; + int nBlockAlign = 0; + int wBitsPerSample = 0; + int format = 0; } @Override @@ -44,7 +51,7 @@ protected void readFilterChunk(byte[] ID, int size, Object context, DataInputStr if(ctx.hasfmt) { throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected fmt chunk was encountered"); } - if(size != 16 && size != 18 && size != 40) { + if(size != FMT_SIZE_BASIC && size != FMT_SIZE_cbSize && size != FMT_SIZE_cbSize_extension) { throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fmt chunk size is invalid"); } ctx.format = Short.reverseBytes(input.readShort()); @@ -63,37 +70,36 @@ protected void readFilterChunk(byte[] ID, int size, Object context, DataInputStr ctx.wBitsPerSample = Short.reverseBytes(input.readShort()); output.writeInt((Short.reverseBytes((short) ctx.nBlockAlign) << 16) | Short.reverseBytes((short) ctx.wBitsPerSample)); ctx.hasfmt = true; - if(size > 16) { + if(size > FMT_SIZE_BASIC) { short cbSize = Short.reverseBytes(input.readShort()); - if(cbSize + 18 != size) { + if(cbSize + FMT_SIZE_cbSize != size) { throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fmt chunk size is invalid"); } output.writeShort(Short.reverseBytes(cbSize)); } - if(size > 18) { + if(size > FMT_SIZE_cbSize) { // wValidBitsPerSample, dwChannelMask, and SubFormat GUID - passthroughBytes(input, output, 22); + passthroughBytes(input, output, FMT_SIZE_cbSize_extension - FMT_SIZE_cbSize); } // Further checks if((ctx.format == WAVE_FORMAT_ALAW || ctx.format == WAVE_FORMAT_MULAW) && ctx.wBitsPerSample != 8) { + // These formats are 8-bit throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected bits per sample value"); } return; - } else if(!ctx.hasfmt) { + } + if(!ctx.hasfmt) { throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected header chunk was encountered, instead of fmt chunk"); } if(ID[0] == 'd' && ID[1] == 'a' && ID[2] == 't' && ID[3] == 'a') { - if(ctx.format == WAVE_FORMAT_PCM || ctx.format == WAVE_FORMAT_IEEE_FLOAT || ctx.format == WAVE_FORMAT_ALAW || ctx.format == WAVE_FORMAT_MULAW) { - // Safe format, pass through - output.write(ID); - writeLittleEndianInt(output, size); - passthroughBytes(input, output, size); - if((size & 1) != 0) // Add padding if necessary - output.writeByte(input.readByte()); - ctx.hasdata = true; - } else { - throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Data format is not yet supported"); + // audio data + output.write(ID); + writeLittleEndianInt(output, size); + passthroughBytes(input, output, size); + if((size & 1) != 0) { // Add padding if necessary + output.writeByte(input.readByte()); } + ctx.hasdata = true; } else if(ID[0] == 'f' && ID[1] == 'a' && ID[2] == 'c' && ID[3] == 't') { if(size < 4) { throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fact chunk must contain at least 4 bytes"); @@ -102,8 +108,9 @@ protected void readFilterChunk(byte[] ID, int size, Object context, DataInputStr output.write(ID); writeLittleEndianInt(output, size); passthroughBytes(input, output, size); - if((size & 1) != 0) // Add padding if necessary + if((size & 1) != 0) { // Add padding if necessary output.writeByte(input.readByte()); + } } else { // Unknown block writeJunkChunk(input, output, size); diff --git a/test/freenet/client/filter/WAVFilterTest.java b/test/freenet/client/filter/WAVFilterTest.java index 4ba51355d4..4399ec058c 100644 --- a/test/freenet/client/filter/WAVFilterTest.java +++ b/test/freenet/client/filter/WAVFilterTest.java @@ -39,36 +39,36 @@ public void testAnotherFile() throws IOException { // There is just a JUNK chunk in the file @Test public void testFileJustJUNK() throws IOException { - ByteBuffer buf = ByteBuffer.allocate(28) - .order(ByteOrder.LITTLE_ENDIAN) - .put(new byte[]{'R', 'I', 'F', 'F'}) - .putInt(20 /* file size */) - .put(new byte[]{'W', 'A', 'V', 'E'}) - .put(new byte[]{'J', 'U', 'N', 'K'}) - .putInt(7 /* chunk size */) - .putLong(0); + ByteBuffer buf = ByteBuffer.allocate(28) + .order(ByteOrder.LITTLE_ENDIAN) + .put(new byte[]{'R', 'I', 'F', 'F'}) + .putInt(20 /* file size */) + .put(new byte[]{'W', 'A', 'V', 'E'}) + .put(new byte[]{'J', 'U', 'N', 'K'}) + .putInt(7 /* chunk size */) + .putLong(0); - Bucket input = new ArrayBucket(buf.array()); - filterWAV(input, DataFilterException.class); + Bucket input = new ArrayBucket(buf.array()); + filterWAV(input, DataFilterException.class); } // There is just a fmt chunk in the file, but no audio data @Test public void testFileNoData() throws IOException { - ByteBuffer buf = ByteBuffer.allocate(36) - .order(ByteOrder.LITTLE_ENDIAN) - .put(new byte[]{'R', 'I', 'F', 'F'}) - .putInt(28 /* file size */) - .put(new byte[]{'W', 'A', 'V', 'E'}) - .put(new byte[]{'f', 'm', 't', ' '}) - .putInt(16 /* chunk size */) - .put(new byte[]{1, 0, 2, 0}) //format, nChannels - .putInt(44100) // nSamplesPerSec - .putInt(44100 * 4) // nAvgBytesPerSec - .put(new byte[]{4, 0, 16, 0}); // nBlockAlign, wBitsPerSample + ByteBuffer buf = ByteBuffer.allocate(36) + .order(ByteOrder.LITTLE_ENDIAN) + .put(new byte[]{'R', 'I', 'F', 'F'}) + .putInt(28 /* file size */) + .put(new byte[]{'W', 'A', 'V', 'E'}) + .put(new byte[]{'f', 'm', 't', ' '}) + .putInt(16 /* chunk size */) + .put(new byte[]{1, 0, 2, 0}) //format, nChannels + .putInt(44100) // nSamplesPerSec + .putInt(44100 * 4) // nAvgBytesPerSec + .put(new byte[]{4, 0, 16, 0}); // nBlockAlign, wBitsPerSample - Bucket input = new ArrayBucket(buf.array()); - filterWAV(input, DataFilterException.class); + Bucket input = new ArrayBucket(buf.array()); + filterWAV(input, DataFilterException.class); } private Bucket filterWAV(Bucket input, Class expected) throws IOException { @@ -78,11 +78,11 @@ private Bucket filterWAV(Bucket input, Class expected) thro InputStream inStream = input.getInputStream(); OutputStream outStream = output.getOutputStream() ) { - if (expected != null) { - assertThrows(expected, () -> objWAVFilter.readFilter(inStream, outStream, "", null, null, null)); - } else { - objWAVFilter.readFilter(inStream, outStream, "", null, null, null); - } + if (expected != null) { + assertThrows(expected, () -> objWAVFilter.readFilter(inStream, outStream, "", null, null, null)); + } else { + objWAVFilter.readFilter(inStream, outStream, "", null, null, null); + } } return output; }