-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add basic AVIF parsing support #193
Conversation
Since the fundamentals of HEIF, upon which AVIF is based, are quite different from video, this adds a new context type, which currently only contains the data for the "primary item", typically an av01 coded image frame. Extracting the primary item requires support for several new box types which specify the location type of various data and metadata. Various features are not yet fully supported, but should provide informative errors. Since the specification does not make many guarantees about the ordering of boxes, there is some additional logic complexity in an attempt to avoid copying buffers when possible. Also, a set of AVIF sample images (see the ReadMe.txt for licensing) is added to exercise the new parsing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I'm still working on fully reviewing this, but here are my comments and questions so far:
- Would it make sense to exclude mp4parse/tests/avif and use a git submodule of https://github.com/AOMediaCodec/av1-avif instead?
- If not, remove .DS_Store from the import please
- New C API needs a new fuzzer added in mp4parse_capi/fuzz/fuzz_targets
- I'm not sure about read_avif_meta diverging from read_meta and generally splitting the parser implementation too much to support AVIF; we should be aiming to share as much of the code as makes sense
- I need to take a closer pass through the spec(s) to understand why we need the extent stuff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last comment missed my comments on fallible allocation.
The existing code uses vec_push
and allocate_read_buf
. It might be simple to adapt to using those, but if it's going to be cleaner to extend the fallible allocation code there may be existing code inside Gecko we could consider sharing.
mp4parse/src/lib.rs
Outdated
// Store any remaining data for potential later extraction | ||
if b.bytes_left() > 0 { | ||
let offset = b.offset(); | ||
let mut data = Vec::with_capacity(b.bytes_left().try_into()?); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use allocate_read_buf
and handle possible failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does allocate_read_buf
explicitly zero the memory as opposed to just allocating the additional capacity? For the single caller, it seems to incur an unnecessary cost.
I have other Vec::with_capacity
callers that aren't Vec<u8>
, so I'll need to make some change here. What's your preference?
- Modify
allocate_read_buf
to take a type parameter and not fill its capacity with zeroes - Leave
allocate_read_buf
as is, but add a new function likefn vec_with_capacity<T>(capacity: usize) -> std::result::Result<Vec<T>, ()> { #[cfg(feature = "mp4parse_fallible")] { let mut v = Vec::new(); FallibleVec::try_reserve(&mut v, capacity)?; Ok(v) } #[cfg(not(feature = "mp4parse_fallible"))] { Ok(Vec::with_capacity(capacity)) } }
- Something else
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The zeroing is due to #172
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh, that's subtle. I'm going to add a comment back to the issue for posterity, if you don't mind.
Do you have any preference regarding whether or not to add vec_with_capacity
versus modifying allocate_read_buf
to handle types other than u8
?
If I implement vec_with_capacity
as described above, I don't think we need to worry about the issue in #172 since it doesn't provide any access to potentially uninitialized memory the way the old version of allocate_read_buf
did. It gets around that by using Read::read_to_end
which takes a &mut Vec<u8>
buf
rather than Read::read
, which takes a &mut [u8]
buf
.
It occurs to me that we could eliminate the need for zeroing out the memory in allocate_read_buf
(and for allocate_read_buf
ifself) if we switched the code here from using read
to read_to_end
with take
like so:
if let Ok(mut buf) = allocate_read_buf(size) {
let r = src.read(&mut buf)?;
becomes
if let Ok(mut buf) = vec_with_capacity(size) {
let r = src.take(size.to_u64()).read_to_end(&mut buf)?;
The only caveat I can see is that if the Vec
passed to read_to_end
didn't already have sufficient capacity, it could end up doing infallible allocation. But that shouldn't be an issue as long as we use vec_with_capacity
.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh, that's subtle. I'm going to add a comment back to the issue for posterity, if you don't mind.
Good idea, thanks.
Do you have any preference regarding whether or not to add
vec_with_capacity
versus modifyingallocate_read_buf
to handle types other thanu8
?
I think we only need one, so making it handle other simple types makes sense. I don't mind which one we keep but vec_with_capacity is probably a better name.
If I implement
vec_with_capacity
as described above, I don't think we need to worry about the issue in #172 since it doesn't provide any access to potentially uninitialized memory the way the old version ofallocate_read_buf
did. It gets around that by usingRead::read_to_end
which takes a&mut Vec<u8>
buf
rather thanRead::read
, which takes a&mut [u8]
buf
.
I think you still have the same issue, in that you can't pass a Vec with an uninitialized backing store to Read::read_to_end because the trait doesn't promise implementations won't read from the &mut Vec before writing to it. Although the docs only include a warning for Read::read, it's actually a restriction on the entire trait. Once Read::initializer is stable, we can use that to signal that our implementation of Read doesn't read from the buffers passed in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm not understanding #172 correctly. Can you let me know where my logic is flawed?
As I understand it, the unsoundness came from this line in the old version of allocate_read_buf
:
unsafe { buf.set_len(size); }
because it violated the safety requirement of Vec::set_len()
:
The elements at
old_len..new_len
must be initialized.
And as mentioned in Read::read
:
It is your responsibility to make sure that
buf
is initialized before callingread
. Callingread
with an uninitializedbuf
(of the kind one obtains viaMaybeUninit<T>
) is not safe, and can lead to undefined behavior.
So, because allocate_read_buf
called set_len
without initializing the additional capacity, it was uninitialized and the call to Read::read
was unsound. More generally, allocate_read_buf
returned a vector that was invalid because uninitialized memory was now accessible via the safe interface.
Hovever, I don't see how the vec_with_capacity
case has the same issue. There's no unsafe
code in its implementation, so how does it introduce any uninitialized memory into the memory of the vector's elements? The only potentially uninitialized memory should be in the internal buffer beyond the valid length of the vector.
Take this example code:
let mut reader: &[u8] = b"1234567890";
let mut buf = Vec::with_capacity(5);
reader.read(&mut buf).unwrap();
After the first call to reader.read
, buf
will be empy. Nothing will be read into it because despite having a capacity of 10, it has a length of 0. Contrast that with identical code except replacing read
with read_to_end
, and the result is that buf
contains all 10 elements.
I think the difference is easy to miss because in both cases we pass &mut buf
, but in the read
call, that is auto derefed to a &mut [u8]
slice, and in the read_to_end
case, it remains a &mut Vec<u8>
. The callee can't change the length of the prior, but it can change the length of the latter, which is why Read::read_to_end
's documentation describes it as "append[ing] data to buf
, whereas Read::read
's says it "pull[s] some bytes from this source into the specified buffer".
Unless I'm missing something, we have the opportunity to avoid the cost of zeroing out the buffer (as with the earlier version of allocate_read_buf
), but eliminating the potential for unsoundness. Does that seem worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confusing things by thinking only about the allocate_read_buf implementation. I agree with your comment (thanks for the detailed reasoning!), so replacing allocate_read_buf + read with vec_with_capacity + read_to_end allows us to eliminate the extra initialization safely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up taking a slightly different approach from what I suggested in #193 (comment) since I realized I could simplify things by making our new read_to_end
with fallible allocation support take care of doing all the allocation.
The heart of the change is here. I ended up splitting out some other cleanup-type changes into the previous commit.
@kinetiknz, @SingingTree: could you please give this one more look before I merge?
// which has no `construction_method` field. It does say: | ||
// "For maximum compatibility, version 0 of this box should be used in preference to | ||
// version 1 with `construction_method==0`, or version 2 when possible." | ||
// We take this to imply version 0 can be interpreted as using file offsets. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is my reading too, and I've been trying to figure out if there's a rule somewhere the makes this explicit. For example if there is a blanket rule with the ISOBMFF where a field that does not exist in for a given version of a box is treated as 0, which would be annoying in it's own way, but would at least give explicit clarity to this case :|
Thanks for all the feedback, @kinetiknz! I'll be addressing all the issues and update the PR soon. To reply to some of the higher level comments:
Ah, good idea. I'll look into that.
Will do.
Yeah, I was hoping to share more code as well, but I didn't see a way that made sense. Since HEIF so so different from video, nothing in If you see a different approach that would allow more code to be shared or reduce complexity, I'm all for it.
The |
Consequently, remove previously added test files from this repo.
Also, update mp4.dict with the new box types added for AVIF.
Makes sense to me, thanks. We can always consider merging/sharing code later, if it makes sense, as the AVIF support grows. |
- Rename ProtectionSchemeInformationBox -> ProtectionSchemeInfoBox to match naming in syntax section of ISO spec. - Remove unused MetadataItemInformationBox (itif). - Rename Mdat -> MediaDataBox. - Add vec_with_capacity, extend_from_slice and read_to_end to support those operations with fallible allocation. - Switch Vec::push calls to use vec_push for fallible allocation. - Rename u32_to_string -> be_u32_to_string - Fix typos, add TODOs with linked issues
This will be busted until mozilla/mp4parse_fallible#6 is merged (presumably requiring a new version and so one final update here), but most of the other issues should be addressed, so I thought it was worth getting the review cycle started again. |
I note that the CI didn't fail, which leads me to believe that it doesn't test building with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
We'll need to do a small rebase to benefit from the switch to a git submodule for the test files.
I see the CI with --all-features is failing to build afl.rs. Rather than any spend time fixing it, I think we can remove anything related to afl.rs fuzzing now, since we're using the cargo fuzz infrastructure under mp4parse_capi/fuzz, making the older fuzzing support obsolete. |
All fuzzing is now handled by libfuzzer_sys
I think the issue is that |
Well, that seemed to solve the After looking more closely, I see the error is
Which may actually mean we've caught something. It seems to consistently occur after |
We want the 4 combinations of --release and --all-features being enabled, not 2 jobs with neither and one job with just one of each.
The tests are crashing with STATUS_HEAP_CORRUPTION. This is because it's not safe to build with mp4parse_fallible enabled unless it's also guaranteed that the system allocator matches the Rust allocator (which is guaranteed within Gecko). That's briefly mentioned here. Since Rust dropped jemalloc, we're getting lucky on the other Travis platforms since the system and Rust allocators happen to match. Windows has different allocator rules, so we hit this issue. I think it's fine to run tests without mp4parse_fallible built on Travis, since we still get coverage via Gecko's testsuite. |
Ah, that makes sense. Thanks for the explanation. In the spirit of getting a little bit of early warning, how about just running |
This is kind of odd. Investigating the CI failures following adding
however both
work fine So, it's not about features, or about running things multiple times, but something that |
build_ffi_test is pretty wacky, sorry. It's trying to link the built mp4parse, so it relies on output from the build phase. The test phase produces a different path/binary, so the makefile fails to find the input. (Re)writing it now, it'd make more sense to use cc-rs instead of shelling out to make, and there's probably a better way to find the path of the link target than hardcoding the makefile. It'd be great to fix it but we can fix it independently from your PR, so don't let it block your progress on merging if you'd prefer to focus on that first. |
Requiring output from the build phase isn't that surprising to me, but what I'm not really grokking is why adding in a In any case, since this is definitely a detour relative to the purpose of this PR, and you seem to be more familiar with the code in question, I'll leave it alone. In the meantime, I think a good workaround would be putting the I'll try that change shortly. And if that doesn't work, I'll just give up on changing the CI to include feature checking for now. |
This simplifies a number of the callers and avoids try_into() calls. Also, simplify the bound on T from ReadBytesExt to Read, which is sufficient.
Since read_to_end() takes a &mut Vec buf rather than a &mut slice, it can increase the size (i.e., length, not capacity) of the buf during operation. This removes the soundness requirement of pre-initializing the full capacity that occurred in allocate_read_buf(), thereby improving efficiency. As this was allocate_read_buf()'s only caller, remove it as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Since the fundamentals of HEIF, upon which AVIF is based, are quite different
from video, this adds a new context type, which currently only contains the
data for the "primary item", typically an av01 coded image frame.
Extracting the primary item requires support for several new box types which
specify the location type of various data and metadata. Various features are
not yet fully supported, but should provide informative errors.
Since the specification does not make many guarantees about the ordering of
boxes, there is some additional logic complexity in an attempt to avoid
copying buffers when possible.
Also, a set of AVIF sample images (see the ReadMe.txt for licensing) is added
to exercise the new parsing code.