diff options
Diffstat (limited to 'toolkit/components/mediasniffer')
22 files changed, 791 insertions, 0 deletions
diff --git a/toolkit/components/mediasniffer/components.conf b/toolkit/components/mediasniffer/components.conf new file mode 100644 index 0000000000..ba3c89b942 --- /dev/null +++ b/toolkit/components/mediasniffer/components.conf @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{3fdd6c28-5b87-4e3e-8b57-8e83c23c1a6d}', + 'contract_ids': ['@mozilla.org/media/sniffer;1'], + 'type': 'nsMediaSniffer', + 'headers': ['nsMediaSniffer.h'], + 'categories': { + 'content-sniffing-services': '@mozilla.org/media/sniffer;1', + 'net-content-sniffers': '@mozilla.org/media/sniffer;1', + 'orb-content-sniffers': '@mozilla.org/media/sniffer;1', + 'net-and-orb-content-sniffers': '@mozilla.org/media/sniffer;1', + }, + }, +] diff --git a/toolkit/components/mediasniffer/moz.build b/toolkit/components/mediasniffer/moz.build new file mode 100644 index 0000000000..783b4555da --- /dev/null +++ b/toolkit/components/mediasniffer/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +EXPORTS += [ + "nsMediaSniffer.h", +] + +UNIFIED_SOURCES += [ + "mp3sniff.c", + "nsMediaSniffer.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" + +with Files("**"): + BUG_COMPONENT = ("Core", "Audio/Video") + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + # For nsHttpChannel.h + "/netwerk/base", + "/netwerk/protocol/http", +] diff --git a/toolkit/components/mediasniffer/mp3sniff.c b/toolkit/components/mediasniffer/mp3sniff.c new file mode 100644 index 0000000000..cb1e2fdc1d --- /dev/null +++ b/toolkit/components/mediasniffer/mp3sniff.c @@ -0,0 +1,156 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* MPEG format parsing */ + +#include "mp3sniff.h" + +/* Maximum packet size is 320 kbits/s * 144 / 32 kHz + 1 padding byte */ +#define MP3_MAX_SIZE 1441 + +typedef struct { + int version; + int layer; + int errp; + int bitrate; + int freq; + int pad; + int priv; + int mode; + int modex; + int copyright; + int original; + int emphasis; +} mp3_header; + +/* Parse the 4-byte header in p and fill in the header struct. */ +static void mp3_parse(const uint8_t* p, mp3_header* header) { + const int bitrates[2][16] = { + /* MPEG version 1 layer 3 bitrates. */ + {0, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, + 160000, 192000, 224000, 256000, 320000, 0}, + /* MPEG Version 2 and 2.5 layer 3 bitrates */ + {0, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, + 112000, 128000, 144000, 160000, 0}}; + const int samplerates[4] = {44100, 48000, 32000, 0}; + + header->version = (p[1] & 0x18) >> 3; + header->layer = 4 - ((p[1] & 0x06) >> 1); + header->errp = (p[1] & 0x01); + + header->bitrate = bitrates[(header->version & 1) ? 0 : 1][(p[2] & 0xf0) >> 4]; + header->freq = samplerates[(p[2] & 0x0c) >> 2]; + if (header->version == 2) + header->freq >>= 1; + else if (header->version == 0) + header->freq >>= 2; + header->pad = (p[2] & 0x02) >> 1; + header->priv = (p[2] & 0x01); + + header->mode = (p[3] & 0xc0) >> 6; + header->modex = (p[3] & 0x30) >> 4; + header->copyright = (p[3] & 0x08) >> 3; + header->original = (p[3] & 0x04) >> 2; + header->emphasis = (p[3] & 0x03); +} + +/* calculate the size of an mp3 frame from its header */ +static int mp3_framesize(mp3_header* header) { + int size; + int scale; + + if ((header->version & 1) == 0) + scale = 72; + else + scale = 144; + size = header->bitrate * scale / header->freq; + if (header->pad) size += 1; + + return size; +} + +static int is_mp3(const uint8_t* p, long length) { + /* Do we have enough room to see a 4 byte header? */ + if (length < 4) return 0; + /* Do we have a sync pattern? */ + if (p[0] == 0xff && (p[1] & 0xe0) == 0xe0) { + /* Do we have any illegal field values? */ + if (((p[1] & 0x06) >> 1) == 0) return 0; /* No layer 4 */ + if (((p[2] & 0xf0) >> 4) == 15) return 0; /* Bitrate can't be 1111 */ + if (((p[2] & 0x0c) >> 2) == 3) return 0; /* Samplerate can't be 11 */ + /* Looks like a header. */ + if ((4 - ((p[1] & 0x06) >> 1)) != 3) return 0; /* Only want level 3 */ + return 1; + } + return 0; +} + +/* Identify an ID3 tag based on its header. */ +/* http://id3.org/id3v2.4.0-structure */ +static int is_id3(const uint8_t* p, long length) { + /* Do we have enough room to see the header? */ + if (length < 10) return 0; + /* Do we have a sync pattern? */ + if (p[0] == 'I' && p[1] == 'D' && p[2] == '3') { + if (p[3] == 0xff || p[4] == 0xff) return 0; /* Illegal version. */ + if (p[6] & 0x80 || p[7] & 0x80 || p[8] & 0x80) + return 0; /* Bad length encoding. */ + /* Looks like an id3 header. */ + return 1; + } + return 0; +} + +/* Calculate the size of an id3 tag structure from its header. */ +static int id3_framesize(const uint8_t* p, long length) { + int size; + + /* Header is 10 bytes. */ + if (length < 10) { + return 0; + } + /* Frame is header plus declared size. */ + size = 10 + (p[9] | (p[8] << 7) | (p[7] << 14) | (p[6] << 21)); + + return size; +} + +int mp3_sniff(const uint8_t* buf, long length) { + mp3_header header; + const uint8_t* p; + long skip; + long avail; + + p = buf; + avail = length; + while (avail >= 4) { + if (is_id3(p, avail)) { + /* Skip over any id3 tags */ + skip = id3_framesize(p, avail); + p += skip; + avail -= skip; + } else if (is_mp3(p, avail)) { + mp3_parse(p, &header); + skip = mp3_framesize(&header); + if (skip < 4 || skip + 4 >= avail) { + return 0; + } + p += skip; + avail -= skip; + /* Check for a second header at the expected offset. */ + if (is_mp3(p, avail)) { + /* Looks like mp3. */ + return 1; + } else { + /* No second header. Not mp3. */ + return 0; + } + } else { + /* No id3 tag or mp3 header. Not mp3. */ + return 0; + } + } + + return 0; +} diff --git a/toolkit/components/mediasniffer/mp3sniff.h b/toolkit/components/mediasniffer/mp3sniff.h new file mode 100644 index 0000000000..b189db1873 --- /dev/null +++ b/toolkit/components/mediasniffer/mp3sniff.h @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +int mp3_sniff(const uint8_t* buf, long length); + +#ifdef __cplusplus +} +#endif diff --git a/toolkit/components/mediasniffer/nsMediaSniffer.cpp b/toolkit/components/mediasniffer/nsMediaSniffer.cpp new file mode 100644 index 0000000000..b6752cb438 --- /dev/null +++ b/toolkit/components/mediasniffer/nsMediaSniffer.cpp @@ -0,0 +1,245 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 tw=80 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ADTSDemuxer.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/Telemetry.h" +#include "mp3sniff.h" +#include "nestegg/nestegg.h" +#include "nsHttpChannel.h" +#include "nsIClassInfoImpl.h" +#include "nsIChannel.h" +#include "nsMediaSniffer.h" +#include "nsMimeTypes.h" +#include "nsQueryObject.h" +#include "nsString.h" + +#include <algorithm> + +// The minimum number of bytes that are needed to attempt to sniff an mp4 file. +static const unsigned MP4_MIN_BYTES_COUNT = 12; +// The maximum number of bytes to consider when attempting to sniff a file. +static const uint32_t MAX_BYTES_SNIFFED = 512; +// The maximum number of bytes to consider when attempting to sniff for a mp3 +// bitstream. +// This is 320kbps * 144 / 32kHz + 1 padding byte + 4 bytes of capture pattern. +static const uint32_t MAX_BYTES_SNIFFED_MP3 = 320 * 144 / 32 + 1 + 4; +// Multi-channel low sample-rate AAC packets can be huge, have a higher maximum +// size. +static const uint32_t MAX_BYTES_SNIFFED_ADTS = 8096; + +NS_IMPL_ISUPPORTS(nsMediaSniffer, nsIContentSniffer) + +nsMediaSnifferEntry nsMediaSniffer::sSnifferEntries[] = { + // The string OggS, followed by the null byte. + PATTERN_ENTRY("\xFF\xFF\xFF\xFF\xFF", "OggS", APPLICATION_OGG), + // The string RIFF, followed by four bytes, followed by the string WAVE, + // followed by 8 bytes, followed by 0x0055, the codec identifier for mp3 in + // a RIFF container. This entry MUST be before the next one, which is + // assumed to be a WAV file containing PCM data. + PATTERN_ENTRY("\xFF\xFF\xFF\xFF" + "\x00\x00\x00\x00" + "\xFF\xFF\xFF\xFF" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\xFF\xFF", + "RIFF" + "\x00\x00\x00\x00" + "WAVE" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x55\x00", + AUDIO_MP3), + // The string RIFF, followed by four bytes, followed by the string WAVE + PATTERN_ENTRY("\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + "RIFF\x00\x00\x00\x00WAVE", AUDIO_WAV), + // mp3 with ID3 tags, the string "ID3". + PATTERN_ENTRY("\xFF\xFF\xFF", "ID3", AUDIO_MP3), + // FLAC with standard header + PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "fLaC", AUDIO_FLAC), + PATTERN_ENTRY("\xFF\xFF\xFF\xFF\xFF\xFF\xFF", "#EXTM3U", + APPLICATION_MPEGURL)}; + +using PatternLabel = mozilla::Telemetry::LABELS_MEDIA_SNIFFER_MP4_BRAND_PATTERN; + +struct nsMediaSnifferFtypEntry : nsMediaSnifferEntry { + nsMediaSnifferFtypEntry(nsMediaSnifferEntry aBase, const PatternLabel aLabel) + : nsMediaSnifferEntry(aBase), mLabel(aLabel) {} + PatternLabel mLabel; +}; + +// For a complete list of file types, see http://www.ftyps.com/index.html +nsMediaSnifferFtypEntry sFtypEntries[] = { + {PATTERN_ENTRY("\xFF\xFF\xFF", "mp4", VIDEO_MP4), + PatternLabel::ftyp_mp4}, // Could be mp41 or mp42. + {PATTERN_ENTRY("\xFF\xFF\xFF", "avc", VIDEO_MP4), + PatternLabel::ftyp_avc}, // Could be avc1, avc2, ... + {PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "3gp4", VIDEO_MP4), + PatternLabel::ftyp_3gp4}, // 3gp4 is based on MP4 + {PATTERN_ENTRY("\xFF\xFF\xFF", "3gp", VIDEO_3GPP), + PatternLabel::ftyp_3gp}, // Could be 3gp5, ... + {PATTERN_ENTRY("\xFF\xFF\xFF", "M4V", VIDEO_MP4), PatternLabel::ftyp_M4V}, + {PATTERN_ENTRY("\xFF\xFF\xFF", "M4A", AUDIO_MP4), PatternLabel::ftyp_M4A}, + {PATTERN_ENTRY("\xFF\xFF\xFF", "M4P", AUDIO_MP4), PatternLabel::ftyp_M4P}, + {PATTERN_ENTRY("\xFF\xFF", "qt", VIDEO_QUICKTIME), PatternLabel::ftyp_qt}, + {PATTERN_ENTRY("\xFF\xFF\xFF", "crx", APPLICATION_OCTET_STREAM), + PatternLabel::ftyp_crx}, + {PATTERN_ENTRY("\xFF\xFF\xFF", "iso", VIDEO_MP4), + PatternLabel::ftyp_iso}, // Could be isom or iso2. + {PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "mmp4", VIDEO_MP4), + PatternLabel::ftyp_mmp4}, + {PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "avif", IMAGE_AVIF), + PatternLabel::ftyp_avif}, +}; + +static bool MatchesBrands(const uint8_t aData[4], nsACString& aSniffedType) { + for (const auto& currentEntry : sFtypEntries) { + bool matched = true; + MOZ_ASSERT(currentEntry.mLength <= 4, + "Pattern is too large to match brand strings."); + for (uint32_t j = 0; j < currentEntry.mLength; ++j) { + if ((currentEntry.mMask[j] & aData[j]) != currentEntry.mPattern[j]) { + matched = false; + break; + } + } + if (matched) { + // If we eventually remove the "iso" pattern entry per bug 1725190, + // this block should be removed and the bug1725190.cr3 test in + // test_mediasniffer_ext.js will need to be updated + if (!mozilla::StaticPrefs::media_mp4_sniff_iso_brand() && + currentEntry.mLabel == PatternLabel::ftyp_iso) { + continue; + } + + aSniffedType.AssignASCII(currentEntry.mContentType); + AccumulateCategorical(currentEntry.mLabel); + return true; + } + } + + return false; +} + +// This function implements sniffing algorithm for MP4 family file types, +// including MP4 (described at +// http://mimesniff.spec.whatwg.org/#signature-for-mp4), M4A (Apple iTunes +// audio), and 3GPP. +bool MatchesMP4(const uint8_t* aData, const uint32_t aLength, + nsACString& aSniffedType) { + if (aLength <= MP4_MIN_BYTES_COUNT) { + return false; + } + // Conversion from big endian to host byte order. + uint32_t boxSize = + (uint32_t)(aData[3] | aData[2] << 8 | aData[1] << 16 | aData[0] << 24); + + // Boxsize should be evenly divisible by 4. + if (boxSize % 4 || aLength < boxSize) { + return false; + } + // The string "ftyp". + if (aData[4] != 0x66 || aData[5] != 0x74 || aData[6] != 0x79 || + aData[7] != 0x70) { + return false; + } + if (MatchesBrands(&aData[8], aSniffedType)) { + return true; + } + // Skip minor_version (bytes 12-15). + uint32_t bytesRead = 16; + while (bytesRead < boxSize) { + if (MatchesBrands(&aData[bytesRead], aSniffedType)) { + return true; + } + bytesRead += 4; + } + + return false; +} + +static bool MatchesWebM(const uint8_t* aData, const uint32_t aLength) { + return nestegg_sniff((uint8_t*)aData, aLength); +} + +// This function implements mp3 sniffing based on parsing +// packet headers and looking for expected boundaries. +static bool MatchesMP3(const uint8_t* aData, const uint32_t aLength) { + return mp3_sniff(aData, (long)aLength); +} + +static bool MatchesADTS(const uint8_t* aData, const uint32_t aLength) { + return mozilla::ADTSDemuxer::ADTSSniffer(aData, aLength); +} + +NS_IMETHODIMP +nsMediaSniffer::GetMIMETypeFromContent(nsIRequest* aRequest, + const uint8_t* aData, + const uint32_t aLength, + nsACString& aSniffedType) { + const uint32_t clampedLength = std::min(aLength, MAX_BYTES_SNIFFED); + + auto maybeUpdate = mozilla::MakeScopeExit([request = RefPtr{aRequest}]() { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + if (channel && XRE_IsParentProcess()) { + if (RefPtr<mozilla::net::nsHttpChannel> httpChannel = + do_QueryObject(channel)) { + // If the audio or video type pattern matching algorithm given bytes + // does not return undefined, then disable the further check and allow + // the response. + httpChannel->DisableIsOpaqueResponseAllowedAfterSniffCheck( + mozilla::net::nsHttpChannel::SnifferType::Media); + } + }; + }); + + for (const auto& currentEntry : sSnifferEntries) { + if (clampedLength < currentEntry.mLength || currentEntry.mLength == 0) { + continue; + } + bool matched = true; + for (uint32_t j = 0; j < currentEntry.mLength; ++j) { + if ((currentEntry.mMask[j] & aData[j]) != currentEntry.mPattern[j]) { + matched = false; + break; + } + } + if (matched) { + aSniffedType.AssignASCII(currentEntry.mContentType); + return NS_OK; + } + } + + if (MatchesMP4(aData, clampedLength, aSniffedType)) { + return NS_OK; + } + + if (MatchesWebM(aData, clampedLength)) { + aSniffedType.AssignLiteral(VIDEO_WEBM); + return NS_OK; + } + + // Bug 950023: 512 bytes are often not enough to sniff for mp3. + if (MatchesMP3(aData, std::min(aLength, MAX_BYTES_SNIFFED_MP3))) { + aSniffedType.AssignLiteral(AUDIO_MP3); + return NS_OK; + } + + if (MatchesADTS(aData, std::min(aLength, MAX_BYTES_SNIFFED_ADTS))) { + aSniffedType.AssignLiteral(AUDIO_AAC); + return NS_OK; + } + + maybeUpdate.release(); + + // Could not sniff the media type, we are required to set it to + // application/octet-stream. + aSniffedType.AssignLiteral(APPLICATION_OCTET_STREAM); + return NS_ERROR_NOT_AVAILABLE; +} diff --git a/toolkit/components/mediasniffer/nsMediaSniffer.h b/toolkit/components/mediasniffer/nsMediaSniffer.h new file mode 100644 index 0000000000..9608e2e643 --- /dev/null +++ b/toolkit/components/mediasniffer/nsMediaSniffer.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 tw=80 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMediaSniffer_h +#define nsMediaSniffer_h + +#include "nsIContentSniffer.h" +#include "mozilla/Attributes.h" + +// ed905ba3-c656-480e-934e-6bc35bd36aff +#define NS_MEDIA_SNIFFER_CID \ + { \ + 0x3fdd6c28, 0x5b87, 0x4e3e, { \ + 0x8b, 0x57, 0x8e, 0x83, 0xc2, 0x3c, 0x1a, 0x6d \ + } \ + } + +#define NS_MEDIA_SNIFFER_CONTRACTID "@mozilla.org/media/sniffer;1" + +#define PATTERN_ENTRY(mask, pattern, contentType) \ + { \ + (const uint8_t*)mask, (const uint8_t*)pattern, sizeof(mask) - 1, \ + contentType \ + } + +struct nsMediaSnifferEntry { + const uint8_t* mMask; + const uint8_t* mPattern; + const uint32_t mLength; + const char* mContentType; +}; + +bool MatchesMP4(const uint8_t* aData, const uint32_t aLength, + nsACString& aSniffedType); + +class nsMediaSniffer final : public nsIContentSniffer { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTSNIFFER + + private: + ~nsMediaSniffer() = default; + + static nsMediaSnifferEntry sSnifferEntries[]; +}; + +#endif diff --git a/toolkit/components/mediasniffer/test/unit/data/bug1079747.mp4 b/toolkit/components/mediasniffer/test/unit/data/bug1079747.mp4 Binary files differnew file mode 100644 index 0000000000..f00731d7e2 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/bug1079747.mp4 diff --git a/toolkit/components/mediasniffer/test/unit/data/bug1725190.cr3 b/toolkit/components/mediasniffer/test/unit/data/bug1725190.cr3 Binary files differnew file mode 100644 index 0000000000..4e46f36b1d --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/bug1725190.cr3 diff --git a/toolkit/components/mediasniffer/test/unit/data/detodos.mp3 b/toolkit/components/mediasniffer/test/unit/data/detodos.mp3 Binary files differnew file mode 100644 index 0000000000..12e3f89c20 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/detodos.mp3 diff --git a/toolkit/components/mediasniffer/test/unit/data/ff-inst.exe b/toolkit/components/mediasniffer/test/unit/data/ff-inst.exe Binary files differnew file mode 100644 index 0000000000..0f02f36e1a --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/ff-inst.exe diff --git a/toolkit/components/mediasniffer/test/unit/data/file.mkv b/toolkit/components/mediasniffer/test/unit/data/file.mkv Binary files differnew file mode 100644 index 0000000000..4618cda032 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/file.mkv diff --git a/toolkit/components/mediasniffer/test/unit/data/file.webm b/toolkit/components/mediasniffer/test/unit/data/file.webm Binary files differnew file mode 100644 index 0000000000..7bc738b8b4 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/file.webm diff --git a/toolkit/components/mediasniffer/test/unit/data/fl10.mp2 b/toolkit/components/mediasniffer/test/unit/data/fl10.mp2 Binary files differnew file mode 100644 index 0000000000..bf84d73675 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/fl10.mp2 diff --git a/toolkit/components/mediasniffer/test/unit/data/he_free.mp3 b/toolkit/components/mediasniffer/test/unit/data/he_free.mp3 Binary files differnew file mode 100644 index 0000000000..e3da8e6a72 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/he_free.mp3 diff --git a/toolkit/components/mediasniffer/test/unit/data/id3tags.mp3 b/toolkit/components/mediasniffer/test/unit/data/id3tags.mp3 Binary files differnew file mode 100644 index 0000000000..23091e6667 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/id3tags.mp3 diff --git a/toolkit/components/mediasniffer/test/unit/data/mp3-in-riff.wav b/toolkit/components/mediasniffer/test/unit/data/mp3-in-riff.wav Binary files differnew file mode 100644 index 0000000000..5ccaf351f3 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/mp3-in-riff.wav diff --git a/toolkit/components/mediasniffer/test/unit/data/notags-bad.mp3 b/toolkit/components/mediasniffer/test/unit/data/notags-bad.mp3 Binary files differnew file mode 100644 index 0000000000..5ad89786fa --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/notags-bad.mp3 diff --git a/toolkit/components/mediasniffer/test/unit/data/notags-scan.mp3 b/toolkit/components/mediasniffer/test/unit/data/notags-scan.mp3 Binary files differnew file mode 100644 index 0000000000..949b7c4687 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/notags-scan.mp3 diff --git a/toolkit/components/mediasniffer/test/unit/data/notags.mp3 b/toolkit/components/mediasniffer/test/unit/data/notags.mp3 Binary files differnew file mode 100644 index 0000000000..c7db943617 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/data/notags.mp3 diff --git a/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js b/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js new file mode 100644 index 0000000000..b33ec2f590 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +const PATH = "/file.meh"; +var httpserver = new HttpServer(); + +// Each time, the data consist in a string that should be sniffed as Ogg. +const data = "OggS\0meeeh."; +var testRan = 0; + +// If the content-type is not present, or if it's application/octet-stream, it +// should be sniffed to application/ogg by the media sniffer. Otherwise, it +// should not be changed. +const tests = [ + // Those three first case are the case of a media loaded in a media element. + // All three should be sniffed. + { + contentType: "", + expectedContentType: "application/ogg", + flags: + Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS | + Ci.nsIChannel.LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE, + }, + { + contentType: "application/octet-stream", + expectedContentType: "application/ogg", + flags: + Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS | + Ci.nsIChannel.LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE, + }, + { + contentType: "application/something", + expectedContentType: "application/ogg", + flags: + Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS | + Ci.nsIChannel.LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE, + }, + // This last cases test the case of a channel opened while allowing content + // sniffers to override the content-type, like in the docshell. + { + contentType: "application/octet-stream", + expectedContentType: "application/ogg", + flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS, + }, + { + contentType: "", + expectedContentType: "application/ogg", + flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS, + }, + { + contentType: "application/something", + expectedContentType: "application/ogg", + flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS, + }, +]; + +// A basic listener that reads checks the if we sniffed properly. +var listener = { + onStartRequest(request) { + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + tests[testRan].expectedContentType + ); + }, + + onDataAvailable(request, stream, offset, count) { + try { + var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bis.setInputStream(stream); + bis.readByteArray(bis.available()); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + testRan++; + runNext(); + }, +}; + +function setupChannel(url, flags) { + let uri = "http://localhost:" + httpserver.identity.primaryPort + url; + var chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_MEDIA, + }); + chan.loadFlags |= flags; + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function runNext() { + if (testRan == tests.length) { + do_test_finished(); + return; + } + var channel = setupChannel(PATH, tests[testRan].flags); + httpserver.registerPathHandler(PATH, function (request, response) { + response.setHeader("Content-Type", tests[testRan].contentType, false); + response.bodyOutputStream.write(data, data.length); + }); + channel.asyncOpen(listener); +} + +function run_test() { + httpserver.start(-1); + do_test_pending(); + try { + runNext(); + } catch (e) { + print("ERROR - " + e + "\n"); + } +} diff --git a/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js b/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js new file mode 100644 index 0000000000..5439e5b51d --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var CC = Components.Constructor; + +var BinaryOutputStream = CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +var httpserver = new HttpServer(); + +var testRan = 0; + +// The tests files we want to test, and the type we should have after sniffing. +const tests = [ + // Real webm and mkv files truncated to 512 bytes. + { path: "data/file.webm", expected: "video/webm" }, + { path: "data/file.mkv", expected: "application/octet-stream" }, + // MP3 files with and without id3 headers truncated to 512 bytes. + // NB these have 208/209 byte frames, but mp3 can require up to + // 1445 bytes to detect with our method. + { path: "data/id3tags.mp3", expected: "audio/mpeg" }, + { path: "data/notags.mp3", expected: "audio/mpeg" }, + // MPEG-2 mp3 files. + { path: "data/detodos.mp3", expected: "audio/mpeg" }, + // Padding bit flipped in the first header: sniffing should fail. + { path: "data/notags-bad.mp3", expected: "application/octet-stream" }, + // Garbage before header: sniffing should fail. + { path: "data/notags-scan.mp3", expected: "application/octet-stream" }, + // VBR from the layer III test patterns. We can't sniff this. + { path: "data/he_free.mp3", expected: "application/octet-stream" }, + // Make sure we reject mp2, which has a similar header. + { path: "data/fl10.mp2", expected: "application/octet-stream" }, + // Truncated ff installer regression test for bug 875769. + { path: "data/ff-inst.exe", expected: "application/octet-stream" }, + // MP4 with invalid box size (0) for "ftyp". + { path: "data/bug1079747.mp4", expected: "application/octet-stream" }, + // An MP3 bytestream in a RIFF container, truncated to 512 bytes. + { path: "data/mp3-in-riff.wav", expected: "audio/mpeg" }, + // The sniffing-relevant portion of a Canon raw image + { path: "data/bug1725190.cr3", expected: "application/octet-stream" }, +]; + +// A basic listener that reads checks the if we sniffed properly. +var listener = { + onStartRequest(request) { + info("Sniffing " + tests[testRan].path); + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + tests[testRan].expected + ); + }, + + onDataAvailable(request, stream, offset, count) { + try { + var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bis.setInputStream(stream); + bis.readByteArray(bis.available()); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + testRan++; + runNext(); + }, +}; + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_MEDIA, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function runNext() { + if (testRan == tests.length) { + do_test_finished(); + return; + } + var channel = setupChannel("/"); + channel.asyncOpen(listener); +} + +function getFileContents(aFile) { + var fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(aFile, 1, -1, null); + var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bis.setInputStream(fileStream); + + var data = bis.readByteArray(bis.available()); + + return data; +} + +function handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + // Send an empty Content-Type, so we are guaranteed to sniff. + response.setHeader("Content-Type", "", false); + var body = getFileContents(do_get_file(tests[testRan].path)); + var bos = new BinaryOutputStream(response.bodyOutputStream); + bos.writeByteArray(body); +} + +function run_test() { + // We use a custom handler so we can change the header to force sniffing. + httpserver.registerPathHandler("/", handler); + httpserver.start(-1); + do_test_pending(); + try { + runNext(); + } catch (e) { + print("ERROR - " + e + "\n"); + } +} diff --git a/toolkit/components/mediasniffer/test/unit/xpcshell.ini b/toolkit/components/mediasniffer/test/unit/xpcshell.ini new file mode 100644 index 0000000000..9b3464a1b3 --- /dev/null +++ b/toolkit/components/mediasniffer/test/unit/xpcshell.ini @@ -0,0 +1,20 @@ +[DEFAULT] +head = +skip-if = toolkit == 'android' +support-files = + data/bug1079747.mp4 + data/bug1725190.cr3 + data/detodos.mp3 + data/ff-inst.exe + data/file.mkv + data/file.webm + data/fl10.mp2 + data/he_free.mp3 + data/id3tags.mp3 + data/mp3-in-riff.wav + data/notags-bad.mp3 + data/notags-scan.mp3 + data/notags.mp3 + +[test_mediasniffer.js] +[test_mediasniffer_ext.js] |