diff --git a/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp b/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp index 7d64b8fbc0..cde16912f2 100644 --- a/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp +++ b/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp @@ -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) diff --git a/modules/juce_audio_basics/midi/juce_MidiFile.cpp b/modules/juce_audio_basics/midi/juce_MidiFile.cpp index 51a88ab11b..a4cd279604 100644 --- a/modules/juce_audio_basics/midi/juce_MidiFile.cpp +++ b/modules/juce_audio_basics/midi/juce_MidiFile.cpp @@ -46,23 +46,77 @@ namespace MidiFileHelpers } } - static bool parseMidiHeader (const uint8* &data, short& timeFormat, short& fileType, short& numberOfTracks) noexcept + template + 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 + struct ReadTrait; + + template <> + struct ReadTrait { static constexpr auto read = ByteOrder::bigEndianInt; }; + + template <> + struct ReadTrait { static constexpr auto read = ByteOrder::bigEndianShort; }; + + template + Optional tryRead (const uint8*& data, size_t& remaining) + { + using Trait = ReadTrait; + constexpr auto size = sizeof (Integral); + + if (remaining < size) + return {}; + + const Optional 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 parseMidiHeader (const uint8* const initialData, + const size_t maxSize) + { + auto* data = initialData; + auto remaining = maxSize; + + auto ch = tryRead (data, remaining); + + if (! ch.valid) + return {}; + + if (ch.value != ByteOrder::bigEndianInt ("MThd")) { - bool ok = false; + auto ok = false; - if (ch == ByteOrder::bigEndianInt ("RIFF")) + if (ch.value == ByteOrder::bigEndianInt ("RIFF")) { for (int i = 0; i < 8; ++i) { - ch = ByteOrder::bigEndianInt (data); - data += 4; + ch = tryRead (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 (data, remaining); - return true; + if (! bytesRemaining.valid || bytesRemaining.value > remaining) + return {}; + + const auto optFileType = tryRead (data, remaining); + + if (! optFileType.valid || 2 < optFileType.value) + return {}; + + const auto optNumTracks = tryRead (data, remaining); + + if (! optNumTracks.valid || (optFileType.value == 0 && optNumTracks.value != 1)) + return {}; + + const auto optTimeFormat = tryRead (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 (data.getData()); + + 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) { - auto size = data.getSize(); - auto d = static_cast (data.getData()); - short fileType, expectedTracks; + const auto optChunkType = MidiFileHelpers::tryRead (d, size); - if (size > 16 && MidiFileHelpers::parseMidiHeader (d, timeFormat, fileType, expectedTracks)) - { - size -= (size_t) (d - static_cast (data.getData())); - int track = 0; + if (! optChunkType.valid) + return false; - for (;;) - { - auto chunkType = (int) ByteOrder::bigEndianInt (d); - d += 4; - auto chunkSize = (int) ByteOrder::bigEndianInt (d); - d += 4; + const auto optChunkSize = MidiFileHelpers::tryRead (d, size); - if (chunkSize <= 0 || (size_t) chunkSize > size) - break; + if (! optChunkSize.valid) + return false; - if (chunkType == (int) ByteOrder::bigEndianInt ("MTrk")) - readNextTrack (d, chunkSize, createMatchingNoteOffs); + const auto chunkSize = optChunkSize.value; - if (++track >= expectedTracks) - break; + if (size < chunkSize) + return false; - size -= (size_t) chunkSize + 8; - d += chunkSize; - } + if (optChunkType.value == ByteOrder::bigEndianInt ("MTrk")) + readNextTrack (d, (int) chunkSize, createMatchingNoteOffs); - return true; - } + size -= chunkSize; + d += chunkSize; } - 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 + static MidiMessageSequence parseSequence (Fn&& fn) + { + MemoryOutputStream os; + fn (os); + + return MidiFileHelpers::readTrack (reinterpret_cast (os.getData()), + (int) os.getDataSize()); + } + + template + static MidiFileHelpers::Optional parseHeader (Fn&& fn) + { + MemoryOutputStream os; + fn (os); + + return MidiFileHelpers::parseMidiHeader (reinterpret_cast (os.getData()), + os.getDataSize()); + } + + template + static MidiFileHelpers::Optional 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& bytes) + { + for (const auto& byte : bytes) + os.writeByte ((char) byte); + } +}; + +static MidiFileTest midiFileTests; + +#endif + } // namespace juce diff --git a/modules/juce_audio_basics/midi/juce_MidiMessage.cpp b/modules/juce_audio_basics/midi/juce_MidiMessage.cpp index cd4675d93e..c9513ee6ff 100644 --- a/modules/juce_audio_basics/midi/juce_MidiMessage.cpp +++ b/modules/juce_audio_basics/midi/juce_MidiMessage.cpp @@ -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 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 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 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& 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 diff --git a/modules/juce_audio_basics/midi/juce_MidiMessage.h b/modules/juce_audio_basics/midi/juce_MidiMessage.h index b72f9ad5c0..d3370accf5 100644 --- a/modules/juce_audio_basics/midi/juce_MidiMessage.h +++ b/modules/juce_audio_basics/midi/juce_MidiMessage.h @@ -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).