mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
MidiFile: Make file-reading more robust
This commit adds tests and fixes some potential crashes caused by out-of-bounds reads.
This commit is contained in:
parent
a98dc7553b
commit
0943291990
4 changed files with 669 additions and 103 deletions
|
|
@ -60,9 +60,8 @@ namespace MidiBufferHelpers
|
|||
if (maxBytes == 1)
|
||||
return 1;
|
||||
|
||||
int n;
|
||||
auto bytesLeft = MidiMessage::readVariableLengthVal (data + 1, n);
|
||||
return jmin (maxBytes, n + 2 + bytesLeft);
|
||||
const auto var = MidiMessage::readVariableLengthValue (data + 1, maxBytes - 1);
|
||||
return jmin (maxBytes, var.value + 2 + var.bytesUsed);
|
||||
}
|
||||
|
||||
if (byte >= 0x80)
|
||||
|
|
|
|||
|
|
@ -46,23 +46,77 @@ namespace MidiFileHelpers
|
|||
}
|
||||
}
|
||||
|
||||
static bool parseMidiHeader (const uint8* &data, short& timeFormat, short& fileType, short& numberOfTracks) noexcept
|
||||
template <typename Value>
|
||||
struct Optional
|
||||
{
|
||||
auto ch = ByteOrder::bigEndianInt (data);
|
||||
data += 4;
|
||||
Optional() = default;
|
||||
|
||||
if (ch != ByteOrder::bigEndianInt ("MThd"))
|
||||
Optional (const Value& v)
|
||||
: value (v), valid (true) {}
|
||||
|
||||
Value value = Value();
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
template <typename Integral>
|
||||
struct ReadTrait;
|
||||
|
||||
template <>
|
||||
struct ReadTrait<uint32> { static constexpr auto read = ByteOrder::bigEndianInt; };
|
||||
|
||||
template <>
|
||||
struct ReadTrait<uint16> { static constexpr auto read = ByteOrder::bigEndianShort; };
|
||||
|
||||
template <typename Integral>
|
||||
Optional<Integral> tryRead (const uint8*& data, size_t& remaining)
|
||||
{
|
||||
bool ok = false;
|
||||
using Trait = ReadTrait<Integral>;
|
||||
constexpr auto size = sizeof (Integral);
|
||||
|
||||
if (ch == ByteOrder::bigEndianInt ("RIFF"))
|
||||
if (remaining < size)
|
||||
return {};
|
||||
|
||||
const Optional<Integral> result { Trait::read (data) };
|
||||
|
||||
data += size;
|
||||
remaining -= size;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
struct HeaderDetails
|
||||
{
|
||||
size_t bytesRead = 0;
|
||||
short timeFormat = 0;
|
||||
short fileType = 0;
|
||||
short numberOfTracks = 0;
|
||||
};
|
||||
|
||||
static Optional<HeaderDetails> parseMidiHeader (const uint8* const initialData,
|
||||
const size_t maxSize)
|
||||
{
|
||||
auto* data = initialData;
|
||||
auto remaining = maxSize;
|
||||
|
||||
auto ch = tryRead<uint32> (data, remaining);
|
||||
|
||||
if (! ch.valid)
|
||||
return {};
|
||||
|
||||
if (ch.value != ByteOrder::bigEndianInt ("MThd"))
|
||||
{
|
||||
auto ok = false;
|
||||
|
||||
if (ch.value == ByteOrder::bigEndianInt ("RIFF"))
|
||||
{
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
ch = ByteOrder::bigEndianInt (data);
|
||||
data += 4;
|
||||
ch = tryRead<uint32> (data, remaining);
|
||||
|
||||
if (ch == ByteOrder::bigEndianInt ("MThd"))
|
||||
if (! ch.valid)
|
||||
return {};
|
||||
|
||||
if (ch.value == ByteOrder::bigEndianInt ("MThd"))
|
||||
{
|
||||
ok = true;
|
||||
break;
|
||||
|
|
@ -71,21 +125,37 @@ namespace MidiFileHelpers
|
|||
}
|
||||
|
||||
if (! ok)
|
||||
return false;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto bytesRemaining = ByteOrder::bigEndianInt (data);
|
||||
data += 4;
|
||||
fileType = (short) ByteOrder::bigEndianShort (data);
|
||||
data += 2;
|
||||
numberOfTracks = (short) ByteOrder::bigEndianShort (data);
|
||||
data += 2;
|
||||
timeFormat = (short) ByteOrder::bigEndianShort (data);
|
||||
data += 2;
|
||||
bytesRemaining -= 6;
|
||||
data += bytesRemaining;
|
||||
const auto bytesRemaining = tryRead<uint32> (data, remaining);
|
||||
|
||||
return true;
|
||||
if (! bytesRemaining.valid || bytesRemaining.value > remaining)
|
||||
return {};
|
||||
|
||||
const auto optFileType = tryRead<uint16> (data, remaining);
|
||||
|
||||
if (! optFileType.valid || 2 < optFileType.value)
|
||||
return {};
|
||||
|
||||
const auto optNumTracks = tryRead<uint16> (data, remaining);
|
||||
|
||||
if (! optNumTracks.valid || (optFileType.value == 0 && optNumTracks.value != 1))
|
||||
return {};
|
||||
|
||||
const auto optTimeFormat = tryRead<uint16> (data, remaining);
|
||||
|
||||
if (! optTimeFormat.valid)
|
||||
return {};
|
||||
|
||||
HeaderDetails result;
|
||||
|
||||
result.fileType = (short) optFileType.value;
|
||||
result.timeFormat = (short) optTimeFormat.value;
|
||||
result.numberOfTracks = (short) optNumTracks.value;
|
||||
result.bytesRead = maxSize - remaining;
|
||||
|
||||
return { result };
|
||||
}
|
||||
|
||||
static double convertTicksToSeconds (double time,
|
||||
|
|
@ -149,6 +219,47 @@ namespace MidiFileHelpers
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
static MidiMessageSequence readTrack (const uint8* data, int size)
|
||||
{
|
||||
double time = 0;
|
||||
uint8 lastStatusByte = 0;
|
||||
|
||||
MidiMessageSequence result;
|
||||
|
||||
while (size > 0)
|
||||
{
|
||||
const auto delay = MidiMessage::readVariableLengthValue (data, (int) size);
|
||||
|
||||
if (delay.bytesUsed == 0)
|
||||
break;
|
||||
|
||||
data += delay.bytesUsed;
|
||||
size -= delay.bytesUsed;
|
||||
time += delay.value;
|
||||
|
||||
if (size <= 0)
|
||||
break;
|
||||
|
||||
int messSize = 0;
|
||||
const MidiMessage mm (data, size, messSize, lastStatusByte, time);
|
||||
|
||||
if (messSize <= 0)
|
||||
break;
|
||||
|
||||
size -= messSize;
|
||||
data += messSize;
|
||||
|
||||
result.addEvent (mm);
|
||||
|
||||
auto firstByte = *(mm.getRawData());
|
||||
|
||||
if ((firstByte & 0xf0) != 0xf0)
|
||||
lastStatusByte = firstByte;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -253,78 +364,56 @@ bool MidiFile::readFrom (InputStream& sourceStream, bool createMatchingNoteOffs)
|
|||
const int maxSensibleMidiFileSize = 200 * 1024 * 1024;
|
||||
|
||||
// (put a sanity-check on the file size, as midi files are generally small)
|
||||
if (sourceStream.readIntoMemoryBlock (data, maxSensibleMidiFileSize))
|
||||
{
|
||||
if (! sourceStream.readIntoMemoryBlock (data, maxSensibleMidiFileSize))
|
||||
return false;
|
||||
|
||||
auto size = data.getSize();
|
||||
auto d = static_cast<const uint8*> (data.getData());
|
||||
short fileType, expectedTracks;
|
||||
|
||||
if (size > 16 && MidiFileHelpers::parseMidiHeader (d, timeFormat, fileType, expectedTracks))
|
||||
const auto optHeader = MidiFileHelpers::parseMidiHeader (d, size);
|
||||
|
||||
if (! optHeader.valid)
|
||||
return false;
|
||||
|
||||
const auto header = optHeader.value;
|
||||
timeFormat = header.timeFormat;
|
||||
|
||||
d += header.bytesRead;
|
||||
size -= (size_t) header.bytesRead;
|
||||
|
||||
for (int track = 0; track < header.numberOfTracks; ++track)
|
||||
{
|
||||
size -= (size_t) (d - static_cast<const uint8*> (data.getData()));
|
||||
int track = 0;
|
||||
const auto optChunkType = MidiFileHelpers::tryRead<uint32> (d, size);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto chunkType = (int) ByteOrder::bigEndianInt (d);
|
||||
d += 4;
|
||||
auto chunkSize = (int) ByteOrder::bigEndianInt (d);
|
||||
d += 4;
|
||||
if (! optChunkType.valid)
|
||||
return false;
|
||||
|
||||
if (chunkSize <= 0 || (size_t) chunkSize > size)
|
||||
break;
|
||||
const auto optChunkSize = MidiFileHelpers::tryRead<uint32> (d, size);
|
||||
|
||||
if (chunkType == (int) ByteOrder::bigEndianInt ("MTrk"))
|
||||
readNextTrack (d, chunkSize, createMatchingNoteOffs);
|
||||
if (! optChunkSize.valid)
|
||||
return false;
|
||||
|
||||
if (++track >= expectedTracks)
|
||||
break;
|
||||
const auto chunkSize = optChunkSize.value;
|
||||
|
||||
size -= (size_t) chunkSize + 8;
|
||||
if (size < chunkSize)
|
||||
return false;
|
||||
|
||||
if (optChunkType.value == ByteOrder::bigEndianInt ("MTrk"))
|
||||
readNextTrack (d, (int) chunkSize, createMatchingNoteOffs);
|
||||
|
||||
size -= chunkSize;
|
||||
d += chunkSize;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return size == 0;
|
||||
}
|
||||
|
||||
void MidiFile::readNextTrack (const uint8* data, int size, bool createMatchingNoteOffs)
|
||||
{
|
||||
double time = 0;
|
||||
uint8 lastStatusByte = 0;
|
||||
|
||||
MidiMessageSequence result;
|
||||
|
||||
while (size > 0)
|
||||
{
|
||||
int bytesUsed;
|
||||
auto delay = MidiMessage::readVariableLengthVal (data, bytesUsed);
|
||||
data += bytesUsed;
|
||||
size -= bytesUsed;
|
||||
time += delay;
|
||||
|
||||
int messSize = 0;
|
||||
const MidiMessage mm (data, size, messSize, lastStatusByte, time);
|
||||
|
||||
if (messSize <= 0)
|
||||
break;
|
||||
|
||||
size -= messSize;
|
||||
data += messSize;
|
||||
|
||||
result.addEvent (mm);
|
||||
|
||||
auto firstByte = *(mm.getRawData());
|
||||
|
||||
if ((firstByte & 0xf0) != 0xf0)
|
||||
lastStatusByte = firstByte;
|
||||
}
|
||||
auto sequence = MidiFileHelpers::readTrack (data, size);
|
||||
|
||||
// sort so that we put all the note-offs before note-ons that have the same time
|
||||
std::stable_sort (result.list.begin(), result.list.end(),
|
||||
std::stable_sort (sequence.list.begin(), sequence.list.end(),
|
||||
[] (const MidiMessageSequence::MidiEventHolder* a,
|
||||
const MidiMessageSequence::MidiEventHolder* b)
|
||||
{
|
||||
|
|
@ -337,10 +426,10 @@ void MidiFile::readNextTrack (const uint8* data, int size, bool createMatchingNo
|
|||
return a->message.isNoteOff() && b->message.isNoteOn();
|
||||
});
|
||||
|
||||
addTrack (result);
|
||||
|
||||
if (createMatchingNoteOffs)
|
||||
tracks.getLast()->updateMatchedPairs();
|
||||
sequence.updateMatchedPairs();
|
||||
|
||||
addTrack (sequence);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -443,4 +532,267 @@ bool MidiFile::writeTrack (OutputStream& mainOut, const MidiMessageSequence& ms)
|
|||
return true;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
struct MidiFileTest : public UnitTest
|
||||
{
|
||||
MidiFileTest()
|
||||
: UnitTest ("MidiFile", UnitTestCategories::midi)
|
||||
{}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("ReadTrack respects running status");
|
||||
{
|
||||
const auto sequence = parseSequence ([] (OutputStream& os)
|
||||
{
|
||||
MidiFileHelpers::writeVariableLengthInt (os, 100);
|
||||
writeBytes (os, { 0x90, 0x40, 0x40 });
|
||||
MidiFileHelpers::writeVariableLengthInt (os, 200);
|
||||
writeBytes (os, { 0x40, 0x40 });
|
||||
MidiFileHelpers::writeVariableLengthInt (os, 300);
|
||||
writeBytes (os, { 0xff, 0x2f, 0x00 });
|
||||
});
|
||||
|
||||
expectEquals (sequence.getNumEvents(), 3);
|
||||
expect (sequence.getEventPointer (0)->message.isNoteOn());
|
||||
expect (sequence.getEventPointer (1)->message.isNoteOn());
|
||||
expect (sequence.getEventPointer (2)->message.isEndOfTrackMetaEvent());
|
||||
}
|
||||
|
||||
beginTest ("ReadTrack returns available messages if input is truncated");
|
||||
{
|
||||
{
|
||||
const auto sequence = parseSequence ([] (OutputStream& os)
|
||||
{
|
||||
// Incomplete delta time
|
||||
writeBytes (os, { 0xff });
|
||||
});
|
||||
|
||||
expectEquals (sequence.getNumEvents(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
const auto sequence = parseSequence ([] (OutputStream& os)
|
||||
{
|
||||
// Complete delta with no following event
|
||||
MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
|
||||
});
|
||||
|
||||
expectEquals (sequence.getNumEvents(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
const auto sequence = parseSequence ([] (OutputStream& os)
|
||||
{
|
||||
// Complete delta with malformed following event
|
||||
MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
|
||||
writeBytes (os, { 0x90, 0x40 });
|
||||
});
|
||||
|
||||
expectEquals (sequence.getNumEvents(), 1);
|
||||
expect (sequence.getEventPointer (0)->message.isNoteOff());
|
||||
expectEquals (sequence.getEventPointer (0)->message.getNoteNumber(), 0x40);
|
||||
expectEquals (sequence.getEventPointer (0)->message.getVelocity(), (uint8) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("Header parsing works");
|
||||
{
|
||||
{
|
||||
// No data
|
||||
const auto header = parseHeader ([] (OutputStream&) {});
|
||||
expect (! header.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Invalid initial byte
|
||||
const auto header = parseHeader ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 0xff });
|
||||
});
|
||||
|
||||
expect (! header.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Type block, but no header data
|
||||
const auto header = parseHeader ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd' });
|
||||
});
|
||||
|
||||
expect (! header.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// We (ll-formed header, but track type is 0 and channels != 1
|
||||
const auto header = parseHeader ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 0, 0, 16, 0, 1 });
|
||||
});
|
||||
|
||||
expect (! header.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Well-formed header, but track type is 5
|
||||
const auto header = parseHeader ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 5, 0, 16, 0, 1 });
|
||||
});
|
||||
|
||||
expect (! header.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Well-formed header
|
||||
const auto header = parseHeader ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 16, 0, 1 });
|
||||
});
|
||||
|
||||
expect (header.valid);
|
||||
|
||||
expectEquals (header.value.fileType, (short) 1);
|
||||
expectEquals (header.value.numberOfTracks, (short) 16);
|
||||
expectEquals (header.value.timeFormat, (short) 1);
|
||||
expectEquals ((int) header.value.bytesRead, 14);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("Read from stream");
|
||||
{
|
||||
{
|
||||
// Empty input
|
||||
const auto file = parseFile ([] (OutputStream&) {});
|
||||
expect (! file.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Malformed header
|
||||
const auto file = parseFile ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd' });
|
||||
});
|
||||
|
||||
expect (! file.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Header, no channels
|
||||
const auto file = parseFile ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 0, 0, 1 });
|
||||
});
|
||||
|
||||
expect (file.valid);
|
||||
expectEquals (file.value.getNumTracks(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
// Header, one malformed channel
|
||||
const auto file = parseFile ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
|
||||
writeBytes (os, { 'M', 'T', 'r', '?' });
|
||||
});
|
||||
|
||||
expect (! file.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Header, one channel with malformed message
|
||||
const auto file = parseFile ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
|
||||
writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 1, 0xff });
|
||||
});
|
||||
|
||||
expect (file.valid);
|
||||
expectEquals (file.value.getNumTracks(), 1);
|
||||
expectEquals (file.value.getTrack (0)->getNumEvents(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
// Header, one channel with incorrect length message
|
||||
const auto file = parseFile ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
|
||||
writeBytes (os, { 'M', 'T', 'r', 'k', 0x0f, 0, 0, 0, 0xff });
|
||||
});
|
||||
|
||||
expect (! file.valid);
|
||||
}
|
||||
|
||||
{
|
||||
// Header, one channel, all well-formed
|
||||
const auto file = parseFile ([] (OutputStream& os)
|
||||
{
|
||||
writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
|
||||
writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 4 });
|
||||
|
||||
MidiFileHelpers::writeVariableLengthInt (os, 0x0f);
|
||||
writeBytes (os, { 0x80, 0x00, 0x00 });
|
||||
});
|
||||
|
||||
expect (file.valid);
|
||||
expectEquals (file.value.getNumTracks(), 1);
|
||||
|
||||
auto& track = *file.value.getTrack (0);
|
||||
expectEquals (track.getNumEvents(), 1);
|
||||
expect (track.getEventPointer (0)->message.isNoteOff());
|
||||
expectEquals (track.getEventPointer (0)->message.getTimeStamp(), (double) 0x0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
static MidiMessageSequence parseSequence (Fn&& fn)
|
||||
{
|
||||
MemoryOutputStream os;
|
||||
fn (os);
|
||||
|
||||
return MidiFileHelpers::readTrack (reinterpret_cast<const uint8*> (os.getData()),
|
||||
(int) os.getDataSize());
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
static MidiFileHelpers::Optional<MidiFileHelpers::HeaderDetails> parseHeader (Fn&& fn)
|
||||
{
|
||||
MemoryOutputStream os;
|
||||
fn (os);
|
||||
|
||||
return MidiFileHelpers::parseMidiHeader (reinterpret_cast<const uint8*> (os.getData()),
|
||||
os.getDataSize());
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
static MidiFileHelpers::Optional<MidiFile> parseFile (Fn&& fn)
|
||||
{
|
||||
MemoryOutputStream os;
|
||||
fn (os);
|
||||
|
||||
MemoryInputStream is (os.getData(), os.getDataSize(), false);
|
||||
MidiFile mf;
|
||||
|
||||
if (mf.readFrom (is))
|
||||
return mf;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static void writeBytes (OutputStream& os, const std::vector<uint8>& bytes)
|
||||
{
|
||||
for (const auto& byte : bytes)
|
||||
os.writeByte ((char) byte);
|
||||
}
|
||||
};
|
||||
|
||||
static MidiFileTest midiFileTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -57,6 +57,32 @@ uint16 MidiMessage::pitchbendToPitchwheelPos (const float pitchbend,
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
MidiMessage::VariableLengthValue MidiMessage::readVariableLengthValue (const uint8* data, int maxBytesToUse) noexcept
|
||||
{
|
||||
uint32 v = 0;
|
||||
|
||||
// The largest allowable variable-length value is 0x0f'ff'ff'ff which is
|
||||
// represented by the 4-byte stream 0xff 0xff 0xff 0x7f.
|
||||
// Longer bytestreams risk overflowing a 32-bit signed int.
|
||||
const auto limit = jmin (maxBytesToUse, 4);
|
||||
|
||||
for (int numBytesUsed = 0; numBytesUsed < limit; ++numBytesUsed)
|
||||
{
|
||||
const auto i = data[numBytesUsed];
|
||||
v = (v << 7) + (i & 0x7f);
|
||||
|
||||
if (! (i & 0x80))
|
||||
return { (int) v, numBytesUsed + 1 };
|
||||
}
|
||||
|
||||
// If this is hit, the input was malformed. Either there were not enough
|
||||
// bytes of input to construct a full value, or no terminating byte was
|
||||
// found. This implementation only supports variable-length values of up
|
||||
// to four bytes.
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
int MidiMessage::readVariableLengthVal (const uint8* data, int& numBytesUsed) noexcept
|
||||
{
|
||||
numBytesUsed = 0;
|
||||
|
|
@ -224,16 +250,8 @@ MidiMessage::MidiMessage (const void* srcData, int sz, int& numBytesUsed, const
|
|||
}
|
||||
else if (byte == 0xff)
|
||||
{
|
||||
if (sz == 1)
|
||||
{
|
||||
size = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
int n;
|
||||
const int bytesLeft = readVariableLengthVal (src + 1, n);
|
||||
size = jmin (sz + 1, n + 2 + bytesLeft);
|
||||
}
|
||||
const auto bytesLeft = readVariableLengthValue (src + 1, sz - 1);
|
||||
size = jmin (sz + 1, bytesLeft.bytesUsed + 2 + bytesLeft.value);
|
||||
|
||||
auto dest = allocateSpace (size);
|
||||
*dest = (uint8) byte;
|
||||
|
|
@ -682,7 +700,7 @@ bool MidiMessage::isActiveSense() const noexcept { return *getRawData() == 0x
|
|||
int MidiMessage::getMetaEventType() const noexcept
|
||||
{
|
||||
auto data = getRawData();
|
||||
return *data != 0xff ? -1 : data[1];
|
||||
return (size < 2 || *data != 0xff) ? -1 : data[1];
|
||||
}
|
||||
|
||||
int MidiMessage::getMetaEventLength() const noexcept
|
||||
|
|
@ -691,8 +709,8 @@ int MidiMessage::getMetaEventLength() const noexcept
|
|||
|
||||
if (*data == 0xff)
|
||||
{
|
||||
int n;
|
||||
return jmin (size - 2, readVariableLengthVal (data + 2, n));
|
||||
const auto var = readVariableLengthValue (data + 2, size - 2);
|
||||
return jmax (0, jmin (size - 2 - var.bytesUsed, var.value));
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
@ -702,10 +720,9 @@ const uint8* MidiMessage::getMetaEventData() const noexcept
|
|||
{
|
||||
jassert (isMetaEvent());
|
||||
|
||||
int n;
|
||||
auto d = getRawData() + 2;
|
||||
readVariableLengthVal (d, n);
|
||||
return d + n;
|
||||
const auto var = readVariableLengthValue (d, size - 2);
|
||||
return d + var.bytesUsed;
|
||||
}
|
||||
|
||||
bool MidiMessage::isTrackMetaEvent() const noexcept { return getMetaEventType() == 0; }
|
||||
|
|
@ -1141,4 +1158,170 @@ const char* MidiMessage::getControllerName (const int n)
|
|||
return isPositiveAndBelow (n, numElementsInArray (names)) ? names[n] : nullptr;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
struct MidiMessageTest : public UnitTest
|
||||
{
|
||||
MidiMessageTest()
|
||||
: UnitTest ("MidiMessage", UnitTestCategories::midi)
|
||||
{}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
using std::begin;
|
||||
using std::end;
|
||||
|
||||
beginTest ("ReadVariableLengthValue should return valid, backward-compatible results");
|
||||
{
|
||||
const std::vector<uint8> inputs[]
|
||||
{
|
||||
{ 0x00 },
|
||||
{ 0x40 },
|
||||
{ 0x7f },
|
||||
{ 0x81, 0x00 },
|
||||
{ 0xc0, 0x00 },
|
||||
{ 0xff, 0x7f },
|
||||
{ 0x81, 0x80, 0x00 },
|
||||
{ 0xc0, 0x80, 0x00 },
|
||||
{ 0xff, 0xff, 0x7f },
|
||||
{ 0x81, 0x80, 0x80, 0x00 },
|
||||
{ 0xc0, 0x80, 0x80, 0x00 },
|
||||
{ 0xff, 0xff, 0xff, 0x7f }
|
||||
};
|
||||
|
||||
const int outputs[]
|
||||
{
|
||||
0x00,
|
||||
0x40,
|
||||
0x7f,
|
||||
0x80,
|
||||
0x2000,
|
||||
0x3fff,
|
||||
0x4000,
|
||||
0x100000,
|
||||
0x1fffff,
|
||||
0x200000,
|
||||
0x8000000,
|
||||
0xfffffff,
|
||||
};
|
||||
|
||||
expectEquals (std::distance (begin (inputs), end (inputs)),
|
||||
std::distance (begin (outputs), end (outputs)));
|
||||
|
||||
size_t index = 0;
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996)
|
||||
|
||||
for (const auto& input : inputs)
|
||||
{
|
||||
auto copy = input;
|
||||
|
||||
while (copy.size() < 16)
|
||||
copy.push_back (0);
|
||||
|
||||
const auto result = MidiMessage::readVariableLengthValue (copy.data(),
|
||||
(int) copy.size());
|
||||
|
||||
expectEquals (result.value, outputs[index]);
|
||||
expectEquals (result.bytesUsed, (int) inputs[index].size());
|
||||
|
||||
int legacyNumUsed = 0;
|
||||
const auto legacyResult = MidiMessage::readVariableLengthVal (copy.data(),
|
||||
legacyNumUsed);
|
||||
|
||||
expectEquals (result.value, legacyResult);
|
||||
expectEquals (result.bytesUsed, legacyNumUsed);
|
||||
|
||||
++index;
|
||||
}
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
JUCE_END_IGNORE_WARNINGS_MSVC
|
||||
}
|
||||
|
||||
beginTest ("ReadVariableLengthVal should return 0 if input is truncated");
|
||||
{
|
||||
for (size_t i = 0; i != 16; ++i)
|
||||
{
|
||||
std::vector<uint8> input;
|
||||
input.resize (i, 0xFF);
|
||||
|
||||
const auto result = MidiMessage::readVariableLengthValue (input.data(),
|
||||
(int) input.size());
|
||||
|
||||
expectEquals (result.value, 0);
|
||||
expectEquals (result.bytesUsed, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<uint8> metaEvents[]
|
||||
{
|
||||
// Format is 0xff, followed by a 'kind' byte, followed by a variable-length
|
||||
// 'data-length' value, followed by that many data bytes
|
||||
{ 0xff, 0x00, 0x02, 0x00, 0x00 }, // Sequence number
|
||||
{ 0xff, 0x01, 0x00 }, // Text event
|
||||
{ 0xff, 0x02, 0x00 }, // Copyright notice
|
||||
{ 0xff, 0x03, 0x00 }, // Track name
|
||||
{ 0xff, 0x04, 0x00 }, // Instrument name
|
||||
{ 0xff, 0x05, 0x00 }, // Lyric
|
||||
{ 0xff, 0x06, 0x00 }, // Marker
|
||||
{ 0xff, 0x07, 0x00 }, // Cue point
|
||||
{ 0xff, 0x20, 0x01, 0x00 }, // Channel prefix
|
||||
{ 0xff, 0x2f, 0x00 }, // End of track
|
||||
{ 0xff, 0x51, 0x03, 0x01, 0x02, 0x03 }, // Set tempo
|
||||
{ 0xff, 0x54, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05 }, // SMPTE offset
|
||||
{ 0xff, 0x58, 0x04, 0x01, 0x02, 0x03, 0x04 }, // Time signature
|
||||
{ 0xff, 0x59, 0x02, 0x01, 0x02 }, // Key signature
|
||||
{ 0xff, 0x7f, 0x00 }, // Sequencer-specific
|
||||
};
|
||||
|
||||
beginTest ("MidiMessage data constructor works for well-formed meta-events");
|
||||
{
|
||||
const auto status = (uint8) 0x90;
|
||||
|
||||
for (const auto& input : metaEvents)
|
||||
{
|
||||
int bytesUsed = 0;
|
||||
const MidiMessage msg (input.data(), (int) input.size(), bytesUsed, status);
|
||||
|
||||
expect (msg.isMetaEvent());
|
||||
expectEquals (msg.getMetaEventLength(), (int) input.size() - 3);
|
||||
expectEquals (msg.getMetaEventType(), (int) input[1]);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("MidiMessage data constructor works for malformed meta-events");
|
||||
{
|
||||
const auto status = (uint8) 0x90;
|
||||
|
||||
const auto runTest = [&] (const std::vector<uint8>& input)
|
||||
{
|
||||
int bytesUsed = 0;
|
||||
const MidiMessage msg (input.data(), (int) input.size(), bytesUsed, status);
|
||||
|
||||
expect (msg.isMetaEvent());
|
||||
expectEquals (msg.getMetaEventLength(), jmax (0, (int) input.size() - 3));
|
||||
expectEquals (msg.getMetaEventType(), input.size() >= 2 ? input[1] : -1);
|
||||
};
|
||||
|
||||
runTest ({ 0xff });
|
||||
|
||||
for (const auto& input : metaEvents)
|
||||
{
|
||||
auto copy = input;
|
||||
copy[2] = 0x40; // Set the size of the message to more bytes than are present
|
||||
|
||||
runTest (copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static MidiMessageTest midiMessageTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -858,11 +858,43 @@ public:
|
|||
//==============================================================================
|
||||
/** Reads a midi variable-length integer.
|
||||
|
||||
@param data the data to read the number from
|
||||
@param numBytesUsed on return, this will be set to the number of bytes that were read
|
||||
This signature has been deprecated in favour of the safer
|
||||
readVariableLengthValue.
|
||||
|
||||
The `data` argument indicates the data to read the number from,
|
||||
and `numBytesUsed` is used as an out-parameter to indicate the number
|
||||
of bytes that were read.
|
||||
*/
|
||||
static int readVariableLengthVal (const uint8* data,
|
||||
int& numBytesUsed) noexcept;
|
||||
JUCE_DEPRECATED (static int readVariableLengthVal (const uint8* data,
|
||||
int& numBytesUsed) noexcept);
|
||||
|
||||
/** Holds information about a variable-length value which was parsed
|
||||
from a stream of bytes.
|
||||
|
||||
A valid value requires that `bytesUsed` is greater than 0.
|
||||
If `bytesUsed <= 0` this object should be considered invalid.
|
||||
*/
|
||||
struct VariableLengthValue
|
||||
{
|
||||
VariableLengthValue() = default;
|
||||
|
||||
VariableLengthValue (int valueIn, int bytesUsedIn)
|
||||
: value (valueIn), bytesUsed (bytesUsedIn) {}
|
||||
|
||||
int value = 0;
|
||||
int bytesUsed = 0;
|
||||
};
|
||||
|
||||
/** Reads a midi variable-length integer, with protection against buffer overflow.
|
||||
|
||||
@param data the data to read the number from
|
||||
@param maxBytesToUse the number of bytes in the region following `data`
|
||||
@returns a struct containing the parsed value, and the number
|
||||
of bytes that were read. If parsing fails, both the
|
||||
`value` and `bytesUsed` fields will be set to 0.
|
||||
*/
|
||||
static VariableLengthValue readVariableLengthValue (const uint8* data,
|
||||
int maxBytesToUse) noexcept;
|
||||
|
||||
/** Based on the first byte of a short midi message, this uses a lookup table
|
||||
to return the message length (either 1, 2, or 3 bytes).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue