Bug 156511

Summary: Implement latest File object spec (including its constructor)
Product: WebKit Reporter: Brady Eidson <beidson>
Component: WebCore Misc.Assignee: Brady Eidson <beidson>
Status: RESOLVED FIXED    
Severity: Normal CC: ap, buildbot, rniwa
Priority: P2    
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
Bug Depends on:    
Bug Blocks: 149117, 171781    
Attachments:
Description Flags
Patch v1
buildbot: commit-queue-
Archive of layout-test-results from ews102 for mac-yosemite
none
Archive of layout-test-results from ews115 for mac-yosemite
none
Patch v2
darin: review+, buildbot: commit-queue-
Archive of layout-test-results from ews101 for mac-yosemite
none
Archive of layout-test-results from ews105 for mac-yosemite-wk2
none
Archive of layout-test-results from ews124 for ios-simulator-wk2
none
Archive of layout-test-results from ews114 for mac-yosemite
none
Patch for landing none

Description Brady Eidson 2016-04-12 12:49:46 PDT
Implement File object constructor

http://www.w3.org/TR/FileAPI/#file-constructor
Comment 1 Brady Eidson 2016-04-12 12:50:14 PDT
Won't be able to pass 100% of all IndexedDB tests until this is implemented.
Comment 2 Brady Eidson 2016-04-22 20:09:18 PDT
I just modernized File.idl and am now building. Wish me luck.
Comment 3 Brady Eidson 2016-04-24 09:57:18 PDT
New title:
Implement latest File object spec (including its constructor)
Comment 4 Brady Eidson 2016-04-24 10:18:24 PDT
Created attachment 277189 [details]
Patch v1

Currently running layout tests locally, but also relying on EWS.
Comment 5 Darin Adler 2016-04-24 10:59:13 PDT
Comment on attachment 277189 [details]
Patch v1

View in context: https://bugs.webkit.org/attachment.cgi?id=277189&action=review

Looks pretty good. Build seems to be failing, though.

> Source/WebCore/bindings/js/JSFileCustom.cpp:45
> +    DOMConstructorObject* jsConstructor = jsCast<DOMConstructorObject*>(exec->callee());

I suggest auto* instead of naming the type twice. I suggest the name "constructor" rather than the name "jsConstructor".

> Source/WebCore/bindings/js/JSFileCustom.cpp:48
> +        return throwVMError(exec, createReferenceError(exec, "File constructor associated document is unavailable"));

Strange that there is not a helper to allow you to use only one function call here. That’s definitely what we want. Same thought on all the other uses of throwVMError.

> Source/WebCore/bindings/js/JSFileCustom.cpp:51
> +    if (exec->argumentCount() < 2)
> +        return throwVMError(exec, createTypeError(exec, "File constructor requires at least 2 arguments"));

As I understand it, in modern bindings it is better to check for undefined rather than actually looking at argument count. We never want "undefined" to behave differently from omitting an argument entirely. Is there some way to write this without a literal argumentCount check the still gives the results we are looking for?

> Source/WebCore/bindings/js/JSFileCustom.cpp:57
> +    ASSERT(blobParts);

Would be an improvement to put the result into a reference rather than a pointer since it can’t be null, although I suppose that seems wrong in the case where there is an exception.

The assertion annoys me since toJSSequence guarantees this, but somehow it can’t convey that guarantee to us clearly enough, so we feel the need to assert. In fact, it would be more efficient if toJSSequence guaranteed it would return nullptr when there’s an exception, since checking that is cheaper than checking exec->hadException().

> Source/WebCore/bindings/js/JSFileCustom.cpp:61
> +    JSValue jsFilename = exec->argument(1);
> +    String filename = jsFilename.toWTFString(exec);
> +    filename.replace('/', ':');

- Since we checked argumentCount, we can use uncheckedArgument(1). But I’d prefer that we not check argumentCount!
- We need a check of hadException here, because converting an object to a string can involve calling a custom valueOf function which may raise an exception.
- Do we want to convert undefined to "undefined", or would it be better to raise an exception in that case? See my comment above about treating undefined consistently with omitted arguments.

If we do keep the code exactly like this, here’s how I would write it:

    String filename = exec->argument(1).toWTFString(exec).replace('/', ':');
    if (exec->hadException())
         return <...>;

I don’t think it’s better spread out over three lines of code with the second local variable. I particularly don’t like the "js" prefix.

Or we could use toStringOrNull so we can make the exception check more efficient. In that case we would have to do the value call separately. Or we could make a new version of toWTFString that returns String() to make the exception check more efficient.

Sorry, not trying to make this excessively complex, just trying to get it right.

> Source/WebCore/bindings/js/JSFileCustom.cpp:66
> +    if (exec->argumentCount() > 2) {

This check isn’t needed, because the handling of "undefined" for argument(2) already does the same thing. So this should be removed.

> Source/WebCore/bindings/js/JSFileCustom.cpp:67
> +        do {

I think the use of "do while (0)" to make a "goto without goto" is not great. Plain old nested if statements would probably be better or perhaps a helper function would be good.

> Source/WebCore/bindings/js/JSFileCustom.cpp:77
> +            // Given the above test, this will always yield an object.
> +            JSObject* filePropertyBagObject = filePropertyBagValue.toObject(exec);

toObject(exec) is not needed when you already know something is an object. Correct idiom in that case is:

    asObject(filePropertyBagValue)

Or you could use getObject() to both check if something is an object and get it. I think I would write this:

    JSObject* filePropertyBagObject = filePropertyBagValue.getObject();
    if (!filePropertyBagObject)
        return throwTypeError(*exec, "Third argument of the constructor is not an object");

> Source/WebCore/bindings/js/JSFileCustom.cpp:86
> +            // Create the dictionary wrapper from the initializer object.
> +            JSDictionary dictionary(exec, filePropertyBagObject);
> +
> +            // Attempt to get the type property.
> +            String type;
> +            dictionary.get("type", type);
> +            if (exec->hadException())
> +                return JSValue::encode(jsUndefined());

Not the greatest way to get a property in the bindings code. We don’t need a JSDictionary just to get a property. But I guess it’s OK for now.

> Source/WebCore/bindings/js/JSFileCustom.cpp:89
> +            // If contenttype was invalid, abort the substeps of parsing the FilePropertyBag

What is "contenttype"? Also we normally use a period for something like this.

> Source/WebCore/bindings/js/JSFileCustom.cpp:111
> +        else if (RefPtr<ArrayBufferView> arrayBufferView = toArrayBufferView(item))

Should use auto here instead of writing out RefPtr.

> Source/WebCore/bindings/js/JSFileCustom.cpp:112
> +            blobBuilder.append(arrayBufferView.release());

This should be WTFMove(), not release().

> Source/WebCore/bindings/js/JSFileCustom.cpp:116
> +            String string = item.toString(exec)->value(exec);

This is toWTFString, but written out.

> Source/WebCore/bindings/js/JSFileCustom.cpp:123
> +    RefPtr<File> file = File::create(blobBuilder.finalize(), filename, normalizedType, lastModified.value());

Normally better to use auto instead of writing out RefPtr here.

> Source/WebCore/fileapi/File.cpp:72
> +    m_size = -1;

Why is this being set with assignment instead of construction syntax? What’s with the magic number -1?

> Source/WebCore/fileapi/File.cpp:82
> +    // FIXME: This does sync-i/o on the main thread and also recalculates every time the method is called.
> +    // The i/o should be performed on a background thread,
> +    // and the result should be cached along with an asynchronous monitor for changes to the file.

The idea that this should monitor changes to the file is possibly peculiar since it sets up even more race conditions between the file system status and the caches here. Is that really right for the intent for this class?
Comment 6 Build Bot 2016-04-24 11:02:50 PDT
Comment on attachment 277189 [details]
Patch v1

Attachment 277189 [details] did not pass mac-ews (mac):
Output: http://webkit-queues.webkit.org/results/1213470

New failing tests:
http/tests/local/fileapi/file-last-modified-after-delete.html
http/tests/local/fileapi/file-last-modified.html
imported/blink/storage/indexeddb/blob-basics-metadata.html
Comment 7 Build Bot 2016-04-24 11:02:53 PDT
Created attachment 277191 [details]
Archive of layout-test-results from ews102 for mac-yosemite

The attached test failures were seen while running run-webkit-tests on the mac-ews.
Bot: ews102  Port: mac-yosemite  Platform: Mac OS X 10.10.5
Comment 8 Build Bot 2016-04-24 11:27:20 PDT
Comment on attachment 277189 [details]
Patch v1

Attachment 277189 [details] did not pass mac-debug-ews (mac):
Output: http://webkit-queues.webkit.org/results/1213485

New failing tests:
http/tests/local/fileapi/file-last-modified-after-delete.html
http/tests/local/fileapi/file-last-modified.html
imported/blink/storage/indexeddb/blob-basics-metadata.html
Comment 9 Build Bot 2016-04-24 11:27:23 PDT
Created attachment 277192 [details]
Archive of layout-test-results from ews115 for mac-yosemite

The attached test failures were seen while running run-webkit-tests on the mac-debug-ews.
Bot: ews115  Port: mac-yosemite  Platform: Mac OS X 10.10.5
Comment 10 Brady Eidson 2016-04-24 21:29:11 PDT
The build breakage is either GCC's inability to deal with this template expansion, or a legitimate issue with "long int" not resolving to one of the signed int types we already have specializations for.
Comment 11 Brady Eidson 2016-04-24 22:01:08 PDT
Most of your comments I simply addressed the way you suggested or a similar way.

(In reply to comment #5)
> Comment on attachment 277189 [details]
> Patch v1
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=277189&action=review
> 
> Looks pretty good. Build seems to be failing, though.
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:45
> > +    DOMConstructorObject* jsConstructor = jsCast<DOMConstructorObject*>(exec->callee());
> 
> I suggest auto* instead of naming the type twice. I suggest the name
> "constructor" rather than the name "jsConstructor".
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:48
> > +        return throwVMError(exec, createReferenceError(exec, "File constructor associated document is unavailable"));
> 
> Strange that there is not a helper to allow you to use only one function
> call here. That’s definitely what we want. Same thought on all the other
> uses of throwVMError.
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:51
> > +    if (exec->argumentCount() < 2)
> > +        return throwVMError(exec, createTypeError(exec, "File constructor requires at least 2 arguments"));
> 
> As I understand it, in modern bindings it is better to check for undefined
> rather than actually looking at argument count. We never want "undefined" to
> behave differently from omitting an argument entirely. Is there some way to
> write this without a literal argumentCount check the still gives the results
> we are looking for?
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:57
> > +    ASSERT(blobParts);
> 
> Would be an improvement to put the result into a reference rather than a
> pointer since it can’t be null, although I suppose that seems wrong in the
> case where there is an exception.
> 
> The assertion annoys me since toJSSequence guarantees this, but somehow it
> can’t convey that guarantee to us clearly enough, so we feel the need to
> assert. In fact, it would be more efficient if toJSSequence guaranteed it
> would return nullptr when there’s an exception, since checking that is
> cheaper than checking exec->hadException().
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:61
> > +    JSValue jsFilename = exec->argument(1);
> > +    String filename = jsFilename.toWTFString(exec);
> > +    filename.replace('/', ':');
> 
> - Since we checked argumentCount, we can use uncheckedArgument(1). But I’d
> prefer that we not check argumentCount!
> - We need a check of hadException here, because converting an object to a
> string can involve calling a custom valueOf function which may raise an
> exception.
> - Do we want to convert undefined to "undefined", or would it be better to
> raise an exception in that case? See my comment above about treating
> undefined consistently with omitted arguments.
> 
> If we do keep the code exactly like this, here’s how I would write it:
> 
>     String filename = exec->argument(1).toWTFString(exec).replace('/', ':');
>     if (exec->hadException())
>          return <...>;
> 
> I don’t think it’s better spread out over three lines of code with the
> second local variable. I particularly don’t like the "js" prefix.
> 
> Or we could use toStringOrNull so we can make the exception check more
> efficient. In that case we would have to do the value call separately. Or we
> could make a new version of toWTFString that returns String() to make the
> exception check more efficient.
> 
> Sorry, not trying to make this excessively complex, just trying to get it
> right.
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:66
> > +    if (exec->argumentCount() > 2) {
> 
> This check isn’t needed, because the handling of "undefined" for argument(2)
> already does the same thing. So this should be removed.
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:67
> > +        do {
> 
> I think the use of "do while (0)" to make a "goto without goto" is not
> great. Plain old nested if statements would probably be better or perhaps a
> helper function would be good.

Originally there were a lot more breaks. But in the uploaded patch there were only two, and after other suggested changes there's only one.

So it's a no brainer to do one nested if.

> > Source/WebCore/bindings/js/JSFileCustom.cpp:89
> > +            // If contenttype was invalid, abort the substeps of parsing the FilePropertyBag
> 
> What is "contenttype"? Also we normally use a period for something like this.

The previous name for "type"
Fixed.

> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:111
> > +        else if (RefPtr<ArrayBufferView> arrayBufferView = toArrayBufferView(item))
> 
> Should use auto here instead of writing out RefPtr.

toArrayBufferView returns a PassRefPtr (still!), so I did leave it specified as a RefPtr. But...

> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:112
> > +            blobBuilder.append(arrayBufferView.release());
> 
> This should be WTFMove(), not release().

I did that!

> > Source/WebCore/fileapi/File.cpp:72
> > +    m_size = -1;
> 
> Why is this being set with assignment instead of construction syntax? What’s
> with the magic number -1?

Needed in an earlier iteration of the patch (since m_size belongs to Blob and not File), but no longer needed.

It tells Blob to lazily fetch the size from the BlobRegistry.

Magic numbers are bad, but this one is fairly established. :(

> > Source/WebCore/fileapi/File.cpp:82
> > +    // FIXME: This does sync-i/o on the main thread and also recalculates every time the method is called.
> > +    // The i/o should be performed on a background thread,
> > +    // and the result should be cached along with an asynchronous monitor for changes to the file.
> 
> The idea that this should monitor changes to the file is possibly peculiar
> since it sets up even more race conditions between the file system status
> and the caches here. Is that really right for the intent for this class?

I think this is right. When a File object is created from an <input type="file"> picker, it points to an underlying file on disk, and the properties you get via javascript are meant to reflect changes to the underlying file that have occurred since the user picked it.

I think setting the object up to know "The file has changed, so the next time somebody asks you have to recalculate the size" is much better than "always calculate the size every time somebody asks"
Comment 12 Brady Eidson 2016-04-24 22:16:59 PDT
Created attachment 277216 [details]
Patch v2
Comment 13 Darin Adler 2016-04-24 22:40:34 PDT
Comment on attachment 277216 [details]
Patch v2

View in context: https://bugs.webkit.org/attachment.cgi?id=277216&action=review

> Source/WebCore/bindings/js/JSFileCustom.cpp:72
> +    if (!arg.isUndefinedOrNull()) {

I understand we should skip this if it’s undefined. But do we really want this treatment for null? Why is null different from false in this respect? Do we have this covered by a test?

> Source/WebCore/bindings/js/JSFileCustom.cpp:73
> +        // Given the above test, this will always yield an object.

This comment is not correct! For example, the argument could be a boolean or a number, and that’s not an object.
Comment 14 Build Bot 2016-04-24 23:01:33 PDT
Comment on attachment 277216 [details]
Patch v2

Attachment 277216 [details] did not pass mac-ews (mac):
Output: http://webkit-queues.webkit.org/results/1216068

New failing tests:
fast/files/file-constructor.html
Comment 15 Build Bot 2016-04-24 23:01:36 PDT
Created attachment 277218 [details]
Archive of layout-test-results from ews101 for mac-yosemite

The attached test failures were seen while running run-webkit-tests on the mac-ews.
Bot: ews101  Port: mac-yosemite  Platform: Mac OS X 10.10.5
Comment 16 Build Bot 2016-04-24 23:06:24 PDT
Comment on attachment 277216 [details]
Patch v2

Attachment 277216 [details] did not pass mac-wk2-ews (mac-wk2):
Output: http://webkit-queues.webkit.org/results/1216082

New failing tests:
fast/files/file-constructor.html
Comment 17 Build Bot 2016-04-24 23:06:28 PDT
Created attachment 277220 [details]
Archive of layout-test-results from ews105 for mac-yosemite-wk2

The attached test failures were seen while running run-webkit-tests on the mac-wk2-ews.
Bot: ews105  Port: mac-yosemite-wk2  Platform: Mac OS X 10.10.5
Comment 18 Build Bot 2016-04-24 23:11:03 PDT
Comment on attachment 277216 [details]
Patch v2

Attachment 277216 [details] did not pass ios-sim-ews (ios-simulator-wk2):
Output: http://webkit-queues.webkit.org/results/1216083

New failing tests:
fast/files/file-constructor.html
Comment 19 Build Bot 2016-04-24 23:11:06 PDT
Created attachment 277221 [details]
Archive of layout-test-results from ews124 for ios-simulator-wk2

The attached test failures were seen while running run-webkit-tests on the ios-sim-ews.
Bot: ews124  Port: ios-simulator-wk2  Platform: Mac OS X 10.11.4
Comment 20 Build Bot 2016-04-24 23:14:50 PDT
Comment on attachment 277216 [details]
Patch v2

Attachment 277216 [details] did not pass mac-debug-ews (mac):
Output: http://webkit-queues.webkit.org/results/1216088

New failing tests:
fast/files/file-constructor.html
Comment 21 Build Bot 2016-04-24 23:14:54 PDT
Created attachment 277222 [details]
Archive of layout-test-results from ews114 for mac-yosemite

The attached test failures were seen while running run-webkit-tests on the mac-debug-ews.
Bot: ews114  Port: mac-yosemite  Platform: Mac OS X 10.10.5
Comment 22 Brady Eidson 2016-04-25 09:03:22 PDT
(In reply to comment #13)
> Comment on attachment 277216 [details]
> Patch v2
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=277216&action=review
> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:72
> > +    if (!arg.isUndefinedOrNull()) {
> 
> I understand we should skip this if it’s undefined. But do we really want
> this treatment for null? Why is null different from false in this respect?
> Do we have this covered by a test?

The IDL specifies the first argument is a non-optional sequence. In IDL parlance, null does not qualify.

Yes, it's covered by a test:
shouldThrow("new File(null, 'world.html')", '"TypeError: Value is not a sequence"');

...And I didn't properly update that test after making the code changes! *sigh* doh!


> 
> > Source/WebCore/bindings/js/JSFileCustom.cpp:73
> > +        // Given the above test, this will always yield an object.
> 
> This comment is not correct! For example, the argument could be a boolean or
> a number, and that’s not an object.

Forgot to remove the comment when removing the isObject test.

Aren't comments fun?!?!
Comment 23 Brady Eidson 2016-04-25 09:47:46 PDT
Created attachment 277256 [details]
Patch for landing
Comment 24 Chris Dumez 2016-04-25 09:48:11 PDT
Comment on attachment 277216 [details]
Patch v2

View in context: https://bugs.webkit.org/attachment.cgi?id=277216&action=review

>>> Source/WebCore/bindings/js/JSFileCustom.cpp:72
>>> +    if (!arg.isUndefinedOrNull()) {
>> 
>> I understand we should skip this if it’s undefined. But do we really want this treatment for null? Why is null different from false in this respect? Do we have this covered by a test?
> 
> The IDL specifies the first argument is a non-optional sequence. In IDL parlance, null does not qualify.
> 
> Yes, it's covered by a test:
> shouldThrow("new File(null, 'world.html')", '"TypeError: Value is not a sequence"');
> 
> ...And I didn't properly update that test after making the code changes! *sigh* doh!

Uh, but the code here is about the 3rd parameter which *is* optional and is a Dictionary. I don't believe allowing null is correct for the 3rd parameter.
Comment 25 Chris Dumez 2016-04-25 09:49:48 PDT
Comment on attachment 277256 [details]
Patch for landing

View in context: https://bugs.webkit.org/attachment.cgi?id=277256&action=review

> Source/WebCore/bindings/js/JSFileCustom.cpp:72
> +    if (!arg.isUndefinedOrNull()) {

This does not seem right:
See http://heycam.github.io/webidl/#es-overloads (10.4). undefined is OK for optional parameters, null is not.
Comment 26 Chris Dumez 2016-04-25 09:51:01 PDT
Comment on attachment 277256 [details]
Patch for landing

View in context: https://bugs.webkit.org/attachment.cgi?id=277256&action=review

>> Source/WebCore/bindings/js/JSFileCustom.cpp:72
>> +    if (!arg.isUndefinedOrNull()) {
> 
> This does not seem right:
> See http://heycam.github.io/webidl/#es-overloads (10.4). undefined is OK for optional parameters, null is not.

Actually never mind my comment, null is OK for a Dictionary parameter as per:
http://heycam.github.io/webidl/#es-dictionary
Comment 27 Brady Eidson 2016-04-25 09:52:00 PDT
(In reply to comment #24)
> Comment on attachment 277216 [details]
> Patch v2
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=277216&action=review
> 
> >>> Source/WebCore/bindings/js/JSFileCustom.cpp:72
> >>> +    if (!arg.isUndefinedOrNull()) {
> >> 
> >> I understand we should skip this if it’s undefined. But do we really want this treatment for null? Why is null different from false in this respect? Do we have this covered by a test?
> > 
> > The IDL specifies the first argument is a non-optional sequence. In IDL parlance, null does not qualify.
> > 
> > Yes, it's covered by a test:
> > shouldThrow("new File(null, 'world.html')", '"TypeError: Value is not a sequence"');
> > 
> > ...And I didn't properly update that test after making the code changes! *sigh* doh!
> 
> Uh, but the code here is about the 3rd parameter which *is* optional and is
> a Dictionary. I don't believe allowing null is correct for the 3rd parameter.

Whoops. Indeed.

I was looking at line 51:
if (arg.isUndefinedOrNull())
Not line 72:
if (!arg.isUndefinedOrNull()) {

There's no doubt that passing null there is not okay.

Updating.
Comment 28 Brady Eidson 2016-04-25 09:52:23 PDT
(In reply to comment #26)
> Comment on attachment 277256 [details]
> Patch for landing
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=277256&action=review
> 
> >> Source/WebCore/bindings/js/JSFileCustom.cpp:72
> >> +    if (!arg.isUndefinedOrNull()) {
> > 
> > This does not seem right:
> > See http://heycam.github.io/webidl/#es-overloads (10.4). undefined is OK for optional parameters, null is not.
> 
> Actually never mind my comment, null is OK for a Dictionary parameter as per:
> http://heycam.github.io/webidl/#es-dictionary

Oh! Well nevermind then!!!
Comment 29 Brady Eidson 2016-04-25 10:32:24 PDT
http://trac.webkit.org/changeset/200032