From 61c507caa2849156f2b856c6e1cf5a598758a540 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Thu, 27 Aug 2026 19:53:13 +0000 Subject: [PATCH 01/28] Replace Haali parser with libmatroska --- meson.build | 3 + src/matroska_demux.cpp | 184 ++++++++++++++++++ src/meson.build | 2 +- tests/matroska/behavior.cpp | 25 +++ tests/matroska/check-fixtures.py | 12 ++ tests/matroska/fixtures/attachment.txt | 1 + .../fixtures/audio-only-opus.behavior | 2 + tests/matroska/fixtures/audio-only-opus.mka | Bin 0 -> 8390 bytes .../fixtures/compressed-zlib.behavior | 3 + tests/matroska/fixtures/compressed-zlib.mkv | Bin 0 -> 5805 bytes tests/matroska/fixtures/generate.py | 11 ++ tests/matroska/fixtures/malformed.behavior | 1 + tests/matroska/fixtures/malformed.mkv | 1 + .../fixtures/subtitle-attachment.behavior | 11 ++ .../matroska/fixtures/subtitle-attachment.mkv | Bin 0 -> 1759 bytes tests/matroska/fixtures/subtitle.ass | 9 + tests/matroska/fixtures/utf8.srt | 3 + tests/matroska/fixtures/video-only.behavior | 2 + tests/matroska/fixtures/video-only.mkv | Bin 0 -> 662 bytes tests/meson.build | 8 + 20 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/matroska_demux.cpp create mode 100644 tests/matroska/behavior.cpp create mode 100755 tests/matroska/check-fixtures.py create mode 100644 tests/matroska/fixtures/attachment.txt create mode 100644 tests/matroska/fixtures/audio-only-opus.behavior create mode 100644 tests/matroska/fixtures/audio-only-opus.mka create mode 100644 tests/matroska/fixtures/compressed-zlib.behavior create mode 100644 tests/matroska/fixtures/compressed-zlib.mkv create mode 100755 tests/matroska/fixtures/generate.py create mode 100644 tests/matroska/fixtures/malformed.behavior create mode 100644 tests/matroska/fixtures/malformed.mkv create mode 100644 tests/matroska/fixtures/subtitle-attachment.behavior create mode 100644 tests/matroska/fixtures/subtitle-attachment.mkv create mode 100644 tests/matroska/fixtures/subtitle.ass create mode 100644 tests/matroska/fixtures/utf8.srt create mode 100644 tests/matroska/fixtures/video-only.behavior create mode 100644 tests/matroska/fixtures/video-only.mkv diff --git a/meson.build b/meson.build index 29b7fd390c..08fbf10d71 100644 --- a/meson.build +++ b/meson.build @@ -179,6 +179,9 @@ endif zlib_dep = dependency('zlib') deps += zlib_dep +libebml = dependency('libebml', version: '>=1.4.0') +libmatroska = dependency('libmatroska', version: '>=1.6.0') +deps += [libebml, libmatroska] use_bundled_wx = force_all_fallbacks or force_fallback_for.contains('wxWidgets') if not use_bundled_wx diff --git a/src/matroska_demux.cpp b/src/matroska_demux.cpp new file mode 100644 index 0000000000..ab63ecf6cf --- /dev/null +++ b/src/matroska_demux.cpp @@ -0,0 +1,184 @@ +// Copyright (c) 2026 Aegisub contributors +// Aegisub-owned Matroska demux adapter. The public ABI intentionally matches +// MatroskaParser.h while EBML parsing and Matroska semantics come from the +// maintained upstream libraries. +#include +#include "MatroskaParser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace libebml; +using namespace libmatroska; + +namespace { +class InputCallback final : public IOCallback { + InputStream *in; + uint64_t pos = 0; +public: + explicit InputCallback(InputStream *input, uint64_t base) : in(input), pos(base) {} + uint32 read(void *buffer, size_t size) override { + if (!in || !in->read) throw std::runtime_error("Matroska input has no read callback"); + if (size > INT_MAX) size = INT_MAX; + int got = in->read(in, pos, buffer, static_cast(size)); + if (got < 0) throw std::runtime_error(in->geterror ? in->geterror(in) : "input error"); + if (static_cast(got) > size) throw std::runtime_error("Matroska input returned too much data"); + pos += got; return got; + } + void setFilePointer(int64 offset, seek_mode mode) override { + int64 next = offset; + if (mode == seek_current) next += pos; + else if (mode == seek_end) next += in->getfilesize(in); + if (next < 0) throw std::runtime_error("invalid negative seek"); + pos = static_cast(next); + } + size_t write(void const *, size_t) override { throw std::runtime_error("read-only input"); } + uint64 getFilePointer() override { return pos; } + void close() override {} +}; + +template T *child(EbmlMaster &m) { return static_cast(m.FindFirstElt(EBML_INFO(T), false)); } +template uint64_t uint_value(EbmlMaster &m, uint64_t fallback = 0) { + if (auto *v = child(m)) return static_cast(*v); + return fallback; +} +template std::string string_value(EbmlMaster &m, std::string fallback = {}) { + if (auto *v = child(m)) return v->GetValue(); + return fallback; +} +template std::string unicode_value(EbmlMaster &m) { + if (auto *v = child(m)) return v->GetValue().GetUTF8(); + return {}; +} + +struct TrackRecord { TrackInfo info{}; std::string name, codec, language = "und"; std::vector priv, comp_private; KaxTrackEntry *entry{}; }; +struct Frame { unsigned track{}; uint64_t start{}, end{}, pos{}; unsigned size{}, flags{}; }; +struct AttachmentRecord { Attachment info{}; std::string name, description, mime; }; +} + +struct MatroskaFile { + InputStream *input{}; std::unique_ptr io; std::unique_ptr stream; + std::unique_ptr segment; std::vector> top; + SegmentInfo info{}; std::vector tracks; std::vector frames; + std::vector attachments; std::vector attachment_view; + std::unordered_map track_by_number; size_t cursor{}; unsigned mask{}; + uint64_t segment_top{}; std::string error; +}; + +namespace { +void parse_track(MatroskaFile &f, KaxTrackEntry &entry) { + TrackRecord r; r.entry = &entry; + r.info.Number = static_cast(uint_value(entry)); + r.info.UID = uint_value(entry); + r.info.Type = static_cast(uint_value(entry)); + r.info.Enabled = uint_value(entry, 1); r.info.Default = uint_value(entry, 1); + r.info.Lacing = uint_value(entry, 1); r.info.MinCache = uint_value(entry); + r.info.MaxCache = uint_value(entry); r.info.DefaultDuration = uint_value(entry); + r.info.TimecodeScale = child(entry) ? static_cast(*child(entry)) : 1.0; + r.name = unicode_value(entry); r.codec = string_value(entry); r.language = string_value(entry, "eng"); + if (auto *p = child(entry)) r.priv.assign(p->GetBuffer(), p->GetBuffer() + p->GetSize()); + if (auto *encodings = child(entry)) if (auto *encoding = child(*encodings)) { + if (uint_value(*encoding, 0) != 0) throw std::runtime_error("encrypted tracks are unsupported"); + if (auto *compression = child(*encoding)) { + r.info.CompEnabled = 1; r.info.CompMethod = static_cast(uint_value(*compression)); + if (auto *settings = child(*compression)) r.comp_private.assign(settings->GetBuffer(), settings->GetBuffer()+settings->GetSize()); + } + } + f.track_by_number[r.info.Number] = f.tracks.size(); f.tracks.push_back(std::move(r)); +} + +void parse_master(MatroskaFile &f, EbmlMaster &master, KaxCluster *cluster = nullptr) { + for (auto *element : master.GetElementList()) { + if (auto *entry = dynamic_cast(element)) parse_track(f, *entry); + else if (auto *attached = dynamic_cast(element)) { + AttachmentRecord a; a.name=unicode_value(*attached); a.description=unicode_value(*attached); a.mime=string_value(*attached); + a.info.UID=uint_value(*attached); if(auto*d=child(*attached)){a.info.Position=d->GetElementPosition()+EBML_ID_LENGTH(static_cast(*d))+d->GetSizeLength();a.info.Length=d->GetSize();} f.attachments.push_back(std::move(a)); + } + else if (auto *block = dynamic_cast(element)) { + auto found=f.track_by_number.find(block->TrackNum()); if(found==f.track_by_number.end()) continue; + unsigned ti=found->second; uint64_t duration=f.tracks[ti].info.DefaultDuration; + if (auto *group=dynamic_cast(&master)) if(auto*d=child(*group)) duration=static_cast(*d)*f.info.TimecodeScale; + unsigned count=block->NumberFrames(); if(count && duration && dynamic_cast(&master)) duration/=count; + int64_t cluster_ticks=cluster?static_cast(uint_value(*cluster)):0; + int64_t ticks=cluster_ticks+block->GetRelativeTimestamp(); + uint64_t base=ticks<0?0:static_cast(ticks)*f.info.TimecodeScale; + bool key=true;if(auto*s=dynamic_cast(block))key=s->IsKeyframe();else if(auto*g=dynamic_cast(&master))key=child(*g)==nullptr; + for(unsigned n=0;nGetDataPosition(n);fr.size=static_cast(block->GetFrameSize(n));fr.flags=key?FRAME_KF:0;f.frames.push_back(fr);} + } + if (auto *nested=dynamic_cast(element)) parse_master(f,*nested,cluster ? cluster : dynamic_cast(nested)); + } +} + +void finalize(MatroskaFile &f) { + for (auto &r:f.tracks) { r.info.Name=r.name.empty()?nullptr:r.name.data(); r.info.CodecID=r.codec.data(); std::memset(r.info.Language,0,4); std::memcpy(r.info.Language,r.language.data(),std::min(3,r.language.size())); r.info.CodecPrivate=r.priv.empty()?nullptr:r.priv.data();r.info.CodecPrivateSize=r.priv.size();r.info.CompMethodPrivate=r.comp_private.empty()?nullptr:r.comp_private.data();r.info.CompMethodPrivateSize=r.comp_private.size(); } + for(auto&r:f.attachments){r.info.Name=r.name.data();r.info.Description=r.description.empty()?nullptr:r.description.data();r.info.MimeType=r.mime.data();f.attachment_view.push_back(r.info);} + std::stable_sort(f.frames.begin(),f.frames.end(),[](auto&a,auto&b){return a.startread||!input->getfilesize)throw std::runtime_error("invalid Matroska input stream"); + auto input_size=input->getfilesize(input);if(input_size<0||base>static_cast(input_size))throw std::runtime_error("invalid Matroska input size"); + auto f=std::make_unique();f->input=input;f->io=std::make_unique(input,base);f->stream=std::make_unique(*f->io); + int upper=0;std::unique_ptr head(f->stream->FindNextID(EBML_INFO(EbmlHead),std::numeric_limits::max()));if(!head)throw std::runtime_error("EBML header not found");head->SkipData(*f->stream,EBML_CONTEXT(head.get())); + std::unique_ptr e(f->stream->FindNextID(EBML_INFO(KaxSegment),std::numeric_limits::max()));if(!e)throw std::runtime_error("Matroska segment not found");f->segment_top=e->GetElementPosition(); + if (std::strcmp(EBML_NAME(e.get()), "Segment")) throw std::runtime_error(std::string("expected Segment, got ")+EBML_NAME(e.get())); + auto*segment=static_cast(e.get()); f->segment=std::move(e); f->info.TimecodeScale=1000000; + EbmlElement *next=f->stream->FindNextElement(EBML_CONTEXT(segment),upper,std::numeric_limits::max(),true); + while(next&&upper<=0){std::unique_ptr current(next);next=nullptr; + bool subtitle_frames=std::any_of(f->tracks.begin(),f->tracks.end(),[](auto const&t){return t.info.Type==TT_SUB;}); + if (dynamic_cast(current.get()) && !subtitle_frames) break; + bool wanted=dynamic_cast(current.get())||dynamic_cast(current.get())||(dynamic_cast(current.get())&&subtitle_frames)||dynamic_cast(current.get()); + if(wanted){auto*master=dynamic_cast(current.get());master->Read(*f->stream,EBML_CONTEXT(current.get()),upper,next,true,SCOPE_ALL_DATA); + if(auto*info=dynamic_cast(master)){f->info.TimecodeScale=uint_value(*info,1000000);if(auto*d=child(*info))f->info.Duration=static_cast(static_cast(*d)*f->info.TimecodeScale);} + parse_master(*f,*master,dynamic_cast(master));f->top.push_back(std::move(current)); + }else current->SkipData(*f->stream,EBML_CONTEXT(segment)); + if(!next)next=f->stream->FindNextElement(EBML_CONTEXT(segment),upper,std::numeric_limits::max(),true); + } + finalize(*f);return f.release(); + } catch(std::exception const&e){if(msg&&n){std::strncpy(msg,e.what(),n-1);msg[n-1]=0;}return nullptr;} + catch(...){if(msg&&n){std::strncpy(msg,"unknown Matroska parsing error",n-1);msg[n-1]=0;}return nullptr;}} +void mkv_Close(MatroskaFile*f){delete f;} const char*mkv_GetLastError(MatroskaFile*f){return f?f->error.c_str():"invalid Matroska handle";} +SegmentInfo*mkv_GetFileInfo(MatroskaFile*f){return &f->info;} unsigned mkv_GetNumTracks(MatroskaFile*f){return f->tracks.size();} TrackInfo*mkv_GetTrackInfo(MatroskaFile*f,unsigned i){return itracks.size()?&f->tracks[i].info:nullptr;} +void mkv_GetAttachments(MatroskaFile*f,Attachment**a,unsigned*n){*a=f->attachment_view.empty()?nullptr:f->attachment_view.data();*n=f->attachment_view.size();} +void mkv_GetChapters(MatroskaFile*,Chapter**c,unsigned*n){*c=nullptr;*n=0;} void mkv_GetTags(MatroskaFile*,Tag**t,unsigned*n){*t=nullptr;*n=0;} +uint64_t mkv_GetSegmentTop(MatroskaFile*f){return f->segment_top;} void mkv_SetTrackMask(MatroskaFile*f,unsigned m){f->mask=m;f->cursor=0;} +int mkv_ReadFrame(MatroskaFile*f,unsigned mask,unsigned*track,uint64_t*start,uint64_t*end,uint64_t*pos,unsigned*size,unsigned*flags){while(f->cursorframes.size()){auto&r=f->frames[f->cursor++];if(((mask|f->mask)>>r.track)&1)continue;*track=r.track;*start=r.start;*end=r.end;*pos=r.pos;*size=r.size;*flags=r.flags;return 0;}return -1;} +void mkv_Seek(MatroskaFile*f,uint64_t t,unsigned){f->cursor=std::lower_bound(f->frames.begin(),f->frames.end(),t,[](auto&a,uint64_t b){return a.startframes.begin();} +void mkv_SkipToKeyframe(MatroskaFile*){} uint64_t mkv_GetLowestQTimecode(MatroskaFile*f){return f->cursorframes.size()?f->frames[f->cursor].start:0;} int mkv_TruncFloat(MKFLOAT v){return static_cast(v);} +} + +struct CompressedStream { MatroskaFile*f;unsigned track;uint64_t pos{};unsigned size{};std::vector data;size_t cursor{};std::string error; }; +extern "C" { +CompressedStream*cs_Create(MatroskaFile*f,unsigned t,char*msg,unsigned n){if(!f||t>=f->tracks.size()){if(msg&&n)std::strncpy(msg,"invalid track",n);return nullptr;}return new CompressedStream{f,t,0,0,{},{},{}};} +void cs_Destroy(CompressedStream*c){delete c;} void cs_NextFrame(CompressedStream*c,uint64_t p,unsigned n){c->pos=p;c->size=n;c->data.clear();c->cursor=0;} +int cs_ReadData(CompressedStream*c,char*out,unsigned cap){try{if(c->data.empty()&&c->size){std::vectorraw(c->size);if(c->f->input->read(c->f->input,c->pos,raw.data(),c->size)!=(int)c->size)throw std::runtime_error("short frame read");auto&r=c->f->tracks[c->track];if(!r.info.CompEnabled)c->data=std::move(raw);else if(r.info.CompMethod==COMP_PREPEND){c->data=r.comp_private;c->data.insert(c->data.end(),raw.begin(),raw.end());}else if(r.info.CompMethod==COMP_ZLIB){z_stream z{};if(inflateInit(&z)!=Z_OK)throw std::runtime_error("zlib initialization failed");z.next_in=raw.data();z.avail_in=raw.size();unsigned char buf[4096];int rc;do{z.next_out=buf;z.avail_out=sizeof buf;rc=inflate(&z,Z_NO_FLUSH);c->data.insert(c->data.end(),buf,buf+sizeof(buf)-z.avail_out);}while(rc==Z_OK);inflateEnd(&z);if(rc!=Z_STREAM_END)throw std::runtime_error("invalid zlib frame");}else throw std::runtime_error("unsupported Matroska compression method");}size_t take=std::min(cap,c->data.size()-c->cursor);std::memcpy(out,c->data.data()+c->cursor,take);c->cursor+=take;return take;}catch(std::exception const&e){c->error=e.what();return-1;}} +const char*cs_GetLastError(CompressedStream*c){return c->error.c_str();} +} diff --git a/src/meson.build b/src/meson.build index 876d551ffb..efd96981a9 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,7 +1,7 @@ subdir('libresrc') aegisub_src = files( - 'MatroskaParser.c', + 'matroska_demux.cpp', 'aegisublocale.cpp', 'ass_attachment.cpp', 'ass_dialogue.cpp', diff --git a/tests/matroska/behavior.cpp b/tests/matroska/behavior.cpp new file mode 100644 index 0000000000..9750dda169 --- /dev/null +++ b/tests/matroska/behavior.cpp @@ -0,0 +1,25 @@ +#include +#include "MatroskaParser.h" +#include +#include +#include +#include +#include +#include + +struct FileInput final : InputStream { + FILE *file{}; std::string error; uint64_t size{}; + explicit FileInput(char const *name) { + file = std::fopen(name, "rb"); if (!file) return; + std::fseek(file, 0, SEEK_END); size = std::ftell(file); std::fseek(file, 0, SEEK_SET); + read=[](InputStream*s,uint64_t p,void*b,int n){auto&i=*static_cast(s);if(p>=i.size)return 0;n=std::min(n,i.size-p);if(std::fseek(i.file,p,SEEK_SET))return-1;return(int)std::fread(b,1,n,i.file);}; + scan=[](InputStream*s,uint64_t p,unsigned sig)->int64_t{unsigned v=0;unsigned char c;for(;static_cast(s)->read(s,p,&c,1)==1;++p){v=(v<<8)|c;if(v==sig)return p-3;}return-1;}; + getcachesize=[](InputStream*){return 1U<<20;}; geterror=[](InputStream*s){return static_cast(s)->error.c_str();}; + memalloc=[](InputStream*,size_t n){return std::malloc(n);}; memrealloc=[](InputStream*,void*p,size_t n){return std::realloc(p,n);}; memfree=[](InputStream*,void*p){std::free(p);}; + progress=[](InputStream*,uint64_t,uint64_t){return 1;}; getfilesize=[](InputStream*s){return(int64_t)static_cast(s)->size;}; + } + ~FileInput(){if(file)std::fclose(file);} +}; +static uint64_t hash(FileInput&i,uint64_t p,unsigned n){uint64_t h=1469598103934665603ULL;std::vectord(n);if(n&&i.read(&i,p,d.data(),n)!=(int)n)return 0;for(auto c:d){h^=c;h*=1099511628211ULL;}return h;} +static uint64_t decoded_hash(MatroskaFile*f,unsigned t,uint64_t p,unsigned n){char error[256]{};auto*c=cs_Create(f,t,error,sizeof error);if(!c)return 0;cs_NextFrame(c,p,n);uint64_t h=1469598103934665603ULL;char data[256];for(;;){int got=cs_ReadData(c,data,sizeof data);if(got<=0)break;for(int i=0;i(data[i]);h*=1099511628211ULL;}}cs_Destroy(c);return h;} +int main(int ac,char**av){if(ac!=2)return 2;FileInput in(av[1]);char e[2048]{};auto*f=mkv_Open(&in,e,sizeof e);if(!f){std::cout<<"error\t"<TimecodeScale<<'\t'<Duration<<'\n';for(unsigned i=0;iNumber)<<'\t'<Type)<<'\t'<<(t->CodecID?t->CodecID:"")<<'\t'<Language<<'\t'<<(t->Name?t->Name:"")<<'\t'<CodecPrivateSize<<'\t'<CompEnabled<<'\t'<CompMethod<<'\n';}Attachment*at;unsigned count;mkv_GetAttachments(f,&at,&count);for(unsigned i=0;iCompEnabled)std::cout<<'\t'<3z?@4c>dU+ex{*Z0M})?v&m zCR0C-2Gn2O{+DQc`W)I%`qA+eLV*88sy9KOu^oz~?UKMok%54n4{~k4c}_r+d1Df` zG)By%*aKS@Edu%>58+teJ2Lu*l)^UdNSW0i@&N7$d0Wcv{bX@+XrZK^iJh8045DCTLswT>Mq>I7AJh>~0`z{PCcGPd9T!?({G@T`o?(#nY?ub5w#P>ztGyI}0*C3e zU2-)y>4CoPad9ZTrIjt#u7pm6=G!&K@U>L~hy~Xz{Q{}RB!W8-s1A&3{LWXdCU$sX2BdsXFN3_KEFmK`!;1S>v_y+-5gW5oojdwHv6Ei~t0HOu9ly9?2%F9aDs_%7` z%+ypAN>zq_*wpOb9cyj&;W)4_BVako+zD)Y@U8GlfD4*^ig=(vo%YU(Q_6Gpllx48HPN zrj|cACn0@Uc(iJ4umHq$O8d0>&VbF(O#+@c#?ZY{b!yk!J;)ODErY=M`7Zy{;_Z=1 z9{g0f#-0gN#l%xamSYK-)<_X!-ERD{i~p)qIk`f}Pq2_F`%@b$FU2K;htgNq>YD>Ike2vr9BHp;oVwTx>5F}YJM|4*CIf8 zWqCFrizJ}>+U#7vcZaU$E1id1FVs8Pq(YL@Me}dxFDK7iCxdPdjkr_Sk&!a)LU)i8j(}efCya=bP#R{=v75y;;S3q*-ef=6y;B zzw=G>nT(%sbyAWA*&qEG6@DIVDbBv? zOz!kX<|lEqR3B@toYGnVZIjiBNpGj$zuBUz+%tVCEBH;Jy=(x-w0Xefi12EzB?qKN z+KXbGv6IcMJJz(nXIcYtu0gw6d@($cyu4L!>3+pH8GucU-bhcC6qPm@nfsMJ#tMne z;}}ve)b`Rlh9*JxE%dv`@0rrpn)sKzeMK5cI!Tnz>rbtJZgMyk+4}w&k!&bBdtm(+ zvVH0`>WPi#8eHGa$yb&et|zfj5$tDW*4n+<0L$d^DV1j&A_dwzPhqzCntf38?{pv1 zzKryd336Cl2&dHEE6QKZ^^4xJE*~~lbkHoQt;$@#Lw+703=At@{OxfaK!L9_qTY>5 zofAjk5WO8!;vI)D(=7jQ>h!q2Ud;I!u}beQ2V2M8qVl72kxqMy`7VW=s|M%P@5`@8 zNlP2KNtT|HJ@|BCV{^=wJW&Q$jl(M=WRe~=&asR>m7H_1O8EF_cpK{l0o8h~(%Q$D zJ(X&y9k&cS#tZ359-i=18fA;(4?+VU3d*_ihjirtHPpR>BJ(PQ0A%6H*aiiiiAQ8h zY|DrkGssiwQUO=ydC~8#(Wa~fJz(Q134Z2VN`)c+x@5C$U9h3SLbf0$QM$osP)P)J zKshm0uis-hMs+>T8|)@^r@p8P!aaFo%L}BX+23qvRtB}}_K+SN8a0(Cd)-PM ztbImjt(BFCh)bcK&D=tpO_GrPx_*)O!eNsK)GZjqZhs%Qm+|nfxu%8-m!>^{)8I>2#-kgir;MEl*WP zu&`3tTz+l8#^NR4>98p?FB$PvonK9SI>wIdV&abK@nzhX%?&>{JZ({aH=MF=XEFHc z77;g2UfMrpq&S~hxoc4IDLeIlSxc7>U~+q!zKrCzxf)U$Jvg4`;yv=%t5^W{$}>Yl zJ_PFNc31aW_{ob=ILB*q`&}tRv9Zo8)cr?O6+nz$4S|H>?k}4Ua*0F3MJE|w$yJo} zfy2Lli@mIx3M3Kq>&v$+Hc%=mf?_Q-jw)nUJgq;`t&LB4(I({u#( z7hNmvKB19oVD*#sHw!q}ji#4X=*SKhQ@@EO1BeEdvwoNi;i6UMB0UdAD9Wa{w|56P zrr9llDplpLzu9Zw1N8%0$&f5=hB^SlT!O*{U;B-$yxUkADeN-Pu8*H@#FIzTxYN6l zi=k)DHzm=ANoq$Oz?h#+mvV^ieH_EE%yNw**wYm;=6v`f^to}HIPrN6_k3=Z_L#4o z8y>87<*8Q6l9}caRH^uhDUGEV=)K#9#9wB@zWZ!TbKzW=1Z?1+KC3A7o}UsbfJXVD zx|2ez^l@@|4OPM65kdtz4+8ZRZRAtz){D91WG(*I8qqL+?()O)n@>C2i?l72*Tw#5 zdojhox_M~dKn&;e%9i1r!4+^gQ7xkLtNrT-zkhtQLeQC!873BVy}jXI!s&r_R}F1G zwD5{(y>a+pvcEK9(d?ragzab;q^o%6l=Y6$UL={O{be-ibXUrK=kG=v{hvlF6*(^L zr+?>3AX`TC62W+AYqhX#l+)At&~Y}Qy8*_jaJHO&TqHNj_i1@*oduh7en%1J=S+n* zJ&BZ1c9S0-uu%+2I3VjV1LEp8BfMl;KQw5{hf4AB!d0O*N zt9ibo9QjS;sAqLNbkWl`q&0Bn_GpG#;rm2S{HTb4voWba0G%zDpdB-#G`^MwUl z;uAN)5LIH+$?%>b@Jd^uN89^{3aCuHNO|xb>&k@=+GIP$-%*5U^IgfK4%q zS1fBkRL`FvSiGSG4i5eG{gl}h$mjLlW)8S?b@qJksM8;38}VQCEd^u_-3velspU{Kea)LE&26~y zKPNSQ68*|{JI=1ksw1R(IG&)^#dUT8GVqh)F=_{78B2J`z6I?k=Sd4;WthW4s@G2j z7SL76lR42KiJjN%)W)k-G(g?u?Z@)PXi@HG8ffQH8x={VHWOPiJiBNz#I9JJ4a(pw z@$bfz%r3w%85j!#OEks?%}6sN1QFHXP`CyZ4ukLX$``l$QYsatL2OCC=uL{&d2})VwHlv zvrG2)?(PX*-6HC_GA(#blK?Y>D;BaY8^mpfaAksbgSeuK5Ux5K5dF{)Hw&e$b3tOc z4Q)vd%>gD2Z(P&{bLft`Z8QW%Ltrq3fV}gz(v8oX7{4r7y>MwG&_xxuxX+&UIp^=W zHK@X@_T^^F?6@_vmQR>7qpU)eq5l$3#Ox;a3!rh&X}hOv2I|}12OL-Z*d+4$LnF&3 zpcH5f)X}`S*wHxoKzA~t>`Fh;?Yg+xDInOTy~)xn{lo&-#T_o8L5?Aw{(dg)lMD>3 zKnPfD!a(|o>8^_>_=Wm9xdz4iyT!W&Ir_Q+bvGJ~^PLUxXi6$eH^b`AC7f+85 z4gne)9OCI593LL!8RF^Z9`6_s;L=V+0iAwgtLx%9*i3M73~`MQ4RLm9pKN4cWTtCi tp=)HWU}#}wVr*pswD~a5waEHH9NmGAS_|~C0?eNZ2scA~gCqd<1ORdb5la97 literal 0 HcmV?d00001 diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py new file mode 100755 index 0000000000..a273ec4ecc --- /dev/null +++ b/tests/matroska/fixtures/generate.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""Regenerate the small, deterministic project-owned parity fixture.""" +import pathlib,subprocess +r=pathlib.Path(__file__).parent +subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-fflags','+bitexact','-f','lavfi','-i','color=size=16x16:rate=1:duration=3','-i',r/'subtitle.ass','-i',r/'utf8.srt','-map','0:v','-map','1','-map','2','-c:v','ffv1','-c:s:0','ass','-c:s:1','srt','-metadata:s:s:0','language=eng','-metadata:s:s:0','title=ASS track','-metadata:s:s:1','language=jpn','-attach',r/'attachment.txt','-metadata:s:t','mimetype=text/plain','-metadata:s:t','filename=attachment.txt','-map_metadata','-1',r/'subtitle-attachment.mkv'],check=True) +subprocess.run(['mkvmerge','-o',r/'compressed-zlib.mkv','--compression','0:zlib',r/'utf8.srt'],check=True) +opus=r/'audio-only.opus' +subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-fflags','+bitexact','-f','lavfi','-i','sine=frequency=440:duration=0.25','-c:a','libopus',opus],check=True) +subprocess.run(['mkvmerge','-o',r/'audio-only-opus.mka',opus],check=True) +opus.unlink() +subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-fflags','+bitexact','-f','lavfi','-i','color=size=16x16:rate=1:duration=1','-map_metadata','-1','-c:v','ffv1',r/'video-only.mkv'],check=True) diff --git a/tests/matroska/fixtures/malformed.behavior b/tests/matroska/fixtures/malformed.behavior new file mode 100644 index 0000000000..656f04c5d3 --- /dev/null +++ b/tests/matroska/fixtures/malformed.behavior @@ -0,0 +1 @@ +error Matroska segment not found diff --git a/tests/matroska/fixtures/malformed.mkv b/tests/matroska/fixtures/malformed.mkv new file mode 100644 index 0000000000..dabc35d2ef --- /dev/null +++ b/tests/matroska/fixtures/malformed.mkv @@ -0,0 +1 @@ +This is deliberately not an EBML document. diff --git a/tests/matroska/fixtures/subtitle-attachment.behavior b/tests/matroska/fixtures/subtitle-attachment.behavior new file mode 100644 index 0000000000..6d19c933c6 --- /dev/null +++ b/tests/matroska/fixtures/subtitle-attachment.behavior @@ -0,0 +1,11 @@ +segment 1000000 3000000000 +track 0 1 1 V_MS/VFW/FOURCC und 40 0 0 +track 1 2 17 S_TEXT/ASS eng ASS track 480 0 0 +track 2 3 17 S_TEXT/UTF8 jpn 0 0 0 +attachment attachment.txt text/plain 42 6e22e83d2777896d +frame 0 0 1000000000 38 4 fea7a8f5d84334f6 +frame 1 250000000 1500000000 25 4 76874757fddd3870 +frame 2 500000000 2500000000 24 4 da1dfbf2ee283ed2 +frame 0 1000000000 2000000000 17 0 e3e1e9af310e5536 +frame 1 1500000000 2750000000 24 4 8f6874da13b53ae +frame 0 2000000000 3000000000 17 0 ee26690468bfc99 diff --git a/tests/matroska/fixtures/subtitle-attachment.mkv b/tests/matroska/fixtures/subtitle-attachment.mkv new file mode 100644 index 0000000000000000000000000000000000000000..1b542f33bc5c9650bfdfc49e0c73b06c764180ea GIT binary patch literal 1759 zcmbtVeM}o=7=K$}VAaT$xXh2li$-JQQm!z>v8c4Prz9&Fu7yop=Iwetj<(l&cV)0F z8E;#mgSp}6VjMU=ia|a?=Ez)J|0r=TGO{SqWoRTBvV`F_H{D1ei{pH+9}HZ!e|+!W zyXSe{=lQ+A_sR3zJ#D!ZiI|$8#nL6?y>Ls z+7K5?*k`0DOcrUzrSf70uBnCEVY6(t<)VckT&wQN^cbhBDMsXdGFB=$z1wvO#n#|s zi?F(ldV=G%}U#Kq*p|BQG}Jz9u~()uOJJ{lEm*uws%C{LyHY1UbojT z;uys;Ucs@#Q0|xAyuhZ`O^D#K>~%YEtW>7mJcDC)!NH2ED*};4-o>IwBr>#{-I{!F zLvJ6=@B)WpMuBr9SCdynVR5=RmfQ<@)zA*_F689qIl;pU2*^r{94~B12^A@!62~k8 z<8>fVT^yzeGqXHXoB)KdS2?y1R#MEU#vO3kvfajrS z9@0~{uG&(EXsyWb7P^5|^C#0HVrvmlW|F5Wi)Z{{{GCpgK|waQUe=}RHFpxGp&F@u z{7UEb3+J+u^;+u_P`x&_CQHL^BVS9_hT654VxfQF@HV>dA}Ji8WtnDb)a2>qdYOE` zMMm(sJ~zz^gtWhoefO{9x1+C;^@;`PyGR5eqHOF1W5h5E*~~G|qotW$2&#V^_9jkMC|z zmg{7#2mBOU+N&sw;Pn^xC`xDSrn1-->Ha(b?f|v^QUx6^Mgqrax37k752Vj@^>lXx zHS@qO@TdjQH(>VDFJ?Q2<_3;o4DCGA-yX=Eh#Y}wa{z2omJ~&~eeJHIyz!=@Osk=R zj45D1)%{=mova*(N1>S$02Cxbe^ZpD-H7F=`V06a7W!d^FNz) 00:00:02,500 +Unicode: 日本語 café diff --git a/tests/matroska/fixtures/video-only.behavior b/tests/matroska/fixtures/video-only.behavior new file mode 100644 index 0000000000..980f1220df --- /dev/null +++ b/tests/matroska/fixtures/video-only.behavior @@ -0,0 +1,2 @@ +segment 1000000 0 +track 0 1 1 V_MS/VFW/FOURCC und 40 0 0 diff --git a/tests/matroska/fixtures/video-only.mkv b/tests/matroska/fixtures/video-only.mkv new file mode 100644 index 0000000000000000000000000000000000000000..6dd0dae1e66c43960d6205b3b0523a72340e0e21 GIT binary patch literal 662 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@&d2})Oi6;i zvr7)_Z`s6FT;sdDJ9u@AsOQSG;5Cg4Aw02=b=g4Pd*9tXKslk1!|B0mni$(5T$$kA zK*=U1w>J>e7=pouQkMX^=j8sD90`F!t?L_``JEhm8+v>a%hJpY_007Q4UEGbQMkoR zCPd4pmUR8gc@evngLO?rb)idVheN*u*jXU|I855#!l@`}wGQg)=If1&#Ya0ny8|IE42xGa zGB7lJIUa7f^2N(?0mhnwzl%7sw`S!sT-yJy{{R0U47RE@|Nqb5|Nj3EP)eA-zh!pb XGt=Gun;RLncL7u2$0jD84~?7vK Date: Thu, 27 Aug 2026 20:03:51 +0000 Subject: [PATCH 02/28] Refactor libmatroska adapter for readability --- src/matroska_demux.cpp | 566 ++++++++++++++++++++++++++++-------- tests/matroska/behavior.cpp | 123 +++++++- 2 files changed, 549 insertions(+), 140 deletions(-) diff --git a/src/matroska_demux.cpp b/src/matroska_demux.cpp index ab63ecf6cf..803e23b146 100644 --- a/src/matroska_demux.cpp +++ b/src/matroska_demux.cpp @@ -37,148 +37,462 @@ using namespace libmatroska; namespace { class InputCallback final : public IOCallback { - InputStream *in; - uint64_t pos = 0; -public: - explicit InputCallback(InputStream *input, uint64_t base) : in(input), pos(base) {} - uint32 read(void *buffer, size_t size) override { - if (!in || !in->read) throw std::runtime_error("Matroska input has no read callback"); - if (size > INT_MAX) size = INT_MAX; - int got = in->read(in, pos, buffer, static_cast(size)); - if (got < 0) throw std::runtime_error(in->geterror ? in->geterror(in) : "input error"); - if (static_cast(got) > size) throw std::runtime_error("Matroska input returned too much data"); - pos += got; return got; - } - void setFilePointer(int64 offset, seek_mode mode) override { - int64 next = offset; - if (mode == seek_current) next += pos; - else if (mode == seek_end) next += in->getfilesize(in); - if (next < 0) throw std::runtime_error("invalid negative seek"); - pos = static_cast(next); - } - size_t write(void const *, size_t) override { throw std::runtime_error("read-only input"); } - uint64 getFilePointer() override { return pos; } - void close() override {} + InputStream *in; + uint64_t pos = 0; + + public: + explicit InputCallback(InputStream *input, uint64_t base) : in(input), pos(base) {} + uint32 read(void *buffer, size_t size) override { + if (!in || !in->read) + throw std::runtime_error("Matroska input has no read callback"); + if (size > INT_MAX) + size = INT_MAX; + int got = in->read(in, pos, buffer, static_cast(size)); + if (got < 0) + throw std::runtime_error(in->geterror ? in->geterror(in) : "input error"); + if (static_cast(got) > size) + throw std::runtime_error("Matroska input returned too much data"); + pos += got; + return got; + } + void setFilePointer(int64 offset, seek_mode mode) override { + int64 next = offset; + if (mode == seek_current) + next += pos; + else if (mode == seek_end) + next += in->getfilesize(in); + if (next < 0) + throw std::runtime_error("invalid negative seek"); + pos = static_cast(next); + } + size_t write(void const *, size_t) override { + throw std::runtime_error("read-only input"); + } + uint64 getFilePointer() override { + return pos; + } + void close() override {} }; -template T *child(EbmlMaster &m) { return static_cast(m.FindFirstElt(EBML_INFO(T), false)); } -template uint64_t uint_value(EbmlMaster &m, uint64_t fallback = 0) { - if (auto *v = child(m)) return static_cast(*v); - return fallback; +template T *child(EbmlMaster &m) { + return static_cast(m.FindFirstElt(EBML_INFO(T), false)); } -template std::string string_value(EbmlMaster &m, std::string fallback = {}) { - if (auto *v = child(m)) return v->GetValue(); - return fallback; +template uint64_t uint_value(EbmlMaster &m, uint64_t fallback = 0) { + if (auto *v = child(m)) + return static_cast(*v); + return fallback; } -template std::string unicode_value(EbmlMaster &m) { - if (auto *v = child(m)) return v->GetValue().GetUTF8(); - return {}; +template std::string string_value(EbmlMaster &m, std::string fallback = {}) { + if (auto *v = child(m)) + return v->GetValue(); + return fallback; } - -struct TrackRecord { TrackInfo info{}; std::string name, codec, language = "und"; std::vector priv, comp_private; KaxTrackEntry *entry{}; }; -struct Frame { unsigned track{}; uint64_t start{}, end{}, pos{}; unsigned size{}, flags{}; }; -struct AttachmentRecord { Attachment info{}; std::string name, description, mime; }; +template std::string unicode_value(EbmlMaster &m) { + if (auto *v = child(m)) + return v->GetValue().GetUTF8(); + return {}; } +struct TrackRecord { + TrackInfo info{}; + std::string name, codec, language = "und"; + std::vector priv, comp_private; + KaxTrackEntry *entry{}; +}; +struct Frame { + unsigned track{}; + uint64_t start{}, end{}, pos{}; + unsigned size{}, flags{}; +}; +struct AttachmentRecord { + Attachment info{}; + std::string name, description, mime; +}; +} // namespace + struct MatroskaFile { - InputStream *input{}; std::unique_ptr io; std::unique_ptr stream; - std::unique_ptr segment; std::vector> top; - SegmentInfo info{}; std::vector tracks; std::vector frames; - std::vector attachments; std::vector attachment_view; - std::unordered_map track_by_number; size_t cursor{}; unsigned mask{}; - uint64_t segment_top{}; std::string error; + InputStream *input{}; + std::unique_ptr io; + std::unique_ptr stream; + std::unique_ptr segment; + std::vector> top; + SegmentInfo info{}; + std::vector tracks; + std::vector frames; + std::vector attachments; + std::vector attachment_view; + std::unordered_map track_by_number; + size_t cursor{}; + unsigned mask{}; + uint64_t segment_top{}; + std::string error; }; namespace { -void parse_track(MatroskaFile &f, KaxTrackEntry &entry) { - TrackRecord r; r.entry = &entry; - r.info.Number = static_cast(uint_value(entry)); - r.info.UID = uint_value(entry); - r.info.Type = static_cast(uint_value(entry)); - r.info.Enabled = uint_value(entry, 1); r.info.Default = uint_value(entry, 1); - r.info.Lacing = uint_value(entry, 1); r.info.MinCache = uint_value(entry); - r.info.MaxCache = uint_value(entry); r.info.DefaultDuration = uint_value(entry); - r.info.TimecodeScale = child(entry) ? static_cast(*child(entry)) : 1.0; - r.name = unicode_value(entry); r.codec = string_value(entry); r.language = string_value(entry, "eng"); - if (auto *p = child(entry)) r.priv.assign(p->GetBuffer(), p->GetBuffer() + p->GetSize()); - if (auto *encodings = child(entry)) if (auto *encoding = child(*encodings)) { - if (uint_value(*encoding, 0) != 0) throw std::runtime_error("encrypted tracks are unsupported"); - if (auto *compression = child(*encoding)) { - r.info.CompEnabled = 1; r.info.CompMethod = static_cast(uint_value(*compression)); - if (auto *settings = child(*compression)) r.comp_private.assign(settings->GetBuffer(), settings->GetBuffer()+settings->GetSize()); - } - } - f.track_by_number[r.info.Number] = f.tracks.size(); f.tracks.push_back(std::move(r)); -} - -void parse_master(MatroskaFile &f, EbmlMaster &master, KaxCluster *cluster = nullptr) { - for (auto *element : master.GetElementList()) { - if (auto *entry = dynamic_cast(element)) parse_track(f, *entry); - else if (auto *attached = dynamic_cast(element)) { - AttachmentRecord a; a.name=unicode_value(*attached); a.description=unicode_value(*attached); a.mime=string_value(*attached); - a.info.UID=uint_value(*attached); if(auto*d=child(*attached)){a.info.Position=d->GetElementPosition()+EBML_ID_LENGTH(static_cast(*d))+d->GetSizeLength();a.info.Length=d->GetSize();} f.attachments.push_back(std::move(a)); - } - else if (auto *block = dynamic_cast(element)) { - auto found=f.track_by_number.find(block->TrackNum()); if(found==f.track_by_number.end()) continue; - unsigned ti=found->second; uint64_t duration=f.tracks[ti].info.DefaultDuration; - if (auto *group=dynamic_cast(&master)) if(auto*d=child(*group)) duration=static_cast(*d)*f.info.TimecodeScale; - unsigned count=block->NumberFrames(); if(count && duration && dynamic_cast(&master)) duration/=count; - int64_t cluster_ticks=cluster?static_cast(uint_value(*cluster)):0; - int64_t ticks=cluster_ticks+block->GetRelativeTimestamp(); - uint64_t base=ticks<0?0:static_cast(ticks)*f.info.TimecodeScale; - bool key=true;if(auto*s=dynamic_cast(block))key=s->IsKeyframe();else if(auto*g=dynamic_cast(&master))key=child(*g)==nullptr; - for(unsigned n=0;nGetDataPosition(n);fr.size=static_cast(block->GetFrameSize(n));fr.flags=key?FRAME_KF:0;f.frames.push_back(fr);} - } - if (auto *nested=dynamic_cast(element)) parse_master(f,*nested,cluster ? cluster : dynamic_cast(nested)); - } +void parse_track(MatroskaFile &file, KaxTrackEntry &entry) { + TrackRecord track; + track.entry = &entry; + track.info.Number = static_cast(uint_value(entry)); + track.info.UID = uint_value(entry); + track.info.Type = static_cast(uint_value(entry)); + track.info.Enabled = uint_value(entry, 1); + track.info.Default = uint_value(entry, 1); + track.info.Lacing = uint_value(entry, 1); + track.info.MinCache = uint_value(entry); + track.info.MaxCache = uint_value(entry); + track.info.DefaultDuration = uint_value(entry); + track.info.TimecodeScale = + child(entry) ? static_cast(*child(entry)) : 1.0; + track.name = unicode_value(entry); + track.codec = string_value(entry); + track.language = string_value(entry, "eng"); + if (auto *codec_private = child(entry)) + track.priv.assign(codec_private->GetBuffer(), codec_private->GetBuffer() + codec_private->GetSize()); + if (auto *encodings = child(entry)) + if (auto *encoding = child(*encodings)) { + if (uint_value(*encoding, 0) != 0) + throw std::runtime_error("encrypted tracks are unsupported"); + if (auto *compression = child(*encoding)) { + track.info.CompEnabled = 1; + track.info.CompMethod = static_cast(uint_value(*compression)); + if (auto *settings = child(*compression)) + track.comp_private.assign(settings->GetBuffer(), settings->GetBuffer() + settings->GetSize()); + } + } + file.track_by_number[track.info.Number] = file.tracks.size(); + file.tracks.push_back(std::move(track)); } -void finalize(MatroskaFile &f) { - for (auto &r:f.tracks) { r.info.Name=r.name.empty()?nullptr:r.name.data(); r.info.CodecID=r.codec.data(); std::memset(r.info.Language,0,4); std::memcpy(r.info.Language,r.language.data(),std::min(3,r.language.size())); r.info.CodecPrivate=r.priv.empty()?nullptr:r.priv.data();r.info.CodecPrivateSize=r.priv.size();r.info.CompMethodPrivate=r.comp_private.empty()?nullptr:r.comp_private.data();r.info.CompMethodPrivateSize=r.comp_private.size(); } - for(auto&r:f.attachments){r.info.Name=r.name.data();r.info.Description=r.description.empty()?nullptr:r.description.data();r.info.MimeType=r.mime.data();f.attachment_view.push_back(r.info);} - std::stable_sort(f.frames.begin(),f.frames.end(),[](auto&a,auto&b){return a.start(element); + attachment.description = unicode_value(element); + attachment.mime = string_value(element); + attachment.info.UID = uint_value(element); + + if (auto *data = child(element)) { + // File data is not copied. The legacy API exposes its location in the input stream. + attachment.info.Position = + data->GetElementPosition() + EBML_ID_LENGTH(static_cast(*data)) + data->GetSizeLength(); + attachment.info.Length = data->GetSize(); + } + + file.attachments.push_back(std::move(attachment)); +} + +void parse_block(MatroskaFile &file, EbmlMaster &parent, KaxCluster *cluster, KaxInternalBlock &block) { + auto track = file.track_by_number.find(block.TrackNum()); + if (track == file.track_by_number.end()) + return; + + auto track_index = track->second; + uint64_t frame_duration = file.tracks[track_index].info.DefaultDuration; + if (auto *group = dynamic_cast(&parent)) + if (auto *duration = child(*group)) + frame_duration = static_cast(*duration) * file.info.TimecodeScale; + + auto frame_count = block.NumberFrames(); + if (frame_count && frame_duration && dynamic_cast(&parent)) + frame_duration /= frame_count; + + int64_t cluster_ticks = cluster ? static_cast(uint_value(*cluster)) : 0; + int64_t frame_ticks = cluster_ticks + block.GetRelativeTimestamp(); + uint64_t first_frame_time = frame_ticks < 0 ? 0 : static_cast(frame_ticks) * file.info.TimecodeScale; + + bool is_keyframe = true; + if (auto *simple_block = dynamic_cast(&block)) + is_keyframe = simple_block->IsKeyframe(); + else if (auto *group = dynamic_cast(&parent)) + is_keyframe = child(*group) == nullptr; + + for (unsigned frame_index = 0; frame_index < frame_count; ++frame_index) { + Frame frame; + frame.track = track_index; + frame.start = first_frame_time + frame_index * frame_duration; + frame.end = frame.start + frame_duration; + frame.pos = block.GetDataPosition(frame_index); + frame.size = static_cast(block.GetFrameSize(frame_index)); + frame.flags = is_keyframe ? FRAME_KF : 0; + file.frames.push_back(frame); + } +} + +void parse_master(MatroskaFile &file, EbmlMaster &master, KaxCluster *cluster = nullptr) { + for (auto *element : master.GetElementList()) { + if (auto *entry = dynamic_cast(element)) + parse_track(file, *entry); + else if (auto *attachment = dynamic_cast(element)) + parse_attachment(file, *attachment); + else if (auto *block = dynamic_cast(element)) + parse_block(file, master, cluster, *block); + + if (auto *nested = dynamic_cast(element)) + parse_master(file, *nested, cluster ? cluster : dynamic_cast(nested)); + } } + +void finalize(MatroskaFile &f) { + for (auto &r : f.tracks) { + r.info.Name = r.name.empty() ? nullptr : r.name.data(); + r.info.CodecID = r.codec.data(); + std::memset(r.info.Language, 0, 4); + std::memcpy(r.info.Language, r.language.data(), std::min(3, r.language.size())); + r.info.CodecPrivate = r.priv.empty() ? nullptr : r.priv.data(); + r.info.CodecPrivateSize = r.priv.size(); + r.info.CompMethodPrivate = r.comp_private.empty() ? nullptr : r.comp_private.data(); + r.info.CompMethodPrivateSize = r.comp_private.size(); + } + for (auto &r : f.attachments) { + r.info.Name = r.name.data(); + r.info.Description = r.description.empty() ? nullptr : r.description.data(); + r.info.MimeType = r.mime.data(); + f.attachment_view.push_back(r.info); + } + std::stable_sort(f.frames.begin(), f.frames.end(), [](auto &a, auto &b) { return a.start < b.start; }); + if (!f.info.Duration) + for (auto const &frame : f.frames) + f.info.Duration = std::max(f.info.Duration, frame.end); } +} // namespace extern "C" { -MatroskaFile *mkv_Open(InputStream *io,char *msg,unsigned n){return mkv_OpenEx(io,0,0,msg,n);} -MatroskaFile *mkv_OpenEx(InputStream *input,uint64_t base,unsigned,char *msg,unsigned n){ - try { if(!input||!input->read||!input->getfilesize)throw std::runtime_error("invalid Matroska input stream"); - auto input_size=input->getfilesize(input);if(input_size<0||base>static_cast(input_size))throw std::runtime_error("invalid Matroska input size"); - auto f=std::make_unique();f->input=input;f->io=std::make_unique(input,base);f->stream=std::make_unique(*f->io); - int upper=0;std::unique_ptr head(f->stream->FindNextID(EBML_INFO(EbmlHead),std::numeric_limits::max()));if(!head)throw std::runtime_error("EBML header not found");head->SkipData(*f->stream,EBML_CONTEXT(head.get())); - std::unique_ptr e(f->stream->FindNextID(EBML_INFO(KaxSegment),std::numeric_limits::max()));if(!e)throw std::runtime_error("Matroska segment not found");f->segment_top=e->GetElementPosition(); - if (std::strcmp(EBML_NAME(e.get()), "Segment")) throw std::runtime_error(std::string("expected Segment, got ")+EBML_NAME(e.get())); - auto*segment=static_cast(e.get()); f->segment=std::move(e); f->info.TimecodeScale=1000000; - EbmlElement *next=f->stream->FindNextElement(EBML_CONTEXT(segment),upper,std::numeric_limits::max(),true); - while(next&&upper<=0){std::unique_ptr current(next);next=nullptr; - bool subtitle_frames=std::any_of(f->tracks.begin(),f->tracks.end(),[](auto const&t){return t.info.Type==TT_SUB;}); - if (dynamic_cast(current.get()) && !subtitle_frames) break; - bool wanted=dynamic_cast(current.get())||dynamic_cast(current.get())||(dynamic_cast(current.get())&&subtitle_frames)||dynamic_cast(current.get()); - if(wanted){auto*master=dynamic_cast(current.get());master->Read(*f->stream,EBML_CONTEXT(current.get()),upper,next,true,SCOPE_ALL_DATA); - if(auto*info=dynamic_cast(master)){f->info.TimecodeScale=uint_value(*info,1000000);if(auto*d=child(*info))f->info.Duration=static_cast(static_cast(*d)*f->info.TimecodeScale);} - parse_master(*f,*master,dynamic_cast(master));f->top.push_back(std::move(current)); - }else current->SkipData(*f->stream,EBML_CONTEXT(segment)); - if(!next)next=f->stream->FindNextElement(EBML_CONTEXT(segment),upper,std::numeric_limits::max(),true); - } - finalize(*f);return f.release(); - } catch(std::exception const&e){if(msg&&n){std::strncpy(msg,e.what(),n-1);msg[n-1]=0;}return nullptr;} - catch(...){if(msg&&n){std::strncpy(msg,"unknown Matroska parsing error",n-1);msg[n-1]=0;}return nullptr;}} -void mkv_Close(MatroskaFile*f){delete f;} const char*mkv_GetLastError(MatroskaFile*f){return f?f->error.c_str():"invalid Matroska handle";} -SegmentInfo*mkv_GetFileInfo(MatroskaFile*f){return &f->info;} unsigned mkv_GetNumTracks(MatroskaFile*f){return f->tracks.size();} TrackInfo*mkv_GetTrackInfo(MatroskaFile*f,unsigned i){return itracks.size()?&f->tracks[i].info:nullptr;} -void mkv_GetAttachments(MatroskaFile*f,Attachment**a,unsigned*n){*a=f->attachment_view.empty()?nullptr:f->attachment_view.data();*n=f->attachment_view.size();} -void mkv_GetChapters(MatroskaFile*,Chapter**c,unsigned*n){*c=nullptr;*n=0;} void mkv_GetTags(MatroskaFile*,Tag**t,unsigned*n){*t=nullptr;*n=0;} -uint64_t mkv_GetSegmentTop(MatroskaFile*f){return f->segment_top;} void mkv_SetTrackMask(MatroskaFile*f,unsigned m){f->mask=m;f->cursor=0;} -int mkv_ReadFrame(MatroskaFile*f,unsigned mask,unsigned*track,uint64_t*start,uint64_t*end,uint64_t*pos,unsigned*size,unsigned*flags){while(f->cursorframes.size()){auto&r=f->frames[f->cursor++];if(((mask|f->mask)>>r.track)&1)continue;*track=r.track;*start=r.start;*end=r.end;*pos=r.pos;*size=r.size;*flags=r.flags;return 0;}return -1;} -void mkv_Seek(MatroskaFile*f,uint64_t t,unsigned){f->cursor=std::lower_bound(f->frames.begin(),f->frames.end(),t,[](auto&a,uint64_t b){return a.startframes.begin();} -void mkv_SkipToKeyframe(MatroskaFile*){} uint64_t mkv_GetLowestQTimecode(MatroskaFile*f){return f->cursorframes.size()?f->frames[f->cursor].start:0;} int mkv_TruncFloat(MKFLOAT v){return static_cast(v);} -} - -struct CompressedStream { MatroskaFile*f;unsigned track;uint64_t pos{};unsigned size{};std::vector data;size_t cursor{};std::string error; }; +MatroskaFile *mkv_Open(InputStream *io, char *msg, unsigned n) { + return mkv_OpenEx(io, 0, 0, msg, n); +} +MatroskaFile *mkv_OpenEx(InputStream *input, uint64_t base, unsigned, char *msg, unsigned n) { + try { + if (!input || !input->read || !input->getfilesize) + throw std::runtime_error("invalid Matroska input stream"); + auto input_size = input->getfilesize(input); + if (input_size < 0 || base > static_cast(input_size)) + throw std::runtime_error("invalid Matroska input size"); + auto f = std::make_unique(); + f->input = input; + f->io = std::make_unique(input, base); + f->stream = std::make_unique(*f->io); + int upper = 0; + std::unique_ptr head( + f->stream->FindNextID(EBML_INFO(EbmlHead), std::numeric_limits::max())); + if (!head) + throw std::runtime_error("EBML header not found"); + head->SkipData(*f->stream, EBML_CONTEXT(head.get())); + std::unique_ptr e( + f->stream->FindNextID(EBML_INFO(KaxSegment), std::numeric_limits::max())); + if (!e) + throw std::runtime_error("Matroska segment not found"); + f->segment_top = e->GetElementPosition(); + if (std::strcmp(EBML_NAME(e.get()), "Segment")) + throw std::runtime_error(std::string("expected Segment, got ") + EBML_NAME(e.get())); + auto *segment = static_cast(e.get()); + f->segment = std::move(e); + f->info.TimecodeScale = 1000000; + EbmlElement *next = + f->stream->FindNextElement(EBML_CONTEXT(segment), upper, std::numeric_limits::max(), true); + while (next && upper <= 0) { + std::unique_ptr current(next); + next = nullptr; + bool subtitle_frames = + std::any_of(f->tracks.begin(), f->tracks.end(), [](auto const &t) { return t.info.Type == TT_SUB; }); + if (dynamic_cast(current.get()) && !subtitle_frames) + break; + bool wanted = dynamic_cast(current.get()) || dynamic_cast(current.get()) || + (dynamic_cast(current.get()) && subtitle_frames) || + dynamic_cast(current.get()); + if (wanted) { + auto *master = dynamic_cast(current.get()); + master->Read(*f->stream, EBML_CONTEXT(current.get()), upper, next, true, SCOPE_ALL_DATA); + if (auto *info = dynamic_cast(master)) { + f->info.TimecodeScale = uint_value(*info, 1000000); + if (auto *d = child(*info)) + f->info.Duration = static_cast(static_cast(*d) * f->info.TimecodeScale); + } + parse_master(*f, *master, dynamic_cast(master)); + f->top.push_back(std::move(current)); + } else + current->SkipData(*f->stream, EBML_CONTEXT(segment)); + if (!next) + next = f->stream->FindNextElement(EBML_CONTEXT(segment), upper, std::numeric_limits::max(), + true); + } + finalize(*f); + return f.release(); + } catch (std::exception const &e) { + if (msg && n) { + std::strncpy(msg, e.what(), n - 1); + msg[n - 1] = 0; + } + return nullptr; + } catch (...) { + if (msg && n) { + std::strncpy(msg, "unknown Matroska parsing error", n - 1); + msg[n - 1] = 0; + } + return nullptr; + } +} +void mkv_Close(MatroskaFile *f) { + delete f; +} +const char *mkv_GetLastError(MatroskaFile *f) { + return f ? f->error.c_str() : "invalid Matroska handle"; +} +SegmentInfo *mkv_GetFileInfo(MatroskaFile *f) { + return &f->info; +} +unsigned mkv_GetNumTracks(MatroskaFile *f) { + return f->tracks.size(); +} +TrackInfo *mkv_GetTrackInfo(MatroskaFile *f, unsigned i) { + return i < f->tracks.size() ? &f->tracks[i].info : nullptr; +} +void mkv_GetAttachments(MatroskaFile *f, Attachment **a, unsigned *n) { + *a = f->attachment_view.empty() ? nullptr : f->attachment_view.data(); + *n = f->attachment_view.size(); +} +void mkv_GetChapters(MatroskaFile *, Chapter **c, unsigned *n) { + *c = nullptr; + *n = 0; +} +void mkv_GetTags(MatroskaFile *, Tag **t, unsigned *n) { + *t = nullptr; + *n = 0; +} +uint64_t mkv_GetSegmentTop(MatroskaFile *f) { + return f->segment_top; +} +void mkv_SetTrackMask(MatroskaFile *f, unsigned m) { + f->mask = m; + f->cursor = 0; +} +int mkv_ReadFrame(MatroskaFile *f, unsigned mask, unsigned *track, uint64_t *start, uint64_t *end, uint64_t *pos, + unsigned *size, unsigned *flags) { + while (f->cursor < f->frames.size()) { + auto &r = f->frames[f->cursor++]; + if (((mask | f->mask) >> r.track) & 1) + continue; + *track = r.track; + *start = r.start; + *end = r.end; + *pos = r.pos; + *size = r.size; + *flags = r.flags; + return 0; + } + return -1; +} +void mkv_Seek(MatroskaFile *f, uint64_t t, unsigned) { + f->cursor = + std::lower_bound(f->frames.begin(), f->frames.end(), t, [](auto &a, uint64_t b) { return a.start < b; }) - + f->frames.begin(); +} +void mkv_SkipToKeyframe(MatroskaFile *) {} +uint64_t mkv_GetLowestQTimecode(MatroskaFile *f) { + return f->cursor < f->frames.size() ? f->frames[f->cursor].start : 0; +} +int mkv_TruncFloat(MKFLOAT v) { + return static_cast(v); +} +} + +struct CompressedStream { + MatroskaFile *file; + unsigned track; + uint64_t frame_position{}; + unsigned frame_size{}; + std::vector data; + size_t cursor{}; + std::string error; +}; + +namespace { +std::vector read_frame(CompressedStream const &stream) { + std::vector frame(stream.frame_size); + auto bytes_read = + stream.file->input->read(stream.file->input, stream.frame_position, frame.data(), stream.frame_size); + if (bytes_read != static_cast(stream.frame_size)) + throw std::runtime_error("short frame read"); + return frame; +} + +std::vector inflate_frame(std::vector const &input) { + z_stream zstream{}; + if (inflateInit(&zstream) != Z_OK) + throw std::runtime_error("zlib initialization failed"); + + struct EndInflate { + z_stream *stream; + ~EndInflate() { + inflateEnd(stream); + } + } end_inflate{&zstream}; + + zstream.next_in = const_cast(input.data()); + zstream.avail_in = input.size(); + + std::vector output; + unsigned char buffer[4096]; + int result; + do { + zstream.next_out = buffer; + zstream.avail_out = sizeof buffer; + result = inflate(&zstream, Z_NO_FLUSH); + output.insert(output.end(), buffer, buffer + sizeof buffer - zstream.avail_out); + } while (result == Z_OK); + + if (result != Z_STREAM_END) + throw std::runtime_error("invalid zlib frame"); + return output; +} + +void decode_frame(CompressedStream &stream) { + auto raw_frame = read_frame(stream); + auto const &track = stream.file->tracks[stream.track]; + + if (!track.info.CompEnabled) + stream.data = std::move(raw_frame); + else if (track.info.CompMethod == COMP_PREPEND) { + stream.data = track.comp_private; + stream.data.insert(stream.data.end(), raw_frame.begin(), raw_frame.end()); + } else if (track.info.CompMethod == COMP_ZLIB) + stream.data = inflate_frame(raw_frame); + else + throw std::runtime_error("unsupported Matroska compression method"); +} +} // namespace + extern "C" { -CompressedStream*cs_Create(MatroskaFile*f,unsigned t,char*msg,unsigned n){if(!f||t>=f->tracks.size()){if(msg&&n)std::strncpy(msg,"invalid track",n);return nullptr;}return new CompressedStream{f,t,0,0,{},{},{}};} -void cs_Destroy(CompressedStream*c){delete c;} void cs_NextFrame(CompressedStream*c,uint64_t p,unsigned n){c->pos=p;c->size=n;c->data.clear();c->cursor=0;} -int cs_ReadData(CompressedStream*c,char*out,unsigned cap){try{if(c->data.empty()&&c->size){std::vectorraw(c->size);if(c->f->input->read(c->f->input,c->pos,raw.data(),c->size)!=(int)c->size)throw std::runtime_error("short frame read");auto&r=c->f->tracks[c->track];if(!r.info.CompEnabled)c->data=std::move(raw);else if(r.info.CompMethod==COMP_PREPEND){c->data=r.comp_private;c->data.insert(c->data.end(),raw.begin(),raw.end());}else if(r.info.CompMethod==COMP_ZLIB){z_stream z{};if(inflateInit(&z)!=Z_OK)throw std::runtime_error("zlib initialization failed");z.next_in=raw.data();z.avail_in=raw.size();unsigned char buf[4096];int rc;do{z.next_out=buf;z.avail_out=sizeof buf;rc=inflate(&z,Z_NO_FLUSH);c->data.insert(c->data.end(),buf,buf+sizeof(buf)-z.avail_out);}while(rc==Z_OK);inflateEnd(&z);if(rc!=Z_STREAM_END)throw std::runtime_error("invalid zlib frame");}else throw std::runtime_error("unsupported Matroska compression method");}size_t take=std::min(cap,c->data.size()-c->cursor);std::memcpy(out,c->data.data()+c->cursor,take);c->cursor+=take;return take;}catch(std::exception const&e){c->error=e.what();return-1;}} -const char*cs_GetLastError(CompressedStream*c){return c->error.c_str();} +CompressedStream *cs_Create(MatroskaFile *file, unsigned track, char *message, unsigned message_size) { + if (!file || track >= file->tracks.size()) { + if (message && message_size) + std::strncpy(message, "invalid track", message_size); + return nullptr; + } + return new CompressedStream{file, track, 0, 0, {}, {}, {}}; +} +void cs_Destroy(CompressedStream *c) { + delete c; +} +void cs_NextFrame(CompressedStream *stream, uint64_t position, unsigned size) { + stream->frame_position = position; + stream->frame_size = size; + stream->data.clear(); + stream->cursor = 0; +} +int cs_ReadData(CompressedStream *stream, char *output, unsigned capacity) { + try { + if (stream->data.empty() && stream->frame_size) + decode_frame(*stream); + + size_t bytes_to_copy = std::min(capacity, stream->data.size() - stream->cursor); + std::memcpy(output, stream->data.data() + stream->cursor, bytes_to_copy); + stream->cursor += bytes_to_copy; + return bytes_to_copy; + } catch (std::exception const &e) { + stream->error = e.what(); + return -1; + } +} +const char *cs_GetLastError(CompressedStream *c) { + return c->error.c_str(); +} } diff --git a/tests/matroska/behavior.cpp b/tests/matroska/behavior.cpp index 9750dda169..d7a7ae990b 100644 --- a/tests/matroska/behavior.cpp +++ b/tests/matroska/behavior.cpp @@ -8,18 +8,113 @@ #include struct FileInput final : InputStream { - FILE *file{}; std::string error; uint64_t size{}; - explicit FileInput(char const *name) { - file = std::fopen(name, "rb"); if (!file) return; - std::fseek(file, 0, SEEK_END); size = std::ftell(file); std::fseek(file, 0, SEEK_SET); - read=[](InputStream*s,uint64_t p,void*b,int n){auto&i=*static_cast(s);if(p>=i.size)return 0;n=std::min(n,i.size-p);if(std::fseek(i.file,p,SEEK_SET))return-1;return(int)std::fread(b,1,n,i.file);}; - scan=[](InputStream*s,uint64_t p,unsigned sig)->int64_t{unsigned v=0;unsigned char c;for(;static_cast(s)->read(s,p,&c,1)==1;++p){v=(v<<8)|c;if(v==sig)return p-3;}return-1;}; - getcachesize=[](InputStream*){return 1U<<20;}; geterror=[](InputStream*s){return static_cast(s)->error.c_str();}; - memalloc=[](InputStream*,size_t n){return std::malloc(n);}; memrealloc=[](InputStream*,void*p,size_t n){return std::realloc(p,n);}; memfree=[](InputStream*,void*p){std::free(p);}; - progress=[](InputStream*,uint64_t,uint64_t){return 1;}; getfilesize=[](InputStream*s){return(int64_t)static_cast(s)->size;}; - } - ~FileInput(){if(file)std::fclose(file);} + FILE *file{}; + std::string error; + uint64_t size{}; + explicit FileInput(char const *name) { + file = std::fopen(name, "rb"); + if (!file) + return; + std::fseek(file, 0, SEEK_END); + size = std::ftell(file); + std::fseek(file, 0, SEEK_SET); + read = [](InputStream *s, uint64_t p, void *b, int n) { + auto &i = *static_cast(s); + if (p >= i.size) + return 0; + n = std::min(n, i.size - p); + if (std::fseek(i.file, p, SEEK_SET)) + return -1; + return (int)std::fread(b, 1, n, i.file); + }; + scan = [](InputStream *s, uint64_t p, unsigned sig) -> int64_t { + unsigned v = 0; + unsigned char c; + for (; static_cast(s)->read(s, p, &c, 1) == 1; ++p) { + v = (v << 8) | c; + if (v == sig) + return p - 3; + } + return -1; + }; + getcachesize = [](InputStream *) { return 1U << 20; }; + geterror = [](InputStream *s) { return static_cast(s)->error.c_str(); }; + memalloc = [](InputStream *, size_t n) { return std::malloc(n); }; + memrealloc = [](InputStream *, void *p, size_t n) { return std::realloc(p, n); }; + memfree = [](InputStream *, void *p) { std::free(p); }; + progress = [](InputStream *, uint64_t, uint64_t) { return 1; }; + getfilesize = [](InputStream *s) { return (int64_t)static_cast(s)->size; }; + } + ~FileInput() { + if (file) + std::fclose(file); + } }; -static uint64_t hash(FileInput&i,uint64_t p,unsigned n){uint64_t h=1469598103934665603ULL;std::vectord(n);if(n&&i.read(&i,p,d.data(),n)!=(int)n)return 0;for(auto c:d){h^=c;h*=1099511628211ULL;}return h;} -static uint64_t decoded_hash(MatroskaFile*f,unsigned t,uint64_t p,unsigned n){char error[256]{};auto*c=cs_Create(f,t,error,sizeof error);if(!c)return 0;cs_NextFrame(c,p,n);uint64_t h=1469598103934665603ULL;char data[256];for(;;){int got=cs_ReadData(c,data,sizeof data);if(got<=0)break;for(int i=0;i(data[i]);h*=1099511628211ULL;}}cs_Destroy(c);return h;} -int main(int ac,char**av){if(ac!=2)return 2;FileInput in(av[1]);char e[2048]{};auto*f=mkv_Open(&in,e,sizeof e);if(!f){std::cout<<"error\t"<TimecodeScale<<'\t'<Duration<<'\n';for(unsigned i=0;iNumber)<<'\t'<Type)<<'\t'<<(t->CodecID?t->CodecID:"")<<'\t'<Language<<'\t'<<(t->Name?t->Name:"")<<'\t'<CodecPrivateSize<<'\t'<CompEnabled<<'\t'<CompMethod<<'\n';}Attachment*at;unsigned count;mkv_GetAttachments(f,&at,&count);for(unsigned i=0;iCompEnabled)std::cout<<'\t'< data(size); + if (size && input.read(&input, position, data.data(), size) != static_cast(size)) + return 0; + for (auto byte : data) { + h ^= byte; + h *= 1099511628211ULL; + } + return h; +} +static uint64_t decoded_hash(MatroskaFile *file, unsigned track, uint64_t position, unsigned size) { + char error[256]{}; + auto *stream = cs_Create(file, track, error, sizeof error); + if (!stream) + return 0; + cs_NextFrame(stream, position, size); + uint64_t h = 1469598103934665603ULL; + char data[256]; + for (;;) { + int bytes_read = cs_ReadData(stream, data, sizeof data); + if (bytes_read <= 0) + break; + for (int i = 0; i < bytes_read; ++i) { + h ^= static_cast(data[i]); + h *= 1099511628211ULL; + } + } + cs_Destroy(stream); + return h; +} +int main(int argc, char **argv) { + if (argc != 2) + return 2; + FileInput input(argv[1]); + char error[2048]{}; + auto *file = mkv_Open(&input, error, sizeof error); + if (!file) { + std::cout << "error\t" << error << '\n'; + return 1; + } + auto *segment = mkv_GetFileInfo(file); + std::cout << "segment\t" << segment->TimecodeScale << '\t' << segment->Duration << '\n'; + for (unsigned track_index = 0; track_index < mkv_GetNumTracks(file); ++track_index) { + auto *track = mkv_GetTrackInfo(file, track_index); + std::cout << "track\t" << track_index << '\t' << unsigned(track->Number) << '\t' << unsigned(track->Type) + << '\t' << (track->CodecID ? track->CodecID : "") << '\t' << track->Language << '\t' + << (track->Name ? track->Name : "") << '\t' << track->CodecPrivateSize << '\t' << track->CompEnabled + << '\t' << track->CompMethod << '\n'; + } + Attachment *attachments; + unsigned attachment_count; + mkv_GetAttachments(file, &attachments, &attachment_count); + for (unsigned i = 0; i < attachment_count; ++i) + std::cout << "attachment\t" << (attachments[i].Name ? attachments[i].Name : "") << '\t' + << (attachments[i].MimeType ? attachments[i].MimeType : "") << '\t' << attachments[i].Length << '\t' + << std::hex << hash(input, attachments[i].Position, attachments[i].Length) << std::dec << '\n'; + unsigned track, size, flags; + uint64_t start, end, position; + while (!mkv_ReadFrame(file, 0, &track, &start, &end, &position, &size, &flags)) { + std::cout << "frame\t" << track << '\t' << start << '\t' << end << '\t' << size << '\t' << flags << '\t' + << std::hex << hash(input, position, size); + if (mkv_GetTrackInfo(file, track)->CompEnabled) + std::cout << '\t' << decoded_hash(file, track, position, size); + std::cout << std::dec << '\n'; + } + mkv_Close(file); +} From 3199c55fdfb0617d3c38aca3fd9004bcf1b141ed Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Thu, 27 Aug 2026 20:21:01 +0000 Subject: [PATCH 03/28] Add proposed native Matroska interface --- src/matroska_proposed.cpp | 296 ++++++++++++++++++++++++++++++++++++++ src/matroska_proposed.h | 106 ++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 src/matroska_proposed.cpp create mode 100644 src/matroska_proposed.h diff --git a/src/matroska_proposed.cpp b/src/matroska_proposed.cpp new file mode 100644 index 0000000000..38b3204724 --- /dev/null +++ b/src/matroska_proposed.cpp @@ -0,0 +1,296 @@ +// Copyright (c) 2026 Aegisub contributors +// +// This is an unused comparison implementation. It deliberately delegates EBML +// parsing to the current adapter while presenting an Aegisub-native C++ +// surface. It is not part of the production build and has no call sites. + +#include "matroska_proposed.h" + +#include "MatroskaParser.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace agi::matroska::proposed { +namespace { +class FileReader final : public Reader { + std::ifstream file; + uint64_t size = 0; + +public: + explicit FileReader(agi::fs::path const &filename) + : file(filename, std::ios::binary) { + if (!file) + throw ReadError("Failed to open Matroska input"); + file.seekg(0, std::ios::end); + auto end = file.tellg(); + if (end < 0) + throw ReadError("Failed to determine Matroska input size"); + size = static_cast(end); + } + uint64_t Size() const override { return size; } + size_t Read(uint64_t position, void *buffer, size_t size) override { + if (position >= this->size) + return 0; + size = std::min(size, this->size - position); + file.clear(); + file.seekg(position); + file.read(static_cast(buffer), size); + return static_cast(file.gcount()); + } +}; + +std::string copy_string(char const *value) { return value ? value : ""; } + +SubtitleCodec codec_from_id(std::string const &id) { + if (id == "S_TEXT/ASS") + return SubtitleCodec::ass; + if (id == "S_TEXT/SSA") + return SubtitleCodec::ssa; + if (id == "S_TEXT/UTF8") + return SubtitleCodec::srt; + return SubtitleCodec::unsupported; +} +} // namespace + +std::unique_ptr OpenFile(agi::fs::path const &filename) { + return std::make_unique(filename); +} + +class Demuxer::Impl final : public InputStream { + std::unique_ptr reader; + CancelCheck cancelled; + std::string input_error; + std::unique_ptr file{nullptr, mkv_Close}; + std::unique_ptr compressed{ + nullptr, cs_Destroy}; + std::vector tracks; + std::vector attachments; + std::vector<::Attachment> attachment_locations; + std::optional duration; + std::optional selected; + + static Impl *Self(InputStream *input) { return static_cast(input); } + + static int Read(InputStream *input, uint64_t position, void *buffer, + int count) { + if (count <= 0) + return 0; + try { + return static_cast( + Self(input)->reader->Read(position, buffer, count)); + } catch (agi::Exception const &error) { + Self(input)->input_error = error.GetMessage(); + return -1; + } catch (std::exception const &error) { + Self(input)->input_error = error.what(); + return -1; + } + } + + static int64_t Scan(InputStream *input, uint64_t start, unsigned signature) { + unsigned window = 0; + for (uint64_t position = start; position < Self(input)->reader->Size(); + ++position) { + unsigned char byte; + if (Read(input, position, &byte, 1) != 1) + return -1; + window = (window << 8 | byte) & 0xffffffffU; + if (window == signature) + return position - 3; + } + return -1; + } + + static int64_t Size(InputStream *input) { + auto size = Self(input)->reader->Size(); + return size > static_cast(INT64_MAX) ? INT64_MAX : size; + } + + void CheckCancelled() const { + if (cancelled && cancelled()) + throw agi::UserCancelException("Matroska read cancelled"); + } + + void ReadMetadata() { + for (unsigned index = 0; index < mkv_GetNumTracks(file.get()); ++index) { + auto const *info = mkv_GetTrackInfo(file.get(), index); + if (info->Type != TT_SUB) + continue; + + SubtitleTrack track; + track.id.value = index; + track.uid = info->UID; + track.codec_id = copy_string(info->CodecID); + track.codec = codec_from_id(track.codec_id); + track.name = copy_string(info->Name); + track.language = copy_string(info->Language); + track.enabled = info->Enabled; + track.is_default = info->Default; + auto private_data = static_cast(info->CodecPrivate); + if (private_data) + track.codec_private.assign(private_data, + private_data + info->CodecPrivateSize); + tracks.push_back(std::move(track)); + } + + ::Attachment *legacy_attachments = nullptr; + unsigned count = 0; + mkv_GetAttachments(file.get(), &legacy_attachments, &count); + if (count) + attachment_locations.assign(legacy_attachments, + legacy_attachments + count); + for (auto const &legacy : attachment_locations) + attachments.push_back({legacy.UID, copy_string(legacy.Name), + copy_string(legacy.Description), + copy_string(legacy.MimeType), legacy.Length}); + + auto const *segment = mkv_GetFileInfo(file.get()); + if (segment->Duration) + duration = Timestamp{static_cast(segment->Duration)}; + } + + std::vector ReadBytes(uint64_t position, uint64_t size) { + if (size > std::numeric_limits::max()) + throw ReadError("Matroska item is too large"); + std::vector result(static_cast(size)); + size_t offset = 0; + while (offset < result.size()) { + CheckCancelled(); + auto read = reader->Read(position + offset, result.data() + offset, + result.size() - offset); + if (!read) + throw ReadError("Unexpected end of Matroska input"); + offset += read; + } + return result; + } + +public: + Impl(std::unique_ptr input, CancelCheck cancel) + : reader(std::move(input)), cancelled(std::move(cancel)) { + if (!reader) + throw ReadError("Cannot open Matroska from a null reader"); + read = &Read; + scan = &Scan; + getfilesize = &Size; + getcachesize = [](InputStream *) { return 16U * 1024 * 1024; }; + geterror = [](InputStream *input) { + return Self(input)->input_error.c_str(); + }; + memalloc = [](InputStream *, size_t size) { return std::malloc(size); }; + memrealloc = [](InputStream *, void *memory, size_t size) { + return std::realloc(memory, size); + }; + memfree = [](InputStream *, void *memory) { std::free(memory); }; + progress = [](InputStream *input, uint64_t, uint64_t) { + auto self = Self(input); + return (!self->cancelled || !self->cancelled()) ? 1 : 0; + }; + + CheckCancelled(); + char error[2048]{}; + file.reset(mkv_Open(this, error, sizeof error)); + if (!file) { + CheckCancelled(); + throw ReadError(error[0] ? error : "Failed to parse Matroska input"); + } + ReadMetadata(); + } + + std::vector const &Tracks() const { return tracks; } + std::vector const &AttachmentList() const { return attachments; } + std::optional Duration() const { return duration; } + + void Select(TrackId track) { + if (track.value >= mkv_GetNumTracks(file.get()) || + track.value >= sizeof(unsigned) * CHAR_BIT) + throw ReadError("Invalid Matroska subtitle track"); + auto found = + std::find_if(tracks.begin(), tracks.end(), [&](auto const &item) { + return item.id.value == track.value; + }); + if (found == tracks.end()) + throw ReadError("Selected Matroska track is not a subtitle track"); + + CheckCancelled(); + mkv_SetTrackMask(file.get(), ~(1U << track.value)); + selected = track; + compressed.reset(); + if (mkv_GetTrackInfo(file.get(), track.value)->CompEnabled) { + char error[2048]{}; + compressed.reset(cs_Create(file.get(), track.value, error, sizeof error)); + if (!compressed) + throw ReadError(error[0] ? error : "Unsupported subtitle compression"); + } + } + + std::optional NextPacket() { + if (!selected) + throw ReadError("No Matroska subtitle track has been selected"); + CheckCancelled(); + unsigned track, frame_size, flags; + uint64_t start, end, position; + if (mkv_ReadFrame(file.get(), ~(1U << selected->value), &track, &start, + &end, &position, &frame_size, &flags) < 0) + return std::nullopt; + + std::vector data; + if (!compressed) + data = ReadBytes(position, frame_size); + else { + cs_NextFrame(compressed.get(), position, frame_size); + unsigned char buffer[4096]; + for (;;) { + CheckCancelled(); + int count = cs_ReadData( + compressed.get(), reinterpret_cast(buffer), sizeof buffer); + if (count < 0) + throw ReadError(copy_string(cs_GetLastError(compressed.get()))); + if (!count) + break; + data.insert(data.end(), buffer, buffer + count); + } + } + return SubtitlePacket{{track}, + {static_cast(start)}, + {static_cast(end)}, + std::move(data)}; + } + + std::vector AttachmentBytes(uint64_t uid) { + auto found = + std::find_if(attachment_locations.begin(), attachment_locations.end(), + [&](auto const &item) { return item.UID == uid; }); + if (found == attachment_locations.end()) + throw ReadError("Unknown Matroska attachment"); + return ReadBytes(found->Position, found->Length); + } +}; + +Demuxer::Demuxer(std::unique_ptr reader, CancelCheck cancelled) + : impl(std::make_unique(std::move(reader), std::move(cancelled))) {} +Demuxer::~Demuxer() = default; +Demuxer::Demuxer(Demuxer &&) noexcept = default; +Demuxer &Demuxer::operator=(Demuxer &&) noexcept = default; +std::vector const &Demuxer::SubtitleTracks() const { + return impl->Tracks(); +} +std::vector const &Demuxer::Attachments() const { + return impl->AttachmentList(); +} +std::optional Demuxer::Duration() const { return impl->Duration(); } +void Demuxer::SelectTrack(TrackId track) { impl->Select(track); } +std::optional Demuxer::ReadPacket() { + return impl->NextPacket(); +} +std::vector Demuxer::ReadAttachment(uint64_t uid) { + return impl->AttachmentBytes(uid); +} + +} // namespace agi::matroska::proposed diff --git a/src/matroska_proposed.h b/src/matroska_proposed.h new file mode 100644 index 0000000000..f1f2f6d085 --- /dev/null +++ b/src/matroska_proposed.h @@ -0,0 +1,106 @@ +// Copyright (c) 2026 Aegisub contributors +// +// Permission to use, copy, modify, and distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright +// notice and this permission notice appear in all copies. + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// This is an unused proposal for side-by-side API review. Nothing in Aegisub +// constructs these types, and this file is intentionally absent from the build. +namespace agi::matroska::proposed { + +DEFINE_EXCEPTION(ReadError, agi::Exception); + +struct Timestamp { + int64_t nanoseconds = 0; +}; + +struct TrackId { + uint32_t value = 0; +}; + +enum class SubtitleCodec { ass, ssa, srt, unsupported }; + +struct SubtitleTrack { + TrackId id; + uint64_t uid = 0; + SubtitleCodec codec = SubtitleCodec::unsupported; + std::string codec_id; + std::string name; + std::string language; + std::vector codec_private; + bool enabled = true; + bool is_default = false; +}; + +struct Attachment { + uint64_t uid = 0; + std::string name; + std::string description; + std::string mime_type; + uint64_t size = 0; +}; + +struct SubtitlePacket { + TrackId track; + Timestamp start; + Timestamp end; + std::vector data; +}; + +using CancelCheck = std::function; + +/// Random-access input owned by Demuxer for its entire lifetime. +class Reader { +public: + virtual ~Reader() = default; + virtual uint64_t Size() const = 0; + /// Read up to size bytes at position. Returning zero means end of input. + virtual size_t Read(uint64_t position, void *buffer, size_t size) = 0; +}; + +/// Open a file-backed reader using Aegisub's normal mapped-file input. +std::unique_ptr OpenFile(agi::fs::path const &filename); + +/// Subtitle/attachment-only Matroska view proposed for Aegisub. +/// +/// The demuxer owns its Reader and all parser state. Returned metadata and +/// packet bytes are values, so none of them borrow storage from the parser. +class Demuxer final { + class Impl; + std::unique_ptr impl; + +public: + explicit Demuxer(std::unique_ptr reader, CancelCheck cancelled = {}); + ~Demuxer(); + + Demuxer(Demuxer &&) noexcept; + Demuxer &operator=(Demuxer &&) noexcept; + Demuxer(Demuxer const &) = delete; + Demuxer &operator=(Demuxer const &) = delete; + + std::vector const &SubtitleTracks() const; + std::vector const &Attachments() const; + std::optional Duration() const; + + /// Select one subtitle track and rewind packet iteration to its beginning. + void SelectTrack(TrackId track); + /// Return the next decoded packet, or nullopt at end of input. + std::optional ReadPacket(); + /// Copy an attachment from the input. The returned vector owns its bytes. + std::vector ReadAttachment(uint64_t uid); +}; + +} // namespace agi::matroska::proposed From b30e6828642528cbe225541f6759cc3c10ec2178 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Thu, 27 Aug 2026 21:17:18 +0000 Subject: [PATCH 04/28] Promote native Matroska interface --- src/{matroska_proposed.cpp => matroska.cpp} | 175 +++++---- src/{matroska_proposed.h => matroska.h} | 46 ++- src/meson.build | 1 + src/mkv_wrap.cpp | 387 ++++---------------- tests/meson.build | 3 +- tests/tests/matroska.cpp | 98 +++++ 6 files changed, 310 insertions(+), 400 deletions(-) rename src/{matroska_proposed.cpp => matroska.cpp} (59%) rename src/{matroska_proposed.h => matroska.h} (64%) create mode 100644 tests/tests/matroska.cpp diff --git a/src/matroska_proposed.cpp b/src/matroska.cpp similarity index 59% rename from src/matroska_proposed.cpp rename to src/matroska.cpp index 38b3204724..5e36d7bfd6 100644 --- a/src/matroska_proposed.cpp +++ b/src/matroska.cpp @@ -1,10 +1,6 @@ // Copyright (c) 2026 Aegisub contributors // -// This is an unused comparison implementation. It deliberately delegates EBML -// parsing to the current adapter while presenting an Aegisub-native C++ -// surface. It is not part of the production build and has no call sites. - -#include "matroska_proposed.h" +#include "matroska.h" #include "MatroskaParser.h" @@ -12,36 +8,25 @@ #include #include #include -#include +#include #include #include -namespace agi::matroska::proposed { +namespace agi::matroska { namespace { class FileReader final : public Reader { - std::ifstream file; - uint64_t size = 0; + agi::read_file_mapping file; public: explicit FileReader(agi::fs::path const &filename) - : file(filename, std::ios::binary) { - if (!file) - throw ReadError("Failed to open Matroska input"); - file.seekg(0, std::ios::end); - auto end = file.tellg(); - if (end < 0) - throw ReadError("Failed to determine Matroska input size"); - size = static_cast(end); - } - uint64_t Size() const override { return size; } + : file(filename) {} + uint64_t Size() const override { return file.size(); } size_t Read(uint64_t position, void *buffer, size_t size) override { - if (position >= this->size) + if (position >= file.size()) return 0; - size = std::min(size, this->size - position); - file.clear(); - file.seekg(position); - file.read(static_cast(buffer), size); - return static_cast(file.gcount()); + size = std::min(size, file.size() - position); + memcpy(buffer, file.read(position, size), size); + return size; } }; @@ -59,12 +44,18 @@ SubtitleCodec codec_from_id(std::string const &id) { } // namespace std::unique_ptr OpenFile(agi::fs::path const &filename) { - return std::make_unique(filename); + try { + return std::make_unique(filename); + } + catch (agi::Exception const& error) { + throw IoError(error.GetMessage()); + } } class Demuxer::Impl final : public InputStream { std::unique_ptr reader; CancelCheck cancelled; + Limits limits; std::string input_error; std::unique_ptr file{nullptr, mkv_Close}; std::unique_ptr compressed{ @@ -78,42 +69,54 @@ class Demuxer::Impl final : public InputStream { static Impl *Self(InputStream *input) { return static_cast(input); } static int Read(InputStream *input, uint64_t position, void *buffer, - int count) { + int count) noexcept { if (count <= 0) return 0; try { - return static_cast( - Self(input)->reader->Read(position, buffer, count)); - } catch (agi::Exception const &error) { - Self(input)->input_error = error.GetMessage(); - return -1; - } catch (std::exception const &error) { - Self(input)->input_error = error.what(); + size_t total = 0; + while (total < static_cast(count)) { + auto read = Self(input)->reader->Read(position + total, + static_cast(buffer) + total, count - total); + if (!read) break; + if (read > static_cast(count) - total) return -1; + total += read; + } + return static_cast(total); + } catch (...) { + try { Self(input)->input_error = "Matroska input read failed"; } + catch (...) {} return -1; } } - static int64_t Scan(InputStream *input, uint64_t start, unsigned signature) { - unsigned window = 0; - for (uint64_t position = start; position < Self(input)->reader->Size(); - ++position) { - unsigned char byte; - if (Read(input, position, &byte, 1) != 1) - return -1; - window = (window << 8 | byte) & 0xffffffffU; - if (window == signature) - return position - 3; - } + static int64_t Scan(InputStream *input, uint64_t start, unsigned signature) noexcept { + try { + unsigned window = 0; + for (uint64_t position = start; position < Self(input)->reader->Size(); ++position) { + unsigned char byte; + if (Read(input, position, &byte, 1) != 1) return -1; + window = (window << 8 | byte) & 0xffffffffU; + if (window == signature) return position - 3; + } + } catch (...) {} return -1; } - static int64_t Size(InputStream *input) { - auto size = Self(input)->reader->Size(); - return size > static_cast(INT64_MAX) ? INT64_MAX : size; + static int64_t Size(InputStream *input) noexcept { + try { + auto size = Self(input)->reader->Size(); + return size > static_cast(INT64_MAX) ? INT64_MAX : size; + } catch (...) { return 0; } + } + + bool IsCancelled() const noexcept { + if (!cancelled) return false; + try { return cancelled(); } + catch (...) { return true; } } void CheckCancelled() const { - if (cancelled && cancelled()) + if (IsCancelled()) throw agi::UserCancelException("Matroska read cancelled"); } @@ -145,19 +148,24 @@ class Demuxer::Impl final : public InputStream { if (count) attachment_locations.assign(legacy_attachments, legacy_attachments + count); - for (auto const &legacy : attachment_locations) - attachments.push_back({legacy.UID, copy_string(legacy.Name), + for (auto const &legacy : attachment_locations) { + if (std::find_if(attachments.begin(), attachments.end(), [&](auto const &item) { + return item.id.value == legacy.UID; + }) != attachments.end()) + throw InvalidDataError("Duplicate Matroska attachment ID"); + attachments.push_back({{legacy.UID}, copy_string(legacy.Name), copy_string(legacy.Description), copy_string(legacy.MimeType), legacy.Length}); + } auto const *segment = mkv_GetFileInfo(file.get()); - if (segment->Duration) + if (segment->Duration <= static_cast(INT64_MAX)) duration = Timestamp{static_cast(segment->Duration)}; } std::vector ReadBytes(uint64_t position, uint64_t size) { if (size > std::numeric_limits::max()) - throw ReadError("Matroska item is too large"); + throw LimitError("Matroska item exceeds addressable memory"); std::vector result(static_cast(size)); size_t offset = 0; while (offset < result.size()) { @@ -165,17 +173,19 @@ class Demuxer::Impl final : public InputStream { auto read = reader->Read(position + offset, result.data() + offset, result.size() - offset); if (!read) - throw ReadError("Unexpected end of Matroska input"); + throw TruncatedError("Unexpected end of Matroska input"); + if (read > result.size() - offset) + throw IoError("Matroska reader returned more bytes than requested"); offset += read; } return result; } public: - Impl(std::unique_ptr input, CancelCheck cancel) - : reader(std::move(input)), cancelled(std::move(cancel)) { + Impl(std::unique_ptr input, CancelCheck cancel, Limits configured_limits) + : reader(std::move(input)), cancelled(std::move(cancel)), limits(configured_limits) { if (!reader) - throw ReadError("Cannot open Matroska from a null reader"); + throw InvalidDataError("Cannot open Matroska from a null reader"); read = &Read; scan = &Scan; getfilesize = &Size; @@ -188,9 +198,8 @@ class Demuxer::Impl final : public InputStream { return std::realloc(memory, size); }; memfree = [](InputStream *, void *memory) { std::free(memory); }; - progress = [](InputStream *input, uint64_t, uint64_t) { - auto self = Self(input); - return (!self->cancelled || !self->cancelled()) ? 1 : 0; + progress = [](InputStream *input, uint64_t, uint64_t) noexcept { + return Self(input)->IsCancelled() ? 0 : 1; }; CheckCancelled(); @@ -198,7 +207,7 @@ class Demuxer::Impl final : public InputStream { file.reset(mkv_Open(this, error, sizeof error)); if (!file) { CheckCancelled(); - throw ReadError(error[0] ? error : "Failed to parse Matroska input"); + throw InvalidDataError(error[0] ? error : "Failed to parse Matroska input"); } ReadMetadata(); } @@ -210,13 +219,15 @@ class Demuxer::Impl final : public InputStream { void Select(TrackId track) { if (track.value >= mkv_GetNumTracks(file.get()) || track.value >= sizeof(unsigned) * CHAR_BIT) - throw ReadError("Invalid Matroska subtitle track"); + throw InvalidDataError("Invalid Matroska subtitle track"); auto found = std::find_if(tracks.begin(), tracks.end(), [&](auto const &item) { return item.id.value == track.value; }); if (found == tracks.end()) - throw ReadError("Selected Matroska track is not a subtitle track"); + throw InvalidDataError("Selected Matroska track is not a subtitle track"); + if (found->codec == SubtitleCodec::unsupported) + throw UnsupportedError("Selected Matroska subtitle codec is unsupported"); CheckCancelled(); mkv_SetTrackMask(file.get(), ~(1U << track.value)); @@ -226,13 +237,13 @@ class Demuxer::Impl final : public InputStream { char error[2048]{}; compressed.reset(cs_Create(file.get(), track.value, error, sizeof error)); if (!compressed) - throw ReadError(error[0] ? error : "Unsupported subtitle compression"); + throw UnsupportedError(error[0] ? error : "Unsupported subtitle compression"); } } std::optional NextPacket() { if (!selected) - throw ReadError("No Matroska subtitle track has been selected"); + throw InvalidDataError("No Matroska subtitle track has been selected"); CheckCancelled(); unsigned track, frame_size, flags; uint64_t start, end, position; @@ -241,6 +252,8 @@ class Demuxer::Impl final : public InputStream { return std::nullopt; std::vector data; + if (frame_size > limits.packet_size) + throw LimitError("Matroska subtitle packet exceeds configured limit"); if (!compressed) data = ReadBytes(position, frame_size); else { @@ -251,30 +264,40 @@ class Demuxer::Impl final : public InputStream { int count = cs_ReadData( compressed.get(), reinterpret_cast(buffer), sizeof buffer); if (count < 0) - throw ReadError(copy_string(cs_GetLastError(compressed.get()))); + throw InvalidDataError(copy_string(cs_GetLastError(compressed.get()))); if (!count) break; + if (data.size() + static_cast(count) > limits.decompressed_size) + throw LimitError("Decompressed Matroska subtitle packet exceeds configured limit"); data.insert(data.end(), buffer, buffer + count); } } + auto checked_time = [](uint64_t value, unsigned unknown_flag, unsigned flags) -> std::optional { + if (flags & unknown_flag) return std::nullopt; + if (value > static_cast(INT64_MAX)) + throw InvalidDataError("Matroska timestamp is out of range"); + return Timestamp{static_cast(value)}; + }; return SubtitlePacket{{track}, - {static_cast(start)}, - {static_cast(end)}, + checked_time(start, FRAME_UNKNOWN_START, flags), + checked_time(end, FRAME_UNKNOWN_END, flags), std::move(data)}; } - std::vector AttachmentBytes(uint64_t uid) { + std::vector AttachmentBytes(AttachmentId id) { auto found = std::find_if(attachment_locations.begin(), attachment_locations.end(), - [&](auto const &item) { return item.UID == uid; }); + [&](auto const &item) { return item.UID == id.value; }); if (found == attachment_locations.end()) - throw ReadError("Unknown Matroska attachment"); + throw InvalidDataError("Unknown Matroska attachment"); + if (found->Length > limits.attachment_size) + throw LimitError("Matroska attachment exceeds configured limit"); return ReadBytes(found->Position, found->Length); } }; -Demuxer::Demuxer(std::unique_ptr reader, CancelCheck cancelled) - : impl(std::make_unique(std::move(reader), std::move(cancelled))) {} +Demuxer::Demuxer(std::unique_ptr reader, CancelCheck cancelled, Limits limits) + : impl(std::make_unique(std::move(reader), std::move(cancelled), limits)) {} Demuxer::~Demuxer() = default; Demuxer::Demuxer(Demuxer &&) noexcept = default; Demuxer &Demuxer::operator=(Demuxer &&) noexcept = default; @@ -289,8 +312,8 @@ void Demuxer::SelectTrack(TrackId track) { impl->Select(track); } std::optional Demuxer::ReadPacket() { return impl->NextPacket(); } -std::vector Demuxer::ReadAttachment(uint64_t uid) { - return impl->AttachmentBytes(uid); +std::vector Demuxer::ReadAttachment(AttachmentId id) { + return impl->AttachmentBytes(id); } -} // namespace agi::matroska::proposed +} // namespace agi::matroska diff --git a/src/matroska_proposed.h b/src/matroska.h similarity index 64% rename from src/matroska_proposed.h rename to src/matroska.h index f1f2f6d085..2af3bd9245 100644 --- a/src/matroska_proposed.h +++ b/src/matroska.h @@ -17,11 +17,14 @@ #include #include -// This is an unused proposal for side-by-side API review. Nothing in Aegisub -// constructs these types, and this file is intentionally absent from the build. -namespace agi::matroska::proposed { +namespace agi::matroska { -DEFINE_EXCEPTION(ReadError, agi::Exception); +DEFINE_EXCEPTION(Error, agi::Exception); +DEFINE_EXCEPTION(IoError, Error); +DEFINE_EXCEPTION(TruncatedError, Error); +DEFINE_EXCEPTION(InvalidDataError, Error); +DEFINE_EXCEPTION(UnsupportedError, Error); +DEFINE_EXCEPTION(LimitError, Error); struct Timestamp { int64_t nanoseconds = 0; @@ -29,6 +32,12 @@ struct Timestamp { struct TrackId { uint32_t value = 0; + friend bool operator==(TrackId lhs, TrackId rhs) { return lhs.value == rhs.value; } +}; + +struct AttachmentId { + uint64_t value = 0; + friend bool operator==(AttachmentId lhs, AttachmentId rhs) { return lhs.value == rhs.value; } }; enum class SubtitleCodec { ass, ssa, srt, unsupported }; @@ -46,7 +55,7 @@ struct SubtitleTrack { }; struct Attachment { - uint64_t uid = 0; + AttachmentId id; std::string name; std::string description; std::string mime_type; @@ -55,27 +64,33 @@ struct Attachment { struct SubtitlePacket { TrackId track; - Timestamp start; - Timestamp end; + std::optional start; + std::optional end; std::vector data; }; +/// Cancellation callbacks are isolated from the C ABI: exceptions are treated as cancellation. using CancelCheck = std::function; +struct Limits { + size_t packet_size = 64 * 1024 * 1024; + size_t attachment_size = 256 * 1024 * 1024; + size_t decompressed_size = 64 * 1024 * 1024; +}; + /// Random-access input owned by Demuxer for its entire lifetime. class Reader { public: virtual ~Reader() = default; virtual uint64_t Size() const = 0; - /// Read up to size bytes at position. Returning zero means end of input. + /// Read up to size bytes at position. A short read is permitted. Returning zero at + /// Size() means EOF; returning zero before Size() is treated as truncation. virtual size_t Read(uint64_t position, void *buffer, size_t size) = 0; }; -/// Open a file-backed reader using Aegisub's normal mapped-file input. +/// Open a file-backed reader using Aegisub's mapped-file input. std::unique_ptr OpenFile(agi::fs::path const &filename); -/// Subtitle/attachment-only Matroska view proposed for Aegisub. -/// /// The demuxer owns its Reader and all parser state. Returned metadata and /// packet bytes are values, so none of them borrow storage from the parser. class Demuxer final { @@ -83,7 +98,7 @@ class Demuxer final { std::unique_ptr impl; public: - explicit Demuxer(std::unique_ptr reader, CancelCheck cancelled = {}); + explicit Demuxer(std::unique_ptr reader, CancelCheck cancelled = {}, Limits limits = {}); ~Demuxer(); Demuxer(Demuxer &&) noexcept; @@ -97,10 +112,11 @@ class Demuxer final { /// Select one subtitle track and rewind packet iteration to its beginning. void SelectTrack(TrackId track); - /// Return the next decoded packet, or nullopt at end of input. + /// Return the next decoded packet, or nullopt at end of input. On error or + /// cancellation the current packet is consumed; callers may retry to read the next packet. std::optional ReadPacket(); /// Copy an attachment from the input. The returned vector owns its bytes. - std::vector ReadAttachment(uint64_t uid); + std::vector ReadAttachment(AttachmentId id); }; -} // namespace agi::matroska::proposed +} // namespace agi::matroska diff --git a/src/meson.build b/src/meson.build index efd96981a9..415b154304 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,6 +2,7 @@ subdir('libresrc') aegisub_src = files( 'matroska_demux.cpp', + 'matroska.cpp', 'aegisublocale.cpp', 'ass_attachment.cpp', 'ass_dialogue.cpp', diff --git a/src/mkv_wrap.cpp b/src/mkv_wrap.cpp index ba56834951..45f565bf5a 100644 --- a/src/mkv_wrap.cpp +++ b/src/mkv_wrap.cpp @@ -1,339 +1,110 @@ // Copyright (c) 2004-2006, Rodrigo Braz Monteiro, Mike Matsnev -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of the Aegisub Group nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// Aegisub Project http://www.aegisub.org/ - -/// @file mkv_wrap.cpp -/// @brief High-level interface for obtaining various data from Matroska files -/// @ingroup video_input -/// +// Copyright (c) 2026 Aegisub contributors +// Permission to use, copy, modify, and distribute this software for any purpose +// with or without fee is hereby granted. #include "mkv_wrap.h" - #include "ass_file.h" #include "ass_parser.h" #include "compat.h" #include "dialog_progress.h" -#include "MatroskaParser.h" +#include "matroska.h" #include "options.h" #include "subtitle_format_srt.h" - #include -#include #include -#include - #include #include #include -#include #include -#include - -#include // Keep this last so wxUSE_CHOICEDLG is set. - -struct MkvStdIO final : InputStream { - agi::read_file_mapping file; - std::string error; - - static int Read(InputStream *st, uint64_t pos, void *buffer, int count) { - auto *self = static_cast(st); - if (pos >= self->file.size()) - return 0; - - auto remaining = self->file.size() - pos; - if (remaining < INT_MAX) - count = std::min(static_cast(remaining), count); - - if (count <= 0) - return 0; - - try { - auto data = self->file.read(pos, count); - if (buffer) - memcpy(buffer, data, count); - } - catch (agi::Exception const& e) { - self->error = e.GetMessage(); - return -1; - } - - return count; - } - - static int64_t Scan(InputStream *st, uint64_t start, unsigned signature) { - auto *self = static_cast(st); - try { - unsigned cmp = 0; - for (auto i : boost::irange(start, self->file.size())) { - int c = *self->file.read(i, 1); - cmp = ((cmp << 8) | c) & 0xffffffff; - if (cmp == signature) - return i - 4; - } - } - catch (agi::Exception const& e) { - self->error = e.GetMessage(); - } - - return -1; - } - - static int64_t Size(InputStream *st) { - return static_cast(st)->file.size(); - } - - MkvStdIO(agi::fs::path const& filename) : file(filename) { - read = &MkvStdIO::Read; - scan = &MkvStdIO::Scan; - getcachesize = [](InputStream *) -> unsigned int { return 16 * 1024 * 1024; }; - geterror = [](InputStream *st) -> const char * { return ((MkvStdIO *)st)->error.c_str(); }; - memalloc = [](InputStream *, size_t size) { return malloc(size); }; - memrealloc = [](InputStream *, void *mem, size_t size) { return realloc(mem, size); }; - memfree = [](InputStream *, void *mem) { free(mem); }; - progress = [](InputStream *, uint64_t, uint64_t) { return 1; }; - getfilesize = &MkvStdIO::Size; - } -}; - -static constexpr ptrdiff_t max_decompressed_subtitle_frame_bytes = 16 * 1024 * 1024; -static constexpr ptrdiff_t max_total_subtitle_bytes = 64 * 1024 * 1024; - -static bool read_subtitles(agi::ProgressSink *ps, MatroskaFile *file, MkvStdIO *input, bool srt, double totalTime, AssParser *parser, CompressedStream *cs) { - std::vector> subList; - ptrdiff_t totalSubtitleBytes = 0; - - // Load blocks - uint64_t startTime, endTime, filePos; - unsigned int rt, frameSize, frameFlags; - - std::vector uncompBuf(cs ? 256 : 0); - - SrtTagParser srtParser; - - while (mkv_ReadFrame(file, 0, &rt, &startTime, &endTime, &filePos, &frameSize, &frameFlags) == 0) { - if (ps->IsCancelled()) return true; - if (frameSize == 0) continue; - - std::string_view readBuf; - - if (cs) { - cs_NextFrame(cs, filePos, frameSize); - ptrdiff_t bytesRead = 0; - - while (true) { - if (bytesRead >= std::ssize(uncompBuf)) { - if (uncompBuf.size() >= max_decompressed_subtitle_frame_bytes) { - ps->Log(agi::format("Decompressed subtitle frame exceeds the %d MiB limit", max_decompressed_subtitle_frame_bytes / 1024 / 1024)); - return false; - } - uncompBuf.resize(std::min(std::ssize(uncompBuf) * 2, max_decompressed_subtitle_frame_bytes)); - } - - int res = cs_ReadData(cs, uncompBuf.data() + bytesRead, static_cast(std::ssize(uncompBuf) - bytesRead)); - if (res < 0) { - const char *err = cs_GetLastError(cs); - if (!err) err = "Unknown error"; - ps->Log("Failed to decompress subtitles: " + std::string(err)); - return false; - } - - bytesRead += res; - if (res == 0) - break; - } - - readBuf = std::string_view(uncompBuf.data(), bytesRead); - } else { - readBuf = std::string_view(input->file.read(filePos, frameSize), frameSize); - } - - if (std::ssize(readBuf) > max_total_subtitle_bytes - totalSubtitleBytes) { - ps->Log(agi::format("Matroska subtitle data exceeds the %d MiB limit", max_total_subtitle_bytes / 1024 / 1024)); - return false; - } - totalSubtitleBytes += std::ssize(readBuf); - - // Get start and end times - int64_t timecodeScaleLow = 1000000; - agi::Time subStart = startTime / timecodeScaleLow; - agi::Time subEnd = endTime / timecodeScaleLow; - - // Process SSA/ASS - if (!srt) { - auto first = readBuf.find(','); - if (first == readBuf.npos) continue; - auto second = readBuf.find(',', first + 1); - if (second == readBuf.npos) continue; +#include +#include + +namespace { +agi::Time to_ass_time(std::optional const& value) { + if (!value) return agi::Time{}; + if (value->nanoseconds < 0) throw MatroskaException("Negative Matroska subtitle timestamp"); + auto milliseconds = value->nanoseconds / 1000000; + if (milliseconds > std::numeric_limits::max()) throw MatroskaException("Matroska subtitle timestamp is out of range"); + return agi::Time(static_cast(milliseconds)); +} - subList.emplace_back( - boost::lexical_cast(readBuf.substr(0, first)), - agi::format("Dialogue: %d,%s,%s,%s" - , boost::lexical_cast(readBuf.substr(first + 1, second - (first + 1))) - , subStart.GetAssFormatted() - , subEnd.GetAssFormatted() - , readBuf.substr(second + 1))); +void read_subtitles(agi::ProgressSink *ps, agi::matroska::Demuxer& demuxer, + agi::matroska::SubtitleTrack const& track, double total_time, AssParser *parser) { + std::vector> subtitles; + SrtTagParser srt_parser; + for (;;) { + auto packet = demuxer.ReadPacket(); + if (!packet) break; + if (packet->data.empty()) continue; + auto start = to_ass_time(packet->start); + auto end = packet->end ? to_ass_time(packet->end) : start; + std::string_view data(reinterpret_cast(packet->data.data()), packet->data.size()); + if (track.codec != agi::matroska::SubtitleCodec::srt) { + auto first = data.find(','); + auto second = first == data.npos ? data.npos : data.find(',', first + 1); + if (second == data.npos) continue; + subtitles.emplace_back(boost::lexical_cast(data.substr(0, first)), + agi::format("Dialogue: %d,%s,%s,%s", boost::lexical_cast(data.substr(first + 1, second - first - 1)), + start.GetAssFormatted(), end.GetAssFormatted(), data.substr(second + 1))); } - // Process SRT else { - auto line = agi::format("Dialogue: 0,%s,%s,Default,,0,0,0,,%s" - , subStart.GetAssFormatted() - , subEnd.GetAssFormatted() - , srtParser.ToAss(std::string(readBuf))); - boost::replace_all(line, "\r\n", "\\N"); - boost::replace_all(line, "\r", "\\N"); - boost::replace_all(line, "\n", "\\N"); - - subList.emplace_back(subList.size(), std::move(line)); + auto line = agi::format("Dialogue: 0,%s,%s,Default,,0,0,0,,%s", start.GetAssFormatted(), end.GetAssFormatted(), srt_parser.ToAss(std::string(data))); + boost::replace_all(line, "\r\n", "\\N"); boost::replace_all(line, "\r", "\\N"); boost::replace_all(line, "\n", "\\N"); + subtitles.emplace_back(subtitles.size(), std::move(line)); } - - ps->SetProgress(startTime / timecodeScaleLow, totalTime); + ps->SetProgress(static_cast(start), total_time); } - - // Insert into file - sort(begin(subList), end(subList)); - for (auto order_value_pair : subList) - parser->AddLine(order_value_pair.second); - return true; + std::sort(subtitles.begin(), subtitles.end()); + for (auto const& item : subtitles) parser->AddLine(item.second); +} } void MatroskaWrapper::GetSubtitles(agi::fs::path const& filename, AssFile *target) { - MkvStdIO input(filename); - char err[2048]; - agi::scoped_holder file(mkv_Open(&input, err, sizeof(err)), mkv_Close); - if (!file) throw MatroskaException(err); - - // Get info - unsigned tracks = mkv_GetNumTracks(file); - std::vector tracksFound; - std::vector tracksNames; - - // Find tracks - for (auto track : boost::irange(0u, tracks)) { - auto trackInfo = mkv_GetTrackInfo(file, track); - if (trackInfo->Type != 0x11) continue; - - // Known subtitle format - std::string CodecID(trackInfo->CodecID); - if (CodecID == "S_TEXT/SSA" || CodecID == "S_TEXT/ASS" || CodecID == "S_TEXT/UTF8") { - tracksFound.push_back(track); - tracksNames.emplace_back(agi::format("%d (%s %s)", track, CodecID, trackInfo->Language)); - if (trackInfo->Name) { - tracksNames.back() += ": "; - tracksNames.back() += trackInfo->Name; + DialogProgress progress(nullptr, _("Parsing Matroska"), _("Reading subtitles from Matroska file.")); + AssFile imported; + std::exception_ptr failure; + progress.Run([&](agi::ProgressSink *ps) { + try { + agi::matroska::Demuxer demuxer(agi::matroska::OpenFile(filename), [ps] { return ps->IsCancelled(); }); + std::vector tracks; + std::vector names; + for (auto const& track : demuxer.SubtitleTracks()) if (track.codec != agi::matroska::SubtitleCodec::unsupported) { + tracks.push_back(&track); + names.emplace_back(agi::format("%d (%s %s)%s%s", track.id.value, track.codec_id, track.language, track.name.empty() ? "" : ": ", track.name)); + } + if (tracks.empty()) throw MatroskaException("File has no recognised subtitle tracks."); + size_t choice = 0; + if (tracks.size() > 1) { + int selected = wxGetSingleChoiceIndex(_("Choose which track to read:"), _("Multiple subtitle tracks found"), to_wx(names)); + if (selected < 0) throw agi::UserCancelException("cancelled"); + choice = static_cast(selected); } + auto const& track = *tracks[choice]; demuxer.SelectTrack(track.id); + bool srt = track.codec == agi::matroska::SubtitleCodec::srt; + AssParser parser(&imported, !srt); + if (!srt) { + std::string private_data(track.codec_private.begin(), track.codec_private.end()); + boost::char_separator separator("\r\n"); + for (auto const& line : boost::tokenizer>(private_data, separator)) parser.AddLine(line); + } + else imported.LoadDefault(false, OPT_GET("Subtitle Format/SRT/Default Style Catalog")->GetString()); + parser.AddLine("[Events]"); + double duration = demuxer.Duration() ? demuxer.Duration()->nanoseconds / 1000000.0 : 0.0; + read_subtitles(ps, demuxer, track, duration, &parser); } - } - - // No tracks found - if (tracksFound.empty()) - throw MatroskaException("File has no recognised subtitle tracks."); - - unsigned trackToRead; - // Only one track found - if (tracksFound.size() == 1) - trackToRead = tracksFound[0]; - // Pick a track - else { - int choice = wxGetSingleChoiceIndex(_("Choose which track to read:"), _("Multiple subtitle tracks found"), to_wx(tracksNames)); - if (choice == -1) - throw agi::UserCancelException("canceled"); - - trackToRead = tracksFound[choice]; - } - - // Picked track - mkv_SetTrackMask(file, ~(1 << trackToRead)); - auto trackInfo = mkv_GetTrackInfo(file, trackToRead); - std::string CodecID(trackInfo->CodecID); - bool srt = CodecID == "S_TEXT/UTF8"; - bool ssa = CodecID == "S_TEXT/SSA"; - - AssParser parser(target, !ssa); - - // Read private data if it's ASS/SSA - if (!srt) { - // Read raw data - std::string priv((const char *)trackInfo->CodecPrivate, trackInfo->CodecPrivateSize); - - // Load into file - boost::char_separator sep("\r\n"); - for (auto const& cur : boost::tokenizer>(priv, sep)) - parser.AddLine(cur); - } - // Load default if it's SRT - else - target->LoadDefault(false, OPT_GET("Subtitle Format/SRT/Default Style Catalog")->GetString()); - - parser.AddLine("[Events]"); - - agi::scoped_holder cs(nullptr, cs_Destroy); - if (trackInfo->CompEnabled) { - cs = cs_Create(file, trackToRead, err, sizeof(err)); - if (!cs) - throw MatroskaException(err); - } - - // Read timecode scale - auto segInfo = mkv_GetFileInfo(file); - int64_t timecodeScale = mkv_TruncFloat(trackInfo->TimecodeScale) * segInfo->TimecodeScale; - - // Progress bar - auto totalTime = double(segInfo->Duration) / timecodeScale; - DialogProgress progress(nullptr, _("Parsing Matroska"), _("Reading subtitles from Matroska file.")); - bool result; - progress.Run([&](agi::ProgressSink *ps) { result = read_subtitles(ps, file, &input, srt, totalTime, &parser, cs); }); - - if (!result) - throw MatroskaException("Failed to read subtitles"); + catch (...) { failure = std::current_exception(); } + }); + if (failure) std::rethrow_exception(failure); + target->swap(imported); } bool MatroskaWrapper::HasSubtitles(agi::fs::path const& filename) { - char err[2048]; + // Compatibility: project loading expects a boolean probe. Corrupt/unreadable files + // therefore return false here; explicit imports preserve their structured error. try { - MkvStdIO input(filename); - agi::scoped_holder file(mkv_Open(&input, err, sizeof(err)), mkv_Close); - if (!file) return false; - - // Find tracks - auto tracks = mkv_GetNumTracks(file); - for (auto track : boost::irange(0u, tracks)) { - auto trackInfo = mkv_GetTrackInfo(file, track); - - if (trackInfo->Type == 0x11) { - std::string CodecID(trackInfo->CodecID); - if (CodecID == "S_TEXT/SSA" || CodecID == "S_TEXT/ASS" || CodecID == "S_TEXT/UTF8") - return true; - } - } + agi::matroska::Demuxer demuxer(agi::matroska::OpenFile(filename)); + return std::any_of(demuxer.SubtitleTracks().begin(), demuxer.SubtitleTracks().end(), [](auto const& track) { return track.codec != agi::matroska::SubtitleCodec::unsupported; }); } - catch (...) { - // We don't care about why we couldn't read subtitles here - } - - return false; + catch (...) { return false; } } diff --git a/tests/meson.build b/tests/meson.build index 5dc9f77630..fd49847228 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -36,6 +36,7 @@ tests_src = [ 'tests/line_wrap.cpp', 'tests/lua_lfs.cpp', 'tests/mru.cpp', + 'tests/matroska.cpp', 'tests/option.cpp', 'tests/path.cpp', 'tests/signals.cpp', @@ -65,7 +66,7 @@ endif runner = executable( 'gtest-main', - tests_src, + tests_src, '../src/matroska.cpp', '../src/matroska_demux.cpp', include_directories: [tests_inc, libaegisub_inc], dependencies: [tests_deps, deps], cpp_args: tests_args + conf_defines, diff --git a/tests/tests/matroska.cpp b/tests/tests/matroska.cpp new file mode 100644 index 0000000000..94c8154e70 --- /dev/null +++ b/tests/tests/matroska.cpp @@ -0,0 +1,98 @@ +#include + +#include "matroska.h" +#include "util.h" + +#include +#include + +namespace { +using namespace agi::matroska; + +agi::fs::path fixture(char const *name) { + return util::test_data_dir() / "matroska" / "fixtures" / name; +} + +class ReaderProxy final : public Reader { + std::unique_ptr source; +public: + bool fail = false; + bool short_reads = false; + explicit ReaderProxy(agi::fs::path const& path) : source(OpenFile(path)) {} + uint64_t Size() const override { return source->Size(); } + size_t Read(uint64_t position, void *buffer, size_t size) override { + if (fail) throw IoError("injected reader failure"); + if (short_reads && size > 1) size = 1; + return source->Read(position, buffer, size); + } +}; +} + +TEST(Matroska, MetadataAndAttachments) { + Demuxer demuxer(OpenFile(fixture("subtitle-attachment.mkv"))); + ASSERT_FALSE(demuxer.SubtitleTracks().empty()); + ASSERT_EQ(1u, demuxer.Attachments().size()); + auto bytes = demuxer.ReadAttachment(demuxer.Attachments()[0].id); + EXPECT_EQ(demuxer.Attachments()[0].size, bytes.size()); + EXPECT_THROW(demuxer.ReadAttachment(AttachmentId{UINT64_MAX}), InvalidDataError); +} + +TEST(Matroska, ShortReadsAreSupported) { + auto reader = std::make_unique(fixture("subtitle-attachment.mkv")); + reader->short_reads = true; + Demuxer demuxer(std::move(reader)); + EXPECT_FALSE(demuxer.SubtitleTracks().empty()); +} + +TEST(Matroska, ReaderFailureIsContainedAtCBoundary) { + auto reader = std::make_unique(fixture("subtitle-attachment.mkv")); + reader->fail = true; + EXPECT_THROW(Demuxer(std::move(reader)), InvalidDataError); +} + +TEST(Matroska, CancellationCallbackExceptionsCannotCrossC) { + EXPECT_THROW(Demuxer(OpenFile(fixture("subtitle-attachment.mkv")), []() -> bool { throw 42; }), agi::UserCancelException); +} + +TEST(Matroska, RejectsUnsupportedTrackAndInvalidIds) { + Demuxer demuxer(OpenFile(fixture("video-only.mkv"))); + EXPECT_TRUE(demuxer.SubtitleTracks().empty()); + EXPECT_THROW(demuxer.SelectTrack(TrackId{UINT32_MAX}), InvalidDataError); +} + +TEST(Matroska, PacketAndAttachmentLimits) { + Limits limits; limits.packet_size = 1; limits.attachment_size = 1; limits.decompressed_size = 1; + Demuxer demuxer(OpenFile(fixture("subtitle-attachment.mkv")), {}, limits); + ASSERT_FALSE(demuxer.SubtitleTracks().empty()); + demuxer.SelectTrack(demuxer.SubtitleTracks()[0].id); + EXPECT_THROW(demuxer.ReadPacket(), LimitError); + ASSERT_FALSE(demuxer.Attachments().empty()); + EXPECT_THROW(demuxer.ReadAttachment(demuxer.Attachments()[0].id), LimitError); +} + +TEST(Matroska, DecompressionLimitAndFailedPacketConsumption) { + Limits limits; limits.decompressed_size = 1; + Demuxer compressed(OpenFile(fixture("compressed-zlib.mkv")), {}, limits); + compressed.SelectTrack(compressed.SubtitleTracks()[0].id); + EXPECT_THROW(compressed.ReadPacket(), LimitError); + + auto reader = std::make_unique(fixture("subtitle-attachment.mkv")); + auto *control = reader.get(); + Demuxer demuxer(std::move(reader)); + demuxer.SelectTrack(demuxer.SubtitleTracks()[0].id); + control->fail = true; + EXPECT_THROW(demuxer.ReadPacket(), IoError); + control->fail = false; + // The failed packet was already consumed before its payload read began. + EXPECT_NO_THROW((void)demuxer.ReadPacket()); +} + +TEST(Matroska, PacketsHaveCheckedOptionalTimestamps) { + Demuxer demuxer(OpenFile(fixture("compressed-zlib.mkv"))); + ASSERT_FALSE(demuxer.SubtitleTracks().empty()); + demuxer.SelectTrack(demuxer.SubtitleTracks()[0].id); + auto packet = demuxer.ReadPacket(); + ASSERT_TRUE(packet); + ASSERT_TRUE(packet->start); + EXPECT_GE(packet->start->nanoseconds, 0); +} From 5a8821ee371a91c7dd735c33c7cea728e937833c Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Thu, 27 Aug 2026 21:29:53 +0000 Subject: [PATCH 05/28] Fix native Matroska Meson integration --- meson.build | 4 ++-- subprojects/libebml.wrap | 8 ++++++++ subprojects/libmatroska.wrap | 8 ++++++++ subprojects/packagefiles/libebml/ebml_export.h | 2 ++ subprojects/packagefiles/libebml/meson.build | 17 +++++++++++++++++ .../packagefiles/libmatroska/matroska_export.h | 2 ++ .../packagefiles/libmatroska/meson.build | 15 +++++++++++++++ tests/matroska/behavior.cpp | 12 +++++++----- tests/matroska/check-fixtures.py | 2 +- tests/meson.build | 2 +- 10 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 subprojects/libebml.wrap create mode 100644 subprojects/libmatroska.wrap create mode 100644 subprojects/packagefiles/libebml/ebml_export.h create mode 100644 subprojects/packagefiles/libebml/meson.build create mode 100644 subprojects/packagefiles/libmatroska/matroska_export.h create mode 100644 subprojects/packagefiles/libmatroska/meson.build diff --git a/meson.build b/meson.build index 08fbf10d71..bb75000852 100644 --- a/meson.build +++ b/meson.build @@ -179,8 +179,8 @@ endif zlib_dep = dependency('zlib') deps += zlib_dep -libebml = dependency('libebml', version: '>=1.4.0') -libmatroska = dependency('libmatroska', version: '>=1.6.0') +libebml = dependency('libebml', version: '>=1.4.0', fallback: ['libebml', 'libebml_dep']) +libmatroska = dependency('libmatroska', version: '>=1.6.0', fallback: ['libmatroska', 'libmatroska_dep']) deps += [libebml, libmatroska] use_bundled_wx = force_all_fallbacks or force_fallback_for.contains('wxWidgets') diff --git a/subprojects/libebml.wrap b/subprojects/libebml.wrap new file mode 100644 index 0000000000..e2adf2add0 --- /dev/null +++ b/subprojects/libebml.wrap @@ -0,0 +1,8 @@ +[wrap-git] +url = https://github.com/Matroska-Org/libebml.git +revision = release-1.4.5 +depth = 1 +patch_directory = libebml + +[provide] +libebml = libebml_dep diff --git a/subprojects/libmatroska.wrap b/subprojects/libmatroska.wrap new file mode 100644 index 0000000000..400109881a --- /dev/null +++ b/subprojects/libmatroska.wrap @@ -0,0 +1,8 @@ +[wrap-git] +url = https://github.com/Matroska-Org/libmatroska.git +revision = release-1.7.1 +depth = 1 +patch_directory = libmatroska + +[provide] +libmatroska = libmatroska_dep diff --git a/subprojects/packagefiles/libebml/ebml_export.h b/subprojects/packagefiles/libebml/ebml_export.h new file mode 100644 index 0000000000..a80445845c --- /dev/null +++ b/subprojects/packagefiles/libebml/ebml_export.h @@ -0,0 +1,2 @@ +#pragma once +#define EBML_DLL_API diff --git a/subprojects/packagefiles/libebml/meson.build b/subprojects/packagefiles/libebml/meson.build new file mode 100644 index 0000000000..0b8d406226 --- /dev/null +++ b/subprojects/packagefiles/libebml/meson.build @@ -0,0 +1,17 @@ +project('libebml', 'cpp', version: '1.4.5', default_options: ['cpp_std=c++14']) + +sources = files( + 'src/Debug.cpp', 'src/EbmlBinary.cpp', 'src/EbmlContexts.cpp', 'src/EbmlCrc32.cpp', + 'src/EbmlDate.cpp', 'src/EbmlDummy.cpp', 'src/EbmlElement.cpp', 'src/EbmlFloat.cpp', + 'src/EbmlHead.cpp', 'src/EbmlMaster.cpp', 'src/EbmlSInteger.cpp', 'src/EbmlStream.cpp', + 'src/EbmlString.cpp', 'src/EbmlSubHead.cpp', 'src/EbmlUInteger.cpp', + 'src/EbmlUnicodeString.cpp', 'src/EbmlVersion.cpp', 'src/EbmlVoid.cpp', + 'src/IOCallback.cpp', 'src/MemIOCallback.cpp', 'src/MemReadIOCallback.cpp', + 'src/SafeReadIOCallback.cpp', 'src/StdIOCallback.cpp', +) + +ebml = static_library('ebml', sources, + include_directories: include_directories('.'), + cpp_args: host_machine.system() == 'windows' ? ['-D_CRT_SECURE_NO_WARNINGS'] : [], + install: false) +libebml_dep = declare_dependency(link_with: ebml, include_directories: include_directories('.')) diff --git a/subprojects/packagefiles/libmatroska/matroska_export.h b/subprojects/packagefiles/libmatroska/matroska_export.h new file mode 100644 index 0000000000..1be1401c7f --- /dev/null +++ b/subprojects/packagefiles/libmatroska/matroska_export.h @@ -0,0 +1,2 @@ +#pragma once +#define MATROSKA_DLL_API diff --git a/subprojects/packagefiles/libmatroska/meson.build b/subprojects/packagefiles/libmatroska/meson.build new file mode 100644 index 0000000000..b6955526be --- /dev/null +++ b/subprojects/packagefiles/libmatroska/meson.build @@ -0,0 +1,15 @@ +project('libmatroska', 'cpp', version: '1.7.1', default_options: ['cpp_std=c++14']) + +libebml_dep = dependency('libebml', version: '>=1.4.3') +sources = files( + 'src/FileKax.cpp', 'src/KaxAttached.cpp', 'src/KaxAttachments.cpp', 'src/KaxBlock.cpp', + 'src/KaxBlockData.cpp', 'src/KaxCluster.cpp', 'src/KaxContexts.cpp', 'src/KaxCues.cpp', + 'src/KaxCuesData.cpp', 'src/KaxInfoData.cpp', 'src/KaxSeekHead.cpp', 'src/KaxSegment.cpp', + 'src/KaxSemantic.cpp', 'src/KaxTracks.cpp', 'src/KaxVersion.cpp', +) +matroska = static_library('matroska', sources, + include_directories: include_directories('.'), dependencies: libebml_dep, + cpp_args: host_machine.system() == 'windows' ? ['-D_CRT_SECURE_NO_WARNINGS'] : [], + install: false) +libmatroska_dep = declare_dependency(link_with: matroska, + include_directories: include_directories('.'), dependencies: libebml_dep) diff --git a/tests/matroska/behavior.cpp b/tests/matroska/behavior.cpp index d7a7ae990b..55d89fb19a 100644 --- a/tests/matroska/behavior.cpp +++ b/tests/matroska/behavior.cpp @@ -13,13 +13,15 @@ struct FileInput final : InputStream { uint64_t size{}; explicit FileInput(char const *name) { file = std::fopen(name, "rb"); - if (!file) - return; - std::fseek(file, 0, SEEK_END); - size = std::ftell(file); - std::fseek(file, 0, SEEK_SET); + if (file) { + std::fseek(file, 0, SEEK_END); + size = std::ftell(file); + std::fseek(file, 0, SEEK_SET); + } read = [](InputStream *s, uint64_t p, void *b, int n) { auto &i = *static_cast(s); + if (!i.file) + return 0; if (p >= i.size) return 0; n = std::min(n, i.size - p); diff --git a/tests/matroska/check-fixtures.py b/tests/matroska/check-fixtures.py index 9953b61e0e..dfe1eb6d62 100755 --- a/tests/matroska/check-fixtures.py +++ b/tests/matroska/check-fixtures.py @@ -8,5 +8,5 @@ expected=case.with_suffix('.behavior').read_text() if got!=expected: raise SystemExit(f'{case.name}: Matroska behavior changed\n{got}') missing=subprocess.run([program,root/'does-not-exist.mkv'],text=True,stdout=subprocess.PIPE) -if missing.stdout!='error\tinvalid Matroska input stream\n': +if missing.returncode != 1 or missing.stdout != 'error\tEBML header not found\n': raise SystemExit(f'missing input: unsafe or unexpected result\n{missing.stdout}') diff --git a/tests/meson.build b/tests/meson.build index fd49847228..cce379c703 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -53,7 +53,7 @@ tests_src = [ 'tests/ycbcr.cpp', ] -tests_inc = include_directories('support') +tests_inc = include_directories('support', '../src') tests_args = [] From e8077c8847de128dc78ea432f450103439506cf1 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Thu, 27 Aug 2026 21:55:12 +0000 Subject: [PATCH 06/28] Use WrapDB for native Matroska fallbacks --- subprojects/libebml.wrap | 15 ++++++++++----- subprojects/libmatroska.wrap | 15 ++++++++++----- subprojects/packagefiles/libebml/ebml_export.h | 2 -- subprojects/packagefiles/libebml/meson.build | 17 ----------------- .../packagefiles/libmatroska/matroska_export.h | 2 -- .../packagefiles/libmatroska/meson.build | 15 --------------- 6 files changed, 20 insertions(+), 46 deletions(-) delete mode 100644 subprojects/packagefiles/libebml/ebml_export.h delete mode 100644 subprojects/packagefiles/libebml/meson.build delete mode 100644 subprojects/packagefiles/libmatroska/matroska_export.h delete mode 100644 subprojects/packagefiles/libmatroska/meson.build diff --git a/subprojects/libebml.wrap b/subprojects/libebml.wrap index e2adf2add0..1f78d2cf44 100644 --- a/subprojects/libebml.wrap +++ b/subprojects/libebml.wrap @@ -1,8 +1,13 @@ -[wrap-git] -url = https://github.com/Matroska-Org/libebml.git -revision = release-1.4.5 -depth = 1 -patch_directory = libebml +[wrap-file] +directory = libebml-release-1.4.5 +source_url = https://github.com/Matroska-Org/libebml/archive/refs/tags/release-1.4.5.tar.gz +source_filename = libebml-release-1.4.5.tar.gz +source_hash = 86c99573cd0957884f26547d1a8fa0c979e4d6d57484dfd387345846e6720f49 +patch_filename = libebml_1.4.5-3_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libebml_1.4.5-3/get_patch +patch_hash = d8a188b12e288c49fe7f738e135cae86b3c09451b4de9ad4ee2ac56478e7640c +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libebml_1.4.5-3/libebml-release-1.4.5.tar.gz +wrapdb_version = 1.4.5-3 [provide] libebml = libebml_dep diff --git a/subprojects/libmatroska.wrap b/subprojects/libmatroska.wrap index 400109881a..3ab3c27e53 100644 --- a/subprojects/libmatroska.wrap +++ b/subprojects/libmatroska.wrap @@ -1,8 +1,13 @@ -[wrap-git] -url = https://github.com/Matroska-Org/libmatroska.git -revision = release-1.7.1 -depth = 1 -patch_directory = libmatroska +[wrap-file] +directory = libmatroska-release-1.7.1 +source_url = https://github.com/Matroska-Org/libmatroska/archive/refs/tags/release-1.7.1.tar.gz +source_filename = libmatroska-release-1.7.1.tar.gz +source_hash = 64763443947833e6c17f1f555f4bb0df6c9f91881810d9d5e0f0bad3622d308b +patch_filename = libmatroska_1.7.1-5_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libmatroska_1.7.1-5/get_patch +patch_hash = a4bf967bf7312f0129d4ce4b000a8f3fe254dce1e74eb42bd0d3dbdafb28e136 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libmatroska_1.7.1-5/libmatroska-release-1.7.1.tar.gz +wrapdb_version = 1.7.1-5 [provide] libmatroska = libmatroska_dep diff --git a/subprojects/packagefiles/libebml/ebml_export.h b/subprojects/packagefiles/libebml/ebml_export.h deleted file mode 100644 index a80445845c..0000000000 --- a/subprojects/packagefiles/libebml/ebml_export.h +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -#define EBML_DLL_API diff --git a/subprojects/packagefiles/libebml/meson.build b/subprojects/packagefiles/libebml/meson.build deleted file mode 100644 index 0b8d406226..0000000000 --- a/subprojects/packagefiles/libebml/meson.build +++ /dev/null @@ -1,17 +0,0 @@ -project('libebml', 'cpp', version: '1.4.5', default_options: ['cpp_std=c++14']) - -sources = files( - 'src/Debug.cpp', 'src/EbmlBinary.cpp', 'src/EbmlContexts.cpp', 'src/EbmlCrc32.cpp', - 'src/EbmlDate.cpp', 'src/EbmlDummy.cpp', 'src/EbmlElement.cpp', 'src/EbmlFloat.cpp', - 'src/EbmlHead.cpp', 'src/EbmlMaster.cpp', 'src/EbmlSInteger.cpp', 'src/EbmlStream.cpp', - 'src/EbmlString.cpp', 'src/EbmlSubHead.cpp', 'src/EbmlUInteger.cpp', - 'src/EbmlUnicodeString.cpp', 'src/EbmlVersion.cpp', 'src/EbmlVoid.cpp', - 'src/IOCallback.cpp', 'src/MemIOCallback.cpp', 'src/MemReadIOCallback.cpp', - 'src/SafeReadIOCallback.cpp', 'src/StdIOCallback.cpp', -) - -ebml = static_library('ebml', sources, - include_directories: include_directories('.'), - cpp_args: host_machine.system() == 'windows' ? ['-D_CRT_SECURE_NO_WARNINGS'] : [], - install: false) -libebml_dep = declare_dependency(link_with: ebml, include_directories: include_directories('.')) diff --git a/subprojects/packagefiles/libmatroska/matroska_export.h b/subprojects/packagefiles/libmatroska/matroska_export.h deleted file mode 100644 index 1be1401c7f..0000000000 --- a/subprojects/packagefiles/libmatroska/matroska_export.h +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -#define MATROSKA_DLL_API diff --git a/subprojects/packagefiles/libmatroska/meson.build b/subprojects/packagefiles/libmatroska/meson.build deleted file mode 100644 index b6955526be..0000000000 --- a/subprojects/packagefiles/libmatroska/meson.build +++ /dev/null @@ -1,15 +0,0 @@ -project('libmatroska', 'cpp', version: '1.7.1', default_options: ['cpp_std=c++14']) - -libebml_dep = dependency('libebml', version: '>=1.4.3') -sources = files( - 'src/FileKax.cpp', 'src/KaxAttached.cpp', 'src/KaxAttachments.cpp', 'src/KaxBlock.cpp', - 'src/KaxBlockData.cpp', 'src/KaxCluster.cpp', 'src/KaxContexts.cpp', 'src/KaxCues.cpp', - 'src/KaxCuesData.cpp', 'src/KaxInfoData.cpp', 'src/KaxSeekHead.cpp', 'src/KaxSegment.cpp', - 'src/KaxSemantic.cpp', 'src/KaxTracks.cpp', 'src/KaxVersion.cpp', -) -matroska = static_library('matroska', sources, - include_directories: include_directories('.'), dependencies: libebml_dep, - cpp_args: host_machine.system() == 'windows' ? ['-D_CRT_SECURE_NO_WARNINGS'] : [], - install: false) -libmatroska_dep = declare_dependency(link_with: matroska, - include_directories: include_directories('.'), dependencies: libebml_dep) From 411492d60afc706a58f9000cc31cab85b4b63914 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Thu, 27 Aug 2026 22:00:49 +0000 Subject: [PATCH 07/28] Clarify Matroska behavior test purpose --- tests/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/meson.build b/tests/meson.build index cce379c703..1984f1fab0 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -88,7 +88,7 @@ test_setup = custom_target('setup-test-data', test('gtest main', runner, env: {'AEGISUB_TEST_DATA_DIR': meson.current_source_dir()}, depends: test_setup) test('luajit-52', executable('luajit-52', 'tests/luajit_52.c', dependencies: luajit)) -# Machine-readable behavioral seam used while replacing the Matroska backend. +# Deterministic integration test for Matroska demux metadata, attachments, frames, compression, and errors. matroska_behavior = executable('matroska-behavior', 'matroska/behavior.cpp', '../src/matroska_demux.cpp', include_directories: include_directories('../src'), From 68cbbffa83827d3b91bf281991669ba530c6f6a4 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Thu, 27 Aug 2026 22:19:37 +0000 Subject: [PATCH 08/28] Remove legacy Haali Matroska parser --- .editorconfig | 6 - .gitattributes | 2 - LICENCE | 3 +- src/MatroskaParser.c | 3350 -------------------- src/include/agi_pre.h | 3 - src/matroska.cpp | 2 +- src/matroska_demux.cpp | 6 +- src/{MatroskaParser.h => matroska_demux.h} | 70 +- tests/matroska/behavior.cpp | 2 +- 9 files changed, 30 insertions(+), 3414 deletions(-) delete mode 100644 src/MatroskaParser.c rename src/{MatroskaParser.h => matroska_demux.h} (83%) diff --git a/.editorconfig b/.editorconfig index 8fa69a5797..0ca45473ef 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,12 +6,6 @@ indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true -[src/MatroskaParser.{c,h}] -indent_style = tab -indent_size = 2 -tab_width = 8 -trim_trailing_whitespace = false - [src/libresrc/**default_*.json] indent_style = tab indent_size = 2 diff --git a/.gitattributes b/.gitattributes index b60332b966..554c5d6e5e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,8 +6,6 @@ subprojects/csri/meson.build -linguist-vendored subprojects/iconv/meson.build -linguist-vendored subprojects/luabins/meson.build -linguist-vendored -src/MatroskaParser.* linguist-vendored - libaegisub/lua/modules/lpeg.* linguist-vendored automation/include/moonscript.lua linguist-vendored diff --git a/LICENCE b/LICENCE index 64032a465f..e6bce2ad2f 100644 --- a/LICENCE +++ b/LICENCE @@ -36,9 +36,8 @@ follows: src/gl/ - MIT license. See src/gl/glext.h -src/MatroskaParser.(c|h) +src/matroska_demux.h - Licensed to BSDL with permission from the author. universalchardet/ - MPL 1.1 - diff --git a/src/MatroskaParser.c b/src/MatroskaParser.c deleted file mode 100644 index 31d5482054..0000000000 --- a/src/MatroskaParser.c +++ /dev/null @@ -1,3350 +0,0 @@ -/* - * Copyright (c) 2004-2009 Mike Matsnev. All Rights Reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice immediately at the beginning of the file, without modification, - * this list of conditions, and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Absolutely no warranty of function or purpose is made by the author - * Mike Matsnev. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// Aegisub Project http://www.aegisub.org/ - -/// @file MatroskaParser.c -/// @brief Haali's low-level Matroska-parsing library -/// @ingroup video_input -/// - - */ - -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#define inline __inline - -#include -#endif - -#ifndef EVCBUG -#define EVCBUG -#endif - -#include "MatroskaParser.h" - -#ifdef HAVE_ALLOCA_H -#include -#elif defined(HAVE_MALLOC_H) -#include -#endif /* HAVE_ALLOCA_H */ - -#ifdef HAVE_UNDERLINE_ALLOCA -#define alloca _alloca -#endif - -#ifdef MATROSKA_COMPRESSION_SUPPORT -#include -#endif - -#define EBML_VERSION 1 -#define EBML_MAX_ID_LENGTH 4 -#define EBML_MAX_SIZE_LENGTH 8 -#define MATROSKA_VERSION 2 -#define MATROSKA_DOCTYPE "matroska" - -#define MAX_STRING_LEN 1023 -#define QSEGSIZE 512 -#define MAX_TRACKS 32 -#define MAX_READAHEAD (256*1024) - -#define MAXCLUSTER (64*1048576) -#define MAXFRAME (4*1048576) - -#ifdef WIN32 -#define LL(x) x##i64 -#define ULL(x) x##ui64 -#else -#define LL(x) x##ll -#define ULL(x) x##ull -#endif - -#define MAXU64 ULL(0xffffffffffffffff) -#define ONE ULL(1) - -// compatibility -static char *mystrdup(struct InputStream *is,const char *src) { - size_t len; - char *dst; - - if (src==NULL) - return NULL; - - len = strlen(src); - dst = is->memalloc(is,len+1); - if (dst==NULL) - return NULL; - - memcpy(dst,src,len+1); - - return dst; -} - -static void mystrlcpy(char *dst,const char *src,unsigned size) { - unsigned i; - - for (i=0;i+1 0) - *dest = '\0'; - return; - } - - while (*fmt && dest < de) - switch (state) { - case 0: - if (*fmt == '%') { - ++fmt; - state = 1; - width = zero = neg = ll = 0; - } else - *dest++ = *fmt++; - break; - case 1: - if (*fmt == '-') { - neg = 1; - ++fmt; - state = 2; - break; - } - if (*fmt == '0') - zero = 1; - state = 2; // fallthrough - case 2: - if (*fmt >= '0' && *fmt <= '9') { - width = width * 10 + *fmt++ - '0'; - break; - } - state = 3; // fallthrough - case 3: - if (*fmt == 'l') { - ++ll; - ++fmt; - break; - } - state = 4; // fallthrough - case 4: - switch (*fmt) { - case 's': - myvsnprintf_string(&dest,de,va_arg(ap,const char *)); - break; - case 'd': - switch (ll) { - case 0: - myvsnprintf_int(&dest,de,width,zero,neg,10,'a',va_arg(ap,int)); - break; - case 1: - myvsnprintf_int(&dest,de,width,zero,neg,10,'a',va_arg(ap,long)); - break; - case 2: - myvsnprintf_int(&dest,de,width,zero,neg,10,'a',va_arg(ap,int64_t)); - break; - } - break; - case 'u': - switch (ll) { - case 0: - myvsnprintf_uint(&dest,de,width,zero,neg,10,'a',va_arg(ap,unsigned int)); - break; - case 1: - myvsnprintf_uint(&dest,de,width,zero,neg,10,'a',va_arg(ap,unsigned long)); - break; - case 2: - myvsnprintf_uint(&dest,de,width,zero,neg,10,'a',va_arg(ap,uint64_t)); - break; - } - break; - case 'x': - switch (ll) { - case 0: - myvsnprintf_uint(&dest,de,width,zero,neg,16,'a',va_arg(ap,unsigned int)); - break; - case 1: - myvsnprintf_uint(&dest,de,width,zero,neg,16,'a',va_arg(ap,unsigned long)); - break; - case 2: - myvsnprintf_uint(&dest,de,width,zero,neg,16,'a',va_arg(ap,uint64_t)); - break; - } - break; - case 'X': - switch (ll) { - case 0: - myvsnprintf_uint(&dest,de,width,zero,neg,16,'A',va_arg(ap,unsigned int)); - break; - case 1: - myvsnprintf_uint(&dest,de,width,zero,neg,16,'A',va_arg(ap,unsigned long)); - break; - case 2: - myvsnprintf_uint(&dest,de,width,zero,neg,16,'A',va_arg(ap,uint64_t)); - break; - } - break; - default: - break; - } - ++fmt; - state = 0; - break; - default: - state = 0; - break; - } - *dest = '\0'; -} - -static void errorjmp(MatroskaFile *mf,const char *fmt, ...) { - va_list ap; - - mf->cache->memfree(mf->cache, mf->cpbuf); - mf->cpbuf = NULL; - - va_start(ap, fmt); - myvsnprintf(mf->errmsg,sizeof(mf->errmsg),fmt,ap); - va_end(ap); - - mf->flags |= MPF_ERROR; - - longjmp(mf->jb,1); -} - -/////////////////////////////////////////////////////////////////////////// -// arrays -static void *ArrayAlloc(MatroskaFile *mf,void **base, - unsigned *cur,unsigned *max,unsigned elem_size) -{ - if (*cur>=*max) { - void *np; - unsigned newsize = *max * 2; - if (newsize==0) - newsize = 1; - - np = mf->cache->memrealloc(mf->cache,*base,newsize*elem_size); - if (np==NULL) - errorjmp(mf,"Out of memory in ArrayAlloc"); - - *base = np; - *max = newsize; - } - - return (char*)*base + elem_size * (*cur)++; -} - -static void ArrayReleaseMemory(MatroskaFile *mf,void **base, - unsigned cur,unsigned *max,unsigned elem_size) -{ - if (cur<*max) { - void *np = mf->cache->memrealloc(mf->cache,*base,cur*elem_size); - *base = np; - *max = cur; - } -} - - -#define ASGET(f,s,name) ArrayAlloc((f),(void**)&(s)->name,&(s)->n##name,&(s)->n##name##Size,sizeof(*((s)->name))) -#define AGET(f,name) ArrayAlloc((f),(void**)&(f)->name,&(f)->n##name,&(f)->n##name##Size,sizeof(*((f)->name))) -#define ARELEASE(f,s,name) ArrayReleaseMemory((f),(void**)&(s)->name,(s)->n##name,&(s)->n##name##Size,sizeof(*((s)->name))) - -/////////////////////////////////////////////////////////////////////////// -// queues -static struct QueueEntry *QPut(struct Queue *q,struct QueueEntry *qe) { - if (q->tail) - q->tail->next = qe; - qe->next = NULL; - q->tail = qe; - if (q->head==NULL) - q->head = qe; - - return qe; -} - -static struct QueueEntry *QGet(struct Queue *q) { - struct QueueEntry *qe = q->head; - if (qe == NULL) - return NULL; - q->head = qe->next; - if (q->tail == qe) - q->tail = NULL; - return qe; -} - -static struct QueueEntry *QAlloc(MatroskaFile *mf) { - struct QueueEntry *qe,**qep; - if (mf->QFreeList == NULL) { - unsigned i; - - qep = AGET(mf,QBlocks); - - *qep = mf->cache->memalloc(mf->cache,QSEGSIZE * sizeof(*qe)); - if (*qep == NULL) - errorjmp(mf,"Ouf of memory"); - - qe = *qep; - - for (i=0;iQFreeList = qe; - } - - qe = mf->QFreeList; - mf->QFreeList = qe->next; - - return qe; -} - -static inline void QFree(MatroskaFile *mf,struct QueueEntry *qe) { - qe->next = mf->QFreeList; - mf->QFreeList = qe; -} - -// fill the buffer at current position -static void fillbuf(MatroskaFile *mf) { - int rd; - - // advance buffer pointers - mf->bufbase += mf->buflen; - mf->buflen = mf->bufpos = 0; - - // get the relevant page - rd = mf->cache->read(mf->cache, mf->bufbase, mf->inbuf, IBSZ); - if (rd<0) - errorjmp(mf,"I/O Error: %s",mf->cache->geterror(mf->cache)); - - mf->buflen = rd; -} - -// fill the buffer and return next char -static int nextbuf(MatroskaFile *mf) { - fillbuf(mf); - - if (mf->bufpos < mf->buflen) - return (unsigned char)(mf->inbuf[mf->bufpos++]); - - return EOF; -} - -static inline int readch(MatroskaFile *mf) { - return mf->bufpos < mf->buflen ? (unsigned char)(mf->inbuf[mf->bufpos++]) : nextbuf(mf); -} - -static inline uint64_t filepos(MatroskaFile *mf) { - return mf->bufbase + mf->bufpos; -} - -static void readbytes(MatroskaFile *mf,void *buffer,uint64_t len) { - char *cp = buffer; - - if (mf->buflen < mf->bufpos) - errorjmp(mf,"Unreachable: buffer position larger than buffer length : %d > %d",mf->buflen,mf->bufpos); - - uint64_t nb = mf->buflen - mf->bufpos; - - if (nb > len) - nb = len; - - memcpy(cp, mf->inbuf + mf->bufpos, nb); - mf->bufpos += nb; - len -= nb; - cp += nb; - - if (len>0) { - mf->bufbase += mf->buflen; - mf->bufpos = mf->buflen = 0; - - nb = mf->cache->read(mf->cache, mf->bufbase, cp, len); - if (nb<0) - errorjmp(mf,"I/O Error: %s",mf->cache->geterror(mf->cache)); - if (nb != len) - errorjmp(mf,"Short read: got %d bytes of %d",nb,len); - mf->bufbase += len; - } -} - -static void skipbytes(MatroskaFile *mf,uint64_t len) { - if (mf->buflen < mf->bufpos) - errorjmp(mf,"Unreachable: buffer position larger than buffer length : %d > %d",mf->buflen,mf->bufpos); - - uint64_t nb = mf->buflen - mf->bufpos; - - if (nb > len) - nb = len; - - mf->bufpos += nb; - len -= nb; - - if (len>0) { - mf->bufbase += mf->buflen; - mf->bufpos = mf->buflen = 0; - - mf->bufbase += len; - } -} - -static void seek(MatroskaFile *mf,uint64_t pos) { - // see if pos is inside buffer - if (pos>=mf->bufbase && posbufbase+mf->buflen) - mf->bufpos = (unsigned)(pos - mf->bufbase); - else { - // invalidate buffer and set pointer - mf->bufbase = pos; - mf->buflen = mf->bufpos = 0; - } -} - -/////////////////////////////////////////////////////////////////////////// -// floating point -static inline MKFLOAT mkfi(int i) { -#ifdef MATROSKA_INTEGER_ONLY - MKFLOAT f; - f.v = (int64_t)i << 32; - return f; -#else - return i; -#endif -} - -static inline int64_t mul3(MKFLOAT scale,int64_t tc) { -#ifdef MATROSKA_INTEGER_ONLY - // x1 x0 - // y1 y0 - // -------------- - // x0*y0 - // x1*y0 - // x0*y1 - // x1*y1 - // -------------- - // .. r1 r0 .. - // - // r = ((x0*y0) >> 32) + (x1*y0) + (x0*y1) + ((x1*y1) << 32) - unsigned x0,x1,y0,y1; - uint64_t p; - char sign = 0; - - if (scale.v < 0) - sign = !sign, scale.v = -scale.v; - if (tc < 0) - sign = !sign, tc = -tc; - - x0 = (unsigned)scale.v; - x1 = (unsigned)((uint64_t)scale.v >> 32); - y0 = (unsigned)tc; - y1 = (unsigned)((uint64_t)tc >> 32); - - p = (uint64_t)x0*y0 >> 32; - p += (uint64_t)x0*y1; - p += (uint64_t)x1*y0; - p += (uint64_t)(x1*y1) << 32; - - return p; -#else - return (int64_t)(scale * tc); -#endif -} - -/////////////////////////////////////////////////////////////////////////// -// EBML support -static int readID(MatroskaFile *mf) { - int c1,c2,c3,c4; - - c1 = readch(mf); - if (c1 == EOF) - return EOF; - - if (c1 & 0x80) - return c1; - - if ((c1 & 0xf0) == 0) - errorjmp(mf,"Invalid first byte of EBML ID: %02X",c1); - - c2 = readch(mf); - if (c2 == EOF) -fail: - errorjmp(mf,"Got EOF while reading EBML ID"); - - if ((c1 & 0xc0) == 0x40) - return (c1<<8) | c2; - - c3 = readch(mf); - if (c3 == EOF) - goto fail; - - if ((c1 & 0xe0) == 0x20) - return (c1<<16) | (c2<<8) | c3; - - c4 = readch(mf); - if (c4 == EOF) - goto fail; - - if ((c1 & 0xf0) == 0x10) - return (c1<<24) | (c2<<16) | (c3<<8) | c4; - - return 0; // NOT REACHED -} - -static uint64_t readVLUIntImp(MatroskaFile *mf,int *mask) { - int c,d,m; - uint64_t v = 0; - - c = readch(mf); - if (c == EOF) - return 0; // XXX should errorjmp()? - - if (c == 0) - errorjmp(mf,"Invalid first byte of EBML integer: 0"); - - for (m=0;;++m) { - if (c & (0x80 >> m)) { - c &= 0x7f >> m; - if (mask) - *mask = m; - return v | ((uint64_t)c << m*8); - } - d = readch(mf); - if (d == EOF) - errorjmp(mf,"Got EOF while reading EBML unsigned integer"); - v = (v<<8) | d; - } - // NOT REACHED -} - -static inline uint64_t readVLUInt(MatroskaFile *mf) { - return readVLUIntImp(mf,NULL); -} - -static uint64_t readSize(MatroskaFile *mf) { - int m; - uint64_t v = readVLUIntImp(mf,&m); - - // see if it's unspecified - if (v == (MAXU64 >> (57-m*7))) - errorjmp(mf,"Unspecified element size is not supported here."); - - return v; -} - -static inline int64_t readVLSInt(MatroskaFile *mf) { - static int64_t bias[8] = { (ONE<<6)-1, (ONE<<13)-1, (ONE<<20)-1, (ONE<<27)-1, - (ONE<<34)-1, (ONE<<41)-1, (ONE<<48)-1, (ONE<<55)-1 }; - - int m; - int64_t v = readVLUIntImp(mf,&m); - - return v - bias[m]; -} - -static uint64_t readUInt(MatroskaFile *mf,unsigned int len) { - int c; - unsigned int m = len; - uint64_t v = 0; - - if (len==0) - return v; - if (len>8) - errorjmp(mf,"Unsupported integer size in readUInt: %u",len); - - do { - c = readch(mf); - if (c == EOF) - errorjmp(mf,"Got EOF while reading EBML unsigned integer"); - v = (v<<8) | c; - } while (--m); - - return v; -} - -static inline int64_t readSInt(MatroskaFile *mf,unsigned int len) { - int64_t v = readUInt(mf,(unsigned)len); - int s = 64 - (len<<3); - return (v << s) >> s; -} - -static MKFLOAT readFloat(MatroskaFile *mf,unsigned int len) { -#ifdef MATROSKA_INTEGER_ONLY - MKFLOAT f; - int shift; -#else - union { - unsigned int ui; - uint64_t ull; - float f; - double d; - } u; -#endif - - if (len!=4 && len!=8) - errorjmp(mf,"Invalid float size in readFloat: %u",len); - -#ifdef MATROSKA_INTEGER_ONLY - if (len == 4) { - unsigned ui = (unsigned)readUInt(mf,(unsigned)len); - f.v = (ui & 0x7fffff) | 0x800000; - if (ui & 0x80000000) - f.v = -f.v; - shift = (ui >> 23) & 0xff; - if (shift == 0) // assume 0 -zero: - shift = 0, f.v = 0; - else if (shift == 255) -inf: - if (ui & 0x80000000) - f.v = LL(0x8000000000000000); - else - f.v = LL(0x7fffffffffffffff); - else { - shift += -127 + 9; - if (shift > 39) - goto inf; -shift: - if (shift < 0) - f.v = f.v >> -shift; - else if (shift > 0) - f.v = f.v << shift; - } - } else if (len == 8) { - uint64_t ui = readUInt(mf,(unsigned)len); - f.v = (ui & LL(0xfffffffffffff)) | LL(0x10000000000000); - if (ui & 0x80000000) - f.v = -f.v; - shift = (int)((ui >> 52) & 0x7ff); - if (shift == 0) // assume 0 - goto zero; - else if (shift == 2047) - goto inf; - else { - shift += -1023 - 20; - if (shift > 10) - goto inf; - goto shift; - } - } - - return f; -#else - if (len==4) { - u.ui = (unsigned int)readUInt(mf,(unsigned)len); - return u.f; - } - - if (len==8) { - u.ull = readUInt(mf,(unsigned)len); - return u.d; - } - - return 0; -#endif -} - -static void readString(MatroskaFile *mf,uint64_t len,char *buffer,int buflen) { - unsigned int nread; - - if (buflen<1) - errorjmp(mf,"Invalid buffer size in readString: %d",buflen); - - nread = buflen - 1; - - if (nread > len) - nread = len; - - readbytes(mf,buffer,nread); - len -= nread; - - if (len>0) - skipbytes(mf,len); - - buffer[nread] = '\0'; -} - -static void readLangCC(MatroskaFile *mf, uint64_t len, char lcc[4]) { - uint64_t todo = len > 3 ? 3 : len; - - lcc[0] = lcc[1] = lcc[2] = lcc[3] = 0; - readbytes(mf, lcc, todo); - skipbytes(mf, len - todo); -} - -/////////////////////////////////////////////////////////////////////////// -// file parser -#define FOREACH(f,tl) \ - { \ - uint64_t tmplen = (tl); \ - { \ - uint64_t start = filepos(f); \ - uint64_t cur,len; \ - int id; \ - for (;;) { \ - cur = filepos(mf); \ - if (cur == start + tmplen) \ - break; \ - id = readID(f); \ - if (id==EOF) \ - errorjmp(mf,"Unexpected EOF while reading EBML container"); \ - len = readSize(mf); \ - switch (id) { - -#define ENDFOR1(f) \ - default: \ - skipbytes(f,len); \ - break; \ - } -#define ENDFOR2() \ - } \ - } \ - } - -#define ENDFOR(f) ENDFOR1(f) ENDFOR2() - -#define myalloca(f,c) alloca(c) -#define STRGETF(f,v,len,func) \ - { \ - char *TmpVal; \ - unsigned TmpLen = (len)>MAX_STRING_LEN ? MAX_STRING_LEN : (unsigned)(len); \ - TmpVal = func(f->cache,TmpLen+1); \ - if (TmpVal == NULL) \ - errorjmp(mf,"Out of memory"); \ - readString(f,len,TmpVal,TmpLen+1); \ - (v) = TmpVal; \ - } - -#define STRGETA(f,v,len) STRGETF(f,v,len,myalloca) -#define STRGETM(f,v,len) STRGETF(f,v,len,f->cache->memalloc) - -/* Removed because it's not used. -static int IsWritingApp(MatroskaFile *mf,const char *str) { - const char *cp = mf->Seg.WritingApp; - if (!cp) - return 0; - - while (*str && *str++==*cp++) ; - - return !*str; -} -*/ -static void parseEBML(MatroskaFile *mf,uint64_t toplen) { - uint64_t v; - char buf[32]; - - FOREACH(mf,toplen) - case 0x4286: // Version - v = readUInt(mf,(unsigned)len); - break; - case 0x42f7: // ReadVersion - v = readUInt(mf,(unsigned)len); - if (v > EBML_VERSION) - errorjmp(mf,"File requires version %d EBML parser",(int)v); - break; - case 0x42f2: // MaxIDLength - v = readUInt(mf,(unsigned)len); - if (v > EBML_MAX_ID_LENGTH) - errorjmp(mf,"File has identifiers longer than %d",(int)v); - break; - case 0x42f3: // MaxSizeLength - v = readUInt(mf,(unsigned)len); - if (v > EBML_MAX_SIZE_LENGTH) - errorjmp(mf,"File has integers longer than %d",(int)v); - break; - case 0x4282: // DocType - readString(mf,len,buf,sizeof(buf)); - if (strcmp(buf,MATROSKA_DOCTYPE)) - errorjmp(mf,"Unsupported DocType: %s",buf); - break; - case 0x4287: // DocTypeVersion - v = readUInt(mf,(unsigned)len); - break; - case 0x4285: // DocTypeReadVersion - v = readUInt(mf,(unsigned)len); - if (v > MATROSKA_VERSION) - errorjmp(mf,"File requires version %d Matroska parser",(int)v); - break; - ENDFOR(mf); -} - -static void parseSeekEntry(MatroskaFile *mf,uint64_t toplen) { - int seekid = 0; - uint64_t pos = (uint64_t)-1; - - FOREACH(mf,toplen) - case 0x53ab: // SeekID - if (len>EBML_MAX_ID_LENGTH) - errorjmp(mf,"Invalid ID size in parseSeekEntry: %d\n",(int)len); - seekid = (int)readUInt(mf,(unsigned)len); - break; - case 0x53ac: // SeekPos - pos = readUInt(mf,(unsigned)len); - break; - ENDFOR(mf); - - if (pos == (uint64_t)-1) - errorjmp(mf,"Invalid element position in parseSeekEntry"); - - pos += mf->pSegment; - switch (seekid) { - case 0x114d9b74: // next SeekHead - if (mf->pSeekHead) - errorjmp(mf,"SeekHead contains more than one SeekHead pointer"); - mf->pSeekHead = pos; - break; - case 0x1549a966: // SegmentInfo - mf->pSegmentInfo = pos; - break; - case 0x1f43b675: // Cluster - if (!mf->pCluster) - mf->pCluster = pos; - break; - case 0x1654ae6b: // Tracks - mf->pTracks = pos; - break; - case 0x1c53bb6b: // Cues - mf->pCues = pos; - break; - case 0x1941a469: // Attachments - mf->pAttachments = pos; - break; - case 0x1043a770: // Chapters - mf->pChapters = pos; - break; - case 0x1254c367: // tags - mf->pTags = pos; - break; - } -} - -static void parseSeekHead(MatroskaFile *mf,uint64_t toplen) { - FOREACH(mf,toplen) - case 0x4dbb: - parseSeekEntry(mf,len); - break; - ENDFOR(mf); -} - -static void parseSegmentInfo(MatroskaFile *mf,uint64_t toplen) { - MKFLOAT duration = mkfi(0); - - if (mf->seen.SegmentInfo) { - skipbytes(mf,toplen); - return; - } - - mf->seen.SegmentInfo = 1; - mf->Seg.TimecodeScale = 1000000; // Default value - - FOREACH(mf,toplen) - case 0x73a4: // SegmentUID - if (len!=sizeof(mf->Seg.UID)) - errorjmp(mf,"SegmentUID size is not %d bytes",mf->Seg.UID); - readbytes(mf,mf->Seg.UID,sizeof(mf->Seg.UID)); - break; - case 0x7384: // SegmentFilename - STRGETM(mf,mf->Seg.Filename,len); - break; - case 0x3cb923: // PrevUID - if (len!=sizeof(mf->Seg.PrevUID)) - errorjmp(mf,"PrevUID size is not %d bytes",mf->Seg.PrevUID); - readbytes(mf,mf->Seg.PrevUID,sizeof(mf->Seg.PrevUID)); - break; - case 0x3c83ab: // PrevFilename - STRGETM(mf,mf->Seg.PrevFilename,len); - break; - case 0x3eb923: // NextUID - if (len!=sizeof(mf->Seg.NextUID)) - errorjmp(mf,"NextUID size is not %d bytes",mf->Seg.NextUID); - readbytes(mf,mf->Seg.NextUID,sizeof(mf->Seg.NextUID)); - break; - case 0x3e83bb: // NextFilename - STRGETM(mf,mf->Seg.NextFilename,len); - break; - case 0x2ad7b1: // TimecodeScale - mf->Seg.TimecodeScale = readUInt(mf,(unsigned)len); - if (mf->Seg.TimecodeScale == 0) - errorjmp(mf,"Segment timecode scale is zero"); - break; - case 0x4489: // Duration - duration = readFloat(mf,(unsigned)len); - break; - case 0x4461: // DateUTC - mf->Seg.DateUTC = readUInt(mf,(unsigned)len); - mf->Seg.DateUTCValid = 1; - break; - case 0x7ba9: // Title - STRGETM(mf,mf->Seg.Title,len); - break; - case 0x4d80: // MuxingApp - STRGETM(mf,mf->Seg.MuxingApp,len); - break; - case 0x5741: // WritingApp - STRGETM(mf,mf->Seg.WritingApp,len); - break; - ENDFOR(mf); - - mf->Seg.Duration = mul3(duration,mf->Seg.TimecodeScale); -} - -static void parseFirstCluster(MatroskaFile *mf,uint64_t toplen) { - uint64_t end = filepos(mf) + toplen; - - mf->seen.Cluster = 1; - mf->firstTimecode = 0; - - FOREACH(mf,toplen) - case 0xe7: // Timecode - mf->firstTimecode += readUInt(mf,(unsigned)len); - break; - case 0xa3: // BlockEx - readVLUInt(mf); // track number - mf->firstTimecode += readSInt(mf, 2); - - skipbytes(mf,end - filepos(mf)); - return; - case 0xa0: // BlockGroup - FOREACH(mf,len) - case 0xa1: // Block - readVLUInt(mf); // track number - mf->firstTimecode += readSInt(mf,2); - - skipbytes(mf,end - filepos(mf)); - return; - ENDFOR(mf); - break; - ENDFOR(mf); -} - -static void parseVideoInfo(MatroskaFile *mf,uint64_t toplen,struct TrackInfo *ti) { - uint64_t v; - char dW = 0, dH = 0; - - FOREACH(mf,toplen) - case 0x9a: // FlagInterlaced - ti->AV.Video.Interlaced = readUInt(mf,(unsigned)len)!=0; - break; - case 0x53b8: // StereoMode - v = readUInt(mf,(unsigned)len); - if (v>3) - errorjmp(mf,"Invalid stereo mode"); - ti->AV.Video.StereoMode = (unsigned char)v; - break; - case 0xb0: // PixelWidth - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"PixelWidth is too large"); - ti->AV.Video.PixelWidth = (unsigned)v; - if (!dW) - ti->AV.Video.DisplayWidth = ti->AV.Video.PixelWidth; - break; - case 0xba: // PixelHeight - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"PixelHeight is too large"); - ti->AV.Video.PixelHeight = (unsigned)v; - if (!dH) - ti->AV.Video.DisplayHeight = ti->AV.Video.PixelHeight; - break; - case 0x54b0: // DisplayWidth - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"DisplayWidth is too large"); - ti->AV.Video.DisplayWidth = (unsigned)v; - dW = 1; - break; - case 0x54ba: // DisplayHeight - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"DisplayHeight is too large"); - ti->AV.Video.DisplayHeight = (unsigned)v; - dH = 1; - break; - case 0x54b2: // DisplayUnit - v = readUInt(mf,(unsigned)len); - if (v>4) - errorjmp(mf,"Invalid DisplayUnit: %d",(int)v); - ti->AV.Video.DisplayUnit = (unsigned char)v; - break; - case 0x54b3: // AspectRatioType - v = readUInt(mf,(unsigned)len); - if (v>2) - errorjmp(mf,"Invalid AspectRatioType: %d",(int)v); - ti->AV.Video.AspectRatioType = (unsigned char)v; - break; - case 0x54aa: // PixelCropBottom - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"PixelCropBottom is too large"); - ti->AV.Video.CropB = (unsigned)v; - break; - case 0x54bb: // PixelCropTop - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"PixelCropTop is too large"); - ti->AV.Video.CropT = (unsigned)v; - break; - case 0x54cc: // PixelCropLeft - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"PixelCropLeft is too large"); - ti->AV.Video.CropL = (unsigned)v; - break; - case 0x54dd: // PixelCropRight - v = readUInt(mf,(unsigned)len); - if (v>0xffffffff) - errorjmp(mf,"PixelCropRight is too large"); - ti->AV.Video.CropR = (unsigned)v; - break; - case 0x2eb524: // ColourSpace - ti->AV.Video.ColourSpace = (unsigned)readUInt(mf,4); - break; - case 0x2fb523: // GammaValue - ti->AV.Video.GammaValue = readFloat(mf,(unsigned)len); - break; - ENDFOR(mf); -} - -static void parseAudioInfo(MatroskaFile *mf,uint64_t toplen,struct TrackInfo *ti) { - uint64_t v; - - FOREACH(mf,toplen) - case 0xb5: // SamplingFrequency - ti->AV.Audio.SamplingFreq = readFloat(mf,(unsigned)len); - break; - case 0x78b5: // OutputSamplingFrequency - ti->AV.Audio.OutputSamplingFreq = readFloat(mf,(unsigned)len); - break; - case 0x9f: // Channels - v = readUInt(mf,(unsigned)len); - if (v<1 || v>255) - errorjmp(mf,"Invalid Channels value"); - ti->AV.Audio.Channels = (unsigned char)v; - break; - case 0x7d7b: // ChannelPositions - skipbytes(mf,len); - break; - case 0x6264: // BitDepth - v = readUInt(mf,(unsigned)len); -#if 0 - if ((v<1 || v>255) && !IsWritingApp(mf,"AVI-Mux GUI")) - errorjmp(mf,"Invalid BitDepth: %d",(int)v); -#endif - ti->AV.Audio.BitDepth = (unsigned char)v; - break; - ENDFOR(mf); - - if (ti->AV.Audio.Channels == 0) - ti->AV.Audio.Channels = 1; - if (mkv_TruncFloat(ti->AV.Audio.SamplingFreq) == 0) - ti->AV.Audio.SamplingFreq = mkfi(8000); - if (mkv_TruncFloat(ti->AV.Audio.OutputSamplingFreq)==0) - ti->AV.Audio.OutputSamplingFreq = ti->AV.Audio.SamplingFreq; -} - -static void CopyStr(char **src,char **dst) { - size_t l; - - if (!*src) - return; - - l = strlen(*src)+1; - memcpy(*dst,*src,l); - *src = *dst; - *dst += l; -} - -static void parseTrackEntry(MatroskaFile *mf,uint64_t toplen) { - struct TrackInfo t,*tp,**tpp; - uint64_t v; - char *cp = NULL, *cs = NULL; - size_t cplen = 0, cslen = 0, cpadd = 0; - unsigned CompScope, num_comp = 0; - - if (mf->nTracks >= MAX_TRACKS) - errorjmp(mf,"Too many tracks."); - - // clear track info - memset(&t,0,sizeof(t)); - - // fill default values - t.Enabled = 1; - t.Default = 1; - t.Lacing = 1; - t.TimecodeScale = mkfi(1); - t.DecodeAll = 1; - - FOREACH(mf,toplen) - case 0xd7: // TrackNumber - v = readUInt(mf,(unsigned)len); - if (v>255) - errorjmp(mf,"Track number is >255 (%d)",(int)v); - t.Number = (unsigned char)v; - break; - case 0x73c5: // TrackUID - t.UID = readUInt(mf,(unsigned)len); - break; - case 0x83: // TrackType - v = readUInt(mf,(unsigned)len); - if (v<1 || v>254) - errorjmp(mf,"Invalid track type: %d",(int)v); - t.Type = (unsigned char)v; - break; - case 0xb9: // Enabled - t.Enabled = readUInt(mf,(unsigned)len)!=0; - break; - case 0x88: // Default - t.Default = readUInt(mf,(unsigned)len)!=0; - break; - case 0x9c: // Lacing - t.Lacing = readUInt(mf,(unsigned)len)!=0; - break; - case 0x6de7: // MinCache - v = readUInt(mf,(unsigned)len); - if (v > 0xffffffff) - errorjmp(mf,"MinCache is too large"); - t.MinCache = (unsigned)v; - break; - case 0x6df8: // MaxCache - v = readUInt(mf,(unsigned)len); - if (v > 0xffffffff) - errorjmp(mf,"MaxCache is too large"); - t.MaxCache = (unsigned)v; - break; - case 0x23e383: // DefaultDuration - t.DefaultDuration = readUInt(mf,(unsigned)len); - break; - case 0x23314f: // TrackTimecodeScale - t.TimecodeScale = readFloat(mf,(unsigned)len); - break; - case 0x55ee: // MaxBlockAdditionID - t.MaxBlockAdditionID = (unsigned)readUInt(mf,(unsigned)len); - break; - case 0x536e: // Name - if (t.Name) - errorjmp(mf,"Duplicate Track Name"); - STRGETA(mf,t.Name,len); - break; - case 0x22b59c: // Language - readLangCC(mf, len, t.Language); - break; - case 0x86: // CodecID - if (t.CodecID) - errorjmp(mf,"Duplicate CodecID"); - STRGETA(mf,t.CodecID,len); - break; - case 0x63a2: // CodecPrivate - if (cp) - errorjmp(mf,"Duplicate CodecPrivate"); - cplen = (unsigned)len; - if (len > 262144) { // 256KB - cp = mf->cpbuf = mf->cache->memalloc(mf->cache, cplen); - if (!cp) - errorjmp(mf,"Out of memory"); - } - else - cp = alloca(cplen); - readbytes(mf,cp,cplen); - break; - case 0x258688: // CodecName - skipbytes(mf,len); - break; - case 0x3a9697: // CodecSettings - skipbytes(mf,len); - break; - case 0x3b4040: // CodecInfoURL - skipbytes(mf,len); - break; - case 0x26b240: // CodecDownloadURL - skipbytes(mf,len); - break; - case 0xaa: // CodecDecodeAll - t.DecodeAll = readUInt(mf,(unsigned)len)!=0; - break; - case 0x6fab: // TrackOverlay - v = readUInt(mf,(unsigned)len); - if (v>255) - errorjmp(mf,"Track number in TrackOverlay is too large: %d",(int)v); - t.TrackOverlay = (unsigned char)v; - break; - case 0xe0: // VideoInfo - parseVideoInfo(mf,len,&t); - break; - case 0xe1: // AudioInfo - parseAudioInfo(mf,len,&t); - break; - case 0x6d80: // ContentEncodings - FOREACH(mf,len) - case 0x6240: // ContentEncoding - // fill in defaults - t.CompEnabled = 1; - t.CompMethod = COMP_ZLIB; - CompScope = 1; - if (++num_comp > 1) - return; // only one compression layer supported - FOREACH(mf,len) - case 0x5031: // ContentEncodingOrder - readUInt(mf,(unsigned)len); - break; - case 0x5032: // ContentEncodingScope - CompScope = (unsigned)readUInt(mf,(unsigned)len); - break; - case 0x5033: // ContentEncodingType - if (readUInt(mf,(unsigned)len) != 0) - return; // encryption is not supported - break; - case 0x5034: // ContentCompression - FOREACH(mf,len) - case 0x4254: // ContentCompAlgo - v = readUInt(mf,(unsigned)len); - t.CompEnabled = 1; - switch (v) { - case 0: // Zlib - t.CompMethod = COMP_ZLIB; - break; - case 3: // prepend fixed data - t.CompMethod = COMP_PREPEND; - break; - default: - return; // unsupported compression, skip track - } - break; - case 0x4255: // ContentCompSettings - if (len > 256) - return; - cslen = (unsigned)len; - cs = alloca(cslen); - readbytes(mf, cs, cslen); - break; - ENDFOR(mf); - break; - // TODO Implement Encryption/Signatures - ENDFOR(mf); - break; - ENDFOR(mf); - break; - ENDFOR(mf); - - // validate track info - if (!t.CodecID) - errorjmp(mf,"Track has no Codec ID"); - - if (t.UID != 0) { - unsigned i; - for (i = 0; i < mf->nTracks; ++i) - if (mf->Tracks[i]->UID == t.UID) // duplicate track entry - return; - } - -#ifdef MATROSKA_COMPRESSION_SUPPORT - // handle compressed CodecPrivate - if (t.CompEnabled && t.CompMethod == COMP_ZLIB && (CompScope & 2) && cplen > 0) { - z_stream zs; - Bytef tmp[64], *ncp; - int code; - uLong ncplen; - - memset(&zs,0,sizeof(zs)); - if (inflateInit(&zs) != Z_OK) - errorjmp(mf, "inflateInit failed"); - - zs.next_in = (Bytef *)cp; - zs.avail_in = cplen; - - do { - zs.next_out = tmp; - zs.avail_out = sizeof(tmp); - - code = inflate(&zs, Z_NO_FLUSH); - } while (code == Z_OK); - - if (code != Z_STREAM_END) - errorjmp(mf, "invalid compressed data in CodecPrivate"); - - ncplen = zs.total_out; - ncp = alloca(ncplen); - - inflateReset(&zs); - - zs.next_in = (Bytef *)cp; - zs.avail_in = cplen; - zs.next_out = ncp; - zs.avail_out = ncplen; - - if (inflate(&zs, Z_FINISH) != Z_STREAM_END) - errorjmp(mf, "inflate failed"); - - inflateEnd(&zs); - - cp = (char *)ncp; - cplen = ncplen; - } -#endif - - if (t.CompEnabled && !(CompScope & 1)) { - t.CompEnabled = 0; - cslen = 0; - } - - // allocate new track - tpp = AGET(mf,Tracks); - - // copy strings - if (t.Name) - cpadd += strlen(t.Name)+1; - if (t.CodecID) - cpadd += strlen(t.CodecID)+1; - - tp = mf->cache->memalloc(mf->cache,sizeof(*tp) + cplen + cslen + cpadd); - if (tp == NULL) - errorjmp(mf,"Out of memory"); - - memcpy(tp,&t,sizeof(*tp)); - if (cplen) { - tp->CodecPrivate = tp+1; - tp->CodecPrivateSize = (unsigned)cplen; - memcpy(tp->CodecPrivate,cp,cplen); - } - if (cslen) { - tp->CompMethodPrivate = (char *)(tp+1) + cplen; - tp->CompMethodPrivateSize = (unsigned)cslen; - memcpy(tp->CompMethodPrivate, cs, cslen); - } - - cp = (char*)(tp+1) + cplen + cslen; - CopyStr(&tp->Name,&cp); - CopyStr(&tp->CodecID,&cp); - - // set default language - if (!tp->Language[0]) - memcpy(tp->Language, "eng", 4); - - *tpp = tp; -} - -static void parseTracks(MatroskaFile *mf,uint64_t toplen) { - mf->seen.Tracks = 1; - mf->cpbuf = NULL; - FOREACH(mf,toplen) - case 0xae: // TrackEntry - parseTrackEntry(mf,len); - mf->cache->memfree(mf->cache, mf->cpbuf); - mf->cpbuf = NULL; - break; - ENDFOR(mf); -} - -static void addCue(MatroskaFile *mf,uint64_t pos,uint64_t timecode) { - struct Cue *cc = AGET(mf,Cues); - cc->Time = timecode; - cc->Position = pos; - cc->Track = 0; - cc->Block = 0; -} - -static void fixupCues(MatroskaFile *mf) { - // adjust cues, shift cues if file does not start at 0 - unsigned i; - int64_t adjust = mf->firstTimecode * mf->Seg.TimecodeScale; - - for (i=0;inCues;++i) { - mf->Cues[i].Time *= mf->Seg.TimecodeScale; - mf->Cues[i].Time -= adjust; - } -} - -static void parseCues(MatroskaFile *mf,uint64_t toplen) { - jmp_buf jb; - uint64_t v; - struct Cue cc; - unsigned i,j,k; - - mf->seen.Cues = 1; - mf->nCues = 0; - cc.Block = 0; - - memcpy(&jb,&mf->jb,sizeof(jb)); - - if (setjmp(mf->jb)) { - memcpy(&mf->jb,&jb,sizeof(jb)); - mf->nCues = 0; - mf->seen.Cues = 0; - return; - } - - FOREACH(mf,toplen) - case 0xbb: // CuePoint - FOREACH(mf,len) - case 0xb3: // CueTime - cc.Time = readUInt(mf,(unsigned)len); - break; - case 0xb7: // CueTrackPositions - FOREACH(mf,len) - case 0xf7: // CueTrack - v = readUInt(mf,(unsigned)len); - if (v>255) - errorjmp(mf,"CueTrack points to an invalid track: %d",(int)v); - cc.Track = (unsigned char)v; - break; - case 0xf1: // CueClusterPosition - cc.Position = readUInt(mf,(unsigned)len); - break; - case 0x5378: // CueBlockNumber - cc.Block = readUInt(mf,(unsigned)len); - break; - case 0xea: // CodecState - readUInt(mf,(unsigned)len); - break; - case 0xdb: // CueReference - FOREACH(mf,len) - case 0x96: // CueRefTime - readUInt(mf,(unsigned)len); - break; - case 0x97: // CueRefCluster - readUInt(mf,(unsigned)len); - break; - case 0x535f: // CueRefNumber - readUInt(mf,(unsigned)len); - break; - case 0xeb: // CueRefCodecState - readUInt(mf,(unsigned)len); - break; - ENDFOR(mf); - break; - ENDFOR(mf); - break; - ENDFOR(mf); - - if (mf->nCues == 0 && mf->pCluster - mf->pSegment != cc.Position) - addCue(mf,mf->pCluster - mf->pSegment,mf->firstTimecode); - - memcpy(AGET(mf,Cues),&cc,sizeof(cc)); - break; - ENDFOR(mf); - - memcpy(&mf->jb,&jb,sizeof(jb)); - - ARELEASE(mf,mf,Cues); - - // bubble sort the cues and fuck the losers that write unordered cues - if (mf->nCues > 0) - for (i = mf->nCues - 1, k = 1; i > 0 && k > 0; --i) - for (j = k = 0; j < i; ++j) - if (mf->Cues[j].Time > mf->Cues[j+1].Time) { - struct Cue tmp = mf->Cues[j+1]; - mf->Cues[j+1] = mf->Cues[j]; - mf->Cues[j] = tmp; - ++k; - } -} - -static void parseAttachment(MatroskaFile *mf,uint64_t toplen) { - struct Attachment a,*pa; - - memset(&a,0,sizeof(a)); - FOREACH(mf,toplen) - case 0x467e: // Description - STRGETA(mf,a.Description,len); - break; - case 0x466e: // Name - STRGETA(mf,a.Name,len); - break; - case 0x4660: // MimeType - STRGETA(mf,a.MimeType,len); - break; - case 0x46ae: // UID - a.UID = readUInt(mf,(unsigned)len); - break; - case 0x465c: // Data - a.Position = filepos(mf); - a.Length = len; - skipbytes(mf,len); - break; - ENDFOR(mf); - - if (!a.Position) - return; - - pa = AGET(mf,Attachments); - memcpy(pa,&a,sizeof(a)); - - if (a.Description) - pa->Description = mystrdup(mf->cache,a.Description); - if (a.Name) - pa->Name = mystrdup(mf->cache,a.Name); - if (a.MimeType) - pa->MimeType = mystrdup(mf->cache,a.MimeType); -} - -static void parseAttachments(MatroskaFile *mf,uint64_t toplen) { - mf->seen.Attachments = 1; - - FOREACH(mf,toplen) - case 0x61a7: // AttachedFile - parseAttachment(mf,len); - break; - ENDFOR(mf); -} - -static void parseChapter(MatroskaFile *mf,uint64_t toplen,struct Chapter *parent) { - struct ChapterDisplay *disp; - struct ChapterProcess *proc; - struct ChapterCommand *cmd; - struct Chapter *ch = ASGET(mf,parent,Children); - - memset(ch,0,sizeof(*ch)); - - ch->Enabled = 1; - - FOREACH(mf,toplen) - case 0x73c4: // ChapterUID - ch->UID = readUInt(mf,(unsigned)len); - break; - case 0x6e67: // ChapterSegmentUID - if (len != sizeof(ch->SegmentUID)) - skipbytes(mf, len); - else - readbytes(mf, ch->SegmentUID, sizeof(ch->SegmentUID)); - break; - case 0x91: // ChapterTimeStart - ch->Start = readUInt(mf,(unsigned)len); - break; - case 0x92: // ChapterTimeEnd - ch->End = readUInt(mf,(unsigned)len); - break; - case 0x98: // ChapterFlagHidden - ch->Hidden = readUInt(mf,(unsigned)len)!=0; - break; - case 0x4598: // ChapterFlagEnabled - ch->Enabled = readUInt(mf,(unsigned)len)!=0; - break; - case 0x8f: // ChapterTrack - FOREACH(mf,len) - case 0x89: // ChapterTrackNumber - *(uint64_t*)(ASGET(mf,ch,Tracks)) = readUInt(mf,(unsigned)len); - break; - ENDFOR(mf); - break; - case 0x80: // ChapterDisplay - disp = NULL; - - FOREACH(mf,len) - case 0x85: // ChapterString - if (disp==NULL) { - disp = ASGET(mf,ch,Display); - memset(disp, 0, sizeof(*disp)); - } - if (disp->String) - skipbytes(mf,len); // Ignore duplicate string - else - STRGETM(mf,disp->String,len); - break; - case 0x437c: // ChapterLanguage - if (disp==NULL) { - disp = ASGET(mf,ch,Display); - memset(disp, 0, sizeof(*disp)); - } - readLangCC(mf, len, disp->Language); - break; - case 0x437e: // ChapterCountry - if (disp==NULL) { - disp = ASGET(mf,ch,Display); - memset(disp, 0, sizeof(*disp)); - } - readLangCC(mf, len, disp->Country); - break; - ENDFOR(mf); - - if (disp && !disp->String) - --ch->nDisplay; - break; - case 0x6944: // ChapProcess - proc = NULL; - - FOREACH(mf,len) - case 0x6955: // ChapProcessCodecID - if (proc == NULL) { - proc = ASGET(mf, ch, Process); - memset(proc, 0, sizeof(*proc)); - } - proc->CodecID = (unsigned)readUInt(mf,(unsigned)len); - break; - case 0x450d: // ChapProcessPrivate - if (proc == NULL) { - proc = ASGET(mf, ch, Process); - memset(proc, 0, sizeof(*proc)); - } - if (proc->CodecPrivate) - skipbytes(mf, len); - else { - proc->CodecPrivateLength = (unsigned)len; - STRGETM(mf,proc->CodecPrivate,len); - } - break; - case 0x6911: // ChapProcessCommand - if (proc == NULL) { - proc = ASGET(mf, ch, Process); - memset(proc, 0, sizeof(*proc)); - } - - cmd = NULL; - - FOREACH(mf,len) - case 0x6922: // ChapterCommandTime - if (cmd == NULL) { - cmd = ASGET(mf,proc,Commands); - memset(cmd, 0, sizeof(*cmd)); - } - cmd->Time = (unsigned)readUInt(mf,(unsigned)len); - break; - case 0x6933: // ChapterCommandString - if (cmd == NULL) { - cmd = ASGET(mf,proc,Commands); - memset(cmd, 0, sizeof(*cmd)); - } - if (cmd->Command) - skipbytes(mf,len); - else { - cmd->CommandLength = (unsigned)len; - STRGETM(mf,cmd->Command,len); - } - break; - ENDFOR(mf); - - if (cmd && !cmd->Command) - --proc->nCommands; - break; - ENDFOR(mf); - - if (proc && !proc->nCommands) - --ch->nProcess; - break; - case 0xb6: // Nested ChapterAtom - parseChapter(mf,len,ch); - break; - ENDFOR(mf); - - ARELEASE(mf,ch,Tracks); - ARELEASE(mf,ch,Display); - ARELEASE(mf,ch,Children); -} - -static void parseChapters(MatroskaFile *mf,uint64_t toplen) { - struct Chapter *ch; - - mf->seen.Chapters = 1; - - FOREACH(mf,toplen) - case 0x45b9: // EditionEntry - ch = AGET(mf,Chapters); - memset(ch, 0, sizeof(*ch)); - FOREACH(mf,len) - case 0x45bc: // EditionUID - ch->UID = readUInt(mf,(unsigned)len); - break; - case 0x45bd: // EditionFlagHidden - ch->Hidden = readUInt(mf,(unsigned)len)!=0; - break; - case 0x45db: // EditionFlagDefault - ch->Default = readUInt(mf,(unsigned)len)!=0; - break; - case 0x45dd: // EditionFlagOrdered - ch->Ordered = readUInt(mf,(unsigned)len)!=0; - break; - case 0xb6: // ChapterAtom - parseChapter(mf,len,ch); - break; - ENDFOR(mf); - break; - ENDFOR(mf); -} - -static void parseTags(MatroskaFile *mf,uint64_t toplen) { - struct Tag *tag; - struct Target *target; - struct SimpleTag *st; - - mf->seen.Tags = 1; - - FOREACH(mf,toplen) - case 0x7373: // Tag - tag = AGET(mf,Tags); - memset(tag,0,sizeof(*tag)); - - FOREACH(mf,len) - case 0x63c0: // Targets - FOREACH(mf,len) - case 0x63c5: // TrackUID - target = ASGET(mf,tag,Targets); - target->UID = readUInt(mf,(unsigned)len); - target->Type = TARGET_TRACK; - break; - case 0x63c4: // ChapterUID - target = ASGET(mf,tag,Targets); - target->UID = readUInt(mf,(unsigned)len); - target->Type = TARGET_CHAPTER; - break; - case 0x63c6: // AttachmentUID - target = ASGET(mf,tag,Targets); - target->UID = readUInt(mf,(unsigned)len); - target->Type = TARGET_ATTACHMENT; - break; - case 0x63c9: // EditionUID - target = ASGET(mf,tag,Targets); - target->UID = readUInt(mf,(unsigned)len); - target->Type = TARGET_EDITION; - break; - ENDFOR(mf); - break; - case 0x67c8: // SimpleTag - st = ASGET(mf,tag,SimpleTags); - memset(st,0,sizeof(*st)); - - FOREACH(mf,len) - case 0x45a3: // TagName - if (st->Name) - skipbytes(mf,len); - else - STRGETM(mf,st->Name,len); - break; - case 0x4487: // TagString - if (st->Value) - skipbytes(mf,len); - else - STRGETM(mf,st->Value,len); - break; - case 0x447a: // TagLanguage - readLangCC(mf, len, st->Language); - break; - case 0x4484: // TagDefault - st->Default = readUInt(mf,(unsigned)len)!=0; - break; - ENDFOR(mf); - - if (!st->Name || !st->Value) { - mf->cache->memfree(mf->cache,st->Name); - mf->cache->memfree(mf->cache,st->Value); - --tag->nSimpleTags; - } - break; - ENDFOR(mf); - break; - ENDFOR(mf); -} - -static void parseContainer(MatroskaFile *mf) { - uint64_t len; - int id = readID(mf); - if (id==EOF) - errorjmp(mf,"Unexpected EOF in parseContainer"); - - len = readSize(mf); - - switch (id) { - case 0x1549a966: // SegmentInfo - parseSegmentInfo(mf,len); - break; - case 0x1f43b675: // Cluster - parseFirstCluster(mf,len); - break; - case 0x1654ae6b: // Tracks - parseTracks(mf,len); - break; - case 0x1c53bb6b: // Cues - parseCues(mf,len); - break; - case 0x1941a469: // Attachments - parseAttachments(mf,len); - break; - case 0x1043a770: // Chapters - parseChapters(mf,len); - break; - case 0x1254c367: // Tags - parseTags(mf,len); - break; - } -} - -static void parseContainerPos(MatroskaFile *mf,uint64_t pos) { - seek(mf,pos); - parseContainer(mf); -} - -static void parsePointers(MatroskaFile *mf) { - jmp_buf jb; - - if (mf->pSegmentInfo && !mf->seen.SegmentInfo) - parseContainerPos(mf,mf->pSegmentInfo); - if (mf->pCluster && !mf->seen.Cluster) - parseContainerPos(mf,mf->pCluster); - if (mf->pTracks && !mf->seen.Tracks) - parseContainerPos(mf,mf->pTracks); - - memcpy(&jb,&mf->jb,sizeof(jb)); - - if (setjmp(mf->jb)) - mf->flags &= ~MPF_ERROR; // ignore errors - else { - if (mf->pCues && !mf->seen.Cues) - parseContainerPos(mf,mf->pCues); - if (mf->pAttachments && !mf->seen.Attachments) - parseContainerPos(mf,mf->pAttachments); - if (mf->pChapters && !mf->seen.Chapters) - parseContainerPos(mf,mf->pChapters); - if (mf->pTags && !mf->seen.Tags) - parseContainerPos(mf,mf->pTags); - } - - memcpy(&mf->jb,&jb,sizeof(jb)); -} - -static void parseSegment(MatroskaFile *mf,uint64_t toplen) { - uint64_t nextpos; - unsigned nSeekHeads = 0, dontstop = 0; - jmp_buf jb; - - memcpy(&jb,&mf->jb,sizeof(jb)); - - if (setjmp(mf->jb)) - mf->flags &= ~MPF_ERROR; - else { - // we want to read data until we find a seekhead or a trackinfo - FOREACH(mf,toplen) - case 0x114d9b74: // SeekHead - if (mf->flags & MKVF_AVOID_SEEKS) { - skipbytes(mf,len); - break; - } - - nextpos = filepos(mf) + len; - do { - mf->pSeekHead = 0; - parseSeekHead(mf,len); - ++nSeekHeads; - if (mf->pSeekHead) { // this is possibly a chained SeekHead - seek(mf,mf->pSeekHead); - id = readID(mf); - if (id==EOF) // chained SeekHead points to EOF? - break; - if (id != 0x114d9b74) // chained SeekHead doesnt point to a SeekHead? - break; - len = readSize(mf); - } - } while (mf->pSeekHead && nSeekHeads < 10); - seek(mf,nextpos); // resume reading segment - break; - case 0x1549a966: // SegmentInfo - mf->pSegmentInfo = cur; - parseSegmentInfo(mf,len); - break; - case 0x1f43b675: // Cluster - if (!mf->pCluster) - mf->pCluster = cur; - if (mf->seen.Cluster) - skipbytes(mf,len); - else - parseFirstCluster(mf,len); - break; - case 0x1654ae6b: // Tracks - mf->pTracks = cur; - parseTracks(mf,len); - break; - case 0x1c53bb6b: // Cues - mf->pCues = cur; - parseCues(mf,len); - break; - case 0x1941a469: // Attachments - mf->pAttachments = cur; - parseAttachments(mf,len); - break; - case 0x1043a770: // Chapters - mf->pChapters = cur; - parseChapters(mf,len); - break; - case 0x1254c367: // Tags - mf->pTags = cur; - parseTags(mf,len); - break; - ENDFOR1(mf); - // if we have pointers to all key elements - if (!dontstop && mf->pSegmentInfo && mf->pTracks && mf->pCluster) - break; - ENDFOR2(); - } - - memcpy(&mf->jb,&jb,sizeof(jb)); - - parsePointers(mf); -} - -static void parseBlockAdditions(MatroskaFile *mf, uint64_t toplen, uint64_t timecode, unsigned track) { - uint64_t add_id = 1, add_pos, add_len; - unsigned char have_add; - - FOREACH(mf, toplen) - case 0xa6: // BlockMore - have_add = 0; - FOREACH(mf, len) - case 0xee: // BlockAddId - add_id = readUInt(mf, (unsigned)len); - break; - case 0xa5: // BlockAddition - add_pos = filepos(mf); - add_len = len; - skipbytes(mf, len); - ++have_add; - break; - ENDFOR(mf); - if (have_add == 1 && id > 0 && id < 255) { - struct QueueEntry *qe = QAlloc(mf); - qe->Start = qe->End = timecode; - qe->Position = add_pos; - qe->Length = (unsigned)add_len; - qe->flags = FRAME_UNKNOWN_START | FRAME_UNKNOWN_END | - (((unsigned)add_id << FRAME_STREAM_SHIFT) & FRAME_STREAM_MASK); - - QPut(&mf->Queues[track],qe); - } - break; - ENDFOR(mf); -} - -static void parseBlockGroup(MatroskaFile *mf,uint64_t toplen,uint64_t timecode, int blockex) { - uint64_t v; - uint64_t duration = 0; - uint64_t dpos; - struct QueueEntry *qe,*qf = NULL; - unsigned char have_duration = 0, have_block = 0; - unsigned char gap = 0; - unsigned char lacing = 0; - unsigned char ref = 0; - unsigned char trackid; - unsigned tracknum = 0; - int c; - unsigned nframes = 0,i; - unsigned *sizes; - signed short block_timecode; - - if (blockex) - goto blockex; - - FOREACH(mf,toplen) - case 0xfb: // ReferenceBlock - readSInt(mf,(unsigned)len); - ref = 1; - break; -blockex: - cur = start = filepos(mf); - len = tmplen = toplen; - // fallthrough - case 0xa1: // Block - have_block = 1; - - dpos = filepos(mf); - - v = readVLUInt(mf); - if (v>255) - errorjmp(mf,"Invalid track number in Block: %d",(int)v); - trackid = (unsigned char)v; - - for (tracknum=0;tracknumnTracks;++tracknum) - if (mf->Tracks[tracknum]->Number == trackid) { - if (mf->trackMask & (1<Tracks[tracknum]->TimecodeScale, - (timecode - mf->firstTimecode + block_timecode) * mf->Seg.TimecodeScale); - - c = readch(mf); - if (c==EOF) - errorjmp(mf,"Unexpected EOF while reading Block flags"); - - if (blockex) - ref = (unsigned char)!(c & 0x80); - - gap = (unsigned char)(c & 0x1); - lacing = (unsigned char)((c >> 1) & 3); - - if (lacing) { - c = readch(mf); - if (c == EOF) - errorjmp(mf,"Unexpected EOF while reading lacing data"); - nframes = c+1; - } else - nframes = 1; - sizes = alloca(nframes*sizeof(*sizes)); - - switch (lacing) { - case 0: // No lacing - sizes[0] = (unsigned)(len - filepos(mf) + dpos); - break; - case 1: // Xiph lacing - sizes[nframes-1] = 0; - for (i=0;i1) - sizes[nframes-1] = (unsigned)(len - filepos(mf) + dpos) - sizes[0] - sizes[nframes-1]; - break; - case 2: // Fixed lacing - sizes[0] = (unsigned)(len - filepos(mf) + dpos)/nframes; - for (i=1;iStart = timecode; - qe->End = timecode; - qe->Position = v; - qe->Length = sizes[i]; - qe->flags = FRAME_UNKNOWN_END | FRAME_KF; - if (i == nframes-1 && gap) - qe->flags |= FRAME_GAP; - if (i > 0) - qe->flags |= FRAME_UNKNOWN_START; - - QPut(&mf->Queues[tracknum],qe); - - v += sizes[i]; - } - - // we want to still load these bytes into cache - for (v = filepos(mf) & ~0x3fff; v < len + dpos; v += 0x4000) - mf->cache->read(mf->cache,v,NULL,0); // touch page (FIXME this doesn't really do anything) - - skipbytes(mf,len - filepos(mf) + dpos); - - if (blockex) - goto out; - break; - case 0x9b: // BlockDuration - duration = readUInt(mf,(unsigned)len); - have_duration = 1; - break; - case 0x75a1: // BlockAdditions - if (nframes > 0) // have some frames - parseBlockAdditions(mf, len, timecode, tracknum); - else - skipbytes(mf, len); - break; - ENDFOR(mf); - -out: - if (!have_block) - errorjmp(mf,"Found a BlockGroup without Block"); - - if (nframes > 1) { - uint64_t defd = mf->Tracks[tracknum]->DefaultDuration; - v = qf->Start; - - if (have_duration) { - duration = mul3(mf->Tracks[tracknum]->TimecodeScale, - duration * mf->Seg.TimecodeScale); - - for (qe = qf; nframes > 1; --nframes, qe = qe->next) { - qe->Start = v; - v += defd; - duration -= defd; - qe->End = v; -#if 0 - qe->flags &= ~(FRAME_UNKNOWN_START|FRAME_UNKNOWN_END); -#endif - } - qe->Start = v; - qe->End = v + duration; - qe->flags &= ~FRAME_UNKNOWN_END; - } else if (mf->Tracks[tracknum]->DefaultDuration) { - for (qe = qf; nframes > 0; --nframes, qe = qe->next) { - qe->Start = v; - v += defd; - qe->End = v; - qe->flags &= ~(FRAME_UNKNOWN_START|FRAME_UNKNOWN_END); - } - } - } else if (nframes == 1) { - if (have_duration) { - qf->End = qf->Start + mul3(mf->Tracks[tracknum]->TimecodeScale, - duration * mf->Seg.TimecodeScale); - qf->flags &= ~FRAME_UNKNOWN_END; - } else if (mf->Tracks[tracknum]->DefaultDuration) { - qf->End = qf->Start + mf->Tracks[tracknum]->DefaultDuration; - qf->flags &= ~FRAME_UNKNOWN_END; - } - } - - if (ref) - while (qf) { - qf->flags &= ~FRAME_KF; - qf = qf->next; - } -} - -static void ClearQueue(MatroskaFile *mf,struct Queue *q) { - struct QueueEntry *qe,*qn; - - for (qe=q->head;qe;qe=qn) { - qn = qe->next; - qe->next = mf->QFreeList; - mf->QFreeList = qe; - } - - q->head = NULL; - q->tail = NULL; -} - -static void EmptyQueues(MatroskaFile *mf) { - unsigned i; - - for (i=0;inTracks;++i) - ClearQueue(mf,&mf->Queues[i]); -} - -static int readMoreBlocks(MatroskaFile *mf) { - uint64_t toplen, cstop; - int64_t cp; - int cid, ret = 0; - jmp_buf jb; - volatile unsigned retries = 0; - - if (mf->readPosition >= mf->pSegmentTop) - return EOF; - - memcpy(&jb,&mf->jb,sizeof(jb)); - - if (setjmp(mf->jb)) { // something evil happened here, try to resync - // always advance read position no matter what so - // we don't get caught in an endless loop - mf->readPosition = filepos(mf); - - ret = EOF; - - if (++retries > 3) // don't try too hard - goto ex; - - for (;;) { - if (filepos(mf) >= mf->pSegmentTop) - goto ex; - - cp = mf->cache->scan(mf->cache,filepos(mf),0x1f43b675); // cluster - - if (cp < 0 || (uint64_t)cp >= mf->pSegmentTop) - goto ex; - - seek(mf,cp); - - cid = readID(mf); - if (cid == EOF) - goto ex; - if (cid == 0x1f43b675) { - toplen = readSize(mf); - if (toplen < MAXCLUSTER) { - // reset error flags - mf->flags &= ~MPF_ERROR; - ret = RBRESYNC; - break; - } - } - } - - mf->readPosition = cp; - } - - cstop = mf->cache->getcachesize(mf->cache)>>1; - if (cstop > MAX_READAHEAD) - cstop = MAX_READAHEAD; - cstop += mf->readPosition; - - seek(mf,mf->readPosition); - - while (filepos(mf) < mf->pSegmentTop) { - cid = readID(mf); - if (cid == EOF) { - ret = EOF; - break; - } - toplen = readSize(mf); - - if (cid == 0x1f43b675) { // Cluster - unsigned char have_timecode = 0; - - FOREACH(mf,toplen) - case 0xe7: // Timecode - mf->tcCluster = readUInt(mf,(unsigned)len); - have_timecode = 1; - break; - case 0xa7: // Position - readUInt(mf,(unsigned)len); - break; - case 0xab: // PrevSize - readUInt(mf,(unsigned)len); - break; - case 0x5854: { // SilentTracks - // unsigned stmask = 0; - unsigned i, trk; - FOREACH(mf, len) - case 0x58d7: // SilentTrackNumber - trk = (unsigned)readUInt(mf, (unsigned)len); - for (i = 0; i < mf->nTracks; ++i) - if (mf->Tracks[i]->Number == trk) { - // stmask |= 1 << i; - break; - } - break; - ENDFOR(mf); - // TODO pass stmask to reading app - break; } - case 0xa0: // BlockGroup - if (!have_timecode) - errorjmp(mf,"Found BlockGroup before cluster TimeCode"); - parseBlockGroup(mf,len,mf->tcCluster, 0); - goto out; - case 0xa3: // BlockEx - if (!have_timecode) - errorjmp(mf,"Found BlockGroup before cluster TimeCode"); - parseBlockGroup(mf, len, mf->tcCluster, 1); - goto out; - ENDFOR(mf); -out:; - } else { - if (toplen > MAXFRAME) - errorjmp(mf,"Element in a cluster is too large around %llu, %X [%u]",filepos(mf),cid,(unsigned)toplen); - if (cid == 0xa0) // BlockGroup - parseBlockGroup(mf,toplen,mf->tcCluster, 0); - else if (cid == 0xa3) // BlockEx - parseBlockGroup(mf, toplen, mf->tcCluster, 1); - else - skipbytes(mf,toplen); - } - - if ((mf->readPosition = filepos(mf)) > cstop) - break; - } - - mf->readPosition = filepos(mf); - -ex: - memcpy(&mf->jb,&jb,sizeof(jb)); - - return ret; -} - -// this is almost the same as readMoreBlocks, except it ensures -// there are no partial frames queued, however empty queues are ok -static int fillQueues(MatroskaFile *mf,unsigned int mask) { - unsigned i,j; - int ret = 0; - - for (;;) { - j = 0; - - for (i=0;inTracks;++i) - if (mf->Queues[i].head && !(mask & (1<0) // have at least some frames - return ret; - - if ((ret = readMoreBlocks(mf)) < 0) { - j = 0; - for (i=0;inTracks;++i) - if (mf->Queues[i].head && !(mask & (1<pCluster; - uint64_t step = 10*1024*1024; - uint64_t size, tc, isize; - int64_t next_cluster; - int id, have_tc, bad; - struct Cue *cue; - - if (pos >= mf->pSegmentTop) - return; - - if (pos + step * 10 > mf->pSegmentTop) - step = (mf->pSegmentTop - pos) / 10; - if (step == 0) - step = 1; - - memcpy(&jb,&mf->jb,sizeof(jb)); - - // remove all cues - mf->nCues = 0; - - bad = 0; - - while (pos < mf->pSegmentTop) { - if (!mf->cache->progress(mf->cache,pos,mf->pSegmentTop)) - break; - - if (++bad > 50) { - pos += step; - bad = 0; - continue; - } - - // find next cluster header - next_cluster = mf->cache->scan(mf->cache,pos,0x1f43b675); // cluster - if (next_cluster < 0 || (uint64_t)next_cluster >= mf->pSegmentTop) - break; - - pos = next_cluster + 4; // prevent endless loops - - if (setjmp(mf->jb)) // something evil happened while reindexing - continue; - - seek(mf,next_cluster); - - id = readID(mf); - if (id == EOF) - break; - if (id != 0x1f43b675) // shouldn't happen - continue; - - size = readVLUInt(mf); - if (size >= MAXCLUSTER || size < 1024) - continue; - - have_tc = 0; - size += filepos(mf); - - while (filepos(mf) < (uint64_t)next_cluster + 1024) { - id = readID(mf); - if (id == EOF) - break; - - isize = readVLUInt(mf); - - if (id == 0xe7) { // cluster timecode - tc = readUInt(mf,(unsigned)isize); - have_tc = 1; - break; - } - - skipbytes(mf,isize); - } - - if (!have_tc) - continue; - - seek(mf,size); - id = readID(mf); - - if (id == EOF) - break; - - if (id != 0x1f43b675) // cluster - continue; - - // good cluster, remember it - cue = AGET(mf,Cues); - cue->Time = tc; - cue->Position = next_cluster - mf->pSegment; - cue->Block = 0; - cue->Track = 0; - - // advance to the next point - pos = next_cluster + step; - if (pos < size) - pos = size; - - bad = 0; - } - - fixupCues(mf); - - if (mf->nCues == 0) { - cue = AGET(mf,Cues); - cue->Time = mf->firstTimecode; - cue->Position = mf->pCluster - mf->pSegment; - cue->Block = 0; - cue->Track = 0; - } - - mf->cache->progress(mf->cache,0,0); - - memcpy(&mf->jb,&jb,sizeof(jb)); -} - -static void fixupChapter(uint64_t adj, struct Chapter *ch) { - unsigned i; - - if (ch->Start != 0) - ch->Start -= adj; - if (ch->End != 0) - ch->End -= adj; - - for (i=0;inChildren;++i) - fixupChapter(adj,&ch->Children[i]); -} - -static int64_t findLastTimecode(MatroskaFile *mf) { - uint64_t nd = 0; - unsigned n,vtrack; - - if (mf->nTracks == 0) - return -1; - - for (n=vtrack=0;nnTracks;++n) - if (mf->Tracks[n]->Type == TT_VIDEO) { - vtrack = n; - goto ok; - } - - return -1; -ok: - - EmptyQueues(mf); - - if (mf->nCues == 0) { - mf->readPosition = mf->pCluster + 13000000 > mf->pSegmentTop ? mf->pCluster : mf->pSegmentTop - 13000000; - mf->tcCluster = 0; - } else { - mf->readPosition = mf->Cues[mf->nCues - 1].Position + mf->pSegment; - mf->tcCluster = mf->Cues[mf->nCues - 1].Time / mf->Seg.TimecodeScale; - } - mf->trackMask = ~(1 << vtrack); - - do - while (mf->Queues[vtrack].head) - { - uint64_t tc = mf->Queues[vtrack].head->flags & FRAME_UNKNOWN_END ? - mf->Queues[vtrack].head->Start : mf->Queues[vtrack].head->End; - if (nd < tc) - nd = tc; - QFree(mf,QGet(&mf->Queues[vtrack])); - } - while (fillQueues(mf,0) != EOF); - - mf->trackMask = 0; - - EmptyQueues(mf); - - // there may have been an error, but at this point we will ignore it - if (mf->flags & MPF_ERROR) { - mf->flags &= ~MPF_ERROR; - if (nd == 0) - return -1; - } - - return nd; -} - -static void parseFile(MatroskaFile *mf) { - uint64_t len = filepos(mf), adjust; - unsigned i; - int id = readID(mf); - int m; - - if (id==EOF) - errorjmp(mf,"Unexpected EOF at start of file"); - - // files with multiple concatenated segments can have only - // one EBML prolog - if (len > 0 && id == 0x18538067) - goto segment; - - if (id!=0x1a45dfa3) - errorjmp(mf,"First element in file is not EBML"); - - parseEBML(mf,readSize(mf)); - - // next we need to find the first segment - for (;;) { - id = readID(mf); - if (id==EOF) - errorjmp(mf,"No segments found in the file"); -segment: - len = readVLUIntImp(mf,&m); - // see if it's unspecified - if (len == (MAXU64 >> (57-m*7))) - len = MAXU64; - if (id == 0x18538067) // Segment - break; - skipbytes(mf,len); - } - - // found it - mf->pSegment = filepos(mf); - if (len == MAXU64) { - mf->pSegmentTop = MAXU64; - if (mf->cache->getfilesize) { - int64_t seglen = mf->cache->getfilesize(mf->cache); - if (seglen > 0) - mf->pSegmentTop = seglen; - } - } else - mf->pSegmentTop = mf->pSegment + len; - parseSegment(mf,len); - - // check if we got all data - if (!mf->seen.SegmentInfo) - errorjmp(mf,"Couldn't find SegmentInfo"); - if (!mf->seen.Cluster) - mf->pCluster = mf->pSegmentTop; - - adjust = mf->firstTimecode * mf->Seg.TimecodeScale; - - for (i=0;inChapters;++i) - fixupChapter(adjust, &mf->Chapters[i]); - - fixupCues(mf); - - // release extra memory - ARELEASE(mf,mf,Tracks); - - // initialize reader - mf->Queues = mf->cache->memalloc(mf->cache,mf->nTracks * sizeof(*mf->Queues)); - if (mf->Queues == NULL) - errorjmp(mf, "Ouf of memory"); - memset(mf->Queues, 0, mf->nTracks * sizeof(*mf->Queues)); - - // try to detect real duration - if (!(mf->flags & MKVF_AVOID_SEEKS)) { - int64_t nd = findLastTimecode(mf); - if (nd > 0) - mf->Seg.Duration = nd; - } - - // move to first frame - mf->readPosition = mf->pCluster; - mf->tcCluster = mf->firstTimecode; -} - -static void DeleteChapter(MatroskaFile *mf,struct Chapter *ch) { - unsigned i,j; - - for (i=0;inDisplay;++i) - mf->cache->memfree(mf->cache,ch->Display[i].String); - mf->cache->memfree(mf->cache,ch->Display); - mf->cache->memfree(mf->cache,ch->Tracks); - - for (i=0;inProcess;++i) { - for (j=0;jProcess[i].nCommands;++j) - mf->cache->memfree(mf->cache,ch->Process[i].Commands[j].Command); - mf->cache->memfree(mf->cache,ch->Process[i].Commands); - mf->cache->memfree(mf->cache,ch->Process[i].CodecPrivate); - } - mf->cache->memfree(mf->cache,ch->Process); - - for (i=0;inChildren;++i) - DeleteChapter(mf,&ch->Children[i]); - mf->cache->memfree(mf->cache,ch->Children); -} - -/////////////////////////////////////////////////////////////////////////// -// public interface -MatroskaFile *mkv_OpenEx(InputStream *io, - uint64_t base, - unsigned flags, - char *err_msg,unsigned msgsize) -{ - MatroskaFile *mf = io->memalloc(io,sizeof(*mf)); - if (mf == NULL) { - mystrlcpy(err_msg,"Out of memory",msgsize); - return NULL; - } - - memset(mf,0,sizeof(*mf)); - - mf->cache = io; - mf->flags = flags; - io->progress(io,0,0); - - if (setjmp(mf->jb)==0) { - seek(mf,base); - parseFile(mf); - } else { // parser error - mystrlcpy(err_msg,mf->errmsg,msgsize); - mkv_Close(mf); - return NULL; - } - - return mf; -} - -MatroskaFile *mkv_Open(InputStream *io, - char *err_msg,unsigned msgsize) -{ - return mkv_OpenEx(io,0,0,err_msg,msgsize); -} - -void mkv_Close(MatroskaFile *mf) { - unsigned i,j; - - if (mf==NULL) - return; - - for (i=0;inTracks;++i) - mf->cache->memfree(mf->cache,mf->Tracks[i]); - mf->cache->memfree(mf->cache,mf->Tracks); - - for (i=0;inQBlocks;++i) - mf->cache->memfree(mf->cache,mf->QBlocks[i]); - mf->cache->memfree(mf->cache,mf->QBlocks); - - mf->cache->memfree(mf->cache,mf->Queues); - - mf->cache->memfree(mf->cache,mf->Seg.Title); - mf->cache->memfree(mf->cache,mf->Seg.MuxingApp); - mf->cache->memfree(mf->cache,mf->Seg.WritingApp); - mf->cache->memfree(mf->cache,mf->Seg.Filename); - mf->cache->memfree(mf->cache,mf->Seg.NextFilename); - mf->cache->memfree(mf->cache,mf->Seg.PrevFilename); - - mf->cache->memfree(mf->cache,mf->Cues); - - for (i=0;inAttachments;++i) { - mf->cache->memfree(mf->cache,mf->Attachments[i].Description); - mf->cache->memfree(mf->cache,mf->Attachments[i].Name); - mf->cache->memfree(mf->cache,mf->Attachments[i].MimeType); - } - mf->cache->memfree(mf->cache,mf->Attachments); - - for (i=0;inChapters;++i) - DeleteChapter(mf,&mf->Chapters[i]); - mf->cache->memfree(mf->cache,mf->Chapters); - - for (i=0;inTags;++i) { - for (j=0;jTags[i].nSimpleTags;++j) { - mf->cache->memfree(mf->cache,mf->Tags[i].SimpleTags[j].Name); - mf->cache->memfree(mf->cache,mf->Tags[i].SimpleTags[j].Value); - } - mf->cache->memfree(mf->cache,mf->Tags[i].Targets); - mf->cache->memfree(mf->cache,mf->Tags[i].SimpleTags); - } - mf->cache->memfree(mf->cache,mf->Tags); - - mf->cache->memfree(mf->cache,mf); -} - -const char *mkv_GetLastError(MatroskaFile *mf) { - return mf->errmsg[0] ? mf->errmsg : NULL; -} - -SegmentInfo *mkv_GetFileInfo(MatroskaFile *mf) { - return &mf->Seg; -} - -unsigned int mkv_GetNumTracks(MatroskaFile *mf) { - return mf->nTracks; -} - -TrackInfo *mkv_GetTrackInfo(MatroskaFile *mf,unsigned track) { - if (track>mf->nTracks) - return NULL; - - return mf->Tracks[track]; -} - -void mkv_GetAttachments(MatroskaFile *mf,Attachment **at,unsigned *count) { - *at = mf->Attachments; - *count = mf->nAttachments; -} - -void mkv_GetChapters(MatroskaFile *mf,Chapter **ch,unsigned *count) { - *ch = mf->Chapters; - *count = mf->nChapters; -} - -void mkv_GetTags(MatroskaFile *mf,Tag **tag,unsigned *count) { - *tag = mf->Tags; - *count = mf->nTags; -} - -uint64_t mkv_GetSegmentTop(MatroskaFile *mf) { - return mf->pSegmentTop; -} - -#define IS_DELTA(f) (!((f)->flags & FRAME_KF) || ((f)->flags & FRAME_UNKNOWN_START)) - -void mkv_Seek(MatroskaFile *mf,uint64_t timecode,unsigned flags) { - int i,j,m,ret; - unsigned n,z,mask; - uint64_t m_kftime[MAX_TRACKS]; - unsigned char m_seendf[MAX_TRACKS]; - - if (mf->flags & MKVF_AVOID_SEEKS) - return; - - if (timecode == 0) { - EmptyQueues(mf); - mf->readPosition = mf->pCluster; - mf->tcCluster = mf->firstTimecode; - mf->flags &= ~MPF_ERROR; - - return; - } - - if (mf->nCues==0) - reindex(mf); - - if (mf->nCues==0) - return; - - mf->flags &= ~MPF_ERROR; - - i = 0; - j = mf->nCues - 1; - - for (;;) { - if (i>j) { - j = j>=0 ? j : 0; - - if (setjmp(mf->jb)!=0) - return; - - mkv_SetTrackMask(mf,mf->trackMask); - - if (flags & (MKVF_SEEK_TO_PREV_KEYFRAME | MKVF_SEEK_TO_PREV_KEYFRAME_STRICT)) { - // we do this in two stages - // a. find the last keyframes before the require position - // b. seek to them - - // pass 1 - for (;;) { - for (n=0;nnTracks;++n) { - m_kftime[n] = MAXU64; - m_seendf[n] = 0; - } - - EmptyQueues(mf); - - mf->readPosition = mf->Cues[j].Position + mf->pSegment; - mf->tcCluster = mf->Cues[j].Time; - - for (;;) { - if ((ret = fillQueues(mf,0)) < 0 || ret == RBRESYNC) - return; - - // drain queues until we get to the required timecode - for (n=0;nnTracks;++n) { - if (mf->Queues[n].head && (mf->Queues[n].head->StartQueues[n].head)) - m_seendf[n] = 1; - else - m_kftime[n] = mf->Queues[n].head->Start; - } - - while (mf->Queues[n].head && mf->Queues[n].head->StartQueues[n].head)) - m_seendf[n] = 1; - else - m_kftime[n] = mf->Queues[n].head->Start; - QFree(mf,QGet(&mf->Queues[n])); - } - - // We've drained the queue, so the frame at head is the next one past the requered point. - // In strict mode we are done, but when seeking is not strict we use the head frame - // if it's not an audio track (we accept preroll within a frame for audio), and the head frame - // is a keyframe - if (!(flags & MKVF_SEEK_TO_PREV_KEYFRAME_STRICT)) - if (mf->Queues[n].head && (mf->Tracks[n]->Type != TT_AUDIO || mf->Queues[n].head->Start<=timecode)) - if (!IS_DELTA(mf->Queues[n].head)) - m_kftime[n] = mf->Queues[n].head->Start; - } - - for (n=0;nnTracks;++n) - if (mf->Queues[n].head && mf->Queues[n].head->Start>=timecode) - goto found; - } -found: - - for (n=0;nnTracks;++n) - if (!(mf->trackMask & (1<0) - { - // we need to restart the search from prev cue - --j; - goto again; - } - - break; -again:; - } - } else - for (n=0;nnTracks;++n) - m_kftime[n] = timecode; - - // now seek to this timecode - EmptyQueues(mf); - - mf->readPosition = mf->Cues[j].Position + mf->pSegment; - mf->tcCluster = mf->Cues[j].Time; - - for (mask=0;;) { - if ((ret = fillQueues(mf,mask)) < 0 || ret == RBRESYNC) - return; - - // drain queues until we get to the required timecode - for (n=0;nnTracks;++n) { - struct QueueEntry *qe; - for (qe = mf->Queues[n].head;qe && qe->StartQueues[n].head) - QFree(mf,QGet(&mf->Queues[n])); - } - - for (n=z=0;nnTracks;++n) - if (m_kftime[n]==MAXU64 || (mf->Queues[n].head && mf->Queues[n].head->Start>=m_kftime[n])) { - ++z; - mask |= 1<nTracks) - return; - } - } - - m = (i+j)>>1; - - if (timecode < mf->Cues[m].Time) - j = m-1; - else - i = m+1; - } -} - -void mkv_SkipToKeyframe(MatroskaFile *mf) { - unsigned n,wait; - uint64_t ht; - - if (setjmp(mf->jb)!=0) - return; - - // remove delta frames from queues - do { - wait = 0; - - if (fillQueues(mf,0)<0) - return; - - for (n=0;nnTracks;++n) - if (mf->Queues[n].head && !(mf->Queues[n].head->flags & FRAME_KF)) { - ++wait; - QFree(mf,QGet(&mf->Queues[n])); - } - } while (wait); - - // find highest queued time - for (n=0,ht=0;nnTracks;++n) - if (mf->Queues[n].head && htQueues[n].head->Start) - ht = mf->Queues[n].head->Start; - - // ensure the time difference is less than 100ms - do { - wait = 0; - - if (fillQueues(mf,0)<0) - return; - - for (n=0;nnTracks;++n) - while (mf->Queues[n].head && mf->Queues[n].head->next && - (mf->Queues[n].head->next->flags & FRAME_KF) && - ht - mf->Queues[n].head->Start > 100000000) - { - ++wait; - QFree(mf,QGet(&mf->Queues[n])); - } - - } while (wait); -} - -uint64_t mkv_GetLowestQTimecode(MatroskaFile *mf) { - unsigned n,seen; - uint64_t t; - - // find the lowest queued timecode - for (n=seen=0,t=0;nnTracks;++n) - if (mf->Queues[n].head && (!seen || t > mf->Queues[n].head->Start)) - t = mf->Queues[n].head->Start, seen=1; - - return seen ? t : (uint64_t)LL(-1); -} - -int mkv_TruncFloat(MKFLOAT f) { -#ifdef MATROSKA_INTEGER_ONLY - return (int)(f.v >> 32); -#else - return (int)f; -#endif -} - -#define FTRACK 0xffffffff - -void mkv_SetTrackMask(MatroskaFile *mf,unsigned int mask) { - unsigned int i; - - if (mf->flags & MPF_ERROR) - return; - - mf->trackMask = mask; - - for (i=0;inTracks;++i) - if (mask & (1<Queues[i]); -} - -int mkv_ReadFrame(MatroskaFile *mf, - unsigned int mask,unsigned int *track, - uint64_t *StartTime,uint64_t *EndTime, - uint64_t *FilePos,unsigned int *FrameSize, - unsigned int *FrameFlags) -{ - unsigned int i,j; - struct QueueEntry *qe; - - if (setjmp(mf->jb)!=0) - return -1; - - do { - // extract required frame, use block with the lowest timecode - for (j=FTRACK,i=0;inTracks;++i) - if (!(mask & (1<Queues[i].head) { - j = i; - ++i; - break; - } - - for (;inTracks;++i) - if (!(mask & (1<Queues[i].head && - mf->Queues[j].head->Start > mf->Queues[i].head->Start) - j = i; - - if (j != FTRACK) { - qe = QGet(&mf->Queues[j]); - - *track = j; - *StartTime = qe->Start; - *EndTime = qe->End; - *FilePos = qe->Position; - *FrameSize = qe->Length; - *FrameFlags = qe->flags; - - QFree(mf,qe); - - return 0; - } - - if (mf->flags & MPF_ERROR) - return -1; - - } while (fillQueues(mf,mask)>=0); - - return EOF; -} - -#ifdef MATROSKA_COMPRESSION_SUPPORT -/************************************************************************* - * Compressed streams support - ************************************************************************/ -struct CompressedStream { - MatroskaFile *mf; - z_stream zs; - - /* current compressed frame */ - uint64_t frame_pos; - unsigned frame_size; - char frame_buffer[2048]; - - /* decoded data buffer */ - char decoded_buffer[2048]; - unsigned decoded_ptr; - unsigned decoded_size; - - /* error handling */ - char errmsg[128]; -}; - -CompressedStream *cs_Create(/* in */ MatroskaFile *mf, - /* in */ unsigned tracknum, - /* out */ char *errormsg, - /* in */ unsigned msgsize) -{ - CompressedStream *cs; - TrackInfo *ti; - int code; - - ti = mkv_GetTrackInfo(mf, tracknum); - if (ti == NULL) { - mystrlcpy(errormsg, "No such track.", msgsize); - return NULL; - } - - if (!ti->CompEnabled) { - mystrlcpy(errormsg, "Track is not compressed.", msgsize); - return NULL; - } - - if (ti->CompMethod != COMP_ZLIB) { - mystrlcpy(errormsg, "Unsupported compression method.", msgsize); - return NULL; - } - - cs = mf->cache->memalloc(mf->cache,sizeof(*cs)); - if (cs == NULL) { - mystrlcpy(errormsg, "Ouf of memory.", msgsize); - return NULL; - } - - memset(&cs->zs,0,sizeof(cs->zs)); - code = inflateInit(&cs->zs); - if (code != Z_OK) { - mystrlcpy(errormsg, "ZLib error.", msgsize); - mf->cache->memfree(mf->cache,cs); - return NULL; - } - - cs->frame_size = 0; - cs->decoded_ptr = cs->decoded_size = 0; - cs->mf = mf; - - return cs; -} - -void cs_Destroy(/* in */ CompressedStream *cs) { - if (cs == NULL) - return; - - inflateEnd(&cs->zs); - cs->mf->cache->memfree(cs->mf->cache,cs); -} - -/* advance to the next frame in matroska stream, you need to pass values returned - * by mkv_ReadFrame */ -void cs_NextFrame(/* in */ CompressedStream *cs, - /* in */ uint64_t pos, - /* in */ unsigned size) -{ - cs->zs.avail_in = 0; - inflateReset(&cs->zs); - cs->frame_pos = pos; - cs->frame_size = size; - cs->decoded_ptr = cs->decoded_size = 0; -} - -/* read and decode more data from current frame, return number of bytes decoded, - * 0 on end of frame, or -1 on error */ -int cs_ReadData(CompressedStream *cs,char *buffer,unsigned bufsize) -{ - char *cp = buffer; - unsigned rd = 0; - unsigned todo; - int code; - - do { - /* try to copy data from decoded buffer */ - if (cs->decoded_ptr < cs->decoded_size) { - todo = cs->decoded_size - cs->decoded_ptr;; - if (todo > bufsize - rd) - todo = bufsize - rd; - - memcpy(cp, cs->decoded_buffer + cs->decoded_ptr, todo); - - rd += todo; - cp += todo; - cs->decoded_ptr += todo; - } else { - /* setup output buffer */ - cs->zs.next_out = (Bytef *)cs->decoded_buffer; - cs->zs.avail_out = sizeof(cs->decoded_buffer); - - /* try to read more data */ - if (cs->zs.avail_in == 0 && cs->frame_size > 0) { - todo = cs->frame_size; - if (todo > sizeof(cs->frame_buffer)) - todo = sizeof(cs->frame_buffer); - - if (cs->mf->cache->read(cs->mf->cache, cs->frame_pos, cs->frame_buffer, todo) != (int)todo) { - mystrlcpy(cs->errmsg, "File read failed", sizeof(cs->errmsg)); - return -1; - } - - cs->zs.next_in = (Bytef *)cs->frame_buffer; - cs->zs.avail_in = todo; - - cs->frame_pos += todo; - cs->frame_size -= todo; - } - - /* try to decode more data */ - code = inflate(&cs->zs,Z_NO_FLUSH); - if (code != Z_OK && code != Z_STREAM_END) { - mystrlcpy(cs->errmsg, "ZLib error.", sizeof(cs->errmsg)); - return -1; - } - - /* handle decoded data */ - if (cs->zs.avail_out == sizeof(cs->decoded_buffer)) /* EOF */ - break; - - cs->decoded_ptr = 0; - cs->decoded_size = sizeof(cs->decoded_buffer) - cs->zs.avail_out; - } - } while (rd < bufsize); - - return rd; -} - -/* return error message for the last error */ -const char *cs_GetLastError(CompressedStream *cs) -{ - if (!cs->errmsg[0]) - return NULL; - return cs->errmsg; -} -#endif - diff --git a/src/include/agi_pre.h b/src/include/agi_pre.h index f611c05436..da531b82d0 100644 --- a/src/include/agi_pre.h +++ b/src/include/agi_pre.h @@ -35,9 +35,6 @@ /// insert it in every source file (under C/C++ -> Advanced -> Force Includes), /// then set stdwx.cpp to generate the precompiled header /// -/// @note Make sure that you disable use of precompiled headers on md5.c and -/// MatroskaParser.c, as well as any possible future .c files. - #ifdef __cplusplus // Block msvc from complaining about not using msvc-specific versions for diff --git a/src/matroska.cpp b/src/matroska.cpp index 5e36d7bfd6..006780e8f1 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -2,7 +2,7 @@ // #include "matroska.h" -#include "MatroskaParser.h" +#include "matroska_demux.h" #include #include diff --git a/src/matroska_demux.cpp b/src/matroska_demux.cpp index 803e23b146..78220e7024 100644 --- a/src/matroska_demux.cpp +++ b/src/matroska_demux.cpp @@ -1,9 +1,7 @@ // Copyright (c) 2026 Aegisub contributors -// Aegisub-owned Matroska demux adapter. The public ABI intentionally matches -// MatroskaParser.h while EBML parsing and Matroska semantics come from the -// maintained upstream libraries. +// Native Matroska demux implementation backed by libebml and libmatroska. #include -#include "MatroskaParser.h" +#include "matroska_demux.h" #include #include diff --git a/src/MatroskaParser.h b/src/matroska_demux.h similarity index 83% rename from src/MatroskaParser.h rename to src/matroska_demux.h index 74d5ba6c64..c4a9f3c009 100644 --- a/src/MatroskaParser.h +++ b/src/matroska_demux.h @@ -29,28 +29,10 @@ #include -#ifndef MATROSKA_PARSER_H -#define MATROSKA_PARSER_H +#ifndef AEGISUB_MATROSKA_DEMUX_H +#define AEGISUB_MATROSKA_DEMUX_H -/* Random notes: - * - * The parser does not process frame data in any way and does not read it into - * the queue. The app should read it via mkv_ReadData if it is interested. - * - * The code here is 64-bit clean and was tested on FreeBSD/sparc 64-bit big endian - * system - */ - -#ifdef MPDLLBUILD -#define X __declspec(dllexport) -#else -#ifdef MPDLL -#define X __declspec(dllimport) -#pragma comment(lib,"MatroskaParser") -#else -#define X -#endif -#endif +// Internal interface between the native Matroska demuxer and its C++ owner. #define MATROSKA_COMPRESSION_SUPPORT @@ -260,13 +242,13 @@ typedef struct Tag Tag; /* Open a matroska file * io pointer is recorded inside MatroskaFile */ -X MatroskaFile *mkv_Open(/* in */ InputStream *io, +MatroskaFile *mkv_Open(/* in */ InputStream *io, /* out */ char *err_msg, /* in */ unsigned msgsize); #define MKVF_AVOID_SEEKS 1 /* use sequential reading only */ -X MatroskaFile *mkv_OpenEx(/* in */ InputStream *io, +MatroskaFile *mkv_OpenEx(/* in */ InputStream *io, /* in */ uint64_t base, /* in */ unsigned flags, /* out */ char *err_msg, @@ -275,30 +257,30 @@ X MatroskaFile *mkv_OpenEx(/* in */ InputStream *io, /* Close and deallocate mf * NULL pointer is ok and is simply ignored */ -X void mkv_Close(/* in */ MatroskaFile *mf); +void mkv_Close(/* in */ MatroskaFile *mf); /* Fetch the error message of the last failed operation */ -X const char *mkv_GetLastError(/* in */ MatroskaFile *mf); +const char *mkv_GetLastError(/* in */ MatroskaFile *mf); /* Get file information */ -X SegmentInfo *mkv_GetFileInfo(/* in */ MatroskaFile *mf); +SegmentInfo *mkv_GetFileInfo(/* in */ MatroskaFile *mf); /* Get track information */ -X unsigned int mkv_GetNumTracks(/* in */ MatroskaFile *mf); -X TrackInfo *mkv_GetTrackInfo(/* in */ MatroskaFile *mf,/* in */ unsigned track); +unsigned int mkv_GetNumTracks(/* in */ MatroskaFile *mf); +TrackInfo *mkv_GetTrackInfo(/* in */ MatroskaFile *mf,/* in */ unsigned track); /* chapters, tags and attachments */ -X void mkv_GetAttachments(/* in */ MatroskaFile *mf, +void mkv_GetAttachments(/* in */ MatroskaFile *mf, /* out */ Attachment **at, /* out */ unsigned *count); -X void mkv_GetChapters(/* in */ MatroskaFile *mf, +void mkv_GetChapters(/* in */ MatroskaFile *mf, /* out */ Chapter **ch, /* out */ unsigned *count); -X void mkv_GetTags(/* in */ MatroskaFile *mf, +void mkv_GetTags(/* in */ MatroskaFile *mf, /* out */ Tag **tag, /* out */ unsigned *count); -X uint64_t mkv_GetSegmentTop(MatroskaFile *mf); +uint64_t mkv_GetSegmentTop(MatroskaFile *mf); /* Seek to specified timecode, * if timecode is past end of file, @@ -308,15 +290,15 @@ X uint64_t mkv_GetSegmentTop(MatroskaFile *mf); #define MKVF_SEEK_TO_PREV_KEYFRAME 1 #define MKVF_SEEK_TO_PREV_KEYFRAME_STRICT 2 -X void mkv_Seek(/* in */ MatroskaFile *mf, +void mkv_Seek(/* in */ MatroskaFile *mf, /* in */ uint64_t timecode /* in ns */, /* in */ unsigned flags); -X void mkv_SkipToKeyframe(MatroskaFile *mf); +void mkv_SkipToKeyframe(MatroskaFile *mf); -X uint64_t mkv_GetLowestQTimecode(MatroskaFile *mf); +uint64_t mkv_GetLowestQTimecode(MatroskaFile *mf); -X int mkv_TruncFloat(MKFLOAT f); +int mkv_TruncFloat(MKFLOAT f); /************************************************************************* * reading data, pull model @@ -335,14 +317,14 @@ X int mkv_TruncFloat(MKFLOAT f); * will be ignored when reading file data. * This call discards all parsed and queued frames */ -X void mkv_SetTrackMask(/* in */ MatroskaFile *mf,/* in */ unsigned int mask); +void mkv_SetTrackMask(/* in */ MatroskaFile *mf,/* in */ unsigned int mask); /* Read one frame from the queue. * mask specifies what tracks to ignore. * Returns -1 if there are no more frames in the specified * set of tracks, 0 on success */ -X int mkv_ReadFrame(/* in */ MatroskaFile *mf, +int mkv_ReadFrame(/* in */ MatroskaFile *mf, /* in */ unsigned int mask, /* out */ unsigned int *track, /* out */ uint64_t *StartTime /* in ns */, @@ -357,30 +339,28 @@ struct CompressedStream; typedef struct CompressedStream CompressedStream; -X CompressedStream *cs_Create(/* in */ MatroskaFile *mf, +CompressedStream *cs_Create(/* in */ MatroskaFile *mf, /* in */ unsigned tracknum, /* out */ char *errormsg, /* in */ unsigned msgsize); -X void cs_Destroy(/* in */ CompressedStream *cs); +void cs_Destroy(/* in */ CompressedStream *cs); /* advance to the next frame in matroska stream, you need to pass values returned * by mkv_ReadFrame */ -X void cs_NextFrame(/* in */ CompressedStream *cs, +void cs_NextFrame(/* in */ CompressedStream *cs, /* in */ uint64_t pos, /* in */ unsigned size); /* read and decode more data from current frame, return number of bytes decoded, * 0 on end of frame, or -1 on error */ -X int cs_ReadData(CompressedStream *cs,char *buffer,unsigned bufsize); +int cs_ReadData(CompressedStream *cs,char *buffer,unsigned bufsize); /* return error message for the last error */ -X const char *cs_GetLastError(CompressedStream *cs); +const char *cs_GetLastError(CompressedStream *cs); #endif #ifdef __cplusplus } #endif -#undef X - #endif diff --git a/tests/matroska/behavior.cpp b/tests/matroska/behavior.cpp index 55d89fb19a..6d47bd7616 100644 --- a/tests/matroska/behavior.cpp +++ b/tests/matroska/behavior.cpp @@ -1,5 +1,5 @@ #include -#include "MatroskaParser.h" +#include "matroska_demux.h" #include #include #include From dd7800f2ca367d50cdb1fb6a12bb3de8c6338514 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Fri, 28 Aug 2026 14:03:45 -0700 Subject: [PATCH 09/28] Clean up Matroska test sources in Meson --- src/meson.build | 7 ++++--- tests/meson.build | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/meson.build b/src/meson.build index 415b154304..7ffb93283b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,8 +1,9 @@ subdir('libresrc') -aegisub_src = files( - 'matroska_demux.cpp', - 'matroska.cpp', +matroska_demux_src = files('matroska_demux.cpp') +matroska_src = files('matroska.cpp') + matroska_demux_src + +aegisub_src = matroska_src + files( 'aegisublocale.cpp', 'ass_attachment.cpp', 'ass_dialogue.cpp', diff --git a/tests/meson.build b/tests/meson.build index 1984f1fab0..dcdd81db1c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -66,7 +66,7 @@ endif runner = executable( 'gtest-main', - tests_src, '../src/matroska.cpp', '../src/matroska_demux.cpp', + tests_src, matroska_src, include_directories: [tests_inc, libaegisub_inc], dependencies: [tests_deps, deps], cpp_args: tests_args + conf_defines, @@ -90,7 +90,7 @@ test('luajit-52', executable('luajit-52', 'tests/luajit_52.c', dependencies: lua # Deterministic integration test for Matroska demux metadata, attachments, frames, compression, and errors. matroska_behavior = executable('matroska-behavior', - 'matroska/behavior.cpp', '../src/matroska_demux.cpp', + 'matroska/behavior.cpp', matroska_demux_src, include_directories: include_directories('../src'), dependencies: [dependency('zlib'), libebml, libmatroska]) test('matroska behavior', find_program('matroska/check-fixtures.py'), From b7ece55bf4d17ced046070c00cc11516cfd4dbaf Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Tue, 1 Sep 2026 12:48:41 -0700 Subject: [PATCH 10/28] Avoid parent paths in Matroska test includes --- src/meson.build | 2 ++ tests/meson.build | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/meson.build b/src/meson.build index 7ffb93283b..501705597d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,7 @@ subdir('libresrc') +aegisub_src_inc = include_directories('.') + matroska_demux_src = files('matroska_demux.cpp') matroska_src = files('matroska.cpp') + matroska_demux_src diff --git a/tests/meson.build b/tests/meson.build index dcdd81db1c..285d09c20f 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -53,7 +53,7 @@ tests_src = [ 'tests/ycbcr.cpp', ] -tests_inc = include_directories('support', '../src') +tests_inc = include_directories('support') tests_args = [] @@ -67,7 +67,7 @@ endif runner = executable( 'gtest-main', tests_src, matroska_src, - include_directories: [tests_inc, libaegisub_inc], + include_directories: [tests_inc, libaegisub_inc, aegisub_src_inc], dependencies: [tests_deps, deps], cpp_args: tests_args + conf_defines, link_with: libaegisub, @@ -91,7 +91,7 @@ test('luajit-52', executable('luajit-52', 'tests/luajit_52.c', dependencies: lua # Deterministic integration test for Matroska demux metadata, attachments, frames, compression, and errors. matroska_behavior = executable('matroska-behavior', 'matroska/behavior.cpp', matroska_demux_src, - include_directories: include_directories('../src'), + include_directories: aegisub_src_inc, dependencies: [dependency('zlib'), libebml, libmatroska]) test('matroska behavior', find_program('matroska/check-fixtures.py'), args: [matroska_behavior, meson.current_source_dir() / 'matroska' / 'fixtures']) From b83e58466468f167ac1fdacc8484eaa2f5764c87 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Tue, 1 Sep 2026 13:54:36 -0700 Subject: [PATCH 11/28] Consolidate Matroska Meson dependency --- meson.build | 3 +-- src/meson.build | 17 ++++++++++++++--- tests/meson.build | 9 ++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/meson.build b/meson.build index bb75000852..7873890174 100644 --- a/meson.build +++ b/meson.build @@ -181,7 +181,6 @@ zlib_dep = dependency('zlib') deps += zlib_dep libebml = dependency('libebml', version: '>=1.4.0', fallback: ['libebml', 'libebml_dep']) libmatroska = dependency('libmatroska', version: '>=1.6.0', fallback: ['libmatroska', 'libmatroska_dep']) -deps += [libebml, libmatroska] use_bundled_wx = force_all_fallbacks or force_fallback_for.contains('wxWidgets') if not use_bundled_wx @@ -577,5 +576,5 @@ aegisub = executable('aegisub', aegisub_src, version_h, resrc, aegisub_order_dep c_pch: aegisub_c_pch, install: true, install_dir: bindir, - dependencies: deps, + dependencies: [deps, matroska_dep], win_subsystem: 'windows') diff --git a/src/meson.build b/src/meson.build index 501705597d..326f7e35b0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,10 +2,21 @@ subdir('libresrc') aegisub_src_inc = include_directories('.') -matroska_demux_src = files('matroska_demux.cpp') -matroska_src = files('matroska.cpp') + matroska_demux_src +matroska_lib = static_library('aegisub-matroska', + 'matroska.cpp', + 'matroska_demux.cpp', + include_directories: libaegisub_inc, + dependencies: [zlib_dep, libebml, libmatroska], + override_options: ['unity=off'], +) + +matroska_dep = declare_dependency( + link_with: matroska_lib, + include_directories: aegisub_src_inc, + dependencies: [zlib_dep, libebml, libmatroska], +) -aegisub_src = matroska_src + files( +aegisub_src = files( 'aegisublocale.cpp', 'ass_attachment.cpp', 'ass_dialogue.cpp', diff --git a/tests/meson.build b/tests/meson.build index 285d09c20f..ee4ba65af8 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -66,9 +66,9 @@ endif runner = executable( 'gtest-main', - tests_src, matroska_src, + tests_src, include_directories: [tests_inc, libaegisub_inc, aegisub_src_inc], - dependencies: [tests_deps, deps], + dependencies: [tests_deps, deps, matroska_dep], cpp_args: tests_args + conf_defines, link_with: libaegisub, ) @@ -90,8 +90,7 @@ test('luajit-52', executable('luajit-52', 'tests/luajit_52.c', dependencies: lua # Deterministic integration test for Matroska demux metadata, attachments, frames, compression, and errors. matroska_behavior = executable('matroska-behavior', - 'matroska/behavior.cpp', matroska_demux_src, - include_directories: aegisub_src_inc, - dependencies: [dependency('zlib'), libebml, libmatroska]) + 'matroska/behavior.cpp', + dependencies: matroska_dep) test('matroska behavior', find_program('matroska/check-fixtures.py'), args: [matroska_behavior, meson.current_source_dir() / 'matroska' / 'fixtures']) From 03d00cdb1522eb7e9243c989d29e5bd4409b24e3 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Tue, 1 Sep 2026 17:44:38 -0700 Subject: [PATCH 12/28] Explain Matroska unity override --- src/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/src/meson.build b/src/meson.build index 326f7e35b0..3056cc4ec3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,6 +7,7 @@ matroska_lib = static_library('aegisub-matroska', 'matroska_demux.cpp', include_directories: libaegisub_inc, dependencies: [zlib_dep, libebml, libmatroska], + # Preserve separate archive members so matroska-behavior can link only the demux object without pulling in the libaegisub-dependent facade. override_options: ['unity=off'], ) From 2352669cfd9adafa5517ac5c2e421682d7dff599 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Tue, 1 Sep 2026 18:08:13 -0700 Subject: [PATCH 13/28] Shorten Matroska unity comment --- src/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meson.build b/src/meson.build index 3056cc4ec3..f724f60588 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,7 +7,7 @@ matroska_lib = static_library('aegisub-matroska', 'matroska_demux.cpp', include_directories: libaegisub_inc, dependencies: [zlib_dep, libebml, libmatroska], - # Preserve separate archive members so matroska-behavior can link only the demux object without pulling in the libaegisub-dependent facade. + # Keep objects separate so matroska-behavior does not require libaegisub. override_options: ['unity=off'], ) From 846c67809daf359164ab6b42a7a994815f53208a Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Tue, 1 Sep 2026 18:09:59 -0700 Subject: [PATCH 14/28] Align Matroska behavior target arguments --- tests/meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/meson.build b/tests/meson.build index ee4ba65af8..5c25d9cd09 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -90,7 +90,7 @@ test('luajit-52', executable('luajit-52', 'tests/luajit_52.c', dependencies: lua # Deterministic integration test for Matroska demux metadata, attachments, frames, compression, and errors. matroska_behavior = executable('matroska-behavior', - 'matroska/behavior.cpp', - dependencies: matroska_dep) + 'matroska/behavior.cpp', + dependencies: matroska_dep) test('matroska behavior', find_program('matroska/check-fixtures.py'), args: [matroska_behavior, meson.current_source_dir() / 'matroska' / 'fixtures']) From dbe9b113d00ee4a6429d8cd56ab1088f56bf4029 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Tue, 1 Sep 2026 18:25:15 -0700 Subject: [PATCH 15/28] Format Matroska behavior target multiline --- tests/meson.build | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/meson.build b/tests/meson.build index 5c25d9cd09..646222c7b5 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -89,8 +89,10 @@ test('gtest main', runner, env: {'AEGISUB_TEST_DATA_DIR': meson.current_source_d test('luajit-52', executable('luajit-52', 'tests/luajit_52.c', dependencies: luajit)) # Deterministic integration test for Matroska demux metadata, attachments, frames, compression, and errors. -matroska_behavior = executable('matroska-behavior', - 'matroska/behavior.cpp', - dependencies: matroska_dep) +matroska_behavior = executable( + 'matroska-behavior', + 'matroska/behavior.cpp', + dependencies: matroska_dep, +) test('matroska behavior', find_program('matroska/check-fixtures.py'), args: [matroska_behavior, meson.current_source_dir() / 'matroska' / 'fixtures']) From 4da8476399c3cae2de3bbec32c4614b7ffdda612 Mon Sep 17 00:00:00 2001 From: Final AI Agent Date: Wed, 2 Sep 2026 12:13:04 -0700 Subject: [PATCH 16/28] Fix Matroska streaming and import regressions --- src/matroska.cpp | 17 +- src/matroska_demux.cpp | 304 ++++++++++++++++-- src/matroska_demux.h | 4 +- src/meson.build | 2 +- src/mkv_wrap.cpp | 61 ++-- tests/matroska/check-fixtures.py | 2 +- tests/matroska/fixtures/generate.py | 25 ++ .../fixtures/tracks-after-cluster.behavior | 3 + .../fixtures/tracks-after-cluster.mkv | Bin 0 -> 128 bytes tests/tests/matroska.cpp | 8 + 10 files changed, 363 insertions(+), 63 deletions(-) create mode 100644 tests/matroska/fixtures/tracks-after-cluster.behavior create mode 100644 tests/matroska/fixtures/tracks-after-cluster.mkv diff --git a/src/matroska.cpp b/src/matroska.cpp index 006780e8f1..6865958f6c 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -152,7 +152,7 @@ class Demuxer::Impl final : public InputStream { if (std::find_if(attachments.begin(), attachments.end(), [&](auto const &item) { return item.id.value == legacy.UID; }) != attachments.end()) - throw InvalidDataError("Duplicate Matroska attachment ID"); + continue; attachments.push_back({{legacy.UID}, copy_string(legacy.Name), copy_string(legacy.Description), copy_string(legacy.MimeType), legacy.Length}); @@ -238,6 +238,7 @@ class Demuxer::Impl final : public InputStream { compressed.reset(cs_Create(file.get(), track.value, error, sizeof error)); if (!compressed) throw UnsupportedError(error[0] ? error : "Unsupported subtitle compression"); + cs_SetOutputLimit(compressed.get(), limits.decompressed_size); } } @@ -247,8 +248,14 @@ class Demuxer::Impl final : public InputStream { CheckCancelled(); unsigned track, frame_size, flags; uint64_t start, end, position; - if (mkv_ReadFrame(file.get(), ~(1U << selected->value), &track, &start, - &end, &position, &frame_size, &flags) < 0) + int read_result = mkv_ReadFrame(file.get(), ~(1U << selected->value), &track, &start, + &end, &position, &frame_size, &flags); + CheckCancelled(); + if (read_result < -1 && !input_error.empty()) + throw IoError(input_error); + if (read_result < -1) + throw InvalidDataError(copy_string(mkv_GetLastError(file.get()))); + if (read_result < 0) return std::nullopt; std::vector data; @@ -263,6 +270,10 @@ class Demuxer::Impl final : public InputStream { CheckCancelled(); int count = cs_ReadData( compressed.get(), reinterpret_cast(buffer), sizeof buffer); + if (count < 0 && cs_OutputLimitExceeded(compressed.get())) + throw LimitError("Decompressed Matroska subtitle packet exceeds configured limit"); + if (count < 0 && !input_error.empty()) + throw IoError(input_error); if (count < 0) throw InvalidDataError(copy_string(cs_GetLastError(compressed.get()))); if (!count) diff --git a/src/matroska_demux.cpp b/src/matroska_demux.cpp index 78220e7024..6741f915ee 100644 --- a/src/matroska_demux.cpp +++ b/src/matroska_demux.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -93,6 +94,7 @@ template std::string unicode_value(EbmlMaster &m) { struct TrackRecord { TrackInfo info{}; + uint64_t number{}; std::string name, codec, language = "und"; std::vector priv, comp_private; KaxTrackEntry *entry{}; @@ -102,6 +104,9 @@ struct Frame { uint64_t start{}, end{}, pos{}; unsigned size{}, flags{}; }; +struct ClusterLocation { + uint64_t data{}, size{}; +}; struct AttachmentRecord { Attachment info{}; std::string name, description, mime; @@ -117,10 +122,12 @@ struct MatroskaFile { SegmentInfo info{}; std::vector tracks; std::vector frames; + std::vector clusters; std::vector attachments; std::vector attachment_view; std::unordered_map track_by_number; size_t cursor{}; + size_t cluster_cursor{}; unsigned mask{}; uint64_t segment_top{}; std::string error; @@ -130,7 +137,8 @@ namespace { void parse_track(MatroskaFile &file, KaxTrackEntry &entry) { TrackRecord track; track.entry = &entry; - track.info.Number = static_cast(uint_value(entry)); + track.number = uint_value(entry); + track.info.Number = static_cast(track.number); track.info.UID = uint_value(entry); track.info.Type = static_cast(uint_value(entry)); track.info.Enabled = uint_value(entry, 1); @@ -157,7 +165,8 @@ void parse_track(MatroskaFile &file, KaxTrackEntry &entry) { track.comp_private.assign(settings->GetBuffer(), settings->GetBuffer() + settings->GetSize()); } } - file.track_by_number[track.info.Number] = file.tracks.size(); + if (!file.track_by_number.emplace(track.number, file.tracks.size()).second) + throw std::runtime_error("duplicate Matroska track number"); file.tracks.push_back(std::move(track)); } @@ -178,6 +187,167 @@ void parse_attachment(MatroskaFile &file, KaxAttached &element) { file.attachments.push_back(std::move(attachment)); } +struct RawElement { + uint64_t id{}, data{}, size{}, end{}; +}; + +void read_exact(MatroskaFile &file, uint64_t pos, void *buffer, size_t size) { + if (size > static_cast(INT_MAX) || pos > static_cast(INT64_MAX)) + throw std::runtime_error("Matroska read is out of range"); + int got = file.input->read(file.input, pos, buffer, static_cast(size)); + if (got != static_cast(size)) throw std::runtime_error("truncated Matroska input"); +} + +uint8_t read_byte(MatroskaFile &file, uint64_t pos) { + uint8_t byte; + read_exact(file, pos, &byte, 1); + return byte; +} + +RawElement raw_element(MatroskaFile &file, uint64_t pos, uint64_t parent_end) { + uint8_t first = read_byte(file, pos); + unsigned id_length = 1; + for (uint8_t mask = 0x80; id_length <= 4 && !(first & mask); mask >>= 1) ++id_length; + if (id_length > 4) throw std::runtime_error("invalid Matroska element ID"); + uint64_t id = first; + for (unsigned i = 1; i < id_length; ++i) id = (id << 8) | read_byte(file, pos + i); + + uint64_t size_pos = pos + id_length; + first = read_byte(file, size_pos); + unsigned size_length = 1; + uint8_t marker = 0x80; + while (size_length <= 8 && !(first & marker)) { marker >>= 1; ++size_length; } + if (size_length > 8) throw std::runtime_error("invalid Matroska element size"); + uint64_t size = first & (marker - 1); + for (unsigned i = 1; i < size_length; ++i) size = (size << 8) | read_byte(file, size_pos + i); + uint64_t unknown = (uint64_t{1} << (7 * size_length)) - 1; + uint64_t data = size_pos + size_length; + if (size == unknown) size = parent_end - data; + if (data > parent_end || size > parent_end - data) throw std::runtime_error("Matroska element exceeds its parent"); + return {id, data, size, data + size}; +} + +uint64_t raw_uint(MatroskaFile &file, RawElement const& element) { + if (!element.size || element.size > 8) throw std::runtime_error("invalid Matroska integer"); + uint64_t value = 0; + for (uint64_t i = 0; i < element.size; ++i) value = (value << 8) | read_byte(file, element.data + i); + return value; +} + +std::pair raw_vint(MatroskaFile &file, uint64_t pos, uint64_t end) { + if (pos >= end) throw std::runtime_error("truncated Matroska variable integer"); + uint8_t first = read_byte(file, pos); + unsigned length = 1; + uint8_t marker = 0x80; + while (length <= 8 && !(first & marker)) { marker >>= 1; ++length; } + if (length > 8) throw std::runtime_error("invalid Matroska variable integer"); + if (length > end - pos) throw std::runtime_error("truncated Matroska variable integer"); + uint64_t value = first & (marker - 1); + for (unsigned i = 1; i < length; ++i) value = (value << 8) | read_byte(file, pos + i); + return {value, length}; +} + +uint64_t checked_add(uint64_t left, uint64_t right, char const *message) { + if (right > UINT64_MAX - left) throw std::runtime_error(message); + return left + right; +} + +uint64_t checked_multiply(uint64_t left, uint64_t right, char const *message) { + if (left && right > UINT64_MAX / left) throw std::runtime_error(message); + return left * right; +} + +void parse_raw_block(MatroskaFile &file, RawElement const& block, uint64_t cluster_time, + uint64_t duration, bool duration_known, bool keyframe) { + auto [track_number, track_bytes] = raw_vint(file, block.data, block.end); + auto found = file.track_by_number.find(track_number); + if (found == file.track_by_number.end()) return; + unsigned track_index = found->second; + if (track_index >= 32 || ((file.mask >> track_index) & 1)) return; + if (block.size < track_bytes + 3) throw std::runtime_error("truncated Matroska block header"); + uint64_t cursor = checked_add(block.data, track_bytes, "Matroska block header is out of range"); + int16_t relative = static_cast((read_byte(file, cursor) << 8) | read_byte(file, cursor + 1)); + uint8_t flags = read_byte(file, cursor + 2); + cursor += 3; + unsigned lace = (flags >> 1) & 3; + if (lace && cursor >= block.end) throw std::runtime_error("truncated Matroska lacing header"); + unsigned count = lace ? read_byte(file, cursor++) + 1 : 1; + std::vector sizes(count); + uint64_t payload_end = block.end; + if (lace == 1) { + uint64_t sum = 0; + for (unsigned i = 0; i + 1 < count; ++i) { + uint64_t value = 0; uint8_t byte; + do { + if (cursor >= payload_end) throw std::runtime_error("truncated Xiph lacing"); + byte = read_byte(file, cursor++); value += byte; + } while (byte == 255); + sizes[i] = value; sum += value; + } + if (cursor > payload_end || sum > payload_end - cursor) throw std::runtime_error("invalid Xiph lacing"); + sizes.back() = payload_end - cursor - sum; + } else if (lace == 2) { + if ((payload_end - cursor) % count) throw std::runtime_error("invalid fixed lacing"); + std::fill(sizes.begin(), sizes.end(), (payload_end - cursor) / count); + } else if (lace == 3) { + auto [first, used] = raw_vint(file, cursor, payload_end); cursor += used; sizes[0] = first; + uint64_t sum = first; + for (unsigned i = 1; i + 1 < count; ++i) { + auto [encoded, bytes] = raw_vint(file, cursor, payload_end); cursor += bytes; + int64_t bias = (int64_t{1} << (7 * bytes - 1)) - 1; + int64_t next = static_cast(sizes[i - 1]) + static_cast(encoded) - bias; + if (next < 0) throw std::runtime_error("invalid EBML lacing"); + sizes[i] = static_cast(next); sum += sizes[i]; + } + if (cursor > payload_end || sum > payload_end - cursor) throw std::runtime_error("invalid EBML lacing"); + sizes.back() = payload_end - cursor - sum; + } else sizes[0] = payload_end - cursor; + + int64_t ticks = static_cast(cluster_time) + relative; + uint64_t scale = file.info.TimecodeScale; + uint64_t frame_duration = duration_known ? + checked_multiply(duration, scale, "Matroska block duration is out of range") / count : + file.tracks[track_index].info.DefaultDuration; + for (unsigned i = 0; i < count; ++i) { + Frame frame{}; + frame.track = track_index; + if (ticks < 0 || static_cast(ticks) > UINT64_MAX / scale) frame.flags |= FRAME_UNKNOWN_START; + else frame.start = checked_add(checked_multiply(static_cast(ticks), scale, + "Matroska timestamp is out of range"), checked_multiply(i, frame_duration, + "Matroska laced timestamp is out of range"), "Matroska timestamp is out of range"); + if (!duration_known && !frame_duration) frame.flags |= FRAME_UNKNOWN_END; + else frame.end = checked_add(frame.start, frame_duration, "Matroska timestamp is out of range"); + frame.pos = cursor; + if (sizes[i] > UINT_MAX) throw std::runtime_error("Matroska frame is too large"); + frame.size = static_cast(sizes[i]); + if (keyframe || (flags & 0x80)) frame.flags |= FRAME_KF; + file.frames.push_back(frame); + cursor = checked_add(cursor, sizes[i], "Matroska frame exceeds its block"); + } +} + +void parse_raw_cluster(MatroskaFile &file, ClusterLocation const& cluster) { + uint64_t cluster_time = 0; + uint64_t cluster_end = checked_add(cluster.data, cluster.size, "Matroska cluster is out of range"); + for (uint64_t pos = cluster.data; pos < cluster_end;) { + auto element = raw_element(file, pos, cluster_end); + if (element.id == 0xE7) cluster_time = raw_uint(file, element); + else if (element.id == 0xA3) parse_raw_block(file, element, cluster_time, 0, false, false); + else if (element.id == 0xA0) { + RawElement block{}; uint64_t duration = 0; bool has_duration = false, keyframe = true; + for (uint64_t child_pos = element.data; child_pos < element.end;) { + auto child = raw_element(file, child_pos, element.end); + if (child.id == 0xA1) block = child; + else if (child.id == 0x9B) { duration = raw_uint(file, child); has_duration = true; } + else if (child.id == 0xFB) keyframe = false; + child_pos = child.end; + } + if (block.id) parse_raw_block(file, block, cluster_time, duration, has_duration, keyframe); + } + pos = element.end; + } +} + void parse_block(MatroskaFile &file, EbmlMaster &parent, KaxCluster *cluster, KaxInternalBlock &block) { auto track = file.track_by_number.find(block.TrackNum()); if (track == file.track_by_number.end()) @@ -289,22 +459,33 @@ MatroskaFile *mkv_OpenEx(InputStream *input, uint64_t base, unsigned, char *msg, while (next && upper <= 0) { std::unique_ptr current(next); next = nullptr; - bool subtitle_frames = - std::any_of(f->tracks.begin(), f->tracks.end(), [](auto const &t) { return t.info.Type == TT_SUB; }); - if (dynamic_cast(current.get()) && !subtitle_frames) - break; + if (input->progress && !input->progress(input, current->GetElementPosition(), static_cast(input_size))) + throw std::runtime_error("Matroska parsing cancelled"); + if (dynamic_cast(current.get())) { + f->clusters.push_back({current->GetElementPosition() + current->HeadSize(), current->GetSize()}); + current->SkipData(*f->stream, EBML_CONTEXT(segment)); + if (!next) + next = f->stream->FindNextElement(EBML_CONTEXT(segment), upper, std::numeric_limits::max(), true); + continue; + } bool wanted = dynamic_cast(current.get()) || dynamic_cast(current.get()) || - (dynamic_cast(current.get()) && subtitle_frames) || dynamic_cast(current.get()); if (wanted) { + unsigned cap = input->getcachesize ? input->getcachesize(input) : 16U * 1024 * 1024; + auto *attachments = dynamic_cast(current.get()); + if (!attachments && current->GetSize() > cap) { + current->SkipData(*f->stream, EBML_CONTEXT(segment)); + throw std::runtime_error("Matroska metadata exceeds the configured read limit"); + } auto *master = dynamic_cast(current.get()); - master->Read(*f->stream, EBML_CONTEXT(current.get()), upper, next, true, SCOPE_ALL_DATA); + master->Read(*f->stream, EBML_CONTEXT(current.get()), upper, next, true, + attachments ? SCOPE_PARTIAL_DATA : SCOPE_ALL_DATA); if (auto *info = dynamic_cast(master)) { f->info.TimecodeScale = uint_value(*info, 1000000); if (auto *d = child(*info)) f->info.Duration = static_cast(static_cast(*d) * f->info.TimecodeScale); } - parse_master(*f, *master, dynamic_cast(master)); + parse_master(*f, *master); f->top.push_back(std::move(current)); } else current->SkipData(*f->stream, EBML_CONTEXT(segment)); @@ -312,6 +493,19 @@ MatroskaFile *mkv_OpenEx(InputStream *input, uint64_t base, unsigned, char *msg, next = f->stream->FindNextElement(EBML_CONTEXT(segment), upper, std::numeric_limits::max(), true); } + if (input->progress && !input->progress(input, static_cast(input_size), static_cast(input_size))) + throw std::runtime_error("Matroska parsing cancelled"); + if (!f->info.Duration && std::any_of(f->tracks.begin(), f->tracks.end(), [](auto const& track) { + return track.info.Type == TT_SUB; + })) { + for (auto cluster = f->clusters.rbegin(); cluster != f->clusters.rend() && !f->info.Duration; ++cluster) { + f->frames.clear(); + parse_raw_cluster(*f, *cluster); + for (auto const& frame : f->frames) + if (!(frame.flags & FRAME_UNKNOWN_END)) f->info.Duration = std::max(f->info.Duration, frame.end); + } + f->frames.clear(); + } finalize(*f); return f.release(); } catch (std::exception const &e) { @@ -361,27 +555,60 @@ uint64_t mkv_GetSegmentTop(MatroskaFile *f) { void mkv_SetTrackMask(MatroskaFile *f, unsigned m) { f->mask = m; f->cursor = 0; + f->cluster_cursor = 0; + f->frames.clear(); } int mkv_ReadFrame(MatroskaFile *f, unsigned mask, unsigned *track, uint64_t *start, uint64_t *end, uint64_t *pos, unsigned *size, unsigned *flags) { - while (f->cursor < f->frames.size()) { - auto &r = f->frames[f->cursor++]; - if (((mask | f->mask) >> r.track) & 1) - continue; - *track = r.track; - *start = r.start; - *end = r.end; - *pos = r.pos; - *size = r.size; - *flags = r.flags; - return 0; + try { + if (std::none_of(f->tracks.begin(), f->tracks.end(), [](auto const& item) { return item.info.Type == TT_SUB; })) return -1; + for (;;) { + while (f->cursor < f->frames.size()) { + auto &r = f->frames[f->cursor++]; + if (r.track >= 32 || (((mask | f->mask) >> r.track) & 1)) continue; + *track = r.track; + *start = r.start; + *end = r.end; + *pos = r.pos; + *size = r.size; + *flags = r.flags; + return 0; + } + if (f->cluster_cursor >= f->clusters.size()) return -1; + auto const& cluster = f->clusters[f->cluster_cursor++]; + if (f->input->progress && !f->input->progress(f->input, cluster.data, static_cast(f->input->getfilesize(f->input)))) { + f->error = "Matroska parsing cancelled"; + return -2; + } + f->frames.clear(); + f->cursor = 0; + parse_raw_cluster(*f, cluster); + } + } catch (std::exception const& e) { + f->error = e.what(); + return -2; } - return -1; } void mkv_Seek(MatroskaFile *f, uint64_t t, unsigned) { - f->cursor = - std::lower_bound(f->frames.begin(), f->frames.end(), t, [](auto &a, uint64_t b) { return a.start < b; }) - - f->frames.begin(); + try { + f->frames.clear(); + f->cursor = 0; + f->cluster_cursor = 0; + while (f->cluster_cursor < f->clusters.size()) { + parse_raw_cluster(*f, f->clusters[f->cluster_cursor++]); + f->cursor = std::lower_bound(f->frames.begin(), f->frames.end(), t, + [](auto const& frame, uint64_t time) { return frame.start < time; }) - f->frames.begin(); + if (f->cursor < f->frames.size()) return; + f->frames.clear(); + f->cursor = 0; + } + } + catch (std::exception const& error) { + f->error = error.what(); + f->frames.clear(); + f->cursor = 0; + f->cluster_cursor = f->clusters.size(); + } } void mkv_SkipToKeyframe(MatroskaFile *) {} uint64_t mkv_GetLowestQTimecode(MatroskaFile *f) { @@ -400,6 +627,8 @@ struct CompressedStream { std::vector data; size_t cursor{}; std::string error; + uint64_t output_limit = UINT64_MAX; + bool output_limit_exceeded = false; }; namespace { @@ -412,7 +641,7 @@ std::vector read_frame(CompressedStream const &stream) { return frame; } -std::vector inflate_frame(std::vector const &input) { +std::vector inflate_frame(std::vector const &input, uint64_t output_limit) { z_stream zstream{}; if (inflateInit(&zstream) != Z_OK) throw std::runtime_error("zlib initialization failed"); @@ -434,7 +663,10 @@ std::vector inflate_frame(std::vector const &input zstream.next_out = buffer; zstream.avail_out = sizeof buffer; result = inflate(&zstream, Z_NO_FLUSH); - output.insert(output.end(), buffer, buffer + sizeof buffer - zstream.avail_out); + auto produced = sizeof buffer - zstream.avail_out; + if (produced > output_limit - std::min(output.size(), output_limit)) + throw std::runtime_error("decompressed Matroska frame exceeds configured limit"); + output.insert(output.end(), buffer, buffer + produced); } while (result == Z_OK); if (result != Z_STREAM_END) @@ -449,10 +681,12 @@ void decode_frame(CompressedStream &stream) { if (!track.info.CompEnabled) stream.data = std::move(raw_frame); else if (track.info.CompMethod == COMP_PREPEND) { + if (track.comp_private.size() > stream.output_limit || raw_frame.size() > stream.output_limit - track.comp_private.size()) + throw std::runtime_error("decompressed Matroska frame exceeds configured limit"); stream.data = track.comp_private; stream.data.insert(stream.data.end(), raw_frame.begin(), raw_frame.end()); } else if (track.info.CompMethod == COMP_ZLIB) - stream.data = inflate_frame(raw_frame); + stream.data = inflate_frame(raw_frame, stream.output_limit); else throw std::runtime_error("unsupported Matroska compression method"); } @@ -461,11 +695,13 @@ void decode_frame(CompressedStream &stream) { extern "C" { CompressedStream *cs_Create(MatroskaFile *file, unsigned track, char *message, unsigned message_size) { if (!file || track >= file->tracks.size()) { - if (message && message_size) - std::strncpy(message, "invalid track", message_size); + if (message && message_size) { + std::strncpy(message, "invalid track", message_size - 1); + message[message_size - 1] = 0; + } return nullptr; } - return new CompressedStream{file, track, 0, 0, {}, {}, {}}; + return new CompressedStream{file, track, 0, 0, {}, {}, {}, UINT64_MAX}; } void cs_Destroy(CompressedStream *c) { delete c; @@ -475,6 +711,10 @@ void cs_NextFrame(CompressedStream *stream, uint64_t position, unsigned size) { stream->frame_size = size; stream->data.clear(); stream->cursor = 0; + stream->output_limit_exceeded = false; +} +void cs_SetOutputLimit(CompressedStream *stream, uint64_t size) { + stream->output_limit = size; } int cs_ReadData(CompressedStream *stream, char *output, unsigned capacity) { try { @@ -487,9 +727,13 @@ int cs_ReadData(CompressedStream *stream, char *output, unsigned capacity) { return bytes_to_copy; } catch (std::exception const &e) { stream->error = e.what(); + stream->output_limit_exceeded = stream->error == "decompressed Matroska frame exceeds configured limit"; return -1; } } +int cs_OutputLimitExceeded(CompressedStream *stream) { + return stream->output_limit_exceeded; +} const char *cs_GetLastError(CompressedStream *c) { return c->error.c_str(); } diff --git a/src/matroska_demux.h b/src/matroska_demux.h index c4a9f3c009..cf87c19488 100644 --- a/src/matroska_demux.h +++ b/src/matroska_demux.h @@ -349,11 +349,13 @@ void cs_Destroy(/* in */ CompressedStream *cs); * by mkv_ReadFrame */ void cs_NextFrame(/* in */ CompressedStream *cs, /* in */ uint64_t pos, - /* in */ unsigned size); + /* in */ unsigned size); +void cs_SetOutputLimit(/* in */ CompressedStream *cs, /* in */ uint64_t size); /* read and decode more data from current frame, return number of bytes decoded, * 0 on end of frame, or -1 on error */ int cs_ReadData(CompressedStream *cs,char *buffer,unsigned bufsize); +int cs_OutputLimitExceeded(CompressedStream *cs); /* return error message for the last error */ const char *cs_GetLastError(CompressedStream *cs); diff --git a/src/meson.build b/src/meson.build index f724f60588..27dcc0957d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,7 +6,7 @@ matroska_lib = static_library('aegisub-matroska', 'matroska.cpp', 'matroska_demux.cpp', include_directories: libaegisub_inc, - dependencies: [zlib_dep, libebml, libmatroska], + dependencies: [boost_dep, zlib_dep, libebml, libmatroska], # Keep objects separate so matroska-behavior does not require libaegisub. override_options: ['unity=off'], ) diff --git a/src/mkv_wrap.cpp b/src/mkv_wrap.cpp index 45f565bf5a..562ef713c5 100644 --- a/src/mkv_wrap.cpp +++ b/src/mkv_wrap.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace { @@ -61,37 +62,43 @@ void read_subtitles(agi::ProgressSink *ps, agi::matroska::Demuxer& demuxer, } void MatroskaWrapper::GetSubtitles(agi::fs::path const& filename, AssFile *target) { - DialogProgress progress(nullptr, _("Parsing Matroska"), _("Reading subtitles from Matroska file.")); + DialogProgress metadata_progress(nullptr, _("Parsing Matroska"), _("Reading Matroska track information.")); + std::optional demuxer; + metadata_progress.Run([&](agi::ProgressSink *ps) { + demuxer.emplace(agi::matroska::OpenFile(filename), [ps] { return ps->IsCancelled(); }); + }); + std::vector tracks; + std::vector names; + for (auto const& track : demuxer->SubtitleTracks()) if (track.codec != agi::matroska::SubtitleCodec::unsupported) { + tracks.push_back(&track); + names.emplace_back(agi::format("%d (%s %s)%s%s", track.id.value, track.codec_id, track.language, track.name.empty() ? "" : ": ", track.name)); + } + if (tracks.empty()) throw MatroskaException("File has no recognised subtitle tracks."); + size_t choice = 0; + if (tracks.size() > 1) { + int selected = wxGetSingleChoiceIndex(_("Choose which track to read:"), _("Multiple subtitle tracks found"), to_wx(names)); + if (selected < 0) throw agi::UserCancelException("cancelled"); + choice = static_cast(selected); + } + auto const& track = *tracks[choice]; + demuxer->SelectTrack(track.id); + bool srt = track.codec == agi::matroska::SubtitleCodec::srt; + bool ssa = track.codec == agi::matroska::SubtitleCodec::ssa; AssFile imported; + AssParser parser(&imported, !ssa); + if (!srt) { + std::string private_data(track.codec_private.begin(), track.codec_private.end()); + boost::char_separator separator("\r\n"); + for (auto const& line : boost::tokenizer>(private_data, separator)) parser.AddLine(line); + } + else imported.LoadDefault(false, OPT_GET("Subtitle Format/SRT/Default Style Catalog")->GetString()); + parser.AddLine("[Events]"); + double duration = demuxer->Duration() ? demuxer->Duration()->nanoseconds / 1000000.0 : 0.0; + DialogProgress progress(nullptr, _("Parsing Matroska"), _("Reading subtitles from Matroska file.")); std::exception_ptr failure; progress.Run([&](agi::ProgressSink *ps) { try { - agi::matroska::Demuxer demuxer(agi::matroska::OpenFile(filename), [ps] { return ps->IsCancelled(); }); - std::vector tracks; - std::vector names; - for (auto const& track : demuxer.SubtitleTracks()) if (track.codec != agi::matroska::SubtitleCodec::unsupported) { - tracks.push_back(&track); - names.emplace_back(agi::format("%d (%s %s)%s%s", track.id.value, track.codec_id, track.language, track.name.empty() ? "" : ": ", track.name)); - } - if (tracks.empty()) throw MatroskaException("File has no recognised subtitle tracks."); - size_t choice = 0; - if (tracks.size() > 1) { - int selected = wxGetSingleChoiceIndex(_("Choose which track to read:"), _("Multiple subtitle tracks found"), to_wx(names)); - if (selected < 0) throw agi::UserCancelException("cancelled"); - choice = static_cast(selected); - } - auto const& track = *tracks[choice]; demuxer.SelectTrack(track.id); - bool srt = track.codec == agi::matroska::SubtitleCodec::srt; - AssParser parser(&imported, !srt); - if (!srt) { - std::string private_data(track.codec_private.begin(), track.codec_private.end()); - boost::char_separator separator("\r\n"); - for (auto const& line : boost::tokenizer>(private_data, separator)) parser.AddLine(line); - } - else imported.LoadDefault(false, OPT_GET("Subtitle Format/SRT/Default Style Catalog")->GetString()); - parser.AddLine("[Events]"); - double duration = demuxer.Duration() ? demuxer.Duration()->nanoseconds / 1000000.0 : 0.0; - read_subtitles(ps, demuxer, track, duration, &parser); + read_subtitles(ps, *demuxer, track, duration, &parser); } catch (...) { failure = std::current_exception(); } }); diff --git a/tests/matroska/check-fixtures.py b/tests/matroska/check-fixtures.py index dfe1eb6d62..cdc018d0f2 100755 --- a/tests/matroska/check-fixtures.py +++ b/tests/matroska/check-fixtures.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import pathlib,subprocess,sys -program,root=sys.argv[1],pathlib.Path(sys.argv[2]); cases=sorted(root.glob('*.mkv')) +program,root=sys.argv[1],pathlib.Path(sys.argv[2]); cases=sorted([*root.glob('*.mkv'), *root.glob('*.mka')]) if not cases: raise SystemExit('Matroska fixtures missing; run fixtures/generate.py') for case in cases: result=subprocess.run([program,case],text=True,stdout=subprocess.PIPE) diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index a273ec4ecc..884c5f12dc 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -9,3 +9,28 @@ subprocess.run(['mkvmerge','-o',r/'audio-only-opus.mka',opus],check=True) opus.unlink() subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-fflags','+bitexact','-f','lavfi','-i','color=size=16x16:rate=1:duration=1','-map_metadata','-1','-c:v','ffv1',r/'video-only.mkv'],check=True) + +def element(element_id, payload): + length=len(payload) + size_length=next(n for n in range(1,9) if length < (1 << (7*n))-1) + encoded=(length | (1 << (7*size_length))).to_bytes(size_length,'big') + return bytes.fromhex(element_id)+encoded+payload +def uint_element(element_id, value): + size=max(1,(value.bit_length()+7)//8) + return element(element_id,value.to_bytes(size,'big')) + +source=(r/'video-only.mkv').read_bytes() +header_size=4+(source[4]&0x7f)+1 +header=source[:header_size] +info=element('1549a966',uint_element('2ad7b1',1000000)) +block=element('a3',b'\x41\x01\x00\x00\x80hello') +cluster=element('1f43b675',uint_element('e7',0)+block) +entry=element('ae',uint_element('d7',257)+uint_element('73c5',257)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) +tracks=element('1654ae6b',entry) +seek_entry=element('4dbb',element('53ab',bytes.fromhex('1654ae6b'))+uint_element('53ac',0)) +seek_head=element('114d9b74',seek_entry) +tracks_position=len(seek_head)+len(info)+len(cluster) +seek_entry=element('4dbb',element('53ab',bytes.fromhex('1654ae6b'))+uint_element('53ac',tracks_position)) +seek_head=element('114d9b74',seek_entry) +segment_payload=seek_head+info+cluster+tracks +(r/'tracks-after-cluster.mkv').write_bytes(header+element('18538067',segment_payload)) diff --git a/tests/matroska/fixtures/tracks-after-cluster.behavior b/tests/matroska/fixtures/tracks-after-cluster.behavior new file mode 100644 index 0000000000..2600ebb09b --- /dev/null +++ b/tests/matroska/fixtures/tracks-after-cluster.behavior @@ -0,0 +1,3 @@ +segment 1000000 0 +track 0 1 17 S_TEXT/UTF8 eng 0 0 0 +frame 0 0 0 5 6 5a0d15131ec7a1 diff --git a/tests/matroska/fixtures/tracks-after-cluster.mkv b/tests/matroska/fixtures/tracks-after-cluster.mkv new file mode 100644 index 0000000000000000000000000000000000000000..71c446ec7a7319761c8be32d4f92a8dd2d8d294f GIT binary patch literal 128 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@ep%3Wc1fS_ z?(X2#En*?-vV+$&8jE_aOl#M=zOk9#$wA(ETWSCEMux>*j*JWp4H>C9Ir$)Uv)9eM c-o(gQd=yADHww0O2girFMug~xhPYV(06=sy;s5{u literal 0 HcmV?d00001 diff --git a/tests/tests/matroska.cpp b/tests/tests/matroska.cpp index 94c8154e70..9ac529bd8d 100644 --- a/tests/tests/matroska.cpp +++ b/tests/tests/matroska.cpp @@ -54,6 +54,14 @@ TEST(Matroska, CancellationCallbackExceptionsCannotCrossC) { EXPECT_THROW(Demuxer(OpenFile(fixture("subtitle-attachment.mkv")), []() -> bool { throw 42; }), agi::UserCancelException); } +TEST(Matroska, PacketScanCanBeCancelled) { + bool cancelled = false; + Demuxer demuxer(OpenFile(fixture("subtitle-attachment.mkv")), [&] { return cancelled; }); + demuxer.SelectTrack(demuxer.SubtitleTracks()[0].id); + cancelled = true; + EXPECT_THROW((void)demuxer.ReadPacket(), agi::UserCancelException); +} + TEST(Matroska, RejectsUnsupportedTrackAndInvalidIds) { Demuxer demuxer(OpenFile(fixture("video-only.mkv"))); EXPECT_TRUE(demuxer.SubtitleTracks().empty()); From 9e20cdf50853e3de038bd9667f1fb4454482ab07 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 11:12:57 -0700 Subject: [PATCH 17/28] Remove the Haali compatibility layer from the Matroska demuxer agi::matroska::Demuxer was implemented on top of a reimplementation of the Haali MatroskaParser C API, which in turn wrapped libebml and libmatroska. Fold the parts that are actually used directly into the demuxer and delete the C layer along with its licence notice. Behavior changes: - Attachment payloads are only located while opening a file rather than read into memory, which libebml's SCOPE_PARTIAL_DATA did not prevent. - Tracks with encryption or an unsupported content encoding are reported as unsupported instead of making the whole file fail to open. - Subtitle tracks are no longer limited to the first 32 tracks. - Reader failures while opening are reported as IoError. - Duration() is nullopt when the file doesn't specify one. - A cluster which fails to parse while computing the duration from the end of the file is skipped rather than failing to open the file. - Default per-packet limits are 16 MiB to match 5a53f192a. The fixture test now dumps through the public Demuxer interface as part of the gtest suite instead of a separate executable using the C API. Its output on the existing fixtures is unchanged from the previous implementation other than unknown durations, and a fixture covering header stripping, block durations and encrypted tracks is added. Co-Authored-By: Claude Opus 5.5 --- LICENCE | 4 +- src/matroska.cpp | 1096 ++++++++++++----- src/matroska.h | 138 ++- src/matroska_demux.cpp | 740 ----------- src/matroska_demux.h | 368 ------ src/meson.build | 4 +- tests/matroska/behavior.cpp | 122 -- tests/matroska/check-fixtures.py | 12 - .../fixtures/audio-only-opus.behavior | 3 +- .../fixtures/compressed-zlib.behavior | 6 +- tests/matroska/fixtures/encodings.behavior | 4 + tests/matroska/fixtures/encodings.mkv | Bin 0 -> 179 bytes tests/matroska/fixtures/generate.py | 15 + .../fixtures/subtitle-attachment.behavior | 16 +- .../fixtures/tracks-after-cluster.behavior | 6 +- tests/matroska/fixtures/video-only.behavior | 3 +- tests/meson.build | 9 - tests/tests/matroska.cpp | 83 +- 18 files changed, 997 insertions(+), 1632 deletions(-) delete mode 100644 src/matroska_demux.cpp delete mode 100644 src/matroska_demux.h delete mode 100644 tests/matroska/behavior.cpp delete mode 100755 tests/matroska/check-fixtures.py create mode 100644 tests/matroska/fixtures/encodings.behavior create mode 100644 tests/matroska/fixtures/encodings.mkv diff --git a/LICENCE b/LICENCE index e6bce2ad2f..6e4e204ec1 100644 --- a/LICENCE +++ b/LICENCE @@ -36,8 +36,6 @@ follows: src/gl/ - MIT license. See src/gl/glext.h -src/matroska_demux.h - - Licensed to BSDL with permission from the author. - universalchardet/ - MPL 1.1 + diff --git a/src/matroska.cpp b/src/matroska.cpp index 6865958f6c..e48fcf56d4 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -1,330 +1,838 @@ -// Copyright (c) 2026 Aegisub contributors +// Copyright (c) 2026, Aegisub contributors // +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file matroska.cpp +/// @brief Subtitle and attachment demuxer for Matroska files +/// +/// The segment's top-level layout, Info and Tracks are read with libebml and +/// libmatroska. Clusters and attachments are walked with a small EBML reader +/// instead so that only the bytes actually needed are read: cluster contents +/// are parsed lazily one cluster at a time, and attachment payloads are only +/// located until something asks for them. + #include "matroska.h" -#include "matroska_demux.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include #include -#include #include -#include +#include namespace agi::matroska { +using namespace libebml; +using namespace libmatroska; + namespace { +// Element IDs handled by the raw EBML reader +constexpr uint64_t id_cluster_timestamp = 0xE7; +constexpr uint64_t id_simple_block = 0xA3; +constexpr uint64_t id_block_group = 0xA0; +constexpr uint64_t id_block = 0xA1; +constexpr uint64_t id_block_duration = 0x9B; +constexpr uint64_t id_attached_file = 0x61A7; +constexpr uint64_t id_file_name = 0x466E; +constexpr uint64_t id_file_description = 0x467E; +constexpr uint64_t id_file_mime_type = 0x4660; +constexpr uint64_t id_file_data = 0x465C; +constexpr uint64_t id_file_uid = 0x46AE; + +constexpr uint64_t track_type_subtitle = 0x11; + class FileReader final : public Reader { - agi::read_file_mapping file; + agi::read_file_mapping file; public: - explicit FileReader(agi::fs::path const &filename) - : file(filename) {} - uint64_t Size() const override { return file.size(); } - size_t Read(uint64_t position, void *buffer, size_t size) override { - if (position >= file.size()) - return 0; - size = std::min(size, file.size() - position); - memcpy(buffer, file.read(position, size), size); - return size; - } + explicit FileReader(agi::fs::path const& filename) : file(filename) { } + + uint64_t Size() const override { return file.size(); } + + size_t Read(uint64_t position, void *buffer, size_t size) override { + if (position >= file.size()) + return 0; + size = std::min(size, file.size() - position); + memcpy(buffer, file.read(position, size), size); + return size; + } }; -std::string copy_string(char const *value) { return value ? value : ""; } +/// Read from a Reader, reporting any failure as an IoError +size_t read_some(Reader& reader, uint64_t position, void *buffer, size_t size) { + size_t read; + try { + read = reader.Read(position, buffer, size); + } + catch (Error const&) { + throw; + } + catch (agi::Exception const& e) { + throw IoError(e.GetMessage()); + } + catch (std::exception const& e) { + throw IoError(e.what()); + } + catch (...) { + throw IoError("Matroska input read failed"); + } + if (read > size) + throw IoError("Matroska reader returned more bytes than requested"); + return read; +} + +/// Read until either the buffer is full or the input ends +size_t read_full(Reader& reader, uint64_t position, void *buffer, size_t size) { + size_t total = 0; + while (total < size) { + size_t read = read_some(reader, position + total, static_cast(buffer) + total, size - total); + if (!read) break; + total += read; + } + return total; +} + +/// Read-only libebml stream over a Reader. Errors are thrown straight through +/// libebml, which only ever catches exceptions to clean up and rethrow. +class ReaderCallback final : public IOCallback { + Reader& reader; + uint64_t position = 0; + +public: + explicit ReaderCallback(Reader& reader) : reader(reader) { } + + uint32 read(void *buffer, size_t size) override { + size = std::min(size, std::numeric_limits::max()); + size_t read = read_full(reader, position, buffer, size); + position += read; + return static_cast(read); + } + + void setFilePointer(int64 offset, seek_mode mode) override { + int64_t base = 0; + if (mode == seek_current) + base = static_cast(position); + else if (mode == seek_end) + base = static_cast(std::min(reader.Size(), INT64_MAX)); + if ((offset > 0 && base > INT64_MAX - offset) || base + offset < 0) + throw InvalidDataError("Invalid seek in Matroska input"); + position = static_cast(base + offset); + } + + size_t write(void const *, size_t) override { + throw IoError("Matroska input is read-only"); + } + + uint64 getFilePointer() override { return position; } + void close() override { } +}; + +template +T *child(EbmlMaster& master) { + return static_cast(master.FindFirstElt(EBML_INFO(T), false)); +} + +template +uint64_t uint_value(EbmlMaster& master, uint64_t fallback = 0) { + if (auto value = child(master)) + return static_cast(*value); + return fallback; +} + +template +std::string string_value(EbmlMaster& master, std::string fallback = "") { + if (auto value = child(master)) + return value->GetValue(); + return fallback; +} + +template +std::string unicode_value(EbmlMaster& master) { + if (auto value = child(master)) + return value->GetValue().GetUTF8(); + return ""; +} + +uint64_t checked_add(uint64_t lhs, uint64_t rhs, char const *message) { + if (rhs > UINT64_MAX - lhs) + throw InvalidDataError(message); + return lhs + rhs; +} + +uint64_t checked_multiply(uint64_t lhs, uint64_t rhs, char const *message) { + if (lhs && rhs > UINT64_MAX / lhs) + throw InvalidDataError(message); + return lhs * rhs; +} -SubtitleCodec codec_from_id(std::string const &id) { - if (id == "S_TEXT/ASS") - return SubtitleCodec::ass; - if (id == "S_TEXT/SSA") - return SubtitleCodec::ssa; - if (id == "S_TEXT/UTF8") - return SubtitleCodec::srt; - return SubtitleCodec::unsupported; +SubtitleCodec codec_from_id(std::string const& id) { + if (id == "S_TEXT/ASS") + return SubtitleCodec::ass; + if (id == "S_TEXT/SSA") + return SubtitleCodec::ssa; + if (id == "S_TEXT/UTF8") + return SubtitleCodec::srt; + return SubtitleCodec::unsupported; } + +enum class Compression { none, zlib, header_strip, unsupported }; + +Compression parse_encodings(KaxContentEncodings& encodings, std::vector& stripped_header) { + KaxContentEncoding *encoding = nullptr; + for (auto element : encodings.GetElementList()) { + if (auto current = dynamic_cast(element)) { + // Chained encodings aren't used by anything that writes subtitles + if (encoding) return Compression::unsupported; + encoding = current; + } + } + if (!encoding) + return Compression::none; + // Type 1 is encryption + if (uint_value(*encoding) != 0) + return Compression::unsupported; + + auto compression = child(*encoding); + if (!compression) + return Compression::none; + switch (uint_value(*compression)) { + case 0: + return Compression::zlib; + case 3: + if (auto settings = child(*compression)) + stripped_header.assign(settings->GetBuffer(), settings->GetBuffer() + settings->GetSize()); + return Compression::header_strip; + default: + return Compression::unsupported; + } +} + +std::vector inflate_packet(std::vector const& input, size_t limit) { + if (input.size() > std::numeric_limits::max()) + throw LimitError("Compressed Matroska subtitle packet is too large"); + + z_stream stream{}; + if (inflateInit(&stream) != Z_OK) + throw InvalidDataError("Failed to initialize zlib"); + struct InflateEnd { + z_stream *stream; + ~InflateEnd() { inflateEnd(stream); } + } inflate_end{&stream}; + + stream.next_in = const_cast(input.data()); + stream.avail_in = static_cast(input.size()); + + std::vector output; + uint8_t buffer[4096]; + int result; + do { + stream.next_out = buffer; + stream.avail_out = sizeof buffer; + result = inflate(&stream, Z_NO_FLUSH); + if (result != Z_OK && result != Z_STREAM_END) + throw InvalidDataError("Invalid zlib-compressed Matroska subtitle packet"); + size_t produced = sizeof buffer - stream.avail_out; + if (produced > limit - output.size()) + throw LimitError("Decompressed Matroska subtitle packet exceeds configured limit"); + output.insert(output.end(), buffer, buffer + produced); + } while (result != Z_STREAM_END); + return output; +} + +/// Demuxing state for every track in the file, not just subtitle tracks +struct TrackState { + uint64_t default_duration = 0; + Compression compression = Compression::none; + std::vector stripped_header; +}; + +struct Frame { + size_t track = 0; + std::optional start; + std::optional end; + uint64_t position = 0; + uint64_t size = 0; +}; + +/// The data portion of an element in the input +struct Location { + uint64_t position = 0; + uint64_t size = 0; +}; + +struct Element { + uint64_t id = 0; + uint64_t data = 0; + uint64_t size = 0; + uint64_t end = 0; +}; } // namespace -std::unique_ptr OpenFile(agi::fs::path const &filename) { - try { - return std::make_unique(filename); - } - catch (agi::Exception const& error) { - throw IoError(error.GetMessage()); - } +std::unique_ptr OpenFile(agi::fs::path const& filename) { + try { + return std::make_unique(filename); + } + catch (agi::Exception const& e) { + throw IoError(e.GetMessage()); + } } -class Demuxer::Impl final : public InputStream { - std::unique_ptr reader; - CancelCheck cancelled; - Limits limits; - std::string input_error; - std::unique_ptr file{nullptr, mkv_Close}; - std::unique_ptr compressed{ - nullptr, cs_Destroy}; - std::vector tracks; - std::vector attachments; - std::vector<::Attachment> attachment_locations; - std::optional duration; - std::optional selected; - - static Impl *Self(InputStream *input) { return static_cast(input); } - - static int Read(InputStream *input, uint64_t position, void *buffer, - int count) noexcept { - if (count <= 0) - return 0; - try { - size_t total = 0; - while (total < static_cast(count)) { - auto read = Self(input)->reader->Read(position + total, - static_cast(buffer) + total, count - total); - if (!read) break; - if (read > static_cast(count) - total) return -1; - total += read; - } - return static_cast(total); - } catch (...) { - try { Self(input)->input_error = "Matroska input read failed"; } - catch (...) {} - return -1; - } - } - - static int64_t Scan(InputStream *input, uint64_t start, unsigned signature) noexcept { - try { - unsigned window = 0; - for (uint64_t position = start; position < Self(input)->reader->Size(); ++position) { - unsigned char byte; - if (Read(input, position, &byte, 1) != 1) return -1; - window = (window << 8 | byte) & 0xffffffffU; - if (window == signature) return position - 3; - } - } catch (...) {} - return -1; - } - - static int64_t Size(InputStream *input) noexcept { - try { - auto size = Self(input)->reader->Size(); - return size > static_cast(INT64_MAX) ? INT64_MAX : size; - } catch (...) { return 0; } - } - - bool IsCancelled() const noexcept { - if (!cancelled) return false; - try { return cancelled(); } - catch (...) { return true; } - } - - void CheckCancelled() const { - if (IsCancelled()) - throw agi::UserCancelException("Matroska read cancelled"); - } - - void ReadMetadata() { - for (unsigned index = 0; index < mkv_GetNumTracks(file.get()); ++index) { - auto const *info = mkv_GetTrackInfo(file.get(), index); - if (info->Type != TT_SUB) - continue; - - SubtitleTrack track; - track.id.value = index; - track.uid = info->UID; - track.codec_id = copy_string(info->CodecID); - track.codec = codec_from_id(track.codec_id); - track.name = copy_string(info->Name); - track.language = copy_string(info->Language); - track.enabled = info->Enabled; - track.is_default = info->Default; - auto private_data = static_cast(info->CodecPrivate); - if (private_data) - track.codec_private.assign(private_data, - private_data + info->CodecPrivateSize); - tracks.push_back(std::move(track)); - } - - ::Attachment *legacy_attachments = nullptr; - unsigned count = 0; - mkv_GetAttachments(file.get(), &legacy_attachments, &count); - if (count) - attachment_locations.assign(legacy_attachments, - legacy_attachments + count); - for (auto const &legacy : attachment_locations) { - if (std::find_if(attachments.begin(), attachments.end(), [&](auto const &item) { - return item.id.value == legacy.UID; - }) != attachments.end()) - continue; - attachments.push_back({{legacy.UID}, copy_string(legacy.Name), - copy_string(legacy.Description), - copy_string(legacy.MimeType), legacy.Length}); - } - - auto const *segment = mkv_GetFileInfo(file.get()); - if (segment->Duration <= static_cast(INT64_MAX)) - duration = Timestamp{static_cast(segment->Duration)}; - } - - std::vector ReadBytes(uint64_t position, uint64_t size) { - if (size > std::numeric_limits::max()) - throw LimitError("Matroska item exceeds addressable memory"); - std::vector result(static_cast(size)); - size_t offset = 0; - while (offset < result.size()) { - CheckCancelled(); - auto read = reader->Read(position + offset, result.data() + offset, - result.size() - offset); - if (!read) - throw TruncatedError("Unexpected end of Matroska input"); - if (read > result.size() - offset) - throw IoError("Matroska reader returned more bytes than requested"); - offset += read; - } - return result; - } +class Demuxer::Impl { + std::unique_ptr reader; + CancelCheck cancelled; + Limits limits; + + uint64_t timestamp_scale = 1000000; + std::optional duration; + std::vector all_tracks; + std::unordered_map track_by_number; + std::vector tracks; + std::vector attachments; + /// Location of each attachment's data, parallel to attachments + std::vector attachment_data; + std::vector clusters; + + /// Index into all_tracks of the selected track + std::optional selected; + size_t next_cluster = 0; + /// Frames of the selected track in the most recently parsed cluster + std::vector frames; + size_t next_frame = 0; + + void CheckCancelled() const { + bool cancel; + try { + cancel = cancelled && cancelled(); + } + catch (...) { + cancel = true; + } + if (cancel) + throw agi::UserCancelException("Matroska read cancelled"); + } + + void ReadExact(uint64_t position, void *buffer, size_t size) { + if (read_full(*reader, position, buffer, size) != size) + throw TruncatedError("Unexpected end of Matroska input"); + } + + uint8_t ReadByte(uint64_t position) { + uint8_t byte; + ReadExact(position, &byte, 1); + return byte; + } + + std::vector ReadBytes(Location const& location) { + if (location.size > std::numeric_limits::max()) + throw LimitError("Matroska element exceeds addressable memory"); + std::vector result(static_cast(location.size)); + ReadExact(location.position, result.data(), result.size()); + return result; + } + + // Raw EBML reading + + /// Read a variable-length integer, returning its value and encoded length + std::pair ReadVint(uint64_t position, uint64_t end) { + if (position >= end) + throw InvalidDataError("Truncated Matroska variable-length integer"); + uint8_t first = ReadByte(position); + unsigned length = 1; + uint8_t marker = 0x80; + while (length <= 8 && !(first & marker)) { + marker >>= 1; + ++length; + } + if (length > 8) + throw InvalidDataError("Invalid Matroska variable-length integer"); + if (length > end - position) + throw InvalidDataError("Truncated Matroska variable-length integer"); + uint64_t value = first & (marker - 1); + for (unsigned i = 1; i < length; ++i) + value = (value << 8) | ReadByte(position + i); + return {value, length}; + } + + Element ReadElement(uint64_t position, uint64_t parent_end) { + uint8_t first = ReadByte(position); + unsigned id_length = 1; + for (uint8_t mask = 0x80; id_length <= 4 && !(first & mask); mask >>= 1) + ++id_length; + if (id_length > 4) + throw InvalidDataError("Invalid Matroska element ID"); + uint64_t id = first; + for (unsigned i = 1; i < id_length; ++i) + id = (id << 8) | ReadByte(position + i); + + auto [size, size_length] = ReadVint(position + id_length, parent_end); + uint64_t data = position + id_length + size_length; + // All ones is an unknown size, which extends to the end of the parent + if (size == (uint64_t{1} << (7 * size_length)) - 1) + size = parent_end - data; + if (data > parent_end || size > parent_end - data) + throw InvalidDataError("Matroska element exceeds its parent"); + return {id, data, size, data + size}; + } + + uint64_t ReadUInt(Element const& element) { + if (element.size > 8) + throw InvalidDataError("Invalid Matroska integer"); + uint64_t value = 0; + for (uint64_t i = 0; i < element.size; ++i) + value = (value << 8) | ReadByte(element.data + i); + return value; + } + + std::string ReadString(Element const& element) { + if (element.size > limits.metadata_size) + throw LimitError("Matroska string exceeds configured limit"); + std::string value(static_cast(element.size), '\0'); + ReadExact(element.data, value.data(), value.size()); + // Strings may be padded with trailing nulls + value.resize(strnlen(value.data(), value.size())); + return value; + } + + // Metadata + + void ParseInfo(KaxInfo& info) { + timestamp_scale = uint_value(info, 1000000); + if (!timestamp_scale) + throw InvalidDataError("Invalid Matroska timestamp scale"); + if (auto value = child(info)) { + double nanoseconds = static_cast(*value) * timestamp_scale; + if (nanoseconds > 0 && nanoseconds < static_cast(INT64_MAX)) + duration = Timestamp{static_cast(nanoseconds)}; + } + } + + void ParseTrack(KaxTrackEntry& entry) { + TrackState state; + state.default_duration = uint_value(entry); + if (auto encodings = child(entry)) + state.compression = parse_encodings(*encodings, state.stripped_header); + + if (!track_by_number.emplace(uint_value(entry), all_tracks.size()).second) + throw InvalidDataError("Duplicate Matroska track number"); + + if (uint_value(entry) == track_type_subtitle) { + SubtitleTrack track; + track.id.value = static_cast(all_tracks.size()); + track.uid = uint_value(entry); + track.codec_id = string_value(entry); + track.codec = state.compression == Compression::unsupported ? SubtitleCodec::unsupported : codec_from_id(track.codec_id); + track.name = unicode_value(entry); + track.language = string_value(entry, "eng"); + track.enabled = uint_value(entry, 1); + track.is_default = uint_value(entry, 1); + if (auto codec_private = child(entry)) + track.codec_private.assign(codec_private->GetBuffer(), codec_private->GetBuffer() + codec_private->GetSize()); + tracks.push_back(std::move(track)); + } + all_tracks.push_back(std::move(state)); + } + + void ParseAttachments(Location const& list) { + uint64_t end = checked_add(list.position, list.size, "Matroska attachments are out of range"); + for (uint64_t position = list.position; position < end;) { + auto file = ReadElement(position, end); + position = file.end; + if (file.id != id_attached_file) + continue; + + Attachment attachment; + Location data; + for (uint64_t child_position = file.data; child_position < file.end;) { + auto element = ReadElement(child_position, file.end); + child_position = element.end; + switch (element.id) { + case id_file_name: attachment.name = ReadString(element); break; + case id_file_description: attachment.description = ReadString(element); break; + case id_file_mime_type: attachment.mime_type = ReadString(element); break; + case id_file_uid: attachment.id.value = ReadUInt(element); break; + case id_file_data: data = {element.data, element.size}; break; + } + } + + bool duplicate = std::any_of(attachments.begin(), attachments.end(), [&](Attachment const& existing) { + return existing.id == attachment.id; + }); + if (duplicate) + continue; + attachment.size = data.size; + attachments.push_back(std::move(attachment)); + attachment_data.push_back(data); + } + } + + void ParseSegment() { + ReaderCallback io(*reader); + EbmlStream stream(io); + + std::unique_ptr head(stream.FindNextID(EBML_INFO(EbmlHead), UINT64_MAX)); + if (!head) + throw InvalidDataError("EBML header not found"); + head->SkipData(stream, EBML_CONTEXT(head.get())); + + std::unique_ptr segment(stream.FindNextID(EBML_INFO(KaxSegment), UINT64_MAX)); + if (!segment) + throw InvalidDataError("Matroska segment not found"); + auto const& context = EBML_CONTEXT(segment.get()); + + int upper = 0; + std::unique_ptr current(stream.FindNextElement(context, upper, UINT64_MAX, true)); + while (current && upper <= 0) { + CheckCancelled(); + std::unique_ptr next; + Location location{current->GetElementPosition() + current->HeadSize(), current->GetSize()}; + + auto info = dynamic_cast(current.get()); + auto track_list = dynamic_cast(current.get()); + if (info || track_list) { + if (current->GetSize() > limits.metadata_size) + throw LimitError("Matroska metadata exceeds configured limit"); + EbmlElement *found = nullptr; + static_cast(*current).Read(stream, EBML_CONTEXT(current.get()), upper, found, true); + next.reset(found); + // A positive level means the element found belongs to an ancestor + if (upper > 0) + --upper; + + if (info) + ParseInfo(*info); + else { + for (auto element : track_list->GetElementList()) { + if (auto entry = dynamic_cast(element)) + ParseTrack(*entry); + } + } + } + else { + if (dynamic_cast(current.get())) + clusters.push_back(location); + else if (dynamic_cast(current.get())) + ParseAttachments(location); + current->SkipData(stream, context); + } + + if (!next) + next.reset(stream.FindNextElement(context, upper, UINT64_MAX, true)); + current = std::move(next); + } + } + + /// Use the end of the last frame as the duration when the header lacks one + void ComputeDuration() { + if (duration || tracks.empty()) + return; + for (auto cluster = clusters.rbegin(); cluster != clusters.rend(); ++cluster) { + CheckCancelled(); + std::vector cluster_frames; + try { + cluster_frames = ParseCluster(*cluster, std::nullopt); + } + catch (InvalidDataError const&) { + continue; + } + catch (TruncatedError const&) { + continue; + } + + int64_t last_end = 0; + for (auto const& frame : cluster_frames) { + if (frame.end) + last_end = std::max(last_end, frame.end->nanoseconds); + } + if (last_end > 0) { + duration = Timestamp{last_end}; + return; + } + } + } + + // Clusters + + std::optional MakeTimestamp(uint64_t nanoseconds) { + if (nanoseconds > static_cast(INT64_MAX)) + throw InvalidDataError("Matroska timestamp is out of range"); + return Timestamp{static_cast(nanoseconds)}; + } + + /// Split a block into its frames, appending those for only_track (or all tracks) to frames + void ParseBlock(Element const& block, uint64_t cluster_time, std::optional block_duration, + std::optional only_track, std::vector& out) { + auto [track_number, track_bytes] = ReadVint(block.data, block.end); + auto found = track_by_number.find(track_number); + if (found == track_by_number.end()) + return; + size_t track = found->second; + if (only_track && *only_track != track) + return; + + if (block.size < track_bytes + 3) + throw InvalidDataError("Truncated Matroska block header"); + uint64_t cursor = block.data + track_bytes; + auto relative = static_cast((ReadByte(cursor) << 8) | ReadByte(cursor + 1)); + uint8_t flags = ReadByte(cursor + 2); + cursor += 3; + + unsigned lacing = (flags >> 1) & 3; + if (lacing && cursor >= block.end) + throw InvalidDataError("Truncated Matroska lacing header"); + unsigned count = lacing ? ReadByte(cursor++) + 1 : 1; + std::vector sizes(count); + uint64_t end = block.end; + if (lacing == 1) { // Xiph + uint64_t sum = 0; + for (unsigned i = 0; i + 1 < count; ++i) { + uint64_t value = 0; + uint8_t byte; + do { + if (cursor >= end) + throw InvalidDataError("Truncated Matroska Xiph lacing"); + byte = ReadByte(cursor++); + value += byte; + } while (byte == 255); + sizes[i] = value; + sum += value; + } + if (cursor > end || sum > end - cursor) + throw InvalidDataError("Invalid Matroska Xiph lacing"); + sizes.back() = end - cursor - sum; + } + else if (lacing == 2) { // Fixed-size + if ((end - cursor) % count) + throw InvalidDataError("Invalid Matroska fixed-size lacing"); + std::fill(sizes.begin(), sizes.end(), (end - cursor) / count); + } + else if (lacing == 3) { // EBML + auto [first, first_length] = ReadVint(cursor, end); + cursor += first_length; + sizes[0] = first; + uint64_t sum = first; + for (unsigned i = 1; i + 1 < count; ++i) { + auto [encoded, length] = ReadVint(cursor, end); + cursor += length; + int64_t bias = (int64_t{1} << (7 * length - 1)) - 1; + int64_t value = static_cast(sizes[i - 1]) + static_cast(encoded) - bias; + if (value < 0 || static_cast(value) > end - cursor) + throw InvalidDataError("Invalid Matroska EBML lacing"); + sizes[i] = static_cast(value); + sum += sizes[i]; + } + if (cursor > end || sum > end - cursor) + throw InvalidDataError("Invalid Matroska EBML lacing"); + sizes.back() = end - cursor - sum; + } + else + sizes[0] = end - cursor; + + if (cluster_time > static_cast(INT64_MAX)) + throw InvalidDataError("Matroska cluster timestamp is out of range"); + int64_t ticks = static_cast(cluster_time) + relative; + uint64_t frame_duration = block_duration + ? checked_multiply(*block_duration, timestamp_scale, "Matroska block duration is out of range") / count + : all_tracks[track].default_duration; + + for (unsigned i = 0; i < count; ++i) { + Frame frame; + frame.track = track; + frame.position = cursor; + frame.size = sizes[i]; + if (ticks >= 0) { + uint64_t start = checked_add( + checked_multiply(static_cast(ticks), timestamp_scale, "Matroska timestamp is out of range"), + checked_multiply(i, frame_duration, "Matroska timestamp is out of range"), + "Matroska timestamp is out of range"); + frame.start = MakeTimestamp(start); + if (block_duration || frame_duration) + frame.end = MakeTimestamp(checked_add(start, frame_duration, "Matroska timestamp is out of range")); + } + out.push_back(frame); + cursor = checked_add(cursor, sizes[i], "Matroska frame exceeds its block"); + } + } + + std::vector ParseCluster(Location const& cluster, std::optional only_track) { + std::vector out; + uint64_t cluster_time = 0; + uint64_t end = checked_add(cluster.position, cluster.size, "Matroska cluster is out of range"); + for (uint64_t position = cluster.position; position < end;) { + auto element = ReadElement(position, end); + position = element.end; + if (element.id == id_cluster_timestamp) + cluster_time = ReadUInt(element); + else if (element.id == id_simple_block) + ParseBlock(element, cluster_time, std::nullopt, only_track, out); + else if (element.id == id_block_group) { + std::optional block; + std::optional block_duration; + for (uint64_t child_position = element.data; child_position < element.end;) { + auto child = ReadElement(child_position, element.end); + child_position = child.end; + if (child.id == id_block) + block = child; + else if (child.id == id_block_duration) + block_duration = ReadUInt(child); + } + if (block) + ParseBlock(*block, cluster_time, block_duration, only_track, out); + } + } + return out; + } + + std::vector Decode(TrackState const& track, std::vector data) const { + switch (track.compression) { + case Compression::none: + return data; + case Compression::header_strip: + if (track.stripped_header.size() > limits.decompressed_size || data.size() > limits.decompressed_size - track.stripped_header.size()) + throw LimitError("Decompressed Matroska subtitle packet exceeds configured limit"); + data.insert(data.begin(), track.stripped_header.begin(), track.stripped_header.end()); + return data; + case Compression::zlib: + return inflate_packet(data, limits.decompressed_size); + case Compression::unsupported: + break; + } + throw UnsupportedError("Unsupported Matroska content encoding"); + } public: - Impl(std::unique_ptr input, CancelCheck cancel, Limits configured_limits) - : reader(std::move(input)), cancelled(std::move(cancel)), limits(configured_limits) { - if (!reader) - throw InvalidDataError("Cannot open Matroska from a null reader"); - read = &Read; - scan = &Scan; - getfilesize = &Size; - getcachesize = [](InputStream *) { return 16U * 1024 * 1024; }; - geterror = [](InputStream *input) { - return Self(input)->input_error.c_str(); - }; - memalloc = [](InputStream *, size_t size) { return std::malloc(size); }; - memrealloc = [](InputStream *, void *memory, size_t size) { - return std::realloc(memory, size); - }; - memfree = [](InputStream *, void *memory) { std::free(memory); }; - progress = [](InputStream *input, uint64_t, uint64_t) noexcept { - return Self(input)->IsCancelled() ? 0 : 1; - }; - - CheckCancelled(); - char error[2048]{}; - file.reset(mkv_Open(this, error, sizeof error)); - if (!file) { - CheckCancelled(); - throw InvalidDataError(error[0] ? error : "Failed to parse Matroska input"); - } - ReadMetadata(); - } - - std::vector const &Tracks() const { return tracks; } - std::vector const &AttachmentList() const { return attachments; } - std::optional Duration() const { return duration; } - - void Select(TrackId track) { - if (track.value >= mkv_GetNumTracks(file.get()) || - track.value >= sizeof(unsigned) * CHAR_BIT) - throw InvalidDataError("Invalid Matroska subtitle track"); - auto found = - std::find_if(tracks.begin(), tracks.end(), [&](auto const &item) { - return item.id.value == track.value; - }); - if (found == tracks.end()) - throw InvalidDataError("Selected Matroska track is not a subtitle track"); - if (found->codec == SubtitleCodec::unsupported) - throw UnsupportedError("Selected Matroska subtitle codec is unsupported"); - - CheckCancelled(); - mkv_SetTrackMask(file.get(), ~(1U << track.value)); - selected = track; - compressed.reset(); - if (mkv_GetTrackInfo(file.get(), track.value)->CompEnabled) { - char error[2048]{}; - compressed.reset(cs_Create(file.get(), track.value, error, sizeof error)); - if (!compressed) - throw UnsupportedError(error[0] ? error : "Unsupported subtitle compression"); - cs_SetOutputLimit(compressed.get(), limits.decompressed_size); - } - } - - std::optional NextPacket() { - if (!selected) - throw InvalidDataError("No Matroska subtitle track has been selected"); - CheckCancelled(); - unsigned track, frame_size, flags; - uint64_t start, end, position; - int read_result = mkv_ReadFrame(file.get(), ~(1U << selected->value), &track, &start, - &end, &position, &frame_size, &flags); - CheckCancelled(); - if (read_result < -1 && !input_error.empty()) - throw IoError(input_error); - if (read_result < -1) - throw InvalidDataError(copy_string(mkv_GetLastError(file.get()))); - if (read_result < 0) - return std::nullopt; - - std::vector data; - if (frame_size > limits.packet_size) - throw LimitError("Matroska subtitle packet exceeds configured limit"); - if (!compressed) - data = ReadBytes(position, frame_size); - else { - cs_NextFrame(compressed.get(), position, frame_size); - unsigned char buffer[4096]; - for (;;) { - CheckCancelled(); - int count = cs_ReadData( - compressed.get(), reinterpret_cast(buffer), sizeof buffer); - if (count < 0 && cs_OutputLimitExceeded(compressed.get())) - throw LimitError("Decompressed Matroska subtitle packet exceeds configured limit"); - if (count < 0 && !input_error.empty()) - throw IoError(input_error); - if (count < 0) - throw InvalidDataError(copy_string(cs_GetLastError(compressed.get()))); - if (!count) - break; - if (data.size() + static_cast(count) > limits.decompressed_size) - throw LimitError("Decompressed Matroska subtitle packet exceeds configured limit"); - data.insert(data.end(), buffer, buffer + count); - } - } - auto checked_time = [](uint64_t value, unsigned unknown_flag, unsigned flags) -> std::optional { - if (flags & unknown_flag) return std::nullopt; - if (value > static_cast(INT64_MAX)) - throw InvalidDataError("Matroska timestamp is out of range"); - return Timestamp{static_cast(value)}; - }; - return SubtitlePacket{{track}, - checked_time(start, FRAME_UNKNOWN_START, flags), - checked_time(end, FRAME_UNKNOWN_END, flags), - std::move(data)}; - } - - std::vector AttachmentBytes(AttachmentId id) { - auto found = - std::find_if(attachment_locations.begin(), attachment_locations.end(), - [&](auto const &item) { return item.UID == id.value; }); - if (found == attachment_locations.end()) - throw InvalidDataError("Unknown Matroska attachment"); - if (found->Length > limits.attachment_size) - throw LimitError("Matroska attachment exceeds configured limit"); - return ReadBytes(found->Position, found->Length); - } + Impl(std::unique_ptr input, CancelCheck cancel, Limits configured_limits) + : reader(std::move(input)) + , cancelled(std::move(cancel)) + , limits(configured_limits) + { + if (!reader) + throw InvalidDataError("Cannot open Matroska from a null reader"); + CheckCancelled(); + try { + ParseSegment(); + } + catch (std::exception const& e) { + // Our own errors are agi::Exceptions, so this is only libebml rejecting the file + throw InvalidDataError(e.what()); + } + ComputeDuration(); + } + + std::vector const& Tracks() const { return tracks; } + std::vector const& AttachmentList() const { return attachments; } + std::optional Duration() const { return duration; } + + void Select(TrackId id) { + auto track = std::find_if(tracks.begin(), tracks.end(), [&](SubtitleTrack const& track) { + return track.id == id; + }); + if (track == tracks.end()) { + if (id.value >= all_tracks.size()) + throw InvalidDataError("Invalid Matroska subtitle track"); + throw InvalidDataError("Selected Matroska track is not a subtitle track"); + } + if (track->codec == SubtitleCodec::unsupported) + throw UnsupportedError("Selected Matroska subtitle codec is unsupported"); + + CheckCancelled(); + selected = id.value; + next_cluster = 0; + frames.clear(); + next_frame = 0; + } + + std::optional NextPacket() { + if (!selected) + throw InvalidDataError("No Matroska subtitle track has been selected"); + CheckCancelled(); + + while (next_frame == frames.size()) { + if (next_cluster == clusters.size()) + return std::nullopt; + // Clear first so that a cluster which fails to parse is skipped on retry + frames.clear(); + next_frame = 0; + frames = ParseCluster(clusters[next_cluster++], selected); + CheckCancelled(); + } + + auto const& frame = frames[next_frame++]; + if (frame.size > limits.packet_size) + throw LimitError("Matroska subtitle packet exceeds configured limit"); + auto data = Decode(all_tracks[*selected], ReadBytes({frame.position, frame.size})); + return SubtitlePacket{{static_cast(*selected)}, frame.start, frame.end, std::move(data)}; + } + + std::vector AttachmentBytes(AttachmentId id) { + auto attachment = std::find_if(attachments.begin(), attachments.end(), [&](Attachment const& attachment) { + return attachment.id == id; + }); + if (attachment == attachments.end()) + throw InvalidDataError("Unknown Matroska attachment"); + auto const& data = attachment_data[attachment - attachments.begin()]; + if (data.size > limits.attachment_size) + throw LimitError("Matroska attachment exceeds configured limit"); + CheckCancelled(); + return ReadBytes(data); + } }; Demuxer::Demuxer(std::unique_ptr reader, CancelCheck cancelled, Limits limits) - : impl(std::make_unique(std::move(reader), std::move(cancelled), limits)) {} +: impl(std::make_unique(std::move(reader), std::move(cancelled), limits)) +{ +} + Demuxer::~Demuxer() = default; -Demuxer::Demuxer(Demuxer &&) noexcept = default; -Demuxer &Demuxer::operator=(Demuxer &&) noexcept = default; -std::vector const &Demuxer::SubtitleTracks() const { - return impl->Tracks(); +Demuxer::Demuxer(Demuxer&&) noexcept = default; +Demuxer& Demuxer::operator=(Demuxer&&) noexcept = default; + +std::vector const& Demuxer::SubtitleTracks() const { + return impl->Tracks(); } -std::vector const &Demuxer::Attachments() const { - return impl->AttachmentList(); + +std::vector const& Demuxer::Attachments() const { + return impl->AttachmentList(); } -std::optional Demuxer::Duration() const { return impl->Duration(); } -void Demuxer::SelectTrack(TrackId track) { impl->Select(track); } + +std::optional Demuxer::Duration() const { + return impl->Duration(); +} + +void Demuxer::SelectTrack(TrackId track) { + impl->Select(track); +} + std::optional Demuxer::ReadPacket() { - return impl->NextPacket(); + return impl->NextPacket(); } + std::vector Demuxer::ReadAttachment(AttachmentId id) { - return impl->AttachmentBytes(id); + return impl->AttachmentBytes(id); } } // namespace agi::matroska diff --git a/src/matroska.h b/src/matroska.h index 2af3bd9245..08319cbc5d 100644 --- a/src/matroska.h +++ b/src/matroska.h @@ -1,8 +1,21 @@ -// Copyright (c) 2026 Aegisub contributors +// Copyright (c) 2026, Aegisub contributors // -// Permission to use, copy, modify, and distribute this software for any purpose -// with or without fee is hereby granted, provided that the above copyright -// notice and this permission notice appear in all copies. +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file matroska.h +/// @brief Subtitle and attachment demuxer for Matroska files #pragma once @@ -27,96 +40,105 @@ DEFINE_EXCEPTION(UnsupportedError, Error); DEFINE_EXCEPTION(LimitError, Error); struct Timestamp { - int64_t nanoseconds = 0; + int64_t nanoseconds = 0; }; struct TrackId { - uint32_t value = 0; - friend bool operator==(TrackId lhs, TrackId rhs) { return lhs.value == rhs.value; } + /// Index of the track among all tracks in the file, including non-subtitle tracks + uint32_t value = 0; + friend bool operator==(TrackId lhs, TrackId rhs) { return lhs.value == rhs.value; } }; struct AttachmentId { - uint64_t value = 0; - friend bool operator==(AttachmentId lhs, AttachmentId rhs) { return lhs.value == rhs.value; } + uint64_t value = 0; + friend bool operator==(AttachmentId lhs, AttachmentId rhs) { return lhs.value == rhs.value; } }; enum class SubtitleCodec { ass, ssa, srt, unsupported }; struct SubtitleTrack { - TrackId id; - uint64_t uid = 0; - SubtitleCodec codec = SubtitleCodec::unsupported; - std::string codec_id; - std::string name; - std::string language; - std::vector codec_private; - bool enabled = true; - bool is_default = false; + TrackId id; + uint64_t uid = 0; + /// unsupported if either the codec or the track's content encoding is unsupported + SubtitleCodec codec = SubtitleCodec::unsupported; + std::string codec_id; + std::string name; + std::string language; + std::vector codec_private; + bool enabled = true; + bool is_default = false; }; struct Attachment { - AttachmentId id; - std::string name; - std::string description; - std::string mime_type; - uint64_t size = 0; + AttachmentId id; + std::string name; + std::string description; + std::string mime_type; + uint64_t size = 0; }; struct SubtitlePacket { - TrackId track; - std::optional start; - std::optional end; - std::vector data; + TrackId track; + std::optional start; + std::optional end; + std::vector data; }; -/// Cancellation callbacks are isolated from the C ABI: exceptions are treated as cancellation. +/// Polled during parsing; returning true or throwing aborts with agi::UserCancelException using CancelCheck = std::function; struct Limits { - size_t packet_size = 64 * 1024 * 1024; - size_t attachment_size = 256 * 1024 * 1024; - size_t decompressed_size = 64 * 1024 * 1024; + /// Maximum size of a single stored subtitle packet + size_t packet_size = 16 * 1024 * 1024; + /// Maximum size of a single subtitle packet after decompression + size_t decompressed_size = 16 * 1024 * 1024; + /// Maximum size of an attachment returned by ReadAttachment + size_t attachment_size = 256 * 1024 * 1024; + /// Maximum size of each top-level metadata element other than attachments + size_t metadata_size = 16 * 1024 * 1024; }; /// Random-access input owned by Demuxer for its entire lifetime. class Reader { public: - virtual ~Reader() = default; - virtual uint64_t Size() const = 0; - /// Read up to size bytes at position. A short read is permitted. Returning zero at - /// Size() means EOF; returning zero before Size() is treated as truncation. - virtual size_t Read(uint64_t position, void *buffer, size_t size) = 0; + virtual ~Reader() = default; + virtual uint64_t Size() const = 0; + /// Read up to size bytes at position. A short read is permitted. Returning zero at + /// Size() means EOF; returning zero before Size() is treated as truncation. + virtual size_t Read(uint64_t position, void *buffer, size_t size) = 0; }; /// Open a file-backed reader using Aegisub's mapped-file input. -std::unique_ptr OpenFile(agi::fs::path const &filename); +std::unique_ptr OpenFile(agi::fs::path const& filename); /// The demuxer owns its Reader and all parser state. Returned metadata and /// packet bytes are values, so none of them borrow storage from the parser. class Demuxer final { - class Impl; - std::unique_ptr impl; + class Impl; + std::unique_ptr impl; public: - explicit Demuxer(std::unique_ptr reader, CancelCheck cancelled = {}, Limits limits = {}); - ~Demuxer(); - - Demuxer(Demuxer &&) noexcept; - Demuxer &operator=(Demuxer &&) noexcept; - Demuxer(Demuxer const &) = delete; - Demuxer &operator=(Demuxer const &) = delete; - - std::vector const &SubtitleTracks() const; - std::vector const &Attachments() const; - std::optional Duration() const; - - /// Select one subtitle track and rewind packet iteration to its beginning. - void SelectTrack(TrackId track); - /// Return the next decoded packet, or nullopt at end of input. On error or - /// cancellation the current packet is consumed; callers may retry to read the next packet. - std::optional ReadPacket(); - /// Copy an attachment from the input. The returned vector owns its bytes. - std::vector ReadAttachment(AttachmentId id); + explicit Demuxer(std::unique_ptr reader, CancelCheck cancelled = {}, Limits limits = {}); + ~Demuxer(); + + Demuxer(Demuxer&&) noexcept; + Demuxer& operator=(Demuxer&&) noexcept; + Demuxer(Demuxer const&) = delete; + Demuxer& operator=(Demuxer const&) = delete; + + std::vector const& SubtitleTracks() const; + std::vector const& Attachments() const; + /// Segment duration, or nullopt if neither the header nor the last cluster gives one + std::optional Duration() const; + + /// Select one subtitle track and rewind packet iteration to its beginning. + void SelectTrack(TrackId track); + /// Return the next decoded packet, or nullopt at end of input. On error or + /// cancellation the failed packet (or cluster) is consumed; callers may + /// retry to read the next packet. + std::optional ReadPacket(); + /// Copy an attachment from the input. The returned vector owns its bytes. + std::vector ReadAttachment(AttachmentId id); }; } // namespace agi::matroska diff --git a/src/matroska_demux.cpp b/src/matroska_demux.cpp deleted file mode 100644 index 6741f915ee..0000000000 --- a/src/matroska_demux.cpp +++ /dev/null @@ -1,740 +0,0 @@ -// Copyright (c) 2026 Aegisub contributors -// Native Matroska demux implementation backed by libebml and libmatroska. -#include -#include "matroska_demux.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace libebml; -using namespace libmatroska; - -namespace { -class InputCallback final : public IOCallback { - InputStream *in; - uint64_t pos = 0; - - public: - explicit InputCallback(InputStream *input, uint64_t base) : in(input), pos(base) {} - uint32 read(void *buffer, size_t size) override { - if (!in || !in->read) - throw std::runtime_error("Matroska input has no read callback"); - if (size > INT_MAX) - size = INT_MAX; - int got = in->read(in, pos, buffer, static_cast(size)); - if (got < 0) - throw std::runtime_error(in->geterror ? in->geterror(in) : "input error"); - if (static_cast(got) > size) - throw std::runtime_error("Matroska input returned too much data"); - pos += got; - return got; - } - void setFilePointer(int64 offset, seek_mode mode) override { - int64 next = offset; - if (mode == seek_current) - next += pos; - else if (mode == seek_end) - next += in->getfilesize(in); - if (next < 0) - throw std::runtime_error("invalid negative seek"); - pos = static_cast(next); - } - size_t write(void const *, size_t) override { - throw std::runtime_error("read-only input"); - } - uint64 getFilePointer() override { - return pos; - } - void close() override {} -}; - -template T *child(EbmlMaster &m) { - return static_cast(m.FindFirstElt(EBML_INFO(T), false)); -} -template uint64_t uint_value(EbmlMaster &m, uint64_t fallback = 0) { - if (auto *v = child(m)) - return static_cast(*v); - return fallback; -} -template std::string string_value(EbmlMaster &m, std::string fallback = {}) { - if (auto *v = child(m)) - return v->GetValue(); - return fallback; -} -template std::string unicode_value(EbmlMaster &m) { - if (auto *v = child(m)) - return v->GetValue().GetUTF8(); - return {}; -} - -struct TrackRecord { - TrackInfo info{}; - uint64_t number{}; - std::string name, codec, language = "und"; - std::vector priv, comp_private; - KaxTrackEntry *entry{}; -}; -struct Frame { - unsigned track{}; - uint64_t start{}, end{}, pos{}; - unsigned size{}, flags{}; -}; -struct ClusterLocation { - uint64_t data{}, size{}; -}; -struct AttachmentRecord { - Attachment info{}; - std::string name, description, mime; -}; -} // namespace - -struct MatroskaFile { - InputStream *input{}; - std::unique_ptr io; - std::unique_ptr stream; - std::unique_ptr segment; - std::vector> top; - SegmentInfo info{}; - std::vector tracks; - std::vector frames; - std::vector clusters; - std::vector attachments; - std::vector attachment_view; - std::unordered_map track_by_number; - size_t cursor{}; - size_t cluster_cursor{}; - unsigned mask{}; - uint64_t segment_top{}; - std::string error; -}; - -namespace { -void parse_track(MatroskaFile &file, KaxTrackEntry &entry) { - TrackRecord track; - track.entry = &entry; - track.number = uint_value(entry); - track.info.Number = static_cast(track.number); - track.info.UID = uint_value(entry); - track.info.Type = static_cast(uint_value(entry)); - track.info.Enabled = uint_value(entry, 1); - track.info.Default = uint_value(entry, 1); - track.info.Lacing = uint_value(entry, 1); - track.info.MinCache = uint_value(entry); - track.info.MaxCache = uint_value(entry); - track.info.DefaultDuration = uint_value(entry); - track.info.TimecodeScale = - child(entry) ? static_cast(*child(entry)) : 1.0; - track.name = unicode_value(entry); - track.codec = string_value(entry); - track.language = string_value(entry, "eng"); - if (auto *codec_private = child(entry)) - track.priv.assign(codec_private->GetBuffer(), codec_private->GetBuffer() + codec_private->GetSize()); - if (auto *encodings = child(entry)) - if (auto *encoding = child(*encodings)) { - if (uint_value(*encoding, 0) != 0) - throw std::runtime_error("encrypted tracks are unsupported"); - if (auto *compression = child(*encoding)) { - track.info.CompEnabled = 1; - track.info.CompMethod = static_cast(uint_value(*compression)); - if (auto *settings = child(*compression)) - track.comp_private.assign(settings->GetBuffer(), settings->GetBuffer() + settings->GetSize()); - } - } - if (!file.track_by_number.emplace(track.number, file.tracks.size()).second) - throw std::runtime_error("duplicate Matroska track number"); - file.tracks.push_back(std::move(track)); -} - -void parse_attachment(MatroskaFile &file, KaxAttached &element) { - AttachmentRecord attachment; - attachment.name = unicode_value(element); - attachment.description = unicode_value(element); - attachment.mime = string_value(element); - attachment.info.UID = uint_value(element); - - if (auto *data = child(element)) { - // File data is not copied. The legacy API exposes its location in the input stream. - attachment.info.Position = - data->GetElementPosition() + EBML_ID_LENGTH(static_cast(*data)) + data->GetSizeLength(); - attachment.info.Length = data->GetSize(); - } - - file.attachments.push_back(std::move(attachment)); -} - -struct RawElement { - uint64_t id{}, data{}, size{}, end{}; -}; - -void read_exact(MatroskaFile &file, uint64_t pos, void *buffer, size_t size) { - if (size > static_cast(INT_MAX) || pos > static_cast(INT64_MAX)) - throw std::runtime_error("Matroska read is out of range"); - int got = file.input->read(file.input, pos, buffer, static_cast(size)); - if (got != static_cast(size)) throw std::runtime_error("truncated Matroska input"); -} - -uint8_t read_byte(MatroskaFile &file, uint64_t pos) { - uint8_t byte; - read_exact(file, pos, &byte, 1); - return byte; -} - -RawElement raw_element(MatroskaFile &file, uint64_t pos, uint64_t parent_end) { - uint8_t first = read_byte(file, pos); - unsigned id_length = 1; - for (uint8_t mask = 0x80; id_length <= 4 && !(first & mask); mask >>= 1) ++id_length; - if (id_length > 4) throw std::runtime_error("invalid Matroska element ID"); - uint64_t id = first; - for (unsigned i = 1; i < id_length; ++i) id = (id << 8) | read_byte(file, pos + i); - - uint64_t size_pos = pos + id_length; - first = read_byte(file, size_pos); - unsigned size_length = 1; - uint8_t marker = 0x80; - while (size_length <= 8 && !(first & marker)) { marker >>= 1; ++size_length; } - if (size_length > 8) throw std::runtime_error("invalid Matroska element size"); - uint64_t size = first & (marker - 1); - for (unsigned i = 1; i < size_length; ++i) size = (size << 8) | read_byte(file, size_pos + i); - uint64_t unknown = (uint64_t{1} << (7 * size_length)) - 1; - uint64_t data = size_pos + size_length; - if (size == unknown) size = parent_end - data; - if (data > parent_end || size > parent_end - data) throw std::runtime_error("Matroska element exceeds its parent"); - return {id, data, size, data + size}; -} - -uint64_t raw_uint(MatroskaFile &file, RawElement const& element) { - if (!element.size || element.size > 8) throw std::runtime_error("invalid Matroska integer"); - uint64_t value = 0; - for (uint64_t i = 0; i < element.size; ++i) value = (value << 8) | read_byte(file, element.data + i); - return value; -} - -std::pair raw_vint(MatroskaFile &file, uint64_t pos, uint64_t end) { - if (pos >= end) throw std::runtime_error("truncated Matroska variable integer"); - uint8_t first = read_byte(file, pos); - unsigned length = 1; - uint8_t marker = 0x80; - while (length <= 8 && !(first & marker)) { marker >>= 1; ++length; } - if (length > 8) throw std::runtime_error("invalid Matroska variable integer"); - if (length > end - pos) throw std::runtime_error("truncated Matroska variable integer"); - uint64_t value = first & (marker - 1); - for (unsigned i = 1; i < length; ++i) value = (value << 8) | read_byte(file, pos + i); - return {value, length}; -} - -uint64_t checked_add(uint64_t left, uint64_t right, char const *message) { - if (right > UINT64_MAX - left) throw std::runtime_error(message); - return left + right; -} - -uint64_t checked_multiply(uint64_t left, uint64_t right, char const *message) { - if (left && right > UINT64_MAX / left) throw std::runtime_error(message); - return left * right; -} - -void parse_raw_block(MatroskaFile &file, RawElement const& block, uint64_t cluster_time, - uint64_t duration, bool duration_known, bool keyframe) { - auto [track_number, track_bytes] = raw_vint(file, block.data, block.end); - auto found = file.track_by_number.find(track_number); - if (found == file.track_by_number.end()) return; - unsigned track_index = found->second; - if (track_index >= 32 || ((file.mask >> track_index) & 1)) return; - if (block.size < track_bytes + 3) throw std::runtime_error("truncated Matroska block header"); - uint64_t cursor = checked_add(block.data, track_bytes, "Matroska block header is out of range"); - int16_t relative = static_cast((read_byte(file, cursor) << 8) | read_byte(file, cursor + 1)); - uint8_t flags = read_byte(file, cursor + 2); - cursor += 3; - unsigned lace = (flags >> 1) & 3; - if (lace && cursor >= block.end) throw std::runtime_error("truncated Matroska lacing header"); - unsigned count = lace ? read_byte(file, cursor++) + 1 : 1; - std::vector sizes(count); - uint64_t payload_end = block.end; - if (lace == 1) { - uint64_t sum = 0; - for (unsigned i = 0; i + 1 < count; ++i) { - uint64_t value = 0; uint8_t byte; - do { - if (cursor >= payload_end) throw std::runtime_error("truncated Xiph lacing"); - byte = read_byte(file, cursor++); value += byte; - } while (byte == 255); - sizes[i] = value; sum += value; - } - if (cursor > payload_end || sum > payload_end - cursor) throw std::runtime_error("invalid Xiph lacing"); - sizes.back() = payload_end - cursor - sum; - } else if (lace == 2) { - if ((payload_end - cursor) % count) throw std::runtime_error("invalid fixed lacing"); - std::fill(sizes.begin(), sizes.end(), (payload_end - cursor) / count); - } else if (lace == 3) { - auto [first, used] = raw_vint(file, cursor, payload_end); cursor += used; sizes[0] = first; - uint64_t sum = first; - for (unsigned i = 1; i + 1 < count; ++i) { - auto [encoded, bytes] = raw_vint(file, cursor, payload_end); cursor += bytes; - int64_t bias = (int64_t{1} << (7 * bytes - 1)) - 1; - int64_t next = static_cast(sizes[i - 1]) + static_cast(encoded) - bias; - if (next < 0) throw std::runtime_error("invalid EBML lacing"); - sizes[i] = static_cast(next); sum += sizes[i]; - } - if (cursor > payload_end || sum > payload_end - cursor) throw std::runtime_error("invalid EBML lacing"); - sizes.back() = payload_end - cursor - sum; - } else sizes[0] = payload_end - cursor; - - int64_t ticks = static_cast(cluster_time) + relative; - uint64_t scale = file.info.TimecodeScale; - uint64_t frame_duration = duration_known ? - checked_multiply(duration, scale, "Matroska block duration is out of range") / count : - file.tracks[track_index].info.DefaultDuration; - for (unsigned i = 0; i < count; ++i) { - Frame frame{}; - frame.track = track_index; - if (ticks < 0 || static_cast(ticks) > UINT64_MAX / scale) frame.flags |= FRAME_UNKNOWN_START; - else frame.start = checked_add(checked_multiply(static_cast(ticks), scale, - "Matroska timestamp is out of range"), checked_multiply(i, frame_duration, - "Matroska laced timestamp is out of range"), "Matroska timestamp is out of range"); - if (!duration_known && !frame_duration) frame.flags |= FRAME_UNKNOWN_END; - else frame.end = checked_add(frame.start, frame_duration, "Matroska timestamp is out of range"); - frame.pos = cursor; - if (sizes[i] > UINT_MAX) throw std::runtime_error("Matroska frame is too large"); - frame.size = static_cast(sizes[i]); - if (keyframe || (flags & 0x80)) frame.flags |= FRAME_KF; - file.frames.push_back(frame); - cursor = checked_add(cursor, sizes[i], "Matroska frame exceeds its block"); - } -} - -void parse_raw_cluster(MatroskaFile &file, ClusterLocation const& cluster) { - uint64_t cluster_time = 0; - uint64_t cluster_end = checked_add(cluster.data, cluster.size, "Matroska cluster is out of range"); - for (uint64_t pos = cluster.data; pos < cluster_end;) { - auto element = raw_element(file, pos, cluster_end); - if (element.id == 0xE7) cluster_time = raw_uint(file, element); - else if (element.id == 0xA3) parse_raw_block(file, element, cluster_time, 0, false, false); - else if (element.id == 0xA0) { - RawElement block{}; uint64_t duration = 0; bool has_duration = false, keyframe = true; - for (uint64_t child_pos = element.data; child_pos < element.end;) { - auto child = raw_element(file, child_pos, element.end); - if (child.id == 0xA1) block = child; - else if (child.id == 0x9B) { duration = raw_uint(file, child); has_duration = true; } - else if (child.id == 0xFB) keyframe = false; - child_pos = child.end; - } - if (block.id) parse_raw_block(file, block, cluster_time, duration, has_duration, keyframe); - } - pos = element.end; - } -} - -void parse_block(MatroskaFile &file, EbmlMaster &parent, KaxCluster *cluster, KaxInternalBlock &block) { - auto track = file.track_by_number.find(block.TrackNum()); - if (track == file.track_by_number.end()) - return; - - auto track_index = track->second; - uint64_t frame_duration = file.tracks[track_index].info.DefaultDuration; - if (auto *group = dynamic_cast(&parent)) - if (auto *duration = child(*group)) - frame_duration = static_cast(*duration) * file.info.TimecodeScale; - - auto frame_count = block.NumberFrames(); - if (frame_count && frame_duration && dynamic_cast(&parent)) - frame_duration /= frame_count; - - int64_t cluster_ticks = cluster ? static_cast(uint_value(*cluster)) : 0; - int64_t frame_ticks = cluster_ticks + block.GetRelativeTimestamp(); - uint64_t first_frame_time = frame_ticks < 0 ? 0 : static_cast(frame_ticks) * file.info.TimecodeScale; - - bool is_keyframe = true; - if (auto *simple_block = dynamic_cast(&block)) - is_keyframe = simple_block->IsKeyframe(); - else if (auto *group = dynamic_cast(&parent)) - is_keyframe = child(*group) == nullptr; - - for (unsigned frame_index = 0; frame_index < frame_count; ++frame_index) { - Frame frame; - frame.track = track_index; - frame.start = first_frame_time + frame_index * frame_duration; - frame.end = frame.start + frame_duration; - frame.pos = block.GetDataPosition(frame_index); - frame.size = static_cast(block.GetFrameSize(frame_index)); - frame.flags = is_keyframe ? FRAME_KF : 0; - file.frames.push_back(frame); - } -} - -void parse_master(MatroskaFile &file, EbmlMaster &master, KaxCluster *cluster = nullptr) { - for (auto *element : master.GetElementList()) { - if (auto *entry = dynamic_cast(element)) - parse_track(file, *entry); - else if (auto *attachment = dynamic_cast(element)) - parse_attachment(file, *attachment); - else if (auto *block = dynamic_cast(element)) - parse_block(file, master, cluster, *block); - - if (auto *nested = dynamic_cast(element)) - parse_master(file, *nested, cluster ? cluster : dynamic_cast(nested)); - } -} - -void finalize(MatroskaFile &f) { - for (auto &r : f.tracks) { - r.info.Name = r.name.empty() ? nullptr : r.name.data(); - r.info.CodecID = r.codec.data(); - std::memset(r.info.Language, 0, 4); - std::memcpy(r.info.Language, r.language.data(), std::min(3, r.language.size())); - r.info.CodecPrivate = r.priv.empty() ? nullptr : r.priv.data(); - r.info.CodecPrivateSize = r.priv.size(); - r.info.CompMethodPrivate = r.comp_private.empty() ? nullptr : r.comp_private.data(); - r.info.CompMethodPrivateSize = r.comp_private.size(); - } - for (auto &r : f.attachments) { - r.info.Name = r.name.data(); - r.info.Description = r.description.empty() ? nullptr : r.description.data(); - r.info.MimeType = r.mime.data(); - f.attachment_view.push_back(r.info); - } - std::stable_sort(f.frames.begin(), f.frames.end(), [](auto &a, auto &b) { return a.start < b.start; }); - if (!f.info.Duration) - for (auto const &frame : f.frames) - f.info.Duration = std::max(f.info.Duration, frame.end); -} -} // namespace - -extern "C" { -MatroskaFile *mkv_Open(InputStream *io, char *msg, unsigned n) { - return mkv_OpenEx(io, 0, 0, msg, n); -} -MatroskaFile *mkv_OpenEx(InputStream *input, uint64_t base, unsigned, char *msg, unsigned n) { - try { - if (!input || !input->read || !input->getfilesize) - throw std::runtime_error("invalid Matroska input stream"); - auto input_size = input->getfilesize(input); - if (input_size < 0 || base > static_cast(input_size)) - throw std::runtime_error("invalid Matroska input size"); - auto f = std::make_unique(); - f->input = input; - f->io = std::make_unique(input, base); - f->stream = std::make_unique(*f->io); - int upper = 0; - std::unique_ptr head( - f->stream->FindNextID(EBML_INFO(EbmlHead), std::numeric_limits::max())); - if (!head) - throw std::runtime_error("EBML header not found"); - head->SkipData(*f->stream, EBML_CONTEXT(head.get())); - std::unique_ptr e( - f->stream->FindNextID(EBML_INFO(KaxSegment), std::numeric_limits::max())); - if (!e) - throw std::runtime_error("Matroska segment not found"); - f->segment_top = e->GetElementPosition(); - if (std::strcmp(EBML_NAME(e.get()), "Segment")) - throw std::runtime_error(std::string("expected Segment, got ") + EBML_NAME(e.get())); - auto *segment = static_cast(e.get()); - f->segment = std::move(e); - f->info.TimecodeScale = 1000000; - EbmlElement *next = - f->stream->FindNextElement(EBML_CONTEXT(segment), upper, std::numeric_limits::max(), true); - while (next && upper <= 0) { - std::unique_ptr current(next); - next = nullptr; - if (input->progress && !input->progress(input, current->GetElementPosition(), static_cast(input_size))) - throw std::runtime_error("Matroska parsing cancelled"); - if (dynamic_cast(current.get())) { - f->clusters.push_back({current->GetElementPosition() + current->HeadSize(), current->GetSize()}); - current->SkipData(*f->stream, EBML_CONTEXT(segment)); - if (!next) - next = f->stream->FindNextElement(EBML_CONTEXT(segment), upper, std::numeric_limits::max(), true); - continue; - } - bool wanted = dynamic_cast(current.get()) || dynamic_cast(current.get()) || - dynamic_cast(current.get()); - if (wanted) { - unsigned cap = input->getcachesize ? input->getcachesize(input) : 16U * 1024 * 1024; - auto *attachments = dynamic_cast(current.get()); - if (!attachments && current->GetSize() > cap) { - current->SkipData(*f->stream, EBML_CONTEXT(segment)); - throw std::runtime_error("Matroska metadata exceeds the configured read limit"); - } - auto *master = dynamic_cast(current.get()); - master->Read(*f->stream, EBML_CONTEXT(current.get()), upper, next, true, - attachments ? SCOPE_PARTIAL_DATA : SCOPE_ALL_DATA); - if (auto *info = dynamic_cast(master)) { - f->info.TimecodeScale = uint_value(*info, 1000000); - if (auto *d = child(*info)) - f->info.Duration = static_cast(static_cast(*d) * f->info.TimecodeScale); - } - parse_master(*f, *master); - f->top.push_back(std::move(current)); - } else - current->SkipData(*f->stream, EBML_CONTEXT(segment)); - if (!next) - next = f->stream->FindNextElement(EBML_CONTEXT(segment), upper, std::numeric_limits::max(), - true); - } - if (input->progress && !input->progress(input, static_cast(input_size), static_cast(input_size))) - throw std::runtime_error("Matroska parsing cancelled"); - if (!f->info.Duration && std::any_of(f->tracks.begin(), f->tracks.end(), [](auto const& track) { - return track.info.Type == TT_SUB; - })) { - for (auto cluster = f->clusters.rbegin(); cluster != f->clusters.rend() && !f->info.Duration; ++cluster) { - f->frames.clear(); - parse_raw_cluster(*f, *cluster); - for (auto const& frame : f->frames) - if (!(frame.flags & FRAME_UNKNOWN_END)) f->info.Duration = std::max(f->info.Duration, frame.end); - } - f->frames.clear(); - } - finalize(*f); - return f.release(); - } catch (std::exception const &e) { - if (msg && n) { - std::strncpy(msg, e.what(), n - 1); - msg[n - 1] = 0; - } - return nullptr; - } catch (...) { - if (msg && n) { - std::strncpy(msg, "unknown Matroska parsing error", n - 1); - msg[n - 1] = 0; - } - return nullptr; - } -} -void mkv_Close(MatroskaFile *f) { - delete f; -} -const char *mkv_GetLastError(MatroskaFile *f) { - return f ? f->error.c_str() : "invalid Matroska handle"; -} -SegmentInfo *mkv_GetFileInfo(MatroskaFile *f) { - return &f->info; -} -unsigned mkv_GetNumTracks(MatroskaFile *f) { - return f->tracks.size(); -} -TrackInfo *mkv_GetTrackInfo(MatroskaFile *f, unsigned i) { - return i < f->tracks.size() ? &f->tracks[i].info : nullptr; -} -void mkv_GetAttachments(MatroskaFile *f, Attachment **a, unsigned *n) { - *a = f->attachment_view.empty() ? nullptr : f->attachment_view.data(); - *n = f->attachment_view.size(); -} -void mkv_GetChapters(MatroskaFile *, Chapter **c, unsigned *n) { - *c = nullptr; - *n = 0; -} -void mkv_GetTags(MatroskaFile *, Tag **t, unsigned *n) { - *t = nullptr; - *n = 0; -} -uint64_t mkv_GetSegmentTop(MatroskaFile *f) { - return f->segment_top; -} -void mkv_SetTrackMask(MatroskaFile *f, unsigned m) { - f->mask = m; - f->cursor = 0; - f->cluster_cursor = 0; - f->frames.clear(); -} -int mkv_ReadFrame(MatroskaFile *f, unsigned mask, unsigned *track, uint64_t *start, uint64_t *end, uint64_t *pos, - unsigned *size, unsigned *flags) { - try { - if (std::none_of(f->tracks.begin(), f->tracks.end(), [](auto const& item) { return item.info.Type == TT_SUB; })) return -1; - for (;;) { - while (f->cursor < f->frames.size()) { - auto &r = f->frames[f->cursor++]; - if (r.track >= 32 || (((mask | f->mask) >> r.track) & 1)) continue; - *track = r.track; - *start = r.start; - *end = r.end; - *pos = r.pos; - *size = r.size; - *flags = r.flags; - return 0; - } - if (f->cluster_cursor >= f->clusters.size()) return -1; - auto const& cluster = f->clusters[f->cluster_cursor++]; - if (f->input->progress && !f->input->progress(f->input, cluster.data, static_cast(f->input->getfilesize(f->input)))) { - f->error = "Matroska parsing cancelled"; - return -2; - } - f->frames.clear(); - f->cursor = 0; - parse_raw_cluster(*f, cluster); - } - } catch (std::exception const& e) { - f->error = e.what(); - return -2; - } -} -void mkv_Seek(MatroskaFile *f, uint64_t t, unsigned) { - try { - f->frames.clear(); - f->cursor = 0; - f->cluster_cursor = 0; - while (f->cluster_cursor < f->clusters.size()) { - parse_raw_cluster(*f, f->clusters[f->cluster_cursor++]); - f->cursor = std::lower_bound(f->frames.begin(), f->frames.end(), t, - [](auto const& frame, uint64_t time) { return frame.start < time; }) - f->frames.begin(); - if (f->cursor < f->frames.size()) return; - f->frames.clear(); - f->cursor = 0; - } - } - catch (std::exception const& error) { - f->error = error.what(); - f->frames.clear(); - f->cursor = 0; - f->cluster_cursor = f->clusters.size(); - } -} -void mkv_SkipToKeyframe(MatroskaFile *) {} -uint64_t mkv_GetLowestQTimecode(MatroskaFile *f) { - return f->cursor < f->frames.size() ? f->frames[f->cursor].start : 0; -} -int mkv_TruncFloat(MKFLOAT v) { - return static_cast(v); -} -} - -struct CompressedStream { - MatroskaFile *file; - unsigned track; - uint64_t frame_position{}; - unsigned frame_size{}; - std::vector data; - size_t cursor{}; - std::string error; - uint64_t output_limit = UINT64_MAX; - bool output_limit_exceeded = false; -}; - -namespace { -std::vector read_frame(CompressedStream const &stream) { - std::vector frame(stream.frame_size); - auto bytes_read = - stream.file->input->read(stream.file->input, stream.frame_position, frame.data(), stream.frame_size); - if (bytes_read != static_cast(stream.frame_size)) - throw std::runtime_error("short frame read"); - return frame; -} - -std::vector inflate_frame(std::vector const &input, uint64_t output_limit) { - z_stream zstream{}; - if (inflateInit(&zstream) != Z_OK) - throw std::runtime_error("zlib initialization failed"); - - struct EndInflate { - z_stream *stream; - ~EndInflate() { - inflateEnd(stream); - } - } end_inflate{&zstream}; - - zstream.next_in = const_cast(input.data()); - zstream.avail_in = input.size(); - - std::vector output; - unsigned char buffer[4096]; - int result; - do { - zstream.next_out = buffer; - zstream.avail_out = sizeof buffer; - result = inflate(&zstream, Z_NO_FLUSH); - auto produced = sizeof buffer - zstream.avail_out; - if (produced > output_limit - std::min(output.size(), output_limit)) - throw std::runtime_error("decompressed Matroska frame exceeds configured limit"); - output.insert(output.end(), buffer, buffer + produced); - } while (result == Z_OK); - - if (result != Z_STREAM_END) - throw std::runtime_error("invalid zlib frame"); - return output; -} - -void decode_frame(CompressedStream &stream) { - auto raw_frame = read_frame(stream); - auto const &track = stream.file->tracks[stream.track]; - - if (!track.info.CompEnabled) - stream.data = std::move(raw_frame); - else if (track.info.CompMethod == COMP_PREPEND) { - if (track.comp_private.size() > stream.output_limit || raw_frame.size() > stream.output_limit - track.comp_private.size()) - throw std::runtime_error("decompressed Matroska frame exceeds configured limit"); - stream.data = track.comp_private; - stream.data.insert(stream.data.end(), raw_frame.begin(), raw_frame.end()); - } else if (track.info.CompMethod == COMP_ZLIB) - stream.data = inflate_frame(raw_frame, stream.output_limit); - else - throw std::runtime_error("unsupported Matroska compression method"); -} -} // namespace - -extern "C" { -CompressedStream *cs_Create(MatroskaFile *file, unsigned track, char *message, unsigned message_size) { - if (!file || track >= file->tracks.size()) { - if (message && message_size) { - std::strncpy(message, "invalid track", message_size - 1); - message[message_size - 1] = 0; - } - return nullptr; - } - return new CompressedStream{file, track, 0, 0, {}, {}, {}, UINT64_MAX}; -} -void cs_Destroy(CompressedStream *c) { - delete c; -} -void cs_NextFrame(CompressedStream *stream, uint64_t position, unsigned size) { - stream->frame_position = position; - stream->frame_size = size; - stream->data.clear(); - stream->cursor = 0; - stream->output_limit_exceeded = false; -} -void cs_SetOutputLimit(CompressedStream *stream, uint64_t size) { - stream->output_limit = size; -} -int cs_ReadData(CompressedStream *stream, char *output, unsigned capacity) { - try { - if (stream->data.empty() && stream->frame_size) - decode_frame(*stream); - - size_t bytes_to_copy = std::min(capacity, stream->data.size() - stream->cursor); - std::memcpy(output, stream->data.data() + stream->cursor, bytes_to_copy); - stream->cursor += bytes_to_copy; - return bytes_to_copy; - } catch (std::exception const &e) { - stream->error = e.what(); - stream->output_limit_exceeded = stream->error == "decompressed Matroska frame exceeds configured limit"; - return -1; - } -} -int cs_OutputLimitExceeded(CompressedStream *stream) { - return stream->output_limit_exceeded; -} -const char *cs_GetLastError(CompressedStream *c) { - return c->error.c_str(); -} -} diff --git a/src/matroska_demux.h b/src/matroska_demux.h deleted file mode 100644 index cf87c19488..0000000000 --- a/src/matroska_demux.h +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright (c) 2004-2009 Mike Matsnev. All Rights Reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice immediately at the beginning of the file, without modification, - * this list of conditions, and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Absolutely no warranty of function or purpose is made by the author - * Mike Matsnev. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#include - -#ifndef AEGISUB_MATROSKA_DEMUX_H -#define AEGISUB_MATROSKA_DEMUX_H - -// Internal interface between the native Matroska demuxer and its C++ owner. - -#define MATROSKA_COMPRESSION_SUPPORT - -#ifdef __cplusplus -extern "C" { -#endif - -/* MKFLOATing point */ -#ifdef MATROSKA_INTEGER_ONLY -typedef struct { - int64_t v; -} MKFLOAT; -#else -typedef double MKFLOAT; -#endif - -/* generic I/O */ -struct InputStream { - /* read bytes from stream */ - int (*read)(struct InputStream *cc,uint64_t pos,void *buffer,int count); - /* scan for a four byte signature, bytes must be nonzero */ - int64_t (*scan)(struct InputStream *cc,uint64_t start,unsigned signature); - /* get cache size, this is used to cap readahead */ - unsigned (*getcachesize)(struct InputStream *cc); - /* fetch last error message */ - const char *(*geterror)(struct InputStream *cc); - /* memory allocation */ - void *(*memalloc)(struct InputStream *cc,size_t size); - void *(*memrealloc)(struct InputStream *cc,void *mem,size_t newsize); - void (*memfree)(struct InputStream *cc,void *mem); - /* zero return causes parser to abort open */ - int (*progress)(struct InputStream *cc,uint64_t cur,uint64_t max); - /* get file size, optional, can be NULL or return -1 if filesize is unknown */ - int64_t (*getfilesize)(struct InputStream *cc); -}; - -typedef struct InputStream InputStream; - -/* matroska file */ -struct MatroskaFile; /* opaque */ - -typedef struct MatroskaFile MatroskaFile; - -#define COMP_ZLIB 0 -#define COMP_BZIP 1 -#define COMP_LZO1X 2 -#define COMP_PREPEND 3 - -#define TT_VIDEO 1 -#define TT_AUDIO 2 -#define TT_SUB 17 - -struct TrackInfo { - unsigned char Number; - unsigned char Type; - unsigned char TrackOverlay; - uint64_t UID; - uint64_t MinCache; - uint64_t MaxCache; - uint64_t DefaultDuration; - MKFLOAT TimecodeScale; - void *CodecPrivate; - unsigned CodecPrivateSize; - unsigned CompMethod; - void *CompMethodPrivate; - unsigned CompMethodPrivateSize; - unsigned MaxBlockAdditionID; - - unsigned int Enabled:1; - unsigned int Default:1; - unsigned int Lacing:1; - unsigned int DecodeAll:1; - unsigned int CompEnabled:1; - - union { - struct { - unsigned char StereoMode; - unsigned char DisplayUnit; - unsigned char AspectRatioType; - unsigned int PixelWidth; - unsigned int PixelHeight; - unsigned int DisplayWidth; - unsigned int DisplayHeight; - unsigned int CropL, CropT, CropR, CropB; - unsigned int ColourSpace; - MKFLOAT GammaValue; - - unsigned int Interlaced:1; - } Video; - struct { - MKFLOAT SamplingFreq; - MKFLOAT OutputSamplingFreq; - unsigned char Channels; - unsigned char BitDepth; - } Audio; - } AV; - - /* various strings */ - char *Name; - char Language[4]; - char *CodecID; -}; - -typedef struct TrackInfo TrackInfo; - -struct SegmentInfo { - char UID[16]; - char PrevUID[16]; - char NextUID[16]; - char *Filename; - char *PrevFilename; - char *NextFilename; - char *Title; - char *MuxingApp; - char *WritingApp; - uint64_t TimecodeScale; - uint64_t Duration; - int64_t DateUTC; - char DateUTCValid; -}; - -typedef struct SegmentInfo SegmentInfo; - -struct Attachment { - uint64_t Position; - uint64_t Length; - uint64_t UID; - char *Name; - char *Description; - char *MimeType; -}; - -typedef struct Attachment Attachment; - -struct ChapterDisplay { - char *String; - char Language[4]; - char Country[4]; -}; - -struct ChapterCommand { - unsigned Time; - unsigned CommandLength; - void *Command; -}; - -struct ChapterProcess { - unsigned CodecID; - unsigned CodecPrivateLength; - void *CodecPrivate; - unsigned nCommands,nCommandsSize; - struct ChapterCommand *Commands; -}; - -struct Chapter { - uint64_t UID; - uint64_t Start; - uint64_t End; - - unsigned nTracks,nTracksSize; - uint64_t *Tracks; - unsigned nDisplay,nDisplaySize; - struct ChapterDisplay *Display; - unsigned nChildren,nChildrenSize; - struct Chapter *Children; - unsigned nProcess,nProcessSize; - struct ChapterProcess *Process; - - char SegmentUID[16]; - - unsigned int Hidden:1; - unsigned int Enabled:1; - - // Editions - unsigned int Default:1; - unsigned int Ordered:1; -}; - -typedef struct Chapter Chapter; - -#define TARGET_TRACK 0 -#define TARGET_CHAPTER 1 -#define TARGET_ATTACHMENT 2 -#define TARGET_EDITION 3 -struct Target { - uint64_t UID; - unsigned Type; -}; - -struct SimpleTag { - char *Name; - char *Value; - char Language[4]; - unsigned Default:1; -}; - -struct Tag { - unsigned nTargets,nTargetsSize; - struct Target *Targets; - - unsigned nSimpleTags,nSimpleTagsSize; - struct SimpleTag *SimpleTags; -}; - -typedef struct Tag Tag; - -/* Open a matroska file - * io pointer is recorded inside MatroskaFile - */ -MatroskaFile *mkv_Open(/* in */ InputStream *io, - /* out */ char *err_msg, - /* in */ unsigned msgsize); - -#define MKVF_AVOID_SEEKS 1 /* use sequential reading only */ - -MatroskaFile *mkv_OpenEx(/* in */ InputStream *io, - /* in */ uint64_t base, - /* in */ unsigned flags, - /* out */ char *err_msg, - /* in */ unsigned msgsize); - -/* Close and deallocate mf - * NULL pointer is ok and is simply ignored - */ -void mkv_Close(/* in */ MatroskaFile *mf); - -/* Fetch the error message of the last failed operation */ -const char *mkv_GetLastError(/* in */ MatroskaFile *mf); - -/* Get file information */ -SegmentInfo *mkv_GetFileInfo(/* in */ MatroskaFile *mf); - -/* Get track information */ -unsigned int mkv_GetNumTracks(/* in */ MatroskaFile *mf); -TrackInfo *mkv_GetTrackInfo(/* in */ MatroskaFile *mf,/* in */ unsigned track); - -/* chapters, tags and attachments */ -void mkv_GetAttachments(/* in */ MatroskaFile *mf, - /* out */ Attachment **at, - /* out */ unsigned *count); -void mkv_GetChapters(/* in */ MatroskaFile *mf, - /* out */ Chapter **ch, - /* out */ unsigned *count); -void mkv_GetTags(/* in */ MatroskaFile *mf, - /* out */ Tag **tag, - /* out */ unsigned *count); - -uint64_t mkv_GetSegmentTop(MatroskaFile *mf); - -/* Seek to specified timecode, - * if timecode is past end of file, - * all tracks are set to return EOF - * on next read - */ -#define MKVF_SEEK_TO_PREV_KEYFRAME 1 -#define MKVF_SEEK_TO_PREV_KEYFRAME_STRICT 2 - -void mkv_Seek(/* in */ MatroskaFile *mf, - /* in */ uint64_t timecode /* in ns */, - /* in */ unsigned flags); - -void mkv_SkipToKeyframe(MatroskaFile *mf); - -uint64_t mkv_GetLowestQTimecode(MatroskaFile *mf); - -int mkv_TruncFloat(MKFLOAT f); - -/************************************************************************* - * reading data, pull model - */ - -/* frame flags */ -#define FRAME_UNKNOWN_START 0x00000001 -#define FRAME_UNKNOWN_END 0x00000002 -#define FRAME_KF 0x00000004 -#define FRAME_GAP 0x00800000 -#define FRAME_STREAM_MASK 0xff000000 -#define FRAME_STREAM_SHIFT 24 - -/* This sets the masking flags for the parser, - * masked tracks [with 1s in their bit positions] - * will be ignored when reading file data. - * This call discards all parsed and queued frames - */ -void mkv_SetTrackMask(/* in */ MatroskaFile *mf,/* in */ unsigned int mask); - -/* Read one frame from the queue. - * mask specifies what tracks to ignore. - * Returns -1 if there are no more frames in the specified - * set of tracks, 0 on success - */ -int mkv_ReadFrame(/* in */ MatroskaFile *mf, - /* in */ unsigned int mask, - /* out */ unsigned int *track, - /* out */ uint64_t *StartTime /* in ns */, - /* out */ uint64_t *EndTime /* in ns */, - /* out */ uint64_t *FilePos /* in bytes from start of file */, - /* out */ unsigned int *FrameSize /* in bytes */, - /* out */ unsigned int *FrameFlags); - -#ifdef MATROSKA_COMPRESSION_SUPPORT -/* Compressed streams support */ -struct CompressedStream; - -typedef struct CompressedStream CompressedStream; - -CompressedStream *cs_Create(/* in */ MatroskaFile *mf, - /* in */ unsigned tracknum, - /* out */ char *errormsg, - /* in */ unsigned msgsize); -void cs_Destroy(/* in */ CompressedStream *cs); - -/* advance to the next frame in matroska stream, you need to pass values returned - * by mkv_ReadFrame */ -void cs_NextFrame(/* in */ CompressedStream *cs, - /* in */ uint64_t pos, - /* in */ unsigned size); -void cs_SetOutputLimit(/* in */ CompressedStream *cs, /* in */ uint64_t size); - -/* read and decode more data from current frame, return number of bytes decoded, - * 0 on end of frame, or -1 on error */ -int cs_ReadData(CompressedStream *cs,char *buffer,unsigned bufsize); -int cs_OutputLimitExceeded(CompressedStream *cs); - -/* return error message for the last error */ -const char *cs_GetLastError(CompressedStream *cs); -#endif - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/meson.build b/src/meson.build index 27dcc0957d..a3d362bce7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,10 +4,10 @@ aegisub_src_inc = include_directories('.') matroska_lib = static_library('aegisub-matroska', 'matroska.cpp', - 'matroska_demux.cpp', include_directories: libaegisub_inc, dependencies: [boost_dep, zlib_dep, libebml, libmatroska], - # Keep objects separate so matroska-behavior does not require libaegisub. + # matroska.cpp brings the libebml/libmatroska namespaces into scope, which + # would leak into other files in a unity build. override_options: ['unity=off'], ) diff --git a/tests/matroska/behavior.cpp b/tests/matroska/behavior.cpp deleted file mode 100644 index 6d47bd7616..0000000000 --- a/tests/matroska/behavior.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include -#include "matroska_demux.h" -#include -#include -#include -#include -#include -#include - -struct FileInput final : InputStream { - FILE *file{}; - std::string error; - uint64_t size{}; - explicit FileInput(char const *name) { - file = std::fopen(name, "rb"); - if (file) { - std::fseek(file, 0, SEEK_END); - size = std::ftell(file); - std::fseek(file, 0, SEEK_SET); - } - read = [](InputStream *s, uint64_t p, void *b, int n) { - auto &i = *static_cast(s); - if (!i.file) - return 0; - if (p >= i.size) - return 0; - n = std::min(n, i.size - p); - if (std::fseek(i.file, p, SEEK_SET)) - return -1; - return (int)std::fread(b, 1, n, i.file); - }; - scan = [](InputStream *s, uint64_t p, unsigned sig) -> int64_t { - unsigned v = 0; - unsigned char c; - for (; static_cast(s)->read(s, p, &c, 1) == 1; ++p) { - v = (v << 8) | c; - if (v == sig) - return p - 3; - } - return -1; - }; - getcachesize = [](InputStream *) { return 1U << 20; }; - geterror = [](InputStream *s) { return static_cast(s)->error.c_str(); }; - memalloc = [](InputStream *, size_t n) { return std::malloc(n); }; - memrealloc = [](InputStream *, void *p, size_t n) { return std::realloc(p, n); }; - memfree = [](InputStream *, void *p) { std::free(p); }; - progress = [](InputStream *, uint64_t, uint64_t) { return 1; }; - getfilesize = [](InputStream *s) { return (int64_t)static_cast(s)->size; }; - } - ~FileInput() { - if (file) - std::fclose(file); - } -}; -static uint64_t hash(FileInput &input, uint64_t position, unsigned size) { - uint64_t h = 1469598103934665603ULL; - std::vector data(size); - if (size && input.read(&input, position, data.data(), size) != static_cast(size)) - return 0; - for (auto byte : data) { - h ^= byte; - h *= 1099511628211ULL; - } - return h; -} -static uint64_t decoded_hash(MatroskaFile *file, unsigned track, uint64_t position, unsigned size) { - char error[256]{}; - auto *stream = cs_Create(file, track, error, sizeof error); - if (!stream) - return 0; - cs_NextFrame(stream, position, size); - uint64_t h = 1469598103934665603ULL; - char data[256]; - for (;;) { - int bytes_read = cs_ReadData(stream, data, sizeof data); - if (bytes_read <= 0) - break; - for (int i = 0; i < bytes_read; ++i) { - h ^= static_cast(data[i]); - h *= 1099511628211ULL; - } - } - cs_Destroy(stream); - return h; -} -int main(int argc, char **argv) { - if (argc != 2) - return 2; - FileInput input(argv[1]); - char error[2048]{}; - auto *file = mkv_Open(&input, error, sizeof error); - if (!file) { - std::cout << "error\t" << error << '\n'; - return 1; - } - auto *segment = mkv_GetFileInfo(file); - std::cout << "segment\t" << segment->TimecodeScale << '\t' << segment->Duration << '\n'; - for (unsigned track_index = 0; track_index < mkv_GetNumTracks(file); ++track_index) { - auto *track = mkv_GetTrackInfo(file, track_index); - std::cout << "track\t" << track_index << '\t' << unsigned(track->Number) << '\t' << unsigned(track->Type) - << '\t' << (track->CodecID ? track->CodecID : "") << '\t' << track->Language << '\t' - << (track->Name ? track->Name : "") << '\t' << track->CodecPrivateSize << '\t' << track->CompEnabled - << '\t' << track->CompMethod << '\n'; - } - Attachment *attachments; - unsigned attachment_count; - mkv_GetAttachments(file, &attachments, &attachment_count); - for (unsigned i = 0; i < attachment_count; ++i) - std::cout << "attachment\t" << (attachments[i].Name ? attachments[i].Name : "") << '\t' - << (attachments[i].MimeType ? attachments[i].MimeType : "") << '\t' << attachments[i].Length << '\t' - << std::hex << hash(input, attachments[i].Position, attachments[i].Length) << std::dec << '\n'; - unsigned track, size, flags; - uint64_t start, end, position; - while (!mkv_ReadFrame(file, 0, &track, &start, &end, &position, &size, &flags)) { - std::cout << "frame\t" << track << '\t' << start << '\t' << end << '\t' << size << '\t' << flags << '\t' - << std::hex << hash(input, position, size); - if (mkv_GetTrackInfo(file, track)->CompEnabled) - std::cout << '\t' << decoded_hash(file, track, position, size); - std::cout << std::dec << '\n'; - } - mkv_Close(file); -} diff --git a/tests/matroska/check-fixtures.py b/tests/matroska/check-fixtures.py deleted file mode 100755 index cdc018d0f2..0000000000 --- a/tests/matroska/check-fixtures.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import pathlib,subprocess,sys -program,root=sys.argv[1],pathlib.Path(sys.argv[2]); cases=sorted([*root.glob('*.mkv'), *root.glob('*.mka')]) -if not cases: raise SystemExit('Matroska fixtures missing; run fixtures/generate.py') -for case in cases: - result=subprocess.run([program,case],text=True,stdout=subprocess.PIPE) - got=result.stdout - expected=case.with_suffix('.behavior').read_text() - if got!=expected: raise SystemExit(f'{case.name}: Matroska behavior changed\n{got}') -missing=subprocess.run([program,root/'does-not-exist.mkv'],text=True,stdout=subprocess.PIPE) -if missing.returncode != 1 or missing.stdout != 'error\tEBML header not found\n': - raise SystemExit(f'missing input: unsafe or unexpected result\n{missing.stdout}') diff --git a/tests/matroska/fixtures/audio-only-opus.behavior b/tests/matroska/fixtures/audio-only-opus.behavior index 6a9f7fa3a6..b2ce40aa5a 100644 --- a/tests/matroska/fixtures/audio-only-opus.behavior +++ b/tests/matroska/fixtures/audio-only-opus.behavior @@ -1,2 +1 @@ -segment 20832 256504416 -track 0 1 2 A_OPUS und 19 0 0 +duration 256504416 diff --git a/tests/matroska/fixtures/compressed-zlib.behavior b/tests/matroska/fixtures/compressed-zlib.behavior index 154e593e08..635234951a 100644 --- a/tests/matroska/fixtures/compressed-zlib.behavior +++ b/tests/matroska/fixtures/compressed-zlib.behavior @@ -1,3 +1,3 @@ -segment 1000000 2000000000 -track 0 1 17 S_TEXT/UTF8 und 0 1 0 -frame 0 500000000 2500000000 33 4 93d0ad5cfaecd1c1 da1dfbf2ee283ed2 +duration 2000000000 +track 0 2756463499428226085 S_TEXT/UTF8 und 0 1 1 +packet 0 500000000 2500000000 24 da1dfbf2ee283ed2 diff --git a/tests/matroska/fixtures/encodings.behavior b/tests/matroska/fixtures/encodings.behavior new file mode 100644 index 0000000000..91aaa3d953 --- /dev/null +++ b/tests/matroska/fixtures/encodings.behavior @@ -0,0 +1,4 @@ +duration 1500000000 +track 0 1 S_TEXT/UTF8 eng 0 1 1 +track 1 2 S_TEXT/UTF8 eng 0 1 1 +packet 0 0 1500000000 5 5a0d15131ec7a1 diff --git a/tests/matroska/fixtures/encodings.mkv b/tests/matroska/fixtures/encodings.mkv new file mode 100644 index 0000000000000000000000000000000000000000..0e76a0ea08f55acd195cb50a3d9efe7b4bcf1022 GIT binary patch literal 179 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@?$9dgxiYO? z>-xrKekTX9kagMD)~&nV$XI-|k+Hc^u&p~dKEyR5L_ajd%_6s9N|M9G0OLl60Fy4K zkVa;w(B_QPoOMgCH!=a$GhwRlN^)ois%H!^ZIE}~Ryy-}Bg2B8g>8)t3=BE>vzu7& NEN*LJU}&hQ000}%MqmH{ literal 0 HcmV?d00001 diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index 884c5f12dc..68e5b2151d 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -34,3 +34,18 @@ def uint_element(element_id, value): seek_head=element('114d9b74',seek_entry) segment_payload=seek_head+info+cluster+tracks (r/'tracks-after-cluster.mkv').write_bytes(header+element('18538067',segment_payload)) + +# Content encodings: a header-stripped track read via a BlockGroup with a +# duration, and an encrypted track which should be reported as unsupported +# without preventing the rest of the file from being read. +def encoding(*children): + return element('6d80',element('6240',b''.join(children))) +stripped=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',17)+element('86',b'S_TEXT/UTF8') + +encoding(uint_element('5033',0)+element('5034',uint_element('4254',3)+element('4255',b'hel')))) +encrypted=element('ae',uint_element('d7',2)+uint_element('73c5',2)+uint_element('83',17)+element('86',b'S_TEXT/UTF8') + +encoding(uint_element('5033',1)+element('5035',b''))) +group=element('a0',element('a1',b'\x81\x00\x00\x00lo')+uint_element('9b',1500)) +encrypted_block=element('a3',b'\x82\x00\x00\x80xx') +cluster=element('1f43b675',uint_element('e7',0)+group+encrypted_block) +segment_payload=info+element('1654ae6b',stripped+encrypted)+cluster +(r/'encodings.mkv').write_bytes(header+element('18538067',segment_payload)) diff --git a/tests/matroska/fixtures/subtitle-attachment.behavior b/tests/matroska/fixtures/subtitle-attachment.behavior index 6d19c933c6..26db917c4a 100644 --- a/tests/matroska/fixtures/subtitle-attachment.behavior +++ b/tests/matroska/fixtures/subtitle-attachment.behavior @@ -1,11 +1,7 @@ -segment 1000000 3000000000 -track 0 1 1 V_MS/VFW/FOURCC und 40 0 0 -track 1 2 17 S_TEXT/ASS eng ASS track 480 0 0 -track 2 3 17 S_TEXT/UTF8 jpn 0 0 0 +duration 3000000000 +track 1 2330455654525085584 S_TEXT/ASS eng ASS track 480 1 1 +track 2 7642018816776784136 S_TEXT/UTF8 jpn 0 1 0 attachment attachment.txt text/plain 42 6e22e83d2777896d -frame 0 0 1000000000 38 4 fea7a8f5d84334f6 -frame 1 250000000 1500000000 25 4 76874757fddd3870 -frame 2 500000000 2500000000 24 4 da1dfbf2ee283ed2 -frame 0 1000000000 2000000000 17 0 e3e1e9af310e5536 -frame 1 1500000000 2750000000 24 4 8f6874da13b53ae -frame 0 2000000000 3000000000 17 0 ee26690468bfc99 +packet 1 250000000 1500000000 25 76874757fddd3870 +packet 1 1500000000 2750000000 24 8f6874da13b53ae +packet 2 500000000 2500000000 24 da1dfbf2ee283ed2 diff --git a/tests/matroska/fixtures/tracks-after-cluster.behavior b/tests/matroska/fixtures/tracks-after-cluster.behavior index 2600ebb09b..767e06bc76 100644 --- a/tests/matroska/fixtures/tracks-after-cluster.behavior +++ b/tests/matroska/fixtures/tracks-after-cluster.behavior @@ -1,3 +1,3 @@ -segment 1000000 0 -track 0 1 17 S_TEXT/UTF8 eng 0 0 0 -frame 0 0 0 5 6 5a0d15131ec7a1 +duration - +track 0 257 S_TEXT/UTF8 eng 0 1 1 +packet 0 0 - 5 5a0d15131ec7a1 diff --git a/tests/matroska/fixtures/video-only.behavior b/tests/matroska/fixtures/video-only.behavior index 980f1220df..6a9c2e5acf 100644 --- a/tests/matroska/fixtures/video-only.behavior +++ b/tests/matroska/fixtures/video-only.behavior @@ -1,2 +1 @@ -segment 1000000 0 -track 0 1 1 V_MS/VFW/FOURCC und 40 0 0 +duration - diff --git a/tests/meson.build b/tests/meson.build index 646222c7b5..38d945b2f7 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -87,12 +87,3 @@ test_setup = custom_target('setup-test-data', test('gtest main', runner, env: {'AEGISUB_TEST_DATA_DIR': meson.current_source_dir()}, depends: test_setup) test('luajit-52', executable('luajit-52', 'tests/luajit_52.c', dependencies: luajit)) - -# Deterministic integration test for Matroska demux metadata, attachments, frames, compression, and errors. -matroska_behavior = executable( - 'matroska-behavior', - 'matroska/behavior.cpp', - dependencies: matroska_dep, -) -test('matroska behavior', find_program('matroska/check-fixtures.py'), - args: [matroska_behavior, meson.current_source_dir() / 'matroska' / 'fixtures']) diff --git a/tests/tests/matroska.cpp b/tests/tests/matroska.cpp index 9ac529bd8d..feeef4a966 100644 --- a/tests/tests/matroska.cpp +++ b/tests/tests/matroska.cpp @@ -3,14 +3,62 @@ #include "matroska.h" #include "util.h" +#include #include +#include +#include #include namespace { using namespace agi::matroska; +agi::fs::path fixture_dir() { + return util::test_data_dir() / "matroska" / "fixtures"; +} + agi::fs::path fixture(char const *name) { - return util::test_data_dir() / "matroska" / "fixtures" / name; + return fixture_dir() / name; +} + +uint64_t fnv1a(std::vector const& data) { + uint64_t hash = 1469598103934665603ULL; + for (auto byte : data) { + hash ^= byte; + hash *= 1099511628211ULL; + } + return hash; +} + +std::string describe_time(std::optional const& time) { + return time ? std::to_string(time->nanoseconds) : "-"; +} + +// Dump everything the demuxer exposes for a file in a stable text format +std::string describe(agi::fs::path const& path) { + std::ostringstream out; + try { + Demuxer demuxer(OpenFile(path)); + out << "duration\t" << describe_time(demuxer.Duration()) << '\n'; + for (auto const& track : demuxer.SubtitleTracks()) + out << "track\t" << track.id.value << '\t' << track.uid << '\t' << track.codec_id << '\t' + << track.language << '\t' << track.name << '\t' << track.codec_private.size() << '\t' + << track.enabled << '\t' << track.is_default << '\n'; + for (auto const& attachment : demuxer.Attachments()) + out << "attachment\t" << attachment.name << '\t' << attachment.mime_type << '\t' << attachment.size + << '\t' << std::hex << fnv1a(demuxer.ReadAttachment(attachment.id)) << std::dec << '\n'; + for (auto const& track : demuxer.SubtitleTracks()) { + if (track.codec == SubtitleCodec::unsupported) continue; + demuxer.SelectTrack(track.id); + while (auto packet = demuxer.ReadPacket()) + out << "packet\t" << packet->track.value << '\t' << describe_time(packet->start) << '\t' + << describe_time(packet->end) << '\t' << packet->data.size() << '\t' + << std::hex << fnv1a(packet->data) << std::dec << '\n'; + } + } + catch (Error const& e) { + out << "error\t" << e.GetMessage() << '\n'; + } + return out.str(); } class ReaderProxy final : public Reader { @@ -44,13 +92,17 @@ TEST(Matroska, ShortReadsAreSupported) { EXPECT_FALSE(demuxer.SubtitleTracks().empty()); } -TEST(Matroska, ReaderFailureIsContainedAtCBoundary) { +TEST(Matroska, ReaderFailureDuringOpenIsAnIoError) { auto reader = std::make_unique(fixture("subtitle-attachment.mkv")); reader->fail = true; - EXPECT_THROW(Demuxer(std::move(reader)), InvalidDataError); + EXPECT_THROW(Demuxer(std::move(reader)), IoError); } -TEST(Matroska, CancellationCallbackExceptionsCannotCrossC) { +TEST(Matroska, MissingFileIsAnIoError) { + EXPECT_THROW(OpenFile(fixture("does-not-exist.mkv")), IoError); +} + +TEST(Matroska, CancellationCallbackExceptionsCancel) { EXPECT_THROW(Demuxer(OpenFile(fixture("subtitle-attachment.mkv")), []() -> bool { throw 42; }), agi::UserCancelException); } @@ -104,3 +156,26 @@ TEST(Matroska, PacketsHaveCheckedOptionalTimestamps) { ASSERT_TRUE(packet->start); EXPECT_GE(packet->start->nanoseconds, 0); } + +// Each fixture's .behavior file records the expected describe() output. When a +// fixture is added or changed, the failure message contains the new output. +TEST(Matroska, FixtureBehavior) { + std::vector fixtures; + for (auto const& entry : std::filesystem::directory_iterator(fixture_dir())) { + auto ext = entry.path().extension(); + if (ext == ".mkv" || ext == ".mka") + fixtures.emplace_back(entry.path()); + } + std::sort(fixtures.begin(), fixtures.end()); + ASSERT_FALSE(fixtures.empty()); + + for (auto const& path : fixtures) { + auto expected_path = path; + expected_path.replace_extension(".behavior"); + std::ifstream expected_file(expected_path, std::ios::binary); + std::stringstream expected; + expected << expected_file.rdbuf(); + auto actual = describe(path); + EXPECT_EQ(expected.str(), actual) << path.filename(); + } +} From c0c2969163de4407688cac38eccbab54847b2ba8 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 11:12:57 -0700 Subject: [PATCH 18/28] Fix Matroska subtitle import cancellation and error handling - The demuxer's cancellation callback captured the metadata progress dialog's sink, which is destroyed when that dialog finishes and was then used while reading subtitles. Route it to whichever dialog is running. - Errors while reading track information were only logged to the progress dialog, after which the empty demuxer was dereferenced. Rethrow them. - Restore the 64 MiB limit on total subtitle data from 5a53f192a. - Report malformed ASS packets instead of letting bad_lexical_cast escape. - Restore the file's original licence header. Co-Authored-By: Claude Opus 5.5 --- src/mkv_wrap.cpp | 267 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 197 insertions(+), 70 deletions(-) diff --git a/src/mkv_wrap.cpp b/src/mkv_wrap.cpp index 562ef713c5..e47029461f 100644 --- a/src/mkv_wrap.cpp +++ b/src/mkv_wrap.cpp @@ -1,9 +1,39 @@ // Copyright (c) 2004-2006, Rodrigo Braz Monteiro, Mike Matsnev -// Copyright (c) 2026 Aegisub contributors -// Permission to use, copy, modify, and distribute this software for any purpose -// with or without fee is hereby granted. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the Aegisub Group nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file mkv_wrap.cpp +/// @brief High-level interface for obtaining various data from Matroska files +/// @ingroup video_input +/// #include "mkv_wrap.h" + #include "ass_file.h" #include "ass_parser.h" #include "compat.h" @@ -11,107 +41,204 @@ #include "matroska.h" #include "options.h" #include "subtitle_format_srt.h" + #include #include + #include #include #include #include +#include +#include #include #include -#include + +#include // Keep this last so wxUSE_CHOICEDLG is set. namespace { -agi::Time to_ass_time(std::optional const& value) { - if (!value) return agi::Time{}; - if (value->nanoseconds < 0) throw MatroskaException("Negative Matroska subtitle timestamp"); - auto milliseconds = value->nanoseconds / 1000000; - if (milliseconds > std::numeric_limits::max()) throw MatroskaException("Matroska subtitle timestamp is out of range"); - return agi::Time(static_cast(milliseconds)); +namespace mkv = agi::matroska; + +/// Limit on the total size of the subtitle data read from a file +constexpr size_t max_total_subtitle_bytes = 64 * 1024 * 1024; + +agi::Time to_ass_time(std::optional const& time) { + if (!time) return 0; + if (time->nanoseconds < 0) + throw MatroskaException("Negative Matroska subtitle timestamp"); + auto milliseconds = time->nanoseconds / 1000000; + if (milliseconds > std::numeric_limits::max()) + throw MatroskaException("Matroska subtitle timestamp is out of range"); + return static_cast(milliseconds); } -void read_subtitles(agi::ProgressSink *ps, agi::matroska::Demuxer& demuxer, - agi::matroska::SubtitleTrack const& track, double total_time, AssParser *parser) { - std::vector> subtitles; - SrtTagParser srt_parser; - for (;;) { - auto packet = demuxer.ReadPacket(); - if (!packet) break; +void read_subtitles(agi::ProgressSink *ps, mkv::Demuxer& demuxer, bool srt, int64_t total_time, AssParser *parser) { + std::vector> subList; + size_t total_bytes = 0; + SrtTagParser srtParser; + + while (auto packet = demuxer.ReadPacket()) { + if (ps->IsCancelled()) return; if (packet->data.empty()) continue; - auto start = to_ass_time(packet->start); - auto end = packet->end ? to_ass_time(packet->end) : start; - std::string_view data(reinterpret_cast(packet->data.data()), packet->data.size()); - if (track.codec != agi::matroska::SubtitleCodec::srt) { - auto first = data.find(','); - auto second = first == data.npos ? data.npos : data.find(',', first + 1); - if (second == data.npos) continue; - subtitles.emplace_back(boost::lexical_cast(data.substr(0, first)), - agi::format("Dialogue: %d,%s,%s,%s", boost::lexical_cast(data.substr(first + 1, second - first - 1)), - start.GetAssFormatted(), end.GetAssFormatted(), data.substr(second + 1))); + + if (packet->data.size() > max_total_subtitle_bytes - total_bytes) + throw MatroskaException(agi::format("Matroska subtitle data exceeds the %d MiB limit", max_total_subtitle_bytes / 1024 / 1024)); + total_bytes += packet->data.size(); + + agi::Time subStart = to_ass_time(packet->start); + agi::Time subEnd = packet->end ? to_ass_time(packet->end) : subStart; + std::string_view readBuf(reinterpret_cast(packet->data.data()), packet->data.size()); + + // Process SSA/ASS + if (!srt) { + auto first = readBuf.find(','); + if (first == readBuf.npos) continue; + auto second = readBuf.find(',', first + 1); + if (second == readBuf.npos) continue; + + try { + subList.emplace_back( + boost::lexical_cast(readBuf.substr(0, first)), + agi::format("Dialogue: %d,%s,%s,%s" + , boost::lexical_cast(readBuf.substr(first + 1, second - (first + 1))) + , subStart.GetAssFormatted() + , subEnd.GetAssFormatted() + , readBuf.substr(second + 1))); + } + catch (boost::bad_lexical_cast const&) { + throw MatroskaException("Malformed ASS packet in Matroska subtitle track"); + } } + // Process SRT else { - auto line = agi::format("Dialogue: 0,%s,%s,Default,,0,0,0,,%s", start.GetAssFormatted(), end.GetAssFormatted(), srt_parser.ToAss(std::string(data))); - boost::replace_all(line, "\r\n", "\\N"); boost::replace_all(line, "\r", "\\N"); boost::replace_all(line, "\n", "\\N"); - subtitles.emplace_back(subtitles.size(), std::move(line)); + auto line = agi::format("Dialogue: 0,%s,%s,Default,,0,0,0,,%s" + , subStart.GetAssFormatted() + , subEnd.GetAssFormatted() + , srtParser.ToAss(std::string(readBuf))); + boost::replace_all(line, "\r\n", "\\N"); + boost::replace_all(line, "\r", "\\N"); + boost::replace_all(line, "\n", "\\N"); + + subList.emplace_back(subList.size(), std::move(line)); } - ps->SetProgress(static_cast(start), total_time); + + if (total_time > 0) + ps->SetProgress(subStart, total_time); } - std::sort(subtitles.begin(), subtitles.end()); - for (auto const& item : subtitles) parser->AddLine(item.second); + + // Insert into file + sort(begin(subList), end(subList)); + for (auto const& order_value_pair : subList) + parser->AddLine(order_value_pair.second); +} + +/// Run a task in a progress dialog, rethrowing any error it fails with +/// rather than just logging it to the dialog +void run_with_progress(wxString const& message, agi::ProgressSink *&active_sink, std::function task) { + DialogProgress progress(nullptr, _("Parsing Matroska"), message); + std::exception_ptr failure; + progress.Run([&](agi::ProgressSink *ps) { + active_sink = ps; + try { + task(ps); + } + catch (...) { + failure = std::current_exception(); + } + // The sink is destroyed when Run returns + active_sink = nullptr; + }); + if (failure) + std::rethrow_exception(failure); } } void MatroskaWrapper::GetSubtitles(agi::fs::path const& filename, AssFile *target) { - DialogProgress metadata_progress(nullptr, _("Parsing Matroska"), _("Reading Matroska track information.")); - std::optional demuxer; - metadata_progress.Run([&](agi::ProgressSink *ps) { - demuxer.emplace(agi::matroska::OpenFile(filename), [ps] { return ps->IsCancelled(); }); + // The demuxer polls for cancellation from whichever progress dialog is + // currently running it + agi::ProgressSink *active_sink = nullptr; + auto cancelled = [&] { return active_sink && active_sink->IsCancelled(); }; + + std::optional demuxer; + run_with_progress(_("Reading Matroska track information."), active_sink, [&](agi::ProgressSink *) { + demuxer.emplace(mkv::OpenFile(filename), cancelled); }); - std::vector tracks; - std::vector names; - for (auto const& track : demuxer->SubtitleTracks()) if (track.codec != agi::matroska::SubtitleCodec::unsupported) { - tracks.push_back(&track); - names.emplace_back(agi::format("%d (%s %s)%s%s", track.id.value, track.codec_id, track.language, track.name.empty() ? "" : ": ", track.name)); + + // Find tracks + std::vector tracksFound; + std::vector tracksNames; + for (auto const& track : demuxer->SubtitleTracks()) { + if (track.codec == mkv::SubtitleCodec::unsupported) continue; + tracksFound.push_back(&track); + tracksNames.emplace_back(agi::format("%d (%s %s)", track.id.value, track.codec_id, track.language)); + if (!track.name.empty()) { + tracksNames.back() += ": "; + tracksNames.back() += track.name; + } } - if (tracks.empty()) throw MatroskaException("File has no recognised subtitle tracks."); - size_t choice = 0; - if (tracks.size() > 1) { - int selected = wxGetSingleChoiceIndex(_("Choose which track to read:"), _("Multiple subtitle tracks found"), to_wx(names)); - if (selected < 0) throw agi::UserCancelException("cancelled"); - choice = static_cast(selected); + + // No tracks found + if (tracksFound.empty()) + throw MatroskaException("File has no recognised subtitle tracks."); + + mkv::SubtitleTrack const *trackToRead; + // Only one track found + if (tracksFound.size() == 1) + trackToRead = tracksFound[0]; + // Pick a track + else { + int choice = wxGetSingleChoiceIndex(_("Choose which track to read:"), _("Multiple subtitle tracks found"), to_wx(tracksNames)); + if (choice == -1) + throw agi::UserCancelException("canceled"); + + trackToRead = tracksFound[choice]; } - auto const& track = *tracks[choice]; - demuxer->SelectTrack(track.id); - bool srt = track.codec == agi::matroska::SubtitleCodec::srt; - bool ssa = track.codec == agi::matroska::SubtitleCodec::ssa; + + // Picked track + demuxer->SelectTrack(trackToRead->id); + bool srt = trackToRead->codec == mkv::SubtitleCodec::srt; + bool ssa = trackToRead->codec == mkv::SubtitleCodec::ssa; + + // Parse into a temporary file so a failure partway through leaves the target untouched AssFile imported; AssParser parser(&imported, !ssa); + + // Read private data if it's ASS/SSA if (!srt) { - std::string private_data(track.codec_private.begin(), track.codec_private.end()); - boost::char_separator separator("\r\n"); - for (auto const& line : boost::tokenizer>(private_data, separator)) parser.AddLine(line); + // Read raw data + std::string priv(trackToRead->codec_private.begin(), trackToRead->codec_private.end()); + + // Load into file + boost::char_separator sep("\r\n"); + for (auto const& cur : boost::tokenizer>(priv, sep)) + parser.AddLine(cur); } - else imported.LoadDefault(false, OPT_GET("Subtitle Format/SRT/Default Style Catalog")->GetString()); + // Load default if it's SRT + else + imported.LoadDefault(false, OPT_GET("Subtitle Format/SRT/Default Style Catalog")->GetString()); + parser.AddLine("[Events]"); - double duration = demuxer->Duration() ? demuxer->Duration()->nanoseconds / 1000000.0 : 0.0; - DialogProgress progress(nullptr, _("Parsing Matroska"), _("Reading subtitles from Matroska file.")); - std::exception_ptr failure; - progress.Run([&](agi::ProgressSink *ps) { - try { - read_subtitles(ps, *demuxer, track, duration, &parser); - } - catch (...) { failure = std::current_exception(); } + + auto duration = demuxer->Duration(); + int64_t totalTime = duration ? duration->nanoseconds / 1000000 : 0; + run_with_progress(_("Reading subtitles from Matroska file."), active_sink, [&](agi::ProgressSink *ps) { + read_subtitles(ps, *demuxer, srt, totalTime, &parser); }); - if (failure) std::rethrow_exception(failure); + target->swap(imported); } bool MatroskaWrapper::HasSubtitles(agi::fs::path const& filename) { - // Compatibility: project loading expects a boolean probe. Corrupt/unreadable files - // therefore return false here; explicit imports preserve their structured error. try { - agi::matroska::Demuxer demuxer(agi::matroska::OpenFile(filename)); - return std::any_of(demuxer.SubtitleTracks().begin(), demuxer.SubtitleTracks().end(), [](auto const& track) { return track.codec != agi::matroska::SubtitleCodec::unsupported; }); + mkv::Demuxer demuxer(mkv::OpenFile(filename)); + auto const& tracks = demuxer.SubtitleTracks(); + return std::any_of(tracks.begin(), tracks.end(), [](mkv::SubtitleTrack const& track) { + return track.codec != mkv::SubtitleCodec::unsupported; + }); } - catch (...) { return false; } + catch (...) { + // We don't care about why we couldn't read subtitles here + } + + return false; } From 20bed13b75a80bab581ef41c0664cee102f76a75 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 11:34:03 -0700 Subject: [PATCH 19/28] Fix Matroska demuxer regressions relative to the Haali parser - Honor ContentEncodingScope: compression which doesn't cover frame contents is no longer applied to frames, and a zlib-compressed CodecPrivate is decompressed. - Skip track entries whose UID was already seen, as Tracks may be repeated in live streams, rather than failing on the duplicate number. - Make AttachmentId an index and expose the UID separately, so that attachments with missing or duplicate UIDs are no longer dropped, and skip attachments without data as the Haali parser did. - Fix signed overflow when a block's relative timestamp is added to a cluster timestamp of INT64_MAX. - Poll for cancellation per element while parsing a cluster, as clusters can be arbitrarily large. Co-Authored-By: Claude Opus 5.5 --- src/matroska.cpp | 71 ++++++++++++------ src/matroska.h | 2 + tests/matroska/fixtures/generate.py | 18 +++++ .../fixtures/repeated-tracks.behavior | 5 ++ tests/matroska/fixtures/repeated-tracks.mkv | Bin 0 -> 310 bytes .../fixtures/timestamp-overflow.behavior | 3 + .../matroska/fixtures/timestamp-overflow.mkv | Bin 0 -> 109 bytes 7 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 tests/matroska/fixtures/repeated-tracks.behavior create mode 100644 tests/matroska/fixtures/repeated-tracks.mkv create mode 100644 tests/matroska/fixtures/timestamp-overflow.behavior create mode 100644 tests/matroska/fixtures/timestamp-overflow.mkv diff --git a/src/matroska.cpp b/src/matroska.cpp index e48fcf56d4..b400be924e 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -202,7 +202,9 @@ SubtitleCodec codec_from_id(std::string const& id) { enum class Compression { none, zlib, header_strip, unsupported }; -Compression parse_encodings(KaxContentEncodings& encodings, std::vector& stripped_header) { +/// Get the compression applied to a track's frames. zlib_codec_private is set +/// if the track's CodecPrivate is zlib-compressed. +Compression parse_encodings(KaxContentEncodings& encodings, std::vector& stripped_header, bool& zlib_codec_private) { KaxContentEncoding *encoding = nullptr; for (auto element : encodings.GetElementList()) { if (auto current = dynamic_cast(element)) { @@ -220,7 +222,19 @@ Compression parse_encodings(KaxContentEncodings& encodings, std::vector auto compression = child(*encoding); if (!compression) return Compression::none; - switch (uint_value(*compression)) { + + // Bit 1 is the frame contents and bit 2 is CodecPrivate + auto scope = uint_value(*encoding, 1); + auto algorithm = uint_value(*compression); + if (scope & 2) { + if (algorithm != 0) + return Compression::unsupported; + zlib_codec_private = true; + } + if (!(scope & 1)) + return Compression::none; + + switch (algorithm) { case 0: return Compression::zlib; case 3: @@ -266,6 +280,7 @@ std::vector inflate_packet(std::vector const& input, size_t li /// Demuxing state for every track in the file, not just subtitle tracks struct TrackState { + uint64_t uid = 0; uint64_t default_duration = 0; Compression compression = Compression::none; std::vector stripped_header; @@ -433,9 +448,18 @@ class Demuxer::Impl { void ParseTrack(KaxTrackEntry& entry) { TrackState state; + state.uid = uint_value(entry); + // Tracks may be repeated so that a reader joining a live stream can + // see them, so skip entries which were already seen + if (state.uid && std::any_of(all_tracks.begin(), all_tracks.end(), [&](TrackState const& track) { + return track.uid == state.uid; + })) + return; + state.default_duration = uint_value(entry); + bool zlib_codec_private = false; if (auto encodings = child(entry)) - state.compression = parse_encodings(*encodings, state.stripped_header); + state.compression = parse_encodings(*encodings, state.stripped_header, zlib_codec_private); if (!track_by_number.emplace(uint_value(entry), all_tracks.size()).second) throw InvalidDataError("Duplicate Matroska track number"); @@ -443,7 +467,7 @@ class Demuxer::Impl { if (uint_value(entry) == track_type_subtitle) { SubtitleTrack track; track.id.value = static_cast(all_tracks.size()); - track.uid = uint_value(entry); + track.uid = state.uid; track.codec_id = string_value(entry); track.codec = state.compression == Compression::unsupported ? SubtitleCodec::unsupported : codec_from_id(track.codec_id); track.name = unicode_value(entry); @@ -452,6 +476,14 @@ class Demuxer::Impl { track.is_default = uint_value(entry, 1); if (auto codec_private = child(entry)) track.codec_private.assign(codec_private->GetBuffer(), codec_private->GetBuffer() + codec_private->GetSize()); + if (zlib_codec_private && !track.codec_private.empty()) { + try { + track.codec_private = inflate_packet(track.codec_private, limits.metadata_size); + } + catch (Error const&) { + track.codec = SubtitleCodec::unsupported; + } + } tracks.push_back(std::move(track)); } all_tracks.push_back(std::move(state)); @@ -466,7 +498,7 @@ class Demuxer::Impl { continue; Attachment attachment; - Location data; + std::optional data; for (uint64_t child_position = file.data; child_position < file.end;) { auto element = ReadElement(child_position, file.end); child_position = element.end; @@ -474,19 +506,17 @@ class Demuxer::Impl { case id_file_name: attachment.name = ReadString(element); break; case id_file_description: attachment.description = ReadString(element); break; case id_file_mime_type: attachment.mime_type = ReadString(element); break; - case id_file_uid: attachment.id.value = ReadUInt(element); break; - case id_file_data: data = {element.data, element.size}; break; + case id_file_uid: attachment.uid = ReadUInt(element); break; + case id_file_data: data = Location{element.data, element.size}; break; } } - - bool duplicate = std::any_of(attachments.begin(), attachments.end(), [&](Attachment const& existing) { - return existing.id == attachment.id; - }); - if (duplicate) + if (!data) continue; - attachment.size = data.size; + + attachment.id.value = attachments.size(); + attachment.size = data->size; attachments.push_back(std::move(attachment)); - attachment_data.push_back(data); + attachment_data.push_back(*data); } } @@ -652,8 +682,8 @@ class Demuxer::Impl { else sizes[0] = end - cursor; - if (cluster_time > static_cast(INT64_MAX)) - throw InvalidDataError("Matroska cluster timestamp is out of range"); + if (cluster_time > static_cast(INT64_MAX) || (relative > 0 && cluster_time > static_cast(INT64_MAX - relative))) + throw InvalidDataError("Matroska timestamp is out of range"); int64_t ticks = static_cast(cluster_time) + relative; uint64_t frame_duration = block_duration ? checked_multiply(*block_duration, timestamp_scale, "Matroska block duration is out of range") / count @@ -683,6 +713,8 @@ class Demuxer::Impl { uint64_t cluster_time = 0; uint64_t end = checked_add(cluster.position, cluster.size, "Matroska cluster is out of range"); for (uint64_t position = cluster.position; position < end;) { + // Clusters have no size limit, so check per element rather than per cluster + CheckCancelled(); auto element = ReadElement(position, end); position = element.end; if (element.id == id_cluster_timestamp) @@ -789,12 +821,9 @@ class Demuxer::Impl { } std::vector AttachmentBytes(AttachmentId id) { - auto attachment = std::find_if(attachments.begin(), attachments.end(), [&](Attachment const& attachment) { - return attachment.id == id; - }); - if (attachment == attachments.end()) + if (id.value >= attachments.size()) throw InvalidDataError("Unknown Matroska attachment"); - auto const& data = attachment_data[attachment - attachments.begin()]; + auto const& data = attachment_data[id.value]; if (data.size > limits.attachment_size) throw LimitError("Matroska attachment exceeds configured limit"); CheckCancelled(); diff --git a/src/matroska.h b/src/matroska.h index 08319cbc5d..b59454fbeb 100644 --- a/src/matroska.h +++ b/src/matroska.h @@ -50,6 +50,7 @@ struct TrackId { }; struct AttachmentId { + /// Index of the attachment in Demuxer::Attachments() uint64_t value = 0; friend bool operator==(AttachmentId lhs, AttachmentId rhs) { return lhs.value == rhs.value; } }; @@ -71,6 +72,7 @@ struct SubtitleTrack { struct Attachment { AttachmentId id; + uint64_t uid = 0; std::string name; std::string description; std::string mime_type; diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index 68e5b2151d..ead11821b5 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -49,3 +49,21 @@ def encoding(*children): cluster=element('1f43b675',uint_element('e7',0)+group+encrypted_block) segment_payload=info+element('1654ae6b',stripped+encrypted)+cluster (r/'encodings.mkv').write_bytes(header+element('18538067',segment_payload)) + +# Tracks repeated as live muxers do, a zlib-compressed CodecPrivate with +# uncompressed frames (scope 2), and attachments without UIDs or data +import zlib +entry=element('ae',uint_element('d7',1)+uint_element('73c5',7)+uint_element('83',17)+element('86',b'S_TEXT/UTF8') + +element('63a2',zlib.compress(b'private data')) + +encoding(uint_element('5032',2)+uint_element('5033',0)+element('5034',uint_element('4254',0)))) +tracks=element('1654ae6b',entry) +def attached(name,data=None): + return element('61a7',element('466e',name)+element('4660',b'text/plain')+(element('465c',data) if data is not None else b'')) +attachments=element('1941a469',attached(b'a.txt',b'aaa')+attached(b'b.txt',b'bb')+attached(b'c.txt')) +cluster=element('1f43b675',uint_element('e7',0)+element('a3',b'\x81\x00\x00\x80hi')) +(r/'repeated-tracks.mkv').write_bytes(header+element('18538067',info+tracks+tracks+attachments+cluster)) + +# A block whose timestamp overflows when added to the cluster's +entry=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) +cluster=element('1f43b675',uint_element('e7',(1<<63)-1)+element('a3',b'\x81\x00\x01\x80x')) +(r/'timestamp-overflow.mkv').write_bytes(header+element('18538067',info+element('1654ae6b',entry)+cluster)) diff --git a/tests/matroska/fixtures/repeated-tracks.behavior b/tests/matroska/fixtures/repeated-tracks.behavior new file mode 100644 index 0000000000..ff66504c1a --- /dev/null +++ b/tests/matroska/fixtures/repeated-tracks.behavior @@ -0,0 +1,5 @@ +duration - +track 0 7 S_TEXT/UTF8 eng 12 1 1 +attachment a.txt text/plain 3 e172ef510dc287ec +attachment b.txt text/plain 2 9ba86500c657e843 +packet 0 0 - 2 9bca6a00c674d728 diff --git a/tests/matroska/fixtures/repeated-tracks.mkv b/tests/matroska/fixtures/repeated-tracks.mkv new file mode 100644 index 0000000000000000000000000000000000000000..51cb26896d14c8ed5963f63a85c9b35175fba733 GIT binary patch literal 310 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@?#LnPxiYO? z>-xrKekTX9kagKd*B!pz$XI-|k-fQ5u&p~dKEyR5L_ajd%_4cxl!`gp8mDx;bv#2o zJas%-7~~hQY|Cw!l;qGKVARMIVBE+MVAA3g(#SwPlO!FNWL{2OKHDv?HBqmmqQots zt0c9eM86;>F*DCCra2J^md^spCSj3nN=iyxJ{2gLj3FuSysfnRc_YK(wnheqhKx)A D)hl+~ literal 0 HcmV?d00001 diff --git a/tests/matroska/fixtures/timestamp-overflow.behavior b/tests/matroska/fixtures/timestamp-overflow.behavior new file mode 100644 index 0000000000..0d68f79a1b --- /dev/null +++ b/tests/matroska/fixtures/timestamp-overflow.behavior @@ -0,0 +1,3 @@ +duration - +track 0 1 S_TEXT/UTF8 eng 0 1 1 +error Matroska timestamp is out of range diff --git a/tests/matroska/fixtures/timestamp-overflow.mkv b/tests/matroska/fixtures/timestamp-overflow.mkv new file mode 100644 index 0000000000000000000000000000000000000000..0f2cdfddf035ee0af734296b6a22c7abedbdffc5 GIT binary patch literal 109 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@en8Z7Wm>z| z^^MK^P7Y!r>#}FAn|{5KvG`~sV{@ZmTX%4Lh-*ZMerSlBg}n2&(uvPI>i Date: Wed, 7 Oct 2026 11:43:36 -0700 Subject: [PATCH 20/28] Match the Haali parser's Matroska timestamp handling - Make block timestamps relative to the first block in the file, as the Haali parser did. Video providers normalize frame timestamps to start at zero, so without this subtitles in files which don't start at zero were imported out of sync. - Apply the deprecated TrackTimecodeScale to block timestamps and durations, as the Haali parser did. - Skip elements such as Void between the EBML header and the Segment. libebml's FindNextID returns whatever element comes next, so these were being treated as the Segment, and anything at all was being accepted as the EBML header. Co-Authored-By: Claude Opus 5.5 --- src/matroska.cpp | 111 ++++++++++++++++-- .../fixtures/compressed-zlib.behavior | 2 +- tests/matroska/fixtures/generate.py | 11 ++ tests/matroska/fixtures/malformed.behavior | 2 +- tests/matroska/fixtures/timing.behavior | 3 + tests/matroska/fixtures/timing.mkv | Bin 0 -> 166 bytes 6 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 tests/matroska/fixtures/timing.behavior create mode 100644 tests/matroska/fixtures/timing.mkv diff --git a/src/matroska.cpp b/src/matroska.cpp index b400be924e..74a81360f6 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -42,6 +42,7 @@ #include #include +#include #include #include #include @@ -184,6 +185,21 @@ uint64_t checked_add(uint64_t lhs, uint64_t rhs, char const *message) { return lhs + rhs; } +int64_t checked_add_signed(int64_t lhs, int64_t rhs, char const *message) { + if ((rhs > 0 && lhs > INT64_MAX - rhs) || (rhs < 0 && lhs < INT64_MIN - rhs)) + throw InvalidDataError(message); + return lhs + rhs; +} + +uint64_t checked_scale(uint64_t value, double scale, char const *message) { + if (scale == 1.0) + return value; + double scaled = static_cast(value) * scale; + if (!(scaled >= 0) || scaled >= 18446744073709551616.0) + throw InvalidDataError(message); + return static_cast(scaled); +} + uint64_t checked_multiply(uint64_t lhs, uint64_t rhs, char const *message) { if (lhs && rhs > UINT64_MAX / lhs) throw InvalidDataError(message); @@ -282,6 +298,8 @@ std::vector inflate_packet(std::vector const& input, size_t li struct TrackState { uint64_t uid = 0; uint64_t default_duration = 0; + /// Deprecated per-track multiplier applied to block timestamps and durations + double timestamp_scale = 1.0; Compression compression = Compression::none; std::vector stripped_header; }; @@ -323,6 +341,10 @@ class Demuxer::Impl { Limits limits; uint64_t timestamp_scale = 1000000; + /// Timestamp in ticks of the first block in the file, which is subtracted + /// from all block timestamps so that the file starts at zero, as the video + /// providers do for video frames + int64_t first_timestamp = 0; std::optional duration; std::vector all_tracks; std::unordered_map track_by_number; @@ -457,6 +479,11 @@ class Demuxer::Impl { return; state.default_duration = uint_value(entry); + if (auto scale = child(entry)) { + double value = static_cast(*scale); + if (value > 0 && std::isfinite(value)) + state.timestamp_scale = value; + } bool zlib_codec_private = false; if (auto encodings = child(entry)) state.compression = parse_encodings(*encodings, state.stripped_header, zlib_codec_private); @@ -524,14 +551,25 @@ class Demuxer::Impl { ReaderCallback io(*reader); EbmlStream stream(io); + // FindNextID returns whatever element comes next, as an EbmlDummy if + // it isn't the requested one std::unique_ptr head(stream.FindNextID(EBML_INFO(EbmlHead), UINT64_MAX)); - if (!head) + if (!dynamic_cast(head.get())) throw InvalidDataError("EBML header not found"); head->SkipData(stream, EBML_CONTEXT(head.get())); - std::unique_ptr segment(stream.FindNextID(EBML_INFO(KaxSegment), UINT64_MAX)); - if (!segment) - throw InvalidDataError("Matroska segment not found"); + std::unique_ptr segment; + for (;;) { + segment.reset(stream.FindNextID(EBML_INFO(KaxSegment), UINT64_MAX)); + if (!segment) + throw InvalidDataError("Matroska segment not found"); + if (dynamic_cast(segment.get())) + break; + // Skip anything else at the top level, such as Void + if (!segment->IsFiniteSize()) + throw InvalidDataError("Matroska segment not found"); + segment->SkipData(stream, EBML_CONTEXT(segment.get())); + } auto const& context = EBML_CONTEXT(segment.get()); int upper = 0; @@ -576,6 +614,48 @@ class Demuxer::Impl { } } + /// Find the timestamp of the first block in the first cluster + void FindFirstTimestamp() { + if (clusters.empty()) + return; + auto const& cluster = clusters.front(); + try { + uint64_t end = checked_add(cluster.position, cluster.size, "Matroska cluster is out of range"); + uint64_t cluster_time = 0; + for (uint64_t position = cluster.position; position < end;) { + auto element = ReadElement(position, end); + position = element.end; + std::optional block; + if (element.id == id_cluster_timestamp) + cluster_time = ReadUInt(element); + else if (element.id == id_simple_block) + block = element; + else if (element.id == id_block_group) { + for (uint64_t child_position = element.data; child_position < element.end && !block;) { + auto child = ReadElement(child_position, element.end); + child_position = child.end; + if (child.id == id_block) + block = child; + } + } + if (!block) + continue; + + if (cluster_time > static_cast(INT64_MAX)) + return; + auto track_bytes = ReadVint(block->data, block->end).second; + if (block->size < track_bytes + 2u) + return; + uint64_t cursor = block->data + track_bytes; + auto relative = static_cast((ReadByte(cursor) << 8) | ReadByte(cursor + 1)); + first_timestamp = checked_add_signed(static_cast(cluster_time), relative, "Matroska timestamp is out of range"); + return; + } + } + catch (InvalidDataError const&) { } + catch (TruncatedError const&) { } + } + /// Use the end of the last frame as the duration when the header lacks one void ComputeDuration() { if (duration || tracks.empty()) @@ -682,12 +762,16 @@ class Demuxer::Impl { else sizes[0] = end - cursor; - if (cluster_time > static_cast(INT64_MAX) || (relative > 0 && cluster_time > static_cast(INT64_MAX - relative))) - throw InvalidDataError("Matroska timestamp is out of range"); - int64_t ticks = static_cast(cluster_time) + relative; + constexpr char const *out_of_range = "Matroska timestamp is out of range"; + if (cluster_time > static_cast(INT64_MAX)) + throw InvalidDataError(out_of_range); + // first_timestamp is at least INT16_MIN, so negating it can't overflow + int64_t ticks = checked_add_signed(checked_add_signed(static_cast(cluster_time), -first_timestamp, out_of_range), relative, out_of_range); + auto const& track_state = all_tracks[track]; uint64_t frame_duration = block_duration - ? checked_multiply(*block_duration, timestamp_scale, "Matroska block duration is out of range") / count - : all_tracks[track].default_duration; + ? checked_scale(checked_multiply(*block_duration, timestamp_scale, "Matroska block duration is out of range"), + track_state.timestamp_scale, "Matroska block duration is out of range") / count + : track_state.default_duration; for (unsigned i = 0; i < count; ++i) { Frame frame; @@ -696,12 +780,12 @@ class Demuxer::Impl { frame.size = sizes[i]; if (ticks >= 0) { uint64_t start = checked_add( - checked_multiply(static_cast(ticks), timestamp_scale, "Matroska timestamp is out of range"), - checked_multiply(i, frame_duration, "Matroska timestamp is out of range"), - "Matroska timestamp is out of range"); + checked_scale(checked_multiply(static_cast(ticks), timestamp_scale, out_of_range), track_state.timestamp_scale, out_of_range), + checked_multiply(i, frame_duration, out_of_range), + out_of_range); frame.start = MakeTimestamp(start); if (block_duration || frame_duration) - frame.end = MakeTimestamp(checked_add(start, frame_duration, "Matroska timestamp is out of range")); + frame.end = MakeTimestamp(checked_add(start, frame_duration, out_of_range)); } out.push_back(frame); cursor = checked_add(cursor, sizes[i], "Matroska frame exceeds its block"); @@ -772,6 +856,7 @@ class Demuxer::Impl { // Our own errors are agi::Exceptions, so this is only libebml rejecting the file throw InvalidDataError(e.what()); } + FindFirstTimestamp(); ComputeDuration(); } diff --git a/tests/matroska/fixtures/compressed-zlib.behavior b/tests/matroska/fixtures/compressed-zlib.behavior index 635234951a..897b4cfd6c 100644 --- a/tests/matroska/fixtures/compressed-zlib.behavior +++ b/tests/matroska/fixtures/compressed-zlib.behavior @@ -1,3 +1,3 @@ duration 2000000000 track 0 2756463499428226085 S_TEXT/UTF8 und 0 1 1 -packet 0 500000000 2500000000 24 da1dfbf2ee283ed2 +packet 0 0 2000000000 24 da1dfbf2ee283ed2 diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index ead11821b5..9a78ff121c 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -67,3 +67,14 @@ def attached(name,data=None): entry=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) cluster=element('1f43b675',uint_element('e7',(1<<63)-1)+element('a3',b'\x81\x00\x01\x80x')) (r/'timestamp-overflow.mkv').write_bytes(header+element('18538067',info+element('1654ae6b',entry)+cluster)) + +# Timing: a Void before the Segment, a first block at 10s which all block +# timestamps are made relative to, and a subtitle track with a (deprecated) +# TrackTimecodeScale of 2 +import struct +video=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',1)+element('86',b'V_TEST')) +scaled=element('ae',uint_element('d7',2)+uint_element('73c5',2)+uint_element('83',17)+element('86',b'S_TEXT/UTF8') + +element('23314f',struct.pack('>d',2.0))) +group=element('a0',element('a1',b'\x82\x01\xf4\x00sub')+uint_element('9b',1000)) +cluster=element('1f43b675',uint_element('e7',10000)+element('a3',b'\x81\x00\x00\x80v')+group) +(r/'timing.mkv').write_bytes(header+element('ec',bytes(14))+element('18538067',info+element('1654ae6b',video+scaled)+cluster)) diff --git a/tests/matroska/fixtures/malformed.behavior b/tests/matroska/fixtures/malformed.behavior index 656f04c5d3..5ceaadcf04 100644 --- a/tests/matroska/fixtures/malformed.behavior +++ b/tests/matroska/fixtures/malformed.behavior @@ -1 +1 @@ -error Matroska segment not found +error EBML header not found diff --git a/tests/matroska/fixtures/timing.behavior b/tests/matroska/fixtures/timing.behavior new file mode 100644 index 0000000000..6ba3bbc958 --- /dev/null +++ b/tests/matroska/fixtures/timing.behavior @@ -0,0 +1,3 @@ +duration 3000000000 +track 1 2 S_TEXT/UTF8 eng 0 1 1 +packet 1 1000000000 3000000000 3 58db605150dd5fa7 diff --git a/tests/matroska/fixtures/timing.mkv b/tests/matroska/fixtures/timing.mkv new file mode 100644 index 0000000000000000000000000000000000000000..ef098fc6cbc4581021a94ee38fb3a62370ada097 GIT binary patch literal 166 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>^rnvi1xN%pq`wsP zT$$Fcb$w$qzmtPl$hz#E>n2@qWGp_~$k^P-*wz*nAL1GuvTpJ9Mkb&bQ*)zWTX!%} zEFwfdG{nt9+0ehk0cxnc^S07i&zsZ*7PmGsFf^1c=v~;}#Q24wxHM^Y6Y~oIj2}D` literal 0 HcmV?d00001 From 36344baf80dd3bf2cfbf5cf63c70e80e9ffe07b2 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 11:51:56 -0700 Subject: [PATCH 21/28] Read Matroska metadata without walking every cluster Opening a file walked every top-level element with libebml, indexing all clusters before returning. MatroskaWrapper::HasSubtitles does this on the UI thread whenever video is opened, which means reads from throughout the file. Like the Haali parser, stop at the first cluster and follow the SeekHead for metadata stored after the clusters, only walking the entire file if the tracks still haven't been found. Clusters are now indexed as packets are read, and the fallback duration is computed the first time Duration() is called rather than while opening the file. This walks the segment with the raw EBML reader and only uses libebml to parse Info and Tracks. That fixes a Void before a CRC-32-prefixed Info or Tracks element leaving libebml's nesting level negative, which made it stop reading the element's children. The existing video-only fixture, written by ffmpeg, has this layout and was losing its duration. Also: - Compute block timestamps relative to the first block exactly, so only the relative timestamp needs to fit in 64 bits. - Find the end of unknown-size clusters, as live muxers write. Co-Authored-By: Claude Opus 5.5 --- src/matroska.cpp | 282 ++++++++++++++---- src/matroska.h | 6 +- src/mkv_wrap.cpp | 5 +- tests/matroska/fixtures/generate.py | 12 +- .../fixtures/timestamp-overflow.behavior | 2 +- tests/matroska/fixtures/video-only.behavior | 2 +- tests/matroska/fixtures/void-crc.behavior | 4 + tests/matroska/fixtures/void-crc.mkv | Bin 0 -> 145 bytes 8 files changed, 250 insertions(+), 63 deletions(-) create mode 100644 tests/matroska/fixtures/void-crc.behavior create mode 100644 tests/matroska/fixtures/void-crc.mkv diff --git a/src/matroska.cpp b/src/matroska.cpp index 74a81360f6..90c0918560 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include namespace agi::matroska { @@ -64,6 +65,27 @@ constexpr uint64_t id_file_description = 0x467E; constexpr uint64_t id_file_mime_type = 0x4660; constexpr uint64_t id_file_data = 0x465C; constexpr uint64_t id_file_uid = 0x46AE; +constexpr uint64_t id_seek_head = 0x114D9B74; +constexpr uint64_t id_seek = 0x4DBB; +constexpr uint64_t id_seek_id = 0x53AB; +constexpr uint64_t id_seek_position = 0x53AC; +constexpr uint64_t id_info = 0x1549A966; +constexpr uint64_t id_tracks = 0x1654AE6B; +constexpr uint64_t id_attachments = 0x1941A469; +constexpr uint64_t id_cluster = 0x1F43B675; +constexpr uint64_t id_cues = 0x1C53BB6B; +constexpr uint64_t id_chapters = 0x1043A770; +constexpr uint64_t id_tags = 0x1254C367; + +bool is_top_level(uint64_t id) { + switch (id) { + case id_seek_head: case id_info: case id_tracks: case id_attachments: + case id_cluster: case id_cues: case id_chapters: case id_tags: + return true; + default: + return false; + } +} constexpr uint64_t track_type_subtitle = 0x11; @@ -320,9 +342,12 @@ struct Location { struct Element { uint64_t id = 0; + /// Position of the element's ID + uint64_t position = 0; uint64_t data = 0; uint64_t size = 0; uint64_t end = 0; + bool unknown_size = false; }; } // namespace @@ -341,18 +366,34 @@ class Demuxer::Impl { Limits limits; uint64_t timestamp_scale = 1000000; - /// Timestamp in ticks of the first block in the file, which is subtracted - /// from all block timestamps so that the file starts at zero, as the video - /// providers do for video frames - int64_t first_timestamp = 0; + /// Timestamp of the first block in the file, which is subtracted from all + /// block timestamps so that the file starts at zero, as the video providers + /// do for video frames. Kept as its parts so the difference is exact. + uint64_t first_cluster_time = 0; + int16_t first_relative = 0; std::optional duration; + bool duration_computed = false; + + /// Data of the segment, clamped to the end of the input + uint64_t segment_start = 0; + uint64_t segment_end = 0; + /// Top-level elements already parsed, by position + std::set parsed_elements; + /// Positions of the metadata elements listed by SeekHeads + std::vector seek_targets; + bool seen_tracks = false; std::vector all_tracks; std::unordered_map track_by_number; std::vector tracks; std::vector attachments; /// Location of each attachment's data, parallel to attachments std::vector attachment_data; + + /// Clusters are indexed lazily, as finding them all means reading from + /// throughout the file std::vector clusters; + uint64_t cluster_scan_position = 0; + bool clusters_complete = true; /// Index into all_tracks of the selected track std::optional selected; @@ -429,11 +470,12 @@ class Demuxer::Impl { auto [size, size_length] = ReadVint(position + id_length, parent_end); uint64_t data = position + id_length + size_length; // All ones is an unknown size, which extends to the end of the parent - if (size == (uint64_t{1} << (7 * size_length)) - 1) + bool unknown_size = size == (uint64_t{1} << (7 * size_length)) - 1; + if (unknown_size) size = parent_end - data; if (data > parent_end || size > parent_end - data) throw InvalidDataError("Matroska element exceeds its parent"); - return {id, data, size, data + size}; + return {id, position, data, size, data + size, unknown_size}; } uint64_t ReadUInt(Element const& element) { @@ -547,6 +589,119 @@ class Demuxer::Impl { } } + /// Read the top-level element at position, finding where it ends if its size is unknown + Element TopLevelElement(uint64_t position) { + auto element = ReadElement(position, segment_end); + if (element.unknown_size) { + // Only clusters are written with unknown sizes in practice. They + // end where the next top-level element starts. + for (uint64_t child_position = element.data; child_position < segment_end;) { + auto child = ReadElement(child_position, segment_end); + if (is_top_level(child.id)) { + element.end = child_position; + break; + } + child_position = child.end; + } + element.size = element.end - element.data; + } + return element; + } + + /// Read a master element with libebml + template + std::unique_ptr ReadMaster(EbmlStream& stream, Element const& element) { + if (element.size > limits.metadata_size) + throw LimitError("Matroska metadata exceeds configured limit"); + stream.I_O().setFilePointer(static_cast(element.position)); + std::unique_ptr master(stream.FindNextID(EBML_INFO(T), UINT64_MAX)); + if (!dynamic_cast(master.get())) + throw InvalidDataError("Unexpected Matroska element"); + int upper = 0; + EbmlElement *found = nullptr; + static_cast(*master).Read(stream, EBML_CONTEXT(master.get()), upper, found, true); + delete found; + return master; + } + + void ParseSeekHead(Element const& seek_head) { + if (seek_head.size > limits.metadata_size) + throw LimitError("Matroska metadata exceeds configured limit"); + for (uint64_t position = seek_head.data; position < seek_head.end;) { + auto seek = ReadElement(position, seek_head.end); + position = seek.end; + if (seek.id != id_seek) + continue; + + std::optional id, offset; + for (uint64_t child_position = seek.data; child_position < seek.end;) { + auto child = ReadElement(child_position, seek.end); + child_position = child.end; + if (child.id == id_seek_id) + id = ReadUInt(child); + else if (child.id == id_seek_position) + offset = ReadUInt(child); + } + if (!id || !offset || *offset >= segment_end - segment_start) + continue; + if (*id == id_info || *id == id_tracks || *id == id_attachments || *id == id_seek_head) + seek_targets.push_back(segment_start + *offset); + } + } + + void ParseMetadata(EbmlStream& stream, Element const& element) { + if (!is_top_level(element.id) || element.id == id_cluster) + return; + if (!parsed_elements.insert(element.position).second) + return; + + if (element.id == id_info) { + auto info = ReadMaster(stream, element); + ParseInfo(static_cast(*info)); + } + else if (element.id == id_tracks) { + auto track_list = ReadMaster(stream, element); + for (auto child : static_cast(*track_list).GetElementList()) { + if (auto entry = dynamic_cast(child)) + ParseTrack(*entry); + } + seen_tracks = true; + } + else if (element.id == id_attachments) + ParseAttachments({element.data, element.size}); + else if (element.id == id_seek_head) + ParseSeekHead(element); + } + + /// Index the next cluster, returning false at the end of the segment. If + /// a stream is given, metadata found along the way is parsed too. + bool ScanNextCluster(EbmlStream *stream = nullptr) { + while (!clusters_complete && cluster_scan_position < segment_end) { + CheckCancelled(); + Element element; + try { + element = TopLevelElement(cluster_scan_position); + } + // Treat damage after the last readable cluster as the end of the + // file, as truncated files are common + catch (InvalidDataError const&) { + break; + } + catch (TruncatedError const&) { + break; + } + cluster_scan_position = element.end; + if (element.id == id_cluster) { + clusters.push_back({element.data, element.size}); + return true; + } + if (stream) + ParseMetadata(*stream, element); + } + clusters_complete = true; + return false; + } + void ParseSegment() { ReaderCallback io(*reader); EbmlStream stream(io); @@ -570,57 +725,51 @@ class Demuxer::Impl { throw InvalidDataError("Matroska segment not found"); segment->SkipData(stream, EBML_CONTEXT(segment.get())); } - auto const& context = EBML_CONTEXT(segment.get()); - int upper = 0; - std::unique_ptr current(stream.FindNextElement(context, upper, UINT64_MAX, true)); - while (current && upper <= 0) { + segment_start = segment->GetElementPosition() + segment->HeadSize(); + segment_end = reader->Size(); + if (segment->IsFiniteSize() && segment->GetSize() < segment_end - std::min(segment_start, segment_end)) + segment_end = segment_start + segment->GetSize(); + + // Metadata is normally before the first cluster + uint64_t position = segment_start; + while (position < segment_end) { CheckCancelled(); - std::unique_ptr next; - Location location{current->GetElementPosition() + current->HeadSize(), current->GetSize()}; - - auto info = dynamic_cast(current.get()); - auto track_list = dynamic_cast(current.get()); - if (info || track_list) { - if (current->GetSize() > limits.metadata_size) - throw LimitError("Matroska metadata exceeds configured limit"); - EbmlElement *found = nullptr; - static_cast(*current).Read(stream, EBML_CONTEXT(current.get()), upper, found, true); - next.reset(found); - // A positive level means the element found belongs to an ancestor - if (upper > 0) - --upper; - - if (info) - ParseInfo(*info); - else { - for (auto element : track_list->GetElementList()) { - if (auto entry = dynamic_cast(element)) - ParseTrack(*entry); - } - } + auto element = TopLevelElement(position); + if (element.id == id_cluster) { + cluster_scan_position = position; + clusters_complete = false; + break; } - else { - if (dynamic_cast(current.get())) - clusters.push_back(location); - else if (dynamic_cast(current.get())) - ParseAttachments(location); - current->SkipData(stream, context); + ParseMetadata(stream, element); + position = element.end; + } + + // Anything after the clusters should be listed in a SeekHead. Nested + // SeekHeads append to seek_targets as it is iterated. + for (size_t i = 0; i < seek_targets.size(); ++i) { + CheckCancelled(); + try { + ParseMetadata(stream, TopLevelElement(seek_targets[i])); } + catch (InvalidDataError const&) { } + catch (TruncatedError const&) { } + } - if (!next) - next.reset(stream.FindNextElement(context, upper, UINT64_MAX, true)); - current = std::move(next); + // Otherwise the only way to find the tracks is to walk the entire file + if (!seen_tracks) { + while (ScanNextCluster(&stream)) + ; } } /// Find the timestamp of the first block in the first cluster void FindFirstTimestamp() { - if (clusters.empty()) + if (clusters.empty() && !ScanNextCluster()) return; auto const& cluster = clusters.front(); try { - uint64_t end = checked_add(cluster.position, cluster.size, "Matroska cluster is out of range"); + uint64_t end = cluster.position + cluster.size; uint64_t cluster_time = 0; for (uint64_t position = cluster.position; position < end;) { auto element = ReadElement(position, end); @@ -641,14 +790,12 @@ class Demuxer::Impl { if (!block) continue; - if (cluster_time > static_cast(INT64_MAX)) - return; auto track_bytes = ReadVint(block->data, block->end).second; if (block->size < track_bytes + 2u) return; uint64_t cursor = block->data + track_bytes; - auto relative = static_cast((ReadByte(cursor) << 8) | ReadByte(cursor + 1)); - first_timestamp = checked_add_signed(static_cast(cluster_time), relative, "Matroska timestamp is out of range"); + first_cluster_time = cluster_time; + first_relative = static_cast((ReadByte(cursor) << 8) | ReadByte(cursor + 1)); return; } } @@ -660,6 +807,8 @@ class Demuxer::Impl { void ComputeDuration() { if (duration || tracks.empty()) return; + while (ScanNextCluster()) + ; for (auto cluster = clusters.rbegin(); cluster != clusters.rend(); ++cluster) { CheckCancelled(); std::vector cluster_frames; @@ -685,6 +834,25 @@ class Demuxer::Impl { } } + /// Get a block's timestamp in ticks relative to the first block + int64_t RelativeTicks(uint64_t cluster_time, int16_t relative) { + constexpr char const *out_of_range = "Matroska timestamp is out of range"; + int64_t ticks; + if (cluster_time >= first_cluster_time) { + uint64_t difference = cluster_time - first_cluster_time; + if (difference > static_cast(INT64_MAX)) + throw InvalidDataError(out_of_range); + ticks = static_cast(difference); + } + else { + uint64_t difference = first_cluster_time - cluster_time; + if (difference > static_cast(INT64_MAX)) + throw InvalidDataError(out_of_range); + ticks = -static_cast(difference); + } + return checked_add_signed(ticks, int64_t{relative} - first_relative, out_of_range); + } + // Clusters std::optional MakeTimestamp(uint64_t nanoseconds) { @@ -763,10 +931,7 @@ class Demuxer::Impl { sizes[0] = end - cursor; constexpr char const *out_of_range = "Matroska timestamp is out of range"; - if (cluster_time > static_cast(INT64_MAX)) - throw InvalidDataError(out_of_range); - // first_timestamp is at least INT16_MIN, so negating it can't overflow - int64_t ticks = checked_add_signed(checked_add_signed(static_cast(cluster_time), -first_timestamp, out_of_range), relative, out_of_range); + int64_t ticks = RelativeTicks(cluster_time, relative); auto const& track_state = all_tracks[track]; uint64_t frame_duration = block_duration ? checked_scale(checked_multiply(*block_duration, timestamp_scale, "Matroska block duration is out of range"), @@ -857,12 +1022,17 @@ class Demuxer::Impl { throw InvalidDataError(e.what()); } FindFirstTimestamp(); - ComputeDuration(); } std::vector const& Tracks() const { return tracks; } std::vector const& AttachmentList() const { return attachments; } - std::optional Duration() const { return duration; } + std::optional Duration() { + if (!duration_computed) { + ComputeDuration(); + duration_computed = true; + } + return duration; + } void Select(TrackId id) { auto track = std::find_if(tracks.begin(), tracks.end(), [&](SubtitleTrack const& track) { @@ -889,7 +1059,7 @@ class Demuxer::Impl { CheckCancelled(); while (next_frame == frames.size()) { - if (next_cluster == clusters.size()) + if (next_cluster == clusters.size() && !ScanNextCluster()) return std::nullopt; // Clear first so that a cluster which fails to parse is skipped on retry frames.clear(); @@ -933,7 +1103,7 @@ std::vector const& Demuxer::Attachments() const { return impl->AttachmentList(); } -std::optional Demuxer::Duration() const { +std::optional Demuxer::Duration() { return impl->Duration(); } diff --git a/src/matroska.h b/src/matroska.h index b59454fbeb..f5d039491a 100644 --- a/src/matroska.h +++ b/src/matroska.h @@ -130,8 +130,10 @@ class Demuxer final { std::vector const& SubtitleTracks() const; std::vector const& Attachments() const; - /// Segment duration, or nullopt if neither the header nor the last cluster gives one - std::optional Duration() const; + /// Segment duration, or nullopt if neither the header nor the last cluster + /// gives one. If the header lacks one, the first call has to find the last + /// cluster, which reads from throughout the file and can be cancelled. + std::optional Duration(); /// Select one subtitle track and rewind packet iteration to its beginning. void SelectTrack(TrackId track); diff --git a/src/mkv_wrap.cpp b/src/mkv_wrap.cpp index e47029461f..dd2dcfd311 100644 --- a/src/mkv_wrap.cpp +++ b/src/mkv_wrap.cpp @@ -219,9 +219,10 @@ void MatroskaWrapper::GetSubtitles(agi::fs::path const& filename, AssFile *targe parser.AddLine("[Events]"); - auto duration = demuxer->Duration(); - int64_t totalTime = duration ? duration->nanoseconds / 1000000 : 0; run_with_progress(_("Reading subtitles from Matroska file."), active_sink, [&](agi::ProgressSink *ps) { + // May need to find the last cluster, so do this inside the progress dialog + auto duration = demuxer->Duration(); + int64_t totalTime = duration ? duration->nanoseconds / 1000000 : 0; read_subtitles(ps, *demuxer, srt, totalTime, &parser); }); diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index 9a78ff121c..30ea74ec78 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -63,7 +63,8 @@ def attached(name,data=None): cluster=element('1f43b675',uint_element('e7',0)+element('a3',b'\x81\x00\x00\x80hi')) (r/'repeated-tracks.mkv').write_bytes(header+element('18538067',info+tracks+tracks+attachments+cluster)) -# A block whose timestamp overflows when added to the cluster's +# A cluster timestamp which overflows int64 when the block's relative timestamp +# is added. Only the offset from the first block matters, so this is fine. entry=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) cluster=element('1f43b675',uint_element('e7',(1<<63)-1)+element('a3',b'\x81\x00\x01\x80x')) (r/'timestamp-overflow.mkv').write_bytes(header+element('18538067',info+element('1654ae6b',entry)+cluster)) @@ -78,3 +79,12 @@ def attached(name,data=None): group=element('a0',element('a1',b'\x82\x01\xf4\x00sub')+uint_element('9b',1000)) cluster=element('1f43b675',uint_element('e7',10000)+element('a3',b'\x81\x00\x00\x80v')+group) (r/'timing.mkv').write_bytes(header+element('ec',bytes(14))+element('18538067',info+element('1654ae6b',video+scaled)+cluster)) + +# A Void followed by CRC-prefixed Info and Tracks, as mkvmerge and ffmpeg +# write. Losing the Info would show as the second packet being at 4ms rather +# than 2ms. +crc=element('bf',bytes(4)) +entry=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) +void_info=element('1549a966',crc+uint_element('2ad7b1',500000)) +clusters=element('1f43b675',uint_element('e7',0)+element('a3',b'\x81\x00\x00\x80a'))+element('1f43b675',uint_element('e7',4)+element('a3',b'\x81\x00\x00\x80b')) +(r/'void-crc.mkv').write_bytes(header+element('18538067',element('ec',bytes(6))+void_info+element('ec',bytes(6))+element('1654ae6b',crc+entry)+clusters)) diff --git a/tests/matroska/fixtures/timestamp-overflow.behavior b/tests/matroska/fixtures/timestamp-overflow.behavior index 0d68f79a1b..7544e3c6bd 100644 --- a/tests/matroska/fixtures/timestamp-overflow.behavior +++ b/tests/matroska/fixtures/timestamp-overflow.behavior @@ -1,3 +1,3 @@ duration - track 0 1 S_TEXT/UTF8 eng 0 1 1 -error Matroska timestamp is out of range +packet 0 0 - 1 44bda3d473cdc381 diff --git a/tests/matroska/fixtures/video-only.behavior b/tests/matroska/fixtures/video-only.behavior index 6a9c2e5acf..c0b7a7338b 100644 --- a/tests/matroska/fixtures/video-only.behavior +++ b/tests/matroska/fixtures/video-only.behavior @@ -1 +1 @@ -duration - +duration 1000000000 diff --git a/tests/matroska/fixtures/void-crc.behavior b/tests/matroska/fixtures/void-crc.behavior new file mode 100644 index 0000000000..e7838ab6c5 --- /dev/null +++ b/tests/matroska/fixtures/void-crc.behavior @@ -0,0 +1,4 @@ +duration - +track 0 1 S_TEXT/UTF8 eng 0 1 1 +packet 0 0 - 1 44bd8ad473cd9906 +packet 0 2000000 - 1 44bd89d473cd9753 diff --git a/tests/matroska/fixtures/void-crc.mkv b/tests/matroska/fixtures/void-crc.mkv new file mode 100644 index 0000000000000000000000000000000000000000..eef333b3366ec074e5ff06d8495195ec53811e36 GIT binary patch literal 145 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@{^U&?0~m;U zu1xFQ-vVN5UEkQuzEA-sBNnnQdp<;F-Sq2?jKxPA8Jimg+q#3}LtG<5^g~13EaaWH Wm3BREWLVtV$iUE$2<5UsxJdv!j5Oi^ literal 0 HcmV?d00001 From 9c3c1b623cd7b78d3391faa44994e41cf6cdeb0e Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 14:25:57 -0700 Subject: [PATCH 22/28] Keep Matroska test expectations LF on Windows The fixture test compares describe() output with the .behavior files byte-for-byte, which failed when Git converted them to CRLF on checkout. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitattributes b/.gitattributes index 554c5d6e5e..195da2c00f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,7 @@ subprojects/luabins/meson.build -linguist-vendored libaegisub/lua/modules/lpeg.* linguist-vendored automation/include/moonscript.lua linguist-vendored + +# Compared byte-for-byte by the Matroska tests +tests/matroska/fixtures/*.behavior eol=lf +tests/matroska/fixtures/*.mk[av] binary From ec571ffaef8d38222ce40c4b297e847631b942c2 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 14:33:52 -0700 Subject: [PATCH 23/28] Make imported Matroska subtitles relative to the start of the file The demuxer now reports timestamps as stored in the file rather than relative to the first block, and exposes the earliest audio or video timestamp as StartTime(). This is what FFmpeg (and so MPC-HC/LAV) reports as the start time, and what mpv uses for Matroska in practice, and both rebase playback so that it is time zero. MatroskaWrapper subtracts it from imported subtitles. This matches the Haali parser for nearly all files, as their first block is normally audio or video, while fixing two cases where that differed: - Files with only subtitles are no longer shifted to start at zero (TypesettingTools/Aegisub#91), as they are never played by themselves. - A subtitle line before the first audio or video block no longer moves every other line earlier. Lines which start before the start of the file are clamped to start at zero, and lines which end before it are dropped. Aegisub's own video timeline makes the first video frame time zero instead (TypesettingTools/Aegisub#21), so in files where video starts after audio, imported lines appear late by that delay, as they did before. They are exported with their original timing, and this will be consistent if the video timeline is changed to use the start of the file. The deprecated TrackTimecodeScale is now applied to the stored timestamp as the specification describes, rather than to the offset from the first block. Co-Authored-By: Claude Opus 5.5 --- src/matroska.cpp | 149 +++++++++--------- src/matroska.h | 7 +- src/mkv_wrap.cpp | 40 +++-- .../fixtures/audio-only-opus.behavior | 1 + .../fixtures/compressed-zlib.behavior | 3 +- tests/matroska/fixtures/encodings.behavior | 1 + tests/matroska/fixtures/generate.py | 12 +- .../fixtures/repeated-tracks.behavior | 1 + tests/matroska/fixtures/start-time.behavior | 4 + tests/matroska/fixtures/start-time.mkv | Bin 0 -> 167 bytes .../fixtures/subtitle-attachment.behavior | 1 + .../fixtures/timestamp-overflow.behavior | 3 +- tests/matroska/fixtures/timing.behavior | 5 +- .../fixtures/tracks-after-cluster.behavior | 1 + tests/matroska/fixtures/video-only.behavior | 1 + tests/matroska/fixtures/void-crc.behavior | 1 + tests/tests/matroska.cpp | 13 +- 17 files changed, 137 insertions(+), 106 deletions(-) create mode 100644 tests/matroska/fixtures/start-time.behavior create mode 100644 tests/matroska/fixtures/start-time.mkv diff --git a/src/matroska.cpp b/src/matroska.cpp index 90c0918560..afedf67897 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -87,6 +87,8 @@ bool is_top_level(uint64_t id) { } } +constexpr uint64_t track_type_video = 0x1; +constexpr uint64_t track_type_audio = 0x2; constexpr uint64_t track_type_subtitle = 0x11; class FileReader final : public Reader { @@ -228,6 +230,16 @@ uint64_t checked_multiply(uint64_t lhs, uint64_t rhs, char const *message) { return lhs * rhs; } +/// Convert a timestamp in ticks, which may be slightly negative, to nanoseconds +int64_t ticks_to_nanoseconds(int64_t ticks, uint64_t timestamp_scale, double track_scale) { + constexpr char const *out_of_range = "Matroska timestamp is out of range"; + uint64_t magnitude = checked_multiply(ticks < 0 ? static_cast(-ticks) : static_cast(ticks), timestamp_scale, out_of_range); + magnitude = checked_scale(magnitude, track_scale, out_of_range); + if (magnitude > static_cast(INT64_MAX)) + throw InvalidDataError(out_of_range); + return ticks < 0 ? -static_cast(magnitude) : static_cast(magnitude); +} + SubtitleCodec codec_from_id(std::string const& id) { if (id == "S_TEXT/ASS") return SubtitleCodec::ass; @@ -319,6 +331,7 @@ std::vector inflate_packet(std::vector const& input, size_t li /// Demuxing state for every track in the file, not just subtitle tracks struct TrackState { uint64_t uid = 0; + uint64_t type = 0; uint64_t default_duration = 0; /// Deprecated per-track multiplier applied to block timestamps and durations double timestamp_scale = 1.0; @@ -328,7 +341,7 @@ struct TrackState { struct Frame { size_t track = 0; - std::optional start; + Timestamp start; std::optional end; uint64_t position = 0; uint64_t size = 0; @@ -366,13 +379,10 @@ class Demuxer::Impl { Limits limits; uint64_t timestamp_scale = 1000000; - /// Timestamp of the first block in the file, which is subtracted from all - /// block timestamps so that the file starts at zero, as the video providers - /// do for video frames. Kept as its parts so the difference is exact. - uint64_t first_cluster_time = 0; - int16_t first_relative = 0; std::optional duration; bool duration_computed = false; + std::optional start_time; + bool start_time_computed = false; /// Data of the segment, clamped to the end of the input uint64_t segment_start = 0; @@ -520,6 +530,7 @@ class Demuxer::Impl { })) return; + state.type = uint_value(entry); state.default_duration = uint_value(entry); if (auto scale = child(entry)) { double value = static_cast(*scale); @@ -533,7 +544,7 @@ class Demuxer::Impl { if (!track_by_number.emplace(uint_value(entry), all_tracks.size()).second) throw InvalidDataError("Duplicate Matroska track number"); - if (uint_value(entry) == track_type_subtitle) { + if (state.type == track_type_subtitle) { SubtitleTrack track; track.id.value = static_cast(all_tracks.size()); track.uid = state.uid; @@ -763,44 +774,39 @@ class Demuxer::Impl { } } - /// Find the timestamp of the first block in the first cluster - void FindFirstTimestamp() { - if (clusters.empty() && !ScanNextCluster()) + bool IsAudioOrVideo(size_t track) const { + return all_tracks[track].type == track_type_video || all_tracks[track].type == track_type_audio; + } + + /// Find the earliest audio or video timestamp, which is what FFmpeg (and + /// so LAV) reports as the start time and what mpv uses for Matroska in + /// practice. Like FFmpeg, subtitle tracks don't count. + void ComputeStartTime() { + bool has_audio_or_video = false; + for (size_t track = 0; track < all_tracks.size(); ++track) + has_audio_or_video = has_audio_or_video || IsAudioOrVideo(track); + if (!has_audio_or_video) return; - auto const& cluster = clusters.front(); - try { - uint64_t end = cluster.position + cluster.size; - uint64_t cluster_time = 0; - for (uint64_t position = cluster.position; position < end;) { - auto element = ReadElement(position, end); - position = element.end; - std::optional block; - if (element.id == id_cluster_timestamp) - cluster_time = ReadUInt(element); - else if (element.id == id_simple_block) - block = element; - else if (element.id == id_block_group) { - for (uint64_t child_position = element.data; child_position < element.end && !block;) { - auto child = ReadElement(child_position, element.end); - child_position = child.end; - if (child.id == id_block) - block = child; - } - } - if (!block) - continue; - - auto track_bytes = ReadVint(block->data, block->end).second; - if (block->size < track_bytes + 2u) - return; - uint64_t cursor = block->data + track_bytes; - first_cluster_time = cluster_time; - first_relative = static_cast((ReadByte(cursor) << 8) | ReadByte(cursor + 1)); - return; + for (size_t i = 0; i < clusters.size() || ScanNextCluster(); ++i) { + CheckCancelled(); + std::vector cluster_frames; + try { + cluster_frames = ParseCluster(clusters[i], std::nullopt); } + catch (InvalidDataError const&) { + continue; + } + catch (TruncatedError const&) { + continue; + } + + for (auto const& frame : cluster_frames) { + if (IsAudioOrVideo(frame.track) && (!start_time || frame.start.nanoseconds < start_time->nanoseconds)) + start_time = frame.start; + } + if (start_time) + return; } - catch (InvalidDataError const&) { } - catch (TruncatedError const&) { } } /// Use the end of the last frame as the duration when the header lacks one @@ -834,33 +840,8 @@ class Demuxer::Impl { } } - /// Get a block's timestamp in ticks relative to the first block - int64_t RelativeTicks(uint64_t cluster_time, int16_t relative) { - constexpr char const *out_of_range = "Matroska timestamp is out of range"; - int64_t ticks; - if (cluster_time >= first_cluster_time) { - uint64_t difference = cluster_time - first_cluster_time; - if (difference > static_cast(INT64_MAX)) - throw InvalidDataError(out_of_range); - ticks = static_cast(difference); - } - else { - uint64_t difference = first_cluster_time - cluster_time; - if (difference > static_cast(INT64_MAX)) - throw InvalidDataError(out_of_range); - ticks = -static_cast(difference); - } - return checked_add_signed(ticks, int64_t{relative} - first_relative, out_of_range); - } - // Clusters - std::optional MakeTimestamp(uint64_t nanoseconds) { - if (nanoseconds > static_cast(INT64_MAX)) - throw InvalidDataError("Matroska timestamp is out of range"); - return Timestamp{static_cast(nanoseconds)}; - } - /// Split a block into its frames, appending those for only_track (or all tracks) to frames void ParseBlock(Element const& block, uint64_t cluster_time, std::optional block_duration, std::optional only_track, std::vector& out) { @@ -931,7 +912,9 @@ class Demuxer::Impl { sizes[0] = end - cursor; constexpr char const *out_of_range = "Matroska timestamp is out of range"; - int64_t ticks = RelativeTicks(cluster_time, relative); + if (cluster_time > static_cast(INT64_MAX)) + throw InvalidDataError(out_of_range); + int64_t ticks = checked_add_signed(static_cast(cluster_time), relative, out_of_range); auto const& track_state = all_tracks[track]; uint64_t frame_duration = block_duration ? checked_scale(checked_multiply(*block_duration, timestamp_scale, "Matroska block duration is out of range"), @@ -943,15 +926,14 @@ class Demuxer::Impl { frame.track = track; frame.position = cursor; frame.size = sizes[i]; - if (ticks >= 0) { - uint64_t start = checked_add( - checked_scale(checked_multiply(static_cast(ticks), timestamp_scale, out_of_range), track_state.timestamp_scale, out_of_range), - checked_multiply(i, frame_duration, out_of_range), - out_of_range); - frame.start = MakeTimestamp(start); - if (block_duration || frame_duration) - frame.end = MakeTimestamp(checked_add(start, frame_duration, out_of_range)); - } + uint64_t lace_offset = checked_multiply(i, frame_duration, out_of_range); + if (lace_offset > static_cast(INT64_MAX) || frame_duration > static_cast(INT64_MAX)) + throw InvalidDataError(out_of_range); + int64_t start = checked_add_signed(ticks_to_nanoseconds(ticks, timestamp_scale, track_state.timestamp_scale), + static_cast(lace_offset), out_of_range); + frame.start = Timestamp{start}; + if (block_duration || frame_duration) + frame.end = Timestamp{checked_add_signed(start, static_cast(frame_duration), out_of_range)}; out.push_back(frame); cursor = checked_add(cursor, sizes[i], "Matroska frame exceeds its block"); } @@ -1021,7 +1003,6 @@ class Demuxer::Impl { // Our own errors are agi::Exceptions, so this is only libebml rejecting the file throw InvalidDataError(e.what()); } - FindFirstTimestamp(); } std::vector const& Tracks() const { return tracks; } @@ -1034,6 +1015,14 @@ class Demuxer::Impl { return duration; } + std::optional StartTime() { + if (!start_time_computed) { + ComputeStartTime(); + start_time_computed = true; + } + return start_time; + } + void Select(TrackId id) { auto track = std::find_if(tracks.begin(), tracks.end(), [&](SubtitleTrack const& track) { return track.id == id; @@ -1107,6 +1096,10 @@ std::optional Demuxer::Duration() { return impl->Duration(); } +std::optional Demuxer::StartTime() { + return impl->StartTime(); +} + void Demuxer::SelectTrack(TrackId track) { impl->Select(track); } diff --git a/src/matroska.h b/src/matroska.h index f5d039491a..83b8b278cb 100644 --- a/src/matroska.h +++ b/src/matroska.h @@ -81,7 +81,8 @@ struct Attachment { struct SubtitlePacket { TrackId track; - std::optional start; + /// Timestamps as stored in the file, without subtracting StartTime() + Timestamp start; std::optional end; std::vector data; }; @@ -134,6 +135,10 @@ class Demuxer final { /// gives one. If the header lacks one, the first call has to find the last /// cluster, which reads from throughout the file and can be cancelled. std::optional Duration(); + /// Earliest audio or video timestamp, which players treat as the start of + /// the file, or nullopt if the file has no audio or video. The first call + /// reads up to the first cluster with audio or video and can be cancelled. + std::optional StartTime(); /// Select one subtitle track and rewind packet iteration to its beginning. void SelectTrack(TrackId track); diff --git a/src/mkv_wrap.cpp b/src/mkv_wrap.cpp index dd2dcfd311..48982d5cea 100644 --- a/src/mkv_wrap.cpp +++ b/src/mkv_wrap.cpp @@ -62,17 +62,22 @@ namespace mkv = agi::matroska; /// Limit on the total size of the subtitle data read from a file constexpr size_t max_total_subtitle_bytes = 64 * 1024 * 1024; -agi::Time to_ass_time(std::optional const& time) { - if (!time) return 0; - if (time->nanoseconds < 0) - throw MatroskaException("Negative Matroska subtitle timestamp"); - auto milliseconds = time->nanoseconds / 1000000; +/// Get a time in milliseconds relative to origin, which may be negative +int64_t relative_ms(mkv::Timestamp time, int64_t origin) { + if ((origin < 0 && time.nanoseconds > INT64_MAX + origin) || (origin > 0 && time.nanoseconds < INT64_MIN + origin)) + throw MatroskaException("Matroska subtitle timestamp is out of range"); + return (time.nanoseconds - origin) / 1000000; +} + +agi::Time to_ass_time(int64_t milliseconds) { if (milliseconds > std::numeric_limits::max()) throw MatroskaException("Matroska subtitle timestamp is out of range"); return static_cast(milliseconds); } -void read_subtitles(agi::ProgressSink *ps, mkv::Demuxer& demuxer, bool srt, int64_t total_time, AssParser *parser) { +/// Read the selected track's events. origin is the time in the file which +/// becomes time zero. +void read_subtitles(agi::ProgressSink *ps, mkv::Demuxer& demuxer, bool srt, int64_t origin, int64_t total_time, AssParser *parser) { std::vector> subList; size_t total_bytes = 0; SrtTagParser srtParser; @@ -85,8 +90,13 @@ void read_subtitles(agi::ProgressSink *ps, mkv::Demuxer& demuxer, bool srt, int6 throw MatroskaException(agi::format("Matroska subtitle data exceeds the %d MiB limit", max_total_subtitle_bytes / 1024 / 1024)); total_bytes += packet->data.size(); - agi::Time subStart = to_ass_time(packet->start); - agi::Time subEnd = packet->end ? to_ass_time(packet->end) : subStart; + int64_t start = relative_ms(packet->start, origin); + int64_t end = packet->end ? relative_ms(*packet->end, origin) : start; + // Lines entirely before the start of the file can't be shown + if (end < 0 || (end == 0 && start < 0)) + continue; + agi::Time subStart = to_ass_time(std::max(start, 0)); + agi::Time subEnd = to_ass_time(end); std::string_view readBuf(reinterpret_cast(packet->data.data()), packet->data.size()); // Process SSA/ASS @@ -220,10 +230,20 @@ void MatroskaWrapper::GetSubtitles(agi::fs::path const& filename, AssFile *targe parser.AddLine("[Events]"); run_with_progress(_("Reading subtitles from Matroska file."), active_sink, [&](agi::ProgressSink *ps) { - // May need to find the last cluster, so do this inside the progress dialog + // Players such as mpv and MPC-HC treat the earliest audio or video + // timestamp as the start of the file, so make that time zero. Files + // with only subtitles are never played by themselves, so keep their + // timestamps as-is. Aegisub's video timeline instead makes the first + // video frame time zero (TypesettingTools/Aegisub#21), so in files + // where video starts after audio, imported lines currently appear late + // by that delay, but are exported with their original timing. + // + // These may need to read through the file, so are done in the progress dialog. + auto file_start = demuxer->StartTime(); + int64_t origin = file_start ? file_start->nanoseconds : 0; auto duration = demuxer->Duration(); int64_t totalTime = duration ? duration->nanoseconds / 1000000 : 0; - read_subtitles(ps, *demuxer, srt, totalTime, &parser); + read_subtitles(ps, *demuxer, srt, origin, totalTime, &parser); }); target->swap(imported); diff --git a/tests/matroska/fixtures/audio-only-opus.behavior b/tests/matroska/fixtures/audio-only-opus.behavior index b2ce40aa5a..f5235479c1 100644 --- a/tests/matroska/fixtures/audio-only-opus.behavior +++ b/tests/matroska/fixtures/audio-only-opus.behavior @@ -1 +1,2 @@ duration 256504416 +start 0 diff --git a/tests/matroska/fixtures/compressed-zlib.behavior b/tests/matroska/fixtures/compressed-zlib.behavior index 897b4cfd6c..6ba2447a4e 100644 --- a/tests/matroska/fixtures/compressed-zlib.behavior +++ b/tests/matroska/fixtures/compressed-zlib.behavior @@ -1,3 +1,4 @@ duration 2000000000 +start - track 0 2756463499428226085 S_TEXT/UTF8 und 0 1 1 -packet 0 0 2000000000 24 da1dfbf2ee283ed2 +packet 0 500000000 2500000000 24 da1dfbf2ee283ed2 diff --git a/tests/matroska/fixtures/encodings.behavior b/tests/matroska/fixtures/encodings.behavior index 91aaa3d953..2ae5c0d284 100644 --- a/tests/matroska/fixtures/encodings.behavior +++ b/tests/matroska/fixtures/encodings.behavior @@ -1,4 +1,5 @@ duration 1500000000 +start - track 0 1 S_TEXT/UTF8 eng 0 1 1 track 1 2 S_TEXT/UTF8 eng 0 1 1 packet 0 0 1500000000 5 5a0d15131ec7a1 diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index 30ea74ec78..805e60033e 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -63,8 +63,8 @@ def attached(name,data=None): cluster=element('1f43b675',uint_element('e7',0)+element('a3',b'\x81\x00\x00\x80hi')) (r/'repeated-tracks.mkv').write_bytes(header+element('18538067',info+tracks+tracks+attachments+cluster)) -# A cluster timestamp which overflows int64 when the block's relative timestamp -# is added. Only the offset from the first block matters, so this is fine. +# A block timestamp which overflows int64 once the block's relative timestamp +# is added to the cluster's entry=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) cluster=element('1f43b675',uint_element('e7',(1<<63)-1)+element('a3',b'\x81\x00\x01\x80x')) (r/'timestamp-overflow.mkv').write_bytes(header+element('18538067',info+element('1654ae6b',entry)+cluster)) @@ -88,3 +88,11 @@ def attached(name,data=None): void_info=element('1549a966',crc+uint_element('2ad7b1',500000)) clusters=element('1f43b675',uint_element('e7',0)+element('a3',b'\x81\x00\x00\x80a'))+element('1f43b675',uint_element('e7',4)+element('a3',b'\x81\x00\x00\x80b')) (r/'void-crc.mkv').write_bytes(header+element('18538067',element('ec',bytes(6))+void_info+element('ec',bytes(6))+element('1654ae6b',crc+entry)+clusters)) + +# The start of the file is the earliest audio or video timestamp, ignoring a +# subtitle line before it +audio=element('ae',uint_element('d7',2)+uint_element('73c5',2)+uint_element('83',2)+element('86',b'A_TEST')) +subtitle=element('ae',uint_element('d7',3)+uint_element('73c5',3)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) +early=element('a0',element('a1',b'\x83\xfe\x0c\x00early')+uint_element('9b',1000)) +cluster=element('1f43b675',uint_element('e7',1000)+early+element('a3',b'\x82\x00\x00\x80a')+element('a3',b'\x81\x00\x53\x80v')) +(r/'start-time.mkv').write_bytes(header+element('18538067',info+element('1654ae6b',video+audio+subtitle)+cluster)) diff --git a/tests/matroska/fixtures/repeated-tracks.behavior b/tests/matroska/fixtures/repeated-tracks.behavior index ff66504c1a..68fccb67db 100644 --- a/tests/matroska/fixtures/repeated-tracks.behavior +++ b/tests/matroska/fixtures/repeated-tracks.behavior @@ -1,4 +1,5 @@ duration - +start - track 0 7 S_TEXT/UTF8 eng 12 1 1 attachment a.txt text/plain 3 e172ef510dc287ec attachment b.txt text/plain 2 9ba86500c657e843 diff --git a/tests/matroska/fixtures/start-time.behavior b/tests/matroska/fixtures/start-time.behavior new file mode 100644 index 0000000000..ef942f83c3 --- /dev/null +++ b/tests/matroska/fixtures/start-time.behavior @@ -0,0 +1,4 @@ +duration 1500000000 +start 1000000000 +track 2 3 S_TEXT/UTF8 eng 0 1 1 +packet 2 500000000 1500000000 5 ae5b9f31c09c6b2a diff --git a/tests/matroska/fixtures/start-time.mkv b/tests/matroska/fixtures/start-time.mkv new file mode 100644 index 0000000000000000000000000000000000000000..edeb852a295bd7a2bbe4fd74c4df0005cf136df2 GIT binary patch literal 167 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@{!7$zWm>z| z^^MK^P7Y!r>#`57n{>UAvG`~sV{;>8TU%Ish-+{NNQ?<6#?;)%)Yj$*5u1L!kr^n) z+}tSG)*TEKiwMyV4RN!OcivXI_<0laiv|4)JDdOUFr+3Htrack.value << '\t' << describe_time(packet->start) << '\t' + out << "packet\t" << packet->track.value << '\t' << packet->start.nanoseconds << '\t' << describe_time(packet->end) << '\t' << packet->data.size() << '\t' << std::hex << fnv1a(packet->data) << std::dec << '\n'; } @@ -147,16 +148,6 @@ TEST(Matroska, DecompressionLimitAndFailedPacketConsumption) { EXPECT_NO_THROW((void)demuxer.ReadPacket()); } -TEST(Matroska, PacketsHaveCheckedOptionalTimestamps) { - Demuxer demuxer(OpenFile(fixture("compressed-zlib.mkv"))); - ASSERT_FALSE(demuxer.SubtitleTracks().empty()); - demuxer.SelectTrack(demuxer.SubtitleTracks()[0].id); - auto packet = demuxer.ReadPacket(); - ASSERT_TRUE(packet); - ASSERT_TRUE(packet->start); - EXPECT_GE(packet->start->nanoseconds, 0); -} - // Each fixture's .behavior file records the expected describe() output. When a // fixture is added or changed, the failure message contains the new output. TEST(Matroska, FixtureBehavior) { From 49da71e00ea5bea04b6955c4e8ee889ba6fa9d8f Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 14:38:56 -0700 Subject: [PATCH 24/28] Read partially downloaded Matroska files and late segment info - A cluster cut off by the end of the file was rejected entirely, which failed to open the file if it was the first cluster and otherwise dropped its subtitles. Read the blocks in it which are complete. - Walk the whole file when Info, not only Tracks, wasn't found before the first cluster or via a SeekHead, as otherwise timestamps are scaled with the default TimestampScale. Co-Authored-By: Claude Opus 5.5 --- src/matroska.cpp | 47 +++++++++++++++++---- tests/matroska/fixtures/generate.py | 11 +++++ tests/matroska/fixtures/late-info.behavior | 5 +++ tests/matroska/fixtures/late-info.mkv | Bin 0 -> 117 bytes tests/matroska/fixtures/truncated.behavior | 5 +++ tests/matroska/fixtures/truncated.mkv | Bin 0 -> 127 bytes 6 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 tests/matroska/fixtures/late-info.behavior create mode 100644 tests/matroska/fixtures/late-info.mkv create mode 100644 tests/matroska/fixtures/truncated.behavior create mode 100644 tests/matroska/fixtures/truncated.mkv diff --git a/src/matroska.cpp b/src/matroska.cpp index afedf67897..ffa874885d 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -351,6 +351,8 @@ struct Frame { struct Location { uint64_t position = 0; uint64_t size = 0; + /// The element was cut off by the end of the file + bool truncated = false; }; struct Element { @@ -361,6 +363,7 @@ struct Element { uint64_t size = 0; uint64_t end = 0; bool unknown_size = false; + bool truncated = false; }; } // namespace @@ -391,6 +394,7 @@ class Demuxer::Impl { std::set parsed_elements; /// Positions of the metadata elements listed by SeekHeads std::vector seek_targets; + bool seen_info = false; bool seen_tracks = false; std::vector all_tracks; std::unordered_map track_by_number; @@ -466,7 +470,9 @@ class Demuxer::Impl { return {value, length}; } - Element ReadElement(uint64_t position, uint64_t parent_end) { + /// Read an element's header. If allow_truncated is set, an element which + /// extends past parent_end is cut off there rather than rejected. + Element ReadElement(uint64_t position, uint64_t parent_end, bool allow_truncated = false) { uint8_t first = ReadByte(position); unsigned id_length = 1; for (uint8_t mask = 0x80; id_length <= 4 && !(first & mask); mask >>= 1) @@ -483,9 +489,15 @@ class Demuxer::Impl { bool unknown_size = size == (uint64_t{1} << (7 * size_length)) - 1; if (unknown_size) size = parent_end - data; - if (data > parent_end || size > parent_end - data) + if (data > parent_end) throw InvalidDataError("Matroska element exceeds its parent"); - return {id, position, data, size, data + size, unknown_size}; + bool truncated = size > parent_end - data; + if (truncated) { + if (!allow_truncated) + throw InvalidDataError("Matroska element exceeds its parent"); + size = parent_end - data; + } + return {id, position, data, size, data + size, unknown_size, truncated}; } uint64_t ReadUInt(Element const& element) { @@ -602,7 +614,11 @@ class Demuxer::Impl { /// Read the top-level element at position, finding where it ends if its size is unknown Element TopLevelElement(uint64_t position) { - auto element = ReadElement(position, segment_end); + // The last cluster of a partially downloaded file is usually cut off, + // and the blocks in it which are complete can still be read + auto element = ReadElement(position, segment_end, true); + if (element.truncated && element.id != id_cluster) + throw TruncatedError("Unexpected end of Matroska input"); if (element.unknown_size) { // Only clusters are written with unknown sizes in practice. They // end where the next top-level element starts. @@ -669,6 +685,7 @@ class Demuxer::Impl { if (element.id == id_info) { auto info = ReadMaster(stream, element); ParseInfo(static_cast(*info)); + seen_info = true; } else if (element.id == id_tracks) { auto track_list = ReadMaster(stream, element); @@ -703,7 +720,7 @@ class Demuxer::Impl { } cluster_scan_position = element.end; if (element.id == id_cluster) { - clusters.push_back({element.data, element.size}); + clusters.push_back({element.data, element.size, element.truncated}); return true; } if (stream) @@ -767,8 +784,8 @@ class Demuxer::Impl { catch (TruncatedError const&) { } } - // Otherwise the only way to find the tracks is to walk the entire file - if (!seen_tracks) { + // Otherwise the only way to find them is to walk the entire file + if (!seen_info || !seen_tracks) { while (ScanNextCluster(&stream)) ; } @@ -946,7 +963,21 @@ class Demuxer::Impl { for (uint64_t position = cluster.position; position < end;) { // Clusters have no size limit, so check per element rather than per cluster CheckCancelled(); - auto element = ReadElement(position, end); + Element element; + try { + element = ReadElement(position, end); + } + catch (InvalidDataError const&) { + // Everything after the first incomplete element is missing + if (cluster.truncated) + break; + throw; + } + catch (TruncatedError const&) { + if (cluster.truncated) + break; + throw; + } position = element.end; if (element.id == id_cluster_timestamp) cluster_time = ReadUInt(element); diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index 805e60033e..460827fab1 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -96,3 +96,14 @@ def attached(name,data=None): early=element('a0',element('a1',b'\x83\xfe\x0c\x00early')+uint_element('9b',1000)) cluster=element('1f43b675',uint_element('e7',1000)+early+element('a3',b'\x82\x00\x00\x80a')+element('a3',b'\x81\x00\x53\x80v')) (r/'start-time.mkv').write_bytes(header+element('18538067',info+element('1654ae6b',video+audio+subtitle)+cluster)) + +# A partially downloaded file, whose last cluster ends partway through a block +entry=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',17)+element('86',b'S_TEXT/UTF8')) +tracks=element('1654ae6b',entry) +cluster=element('1f43b675',uint_element('e7',0)+element('a3',b'\x81\x00\x00\x80one')+element('a3',b'\x81\x00\x0a\x80two') + +element('a3',b'\x81\x00\x14\x80three-is-cut-off')) +(r/'truncated.mkv').write_bytes((header+element('18538067',info+tracks+cluster))[:-8]) + +# Info after the clusters with no SeekHead to find it +clusters=element('1f43b675',uint_element('e7',0)+element('a3',b'\x81\x00\x00\x80a'))+element('1f43b675',uint_element('e7',4)+element('a3',b'\x81\x00\x00\x80b')) +(r/'late-info.mkv').write_bytes(header+element('18538067',tracks+clusters+element('1549a966',uint_element('2ad7b1',500000)))) diff --git a/tests/matroska/fixtures/late-info.behavior b/tests/matroska/fixtures/late-info.behavior new file mode 100644 index 0000000000..6f1ada46db --- /dev/null +++ b/tests/matroska/fixtures/late-info.behavior @@ -0,0 +1,5 @@ +duration - +start - +track 0 1 S_TEXT/UTF8 eng 0 1 1 +packet 0 0 - 1 44bd8ad473cd9906 +packet 0 2000000 - 1 44bd89d473cd9753 diff --git a/tests/matroska/fixtures/late-info.mkv b/tests/matroska/fixtures/late-info.mkv new file mode 100644 index 0000000000000000000000000000000000000000..123e561898208e2aea923cf47fbbc563281d261d GIT binary patch literal 117 zcmb1gy}x*|Q(GgW({~{L)X3uWxsk)EsUtVBq$s~QJJG2fDAd}>BoW+@enKo{UG~g% z)2}x&79VY7Y;F{6>kf_&ag7Mk4-Ij%kaylz+V#AVVR36C14Bb1l*BoW+@eoNGIWm>z| z^^MK^P7Y!r>#}FAn|{5KvG`~sV{@ZmTX%4Lh-*ZMerSlBg}n2&($&u!85XxUGB7mc c=cR%eTn#1V`HQDCGKe&kWE7>Q>Sh)L00I~@9RL6T literal 0 HcmV?d00001 From 56780aaa3fa7f5d4f59c0ac0f9e6c1841c208ee4 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 14:46:38 -0700 Subject: [PATCH 25/28] Bound Matroska cluster parsing memory and fix libebml fallback - Read clusters a block at a time rather than collecting every frame in a cluster before returning the first. Clusters have no size limit and lacing turns each block into up to 256 frames, so a crafted file could use an unbounded amount of memory before any packet limit was checked. - Choose libebml based on which libmatroska is used. With an older system libebml and no system libmatroska, the bundled libmatroska requires a newer libebml than the one already found, which failed to configure. Co-Authored-By: Claude Opus 5.5 --- meson.build | 15 +++++- src/matroska.cpp | 137 +++++++++++++++++++++++++++++------------------ 2 files changed, 97 insertions(+), 55 deletions(-) diff --git a/meson.build b/meson.build index 7873890174..b0a0d25679 100644 --- a/meson.build +++ b/meson.build @@ -179,8 +179,19 @@ endif zlib_dep = dependency('zlib') deps += zlib_dep -libebml = dependency('libebml', version: '>=1.4.0', fallback: ['libebml', 'libebml_dep']) -libmatroska = dependency('libmatroska', version: '>=1.6.0', fallback: ['libmatroska', 'libmatroska_dep']) +# A system libmatroska needs the libebml it was built against, while the +# bundled one needs a newer libebml than the minimum Aegisub supports, so +# choose libebml based on which libmatroska is used. +libmatroska = dependency('', required: false) +if not (force_all_fallbacks or force_fallback_for.contains('libmatroska')) + libmatroska = dependency('libmatroska', version: '>=1.6.0', required: false, allow_fallback: false) +endif +if libmatroska.found() + libebml = dependency('libebml', version: '>=1.4.0', fallback: ['libebml', 'libebml_dep']) +else + libebml = dependency('libebml', version: '>=1.4.4', fallback: ['libebml', 'libebml_dep']) + libmatroska = dependency('libmatroska', version: '>=1.6.0', fallback: ['libmatroska', 'libmatroska_dep']) +endif use_bundled_wx = force_all_fallbacks or force_fallback_for.contains('wxWidgets') if not use_bundled_wx diff --git a/src/matroska.cpp b/src/matroska.cpp index ffa874885d..b00f340efe 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -355,6 +355,15 @@ struct Location { bool truncated = false; }; +/// Position within a cluster, for reading it one block at a time +struct ClusterCursor { + Location cluster; + uint64_t position; + uint64_t cluster_time = 0; + + explicit ClusterCursor(Location const& cluster) : cluster(cluster), position(cluster.position) { } +}; + struct Element { uint64_t id = 0; /// Position of the element's ID @@ -412,7 +421,9 @@ class Demuxer::Impl { /// Index into all_tracks of the selected track std::optional selected; size_t next_cluster = 0; - /// Frames of the selected track in the most recently parsed cluster + /// The cluster currently being read, if any + std::optional cluster_cursor; + /// Frames of the selected track in the most recently read block std::vector frames; size_t next_frame = 0; @@ -806,21 +817,14 @@ class Demuxer::Impl { return; for (size_t i = 0; i < clusters.size() || ScanNextCluster(); ++i) { CheckCancelled(); - std::vector cluster_frames; try { - cluster_frames = ParseCluster(clusters[i], std::nullopt); - } - catch (InvalidDataError const&) { - continue; - } - catch (TruncatedError const&) { - continue; - } - - for (auto const& frame : cluster_frames) { - if (IsAudioOrVideo(frame.track) && (!start_time || frame.start.nanoseconds < start_time->nanoseconds)) - start_time = frame.start; + ForEachFrame(clusters[i], [&](Frame const& frame) { + if (IsAudioOrVideo(frame.track) && (!start_time || frame.start.nanoseconds < start_time->nanoseconds)) + start_time = frame.start; + }); } + catch (InvalidDataError const&) { } + catch (TruncatedError const&) { } if (start_time) return; } @@ -834,22 +838,15 @@ class Demuxer::Impl { ; for (auto cluster = clusters.rbegin(); cluster != clusters.rend(); ++cluster) { CheckCancelled(); - std::vector cluster_frames; - try { - cluster_frames = ParseCluster(*cluster, std::nullopt); - } - catch (InvalidDataError const&) { - continue; - } - catch (TruncatedError const&) { - continue; - } - int64_t last_end = 0; - for (auto const& frame : cluster_frames) { - if (frame.end) - last_end = std::max(last_end, frame.end->nanoseconds); + try { + ForEachFrame(*cluster, [&](Frame const& frame) { + if (frame.end) + last_end = std::max(last_end, frame.end->nanoseconds); + }); } + catch (InvalidDataError const&) { } + catch (TruncatedError const&) { } if (last_end > 0) { duration = Timestamp{last_end}; return; @@ -956,33 +953,39 @@ class Demuxer::Impl { } } - std::vector ParseCluster(Location const& cluster, std::optional only_track) { - std::vector out; - uint64_t cluster_time = 0; - uint64_t end = checked_add(cluster.position, cluster.size, "Matroska cluster is out of range"); - for (uint64_t position = cluster.position; position < end;) { - // Clusters have no size limit, so check per element rather than per cluster + /// Read the cluster's elements up to and including its next block, + /// appending that block's frames for only_track (or all tracks) to out. + /// Returns false at the end of the cluster. Going a block at a time keeps + /// memory use bounded, as clusters have no size limit. + bool ReadNextBlock(ClusterCursor& cursor, std::optional only_track, std::vector& out) { + uint64_t end = cursor.cluster.position + cursor.cluster.size; + while (cursor.position < end) { CheckCancelled(); Element element; try { - element = ReadElement(position, end); + element = ReadElement(cursor.position, end); } catch (InvalidDataError const&) { // Everything after the first incomplete element is missing - if (cluster.truncated) - break; - throw; + if (!cursor.cluster.truncated) + throw; + cursor.position = end; + return false; } catch (TruncatedError const&) { - if (cluster.truncated) - break; - throw; + if (!cursor.cluster.truncated) + throw; + cursor.position = end; + return false; } - position = element.end; + cursor.position = element.end; + if (element.id == id_cluster_timestamp) - cluster_time = ReadUInt(element); - else if (element.id == id_simple_block) - ParseBlock(element, cluster_time, std::nullopt, only_track, out); + cursor.cluster_time = ReadUInt(element); + else if (element.id == id_simple_block) { + ParseBlock(element, cursor.cluster_time, std::nullopt, only_track, out); + return true; + } else if (element.id == id_block_group) { std::optional block; std::optional block_duration; @@ -994,11 +997,25 @@ class Demuxer::Impl { else if (child.id == id_block_duration) block_duration = ReadUInt(child); } - if (block) - ParseBlock(*block, cluster_time, block_duration, only_track, out); + if (block) { + ParseBlock(*block, cursor.cluster_time, block_duration, only_track, out); + return true; + } } } - return out; + return false; + } + + /// Call fn with each frame of every track in a cluster + template + void ForEachFrame(Location const& cluster, Fn&& fn) { + ClusterCursor cursor(cluster); + std::vector block_frames; + while (ReadNextBlock(cursor, std::nullopt, block_frames)) { + for (auto const& frame : block_frames) + fn(frame); + block_frames.clear(); + } } std::vector Decode(TrackState const& track, std::vector data) const { @@ -1069,6 +1086,7 @@ class Demuxer::Impl { CheckCancelled(); selected = id.value; next_cluster = 0; + cluster_cursor.reset(); frames.clear(); next_frame = 0; } @@ -1079,13 +1097,26 @@ class Demuxer::Impl { CheckCancelled(); while (next_frame == frames.size()) { - if (next_cluster == clusters.size() && !ScanNextCluster()) - return std::nullopt; - // Clear first so that a cluster which fails to parse is skipped on retry frames.clear(); next_frame = 0; - frames = ParseCluster(clusters[next_cluster++], selected); - CheckCancelled(); + if (!cluster_cursor) { + if (next_cluster == clusters.size() && !ScanNextCluster()) + return std::nullopt; + cluster_cursor.emplace(clusters[next_cluster++]); + } + bool more; + try { + more = ReadNextBlock(*cluster_cursor, selected, frames); + } + catch (...) { + // Skip the rest of a cluster which fails to parse, so that + // retrying moves on to the next one + cluster_cursor.reset(); + frames.clear(); + throw; + } + if (!more) + cluster_cursor.reset(); } auto const& frame = frames[next_frame++]; From 196fde42dbbba20d23b178cb83263412632085ee Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 14:54:34 -0700 Subject: [PATCH 26/28] Apply TrackTimecodeScale only to block-relative timestamps RFC 9559 section 11.2 gives a block's timestamp as (Cluster\Timestamp + block timestamp * TrackTimestampScale) * TimestampScale, so the track scale doesn't apply to the cluster's timestamp. The previous commit wrongly scaled the sum. Co-Authored-By: Claude Opus 5.5 --- src/matroska.cpp | 26 ++++++++++++++++--------- tests/matroska/fixtures/generate.py | 7 ++++--- tests/matroska/fixtures/timing.behavior | 4 ++-- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/matroska.cpp b/src/matroska.cpp index b00f340efe..d2b7e6c45b 100644 --- a/src/matroska.cpp +++ b/src/matroska.cpp @@ -230,11 +230,22 @@ uint64_t checked_multiply(uint64_t lhs, uint64_t rhs, char const *message) { return lhs * rhs; } -/// Convert a timestamp in ticks, which may be slightly negative, to nanoseconds -int64_t ticks_to_nanoseconds(int64_t ticks, uint64_t timestamp_scale, double track_scale) { +/// Get a block's timestamp in nanoseconds, which is +/// (cluster timestamp + block timestamp * TrackTimestampScale) * TimestampScale +/// as per RFC 9559 section 11.2. The result may be slightly negative. +int64_t block_timestamp(uint64_t cluster_time, int16_t relative, uint64_t timestamp_scale, double track_scale) { constexpr char const *out_of_range = "Matroska timestamp is out of range"; + if (track_scale != 1.0) { + double nanoseconds = (static_cast(cluster_time) + relative * track_scale) * static_cast(timestamp_scale); + if (!(nanoseconds > -9.2e18 && nanoseconds < 9.2e18)) + throw InvalidDataError(out_of_range); + return static_cast(nanoseconds); + } + + if (cluster_time > static_cast(INT64_MAX)) + throw InvalidDataError(out_of_range); + int64_t ticks = checked_add_signed(static_cast(cluster_time), relative, out_of_range); uint64_t magnitude = checked_multiply(ticks < 0 ? static_cast(-ticks) : static_cast(ticks), timestamp_scale, out_of_range); - magnitude = checked_scale(magnitude, track_scale, out_of_range); if (magnitude > static_cast(INT64_MAX)) throw InvalidDataError(out_of_range); return ticks < 0 ? -static_cast(magnitude) : static_cast(magnitude); @@ -333,7 +344,7 @@ struct TrackState { uint64_t uid = 0; uint64_t type = 0; uint64_t default_duration = 0; - /// Deprecated per-track multiplier applied to block timestamps and durations + /// Deprecated multiplier for block-relative timestamps and block durations double timestamp_scale = 1.0; Compression compression = Compression::none; std::vector stripped_header; @@ -926,10 +937,8 @@ class Demuxer::Impl { sizes[0] = end - cursor; constexpr char const *out_of_range = "Matroska timestamp is out of range"; - if (cluster_time > static_cast(INT64_MAX)) - throw InvalidDataError(out_of_range); - int64_t ticks = checked_add_signed(static_cast(cluster_time), relative, out_of_range); auto const& track_state = all_tracks[track]; + int64_t block_start = block_timestamp(cluster_time, relative, timestamp_scale, track_state.timestamp_scale); uint64_t frame_duration = block_duration ? checked_scale(checked_multiply(*block_duration, timestamp_scale, "Matroska block duration is out of range"), track_state.timestamp_scale, "Matroska block duration is out of range") / count @@ -943,8 +952,7 @@ class Demuxer::Impl { uint64_t lace_offset = checked_multiply(i, frame_duration, out_of_range); if (lace_offset > static_cast(INT64_MAX) || frame_duration > static_cast(INT64_MAX)) throw InvalidDataError(out_of_range); - int64_t start = checked_add_signed(ticks_to_nanoseconds(ticks, timestamp_scale, track_state.timestamp_scale), - static_cast(lace_offset), out_of_range); + int64_t start = checked_add_signed(block_start, static_cast(lace_offset), out_of_range); frame.start = Timestamp{start}; if (block_duration || frame_duration) frame.end = Timestamp{checked_add_signed(start, static_cast(frame_duration), out_of_range)}; diff --git a/tests/matroska/fixtures/generate.py b/tests/matroska/fixtures/generate.py index 460827fab1..1700ad9b4c 100755 --- a/tests/matroska/fixtures/generate.py +++ b/tests/matroska/fixtures/generate.py @@ -69,9 +69,10 @@ def attached(name,data=None): cluster=element('1f43b675',uint_element('e7',(1<<63)-1)+element('a3',b'\x81\x00\x01\x80x')) (r/'timestamp-overflow.mkv').write_bytes(header+element('18538067',info+element('1654ae6b',entry)+cluster)) -# Timing: a Void before the Segment, a first block at 10s which all block -# timestamps are made relative to, and a subtitle track with a (deprecated) -# TrackTimecodeScale of 2 +# Timing: a Void before the Segment, a file which starts at 10s, and a +# subtitle track with a (deprecated) +# TrackTimecodeScale of 2, which applies to the block's relative timestamp and +# duration but not the cluster's timestamp import struct video=element('ae',uint_element('d7',1)+uint_element('73c5',1)+uint_element('83',1)+element('86',b'V_TEST')) scaled=element('ae',uint_element('d7',2)+uint_element('73c5',2)+uint_element('83',17)+element('86',b'S_TEXT/UTF8') diff --git a/tests/matroska/fixtures/timing.behavior b/tests/matroska/fixtures/timing.behavior index e2a10821aa..5208e50dc4 100644 --- a/tests/matroska/fixtures/timing.behavior +++ b/tests/matroska/fixtures/timing.behavior @@ -1,4 +1,4 @@ -duration 23000000000 +duration 13000000000 start 10000000000 track 1 2 S_TEXT/UTF8 eng 0 1 1 -packet 1 21000000000 23000000000 3 58db605150dd5fa7 +packet 1 11000000000 13000000000 3 58db605150dd5fa7 From 6d2e8afd416a694630ac153073ac8020eb0a0eda Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 19:03:16 -0700 Subject: [PATCH 27/28] Require the libebml and libmatroska versions that are bundled Rather than choosing libebml based on which libmatroska was found, raise the minimums to the bundled versions so a system libebml is always new enough for the bundled libmatroska. Older distributions such as Ubuntu 22.04 now use the bundled copies of both. Co-Authored-By: Claude Opus 5.5 --- meson.build | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/meson.build b/meson.build index b0a0d25679..528dd5906e 100644 --- a/meson.build +++ b/meson.build @@ -179,19 +179,9 @@ endif zlib_dep = dependency('zlib') deps += zlib_dep -# A system libmatroska needs the libebml it was built against, while the -# bundled one needs a newer libebml than the minimum Aegisub supports, so -# choose libebml based on which libmatroska is used. -libmatroska = dependency('', required: false) -if not (force_all_fallbacks or force_fallback_for.contains('libmatroska')) - libmatroska = dependency('libmatroska', version: '>=1.6.0', required: false, allow_fallback: false) -endif -if libmatroska.found() - libebml = dependency('libebml', version: '>=1.4.0', fallback: ['libebml', 'libebml_dep']) -else - libebml = dependency('libebml', version: '>=1.4.4', fallback: ['libebml', 'libebml_dep']) - libmatroska = dependency('libmatroska', version: '>=1.6.0', fallback: ['libmatroska', 'libmatroska_dep']) -endif +# The bundled libmatroska needs libebml >= 1.4.4 +libebml = dependency('libebml', version: '>=1.4.4', fallback: ['libebml', 'libebml_dep']) +libmatroska = dependency('libmatroska', version: '>=1.7.1', fallback: ['libmatroska', 'libmatroska_dep']) use_bundled_wx = force_all_fallbacks or force_fallback_for.contains('wxWidgets') if not use_bundled_wx From 4d10c1400af123c7d2d5547a3ff10c66c9c57c39 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 19:05:02 -0700 Subject: [PATCH 28/28] Add the Matroska library to deps It was kept separate so the matroska-behavior executable could link it without everything else, but that has since been folded into the gtest suite. This also restores the executable's original dependencies line, which had lost a space of indentation. Co-Authored-By: Claude Opus 5.5 --- meson.build | 2 +- src/meson.build | 2 +- tests/meson.build | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/meson.build b/meson.build index 528dd5906e..0edf405cad 100644 --- a/meson.build +++ b/meson.build @@ -577,5 +577,5 @@ aegisub = executable('aegisub', aegisub_src, version_h, resrc, aegisub_order_dep c_pch: aegisub_c_pch, install: true, install_dir: bindir, - dependencies: [deps, matroska_dep], + dependencies: deps, win_subsystem: 'windows') diff --git a/src/meson.build b/src/meson.build index a3d362bce7..74154d0326 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,7 +11,7 @@ matroska_lib = static_library('aegisub-matroska', override_options: ['unity=off'], ) -matroska_dep = declare_dependency( +deps += declare_dependency( link_with: matroska_lib, include_directories: aegisub_src_inc, dependencies: [zlib_dep, libebml, libmatroska], diff --git a/tests/meson.build b/tests/meson.build index 38d945b2f7..da7c381a68 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -35,8 +35,8 @@ tests_src = [ 'tests/line_iterator.cpp', 'tests/line_wrap.cpp', 'tests/lua_lfs.cpp', - 'tests/mru.cpp', 'tests/matroska.cpp', + 'tests/mru.cpp', 'tests/option.cpp', 'tests/path.cpp', 'tests/signals.cpp', @@ -67,8 +67,8 @@ endif runner = executable( 'gtest-main', tests_src, - include_directories: [tests_inc, libaegisub_inc, aegisub_src_inc], - dependencies: [tests_deps, deps, matroska_dep], + include_directories: [tests_inc, libaegisub_inc], + dependencies: [tests_deps, deps], cpp_args: tests_args + conf_defines, link_with: libaegisub, )