Implement latest File object spec (including its constructor).
authorbeidson@apple.com <beidson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 25 Apr 2016 17:31:29 +0000 (17:31 +0000)
committerbeidson@apple.com <beidson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 25 Apr 2016 17:31:29 +0000 (17:31 +0000)
https://bugs.webkit.org/show_bug.cgi?id=156511

Reviewed by Darin Adler.

Source/WebCore:

Test: fast/files/file-constructor.html

* CMakeLists.txt:
* WebCore.xcodeproj/project.pbxproj:

* bindings/js/JSDictionary.cpp:
(WebCore::JSDictionary::convertValue):
* bindings/js/JSDictionary.h:

* bindings/js/JSFileCustom.cpp: Added.
(WebCore::constructJSFile):

* fileapi/File.cpp:
(WebCore::File::File):
(WebCore::File::lastModified):
(WebCore::File::lastModifiedDate): Deleted.
* fileapi/File.h:
* fileapi/File.idl:

LayoutTests:

* fast/files/file-constructor-expected.txt: Added.
* fast/files/file-constructor.html: Added.

* http/tests/local/fileapi/file-last-modified-after-delete-expected.txt:
* http/tests/local/fileapi/script-tests/file-last-modified-after-delete.js:

* http/tests/local/fileapi/file-last-modified-expected.txt:
* http/tests/local/fileapi/script-tests/file-last-modified.js:

* imported/blink/storage/indexeddb/blob-basics-metadata-expected.txt:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@200032 268f45cc-cd09-0410-ab3c-d52691b4dbfc

17 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/files/file-constructor-expected.txt [new file with mode: 0644]
LayoutTests/fast/files/file-constructor.html [new file with mode: 0644]
LayoutTests/http/tests/local/fileapi/file-last-modified-after-delete-expected.txt
LayoutTests/http/tests/local/fileapi/file-last-modified-expected.txt
LayoutTests/http/tests/local/fileapi/script-tests/file-last-modified-after-delete.js
LayoutTests/http/tests/local/fileapi/script-tests/file-last-modified.js
LayoutTests/imported/blink/storage/indexeddb/blob-basics-metadata-expected.txt
Source/WebCore/CMakeLists.txt
Source/WebCore/ChangeLog
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/bindings/js/JSDictionary.cpp
Source/WebCore/bindings/js/JSDictionary.h
Source/WebCore/bindings/js/JSFileCustom.cpp [new file with mode: 0644]
Source/WebCore/fileapi/File.cpp
Source/WebCore/fileapi/File.h
Source/WebCore/fileapi/File.idl

index 8dcd6ab..4ef6da0 100644 (file)
@@ -1,3 +1,21 @@
+2016-04-25  Brady Eidson  <beidson@apple.com>
+
+        Implement latest File object spec (including its constructor).
+        https://bugs.webkit.org/show_bug.cgi?id=156511
+
+        Reviewed by Darin Adler.
+
+        * fast/files/file-constructor-expected.txt: Added.
+        * fast/files/file-constructor.html: Added.
+        
+        * http/tests/local/fileapi/file-last-modified-after-delete-expected.txt:
+        * http/tests/local/fileapi/script-tests/file-last-modified-after-delete.js:
+
+        * http/tests/local/fileapi/file-last-modified-expected.txt:
+        * http/tests/local/fileapi/script-tests/file-last-modified.js:
+
+        * imported/blink/storage/indexeddb/blob-basics-metadata-expected.txt:
+
 2016-04-25  Daniel Bates  <dabates@apple.com>
 
         REGRESSION (r196012): Subresource may be blocked by Content Security Policy if it only matches 'self'
diff --git a/LayoutTests/fast/files/file-constructor-expected.txt b/LayoutTests/fast/files/file-constructor-expected.txt
new file mode 100644 (file)
index 0000000..78ab21e
--- /dev/null
@@ -0,0 +1,104 @@
+Test the File constructor.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS (new File([], 'world.html')) instanceof window.File is true
+PASS (new File(['hello'], 'world.html')) instanceof window.File is true
+PASS (new File(['hello'], 'world.html', {})) instanceof window.File is true
+PASS (new File(['hello'], 'world.html', {type:'text/html'})) instanceof window.File is true
+PASS (new File(['hello'], 'world.html', {type:'text/html', endings:'native'})) instanceof window.File is true
+PASS (new File(['hello'], 'world.html', {type:'text/html', endings:'transparent'})) instanceof window.File is true
+PASS (new File([], 'world.html')) instanceof window.File is true
+PASS (new File()) threw exception TypeError: First argument to File constructor must be a valid sequence, was undefined or null.
+PASS (new File([])) threw exception TypeError: Second argument to File constructor must be a valid string, was undefined.
+PASS (new File([], null)) instanceof window.File is true
+PASS (new File([], 1)) instanceof window.File is true
+PASS (new File([], '')) instanceof window.File is true
+PASS (new File([], document)) instanceof window.File is true
+PASS new File('hello', 'world.html') threw exception TypeError: Value is not a sequence.
+PASS new File(0, 'world.html') threw exception TypeError: Value is not a sequence.
+PASS new File(null, 'world.html') threw exception TypeError: First argument to File constructor must be a valid sequence, was undefined or null.
+PASS (new File([], 'world.html')) instanceof window.File is true
+PASS (new File(['stringPrimitive'], 'world.html')) instanceof window.File is true
+PASS (new File([String('stringObject')], 'world.html')) instanceof window.File is true
+PASS (new File([new Blob], 'world.html')) instanceof window.File is true
+PASS (new File([new Blob([new Blob])], 'world.html')) instanceof window.File is true
+PASS (new Blob([new File([], 'world.txt')])) instanceof window.Blob is true
+PASS (new Blob([new Blob([new File([new Blob], 'world.txt')])])) instanceof window.Blob is true
+PASS (new File([new File([], 'world.txt')], 'world.html')) instanceof window.File is true
+PASS (new File([new Blob([new File([new Blob], 'world.txt')])], 'world.html')) instanceof window.File is true
+PASS (new File([12], 'world.html')).size is 2
+PASS (new File([[]], 'world.html')).size is 0
+PASS (new File([{}], 'world.html')).size is 15
+PASS (new File([document], 'world.html')).size is 21
+PASS (new File([toStringingObj], 'world.html')).size is 8
+PASS new File([throwingObj], 'world.html') threw exception Error.
+PASS (new File([], null)).name is 'null'
+PASS (new File([], 12)).name is '12'
+PASS (new File([], '')).name is ''
+PASS (new File([], {})).name is '[object Object]'
+PASS (new File([], document)).name is '[object HTMLDocument]'
+PASS (new File([], toStringingObj)).name is 'A string'
+PASS (new File([], throwingObj)).name threw exception Error.
+PASS (new File([], 'world.html', {unknownKey:'value'})) instanceof window.File is true
+PASS new File([], 'world.html', {type:throwingObj}) threw exception Error.
+PASS (new File([], 'world.html', {type:'helloî'})) instanceof window.File is true
+PASS (new File([], 'world.html', {type:'helloî'})).type is ''
+PASS (new File([], 'world.html', {lastModified: 555, type:'goodbye'})).lastModified is 555
+PASS (new File([], 'world.html', {lastModified: 555, type:'goodbyeî'})).lastModified is not 555
+PASS (new File([], 'world.html', null)) instanceof window.File is true
+PASS (new File([], 'world.html', undefined)) instanceof window.File is true
+PASS (new File([], 'world.html', 123)) instanceof window.File threw exception TypeError: Third argument of the constructor is not of type Object.
+PASS (new File([], 'world.html', 123.4)) instanceof window.File threw exception TypeError: Third argument of the constructor is not of type Object.
+PASS (new File([], 'world.html', true)) instanceof window.File threw exception TypeError: Third argument of the constructor is not of type Object.
+PASS (new File([], 'world.html', 'abc')) instanceof window.File threw exception TypeError: Third argument of the constructor is not of type Object.
+PASS (new File([], 'world.html', [])) instanceof window.File is true
+PASS (new File([], 'world.html', /abc/)) instanceof window.File is true
+PASS (new File([], 'world.html', function () {})) instanceof window.File is true
+PASS (new File([], 'world.html')).name is 'world.html'
+PASS (new File([], 'w/orld/ht/m.l')).name is 'w:orld:ht:m.l'
+PASS (new File([], 'world.html', {type:'text/html'})).name is 'world.html'
+PASS (new File([], 'world.html', {type:'text/html'})).type is 'text/html'
+PASS (new File([], 'world.html', {type:'text/html'})).size is 0
+PASS (new File([], 'world.html', {type:'text/plain;charset=UTF-8'})).type is 'text/plain;charset=utf-8'
+PASS (new File([], 'world.html', {lastModified: 441532800000})).lastModified is 441532800000
+PASS (new File([], 'world.html')).lastModified is equivalent to Date.now().
+PASS (new File([], 'world.html', {})).lastModified is equivalent to Date.now().
+PASS (new File([], 'world.html', {type: 'text/plain'})).lastModified is equivalent to Date.now().
+PASS (new File([], 'world.html', {lastModified: new Date(441532800000)})).lastModified is 441532800000
+PASS window.File.length is 2
+PASS new File([new DataView(new ArrayBuffer(100))], 'world.html').size is 100
+PASS new File([new Uint8Array(100)], 'world.html').size is 100
+PASS new File([new Uint8ClampedArray(100)], 'world.html').size is 100
+PASS new File([new Uint16Array(100)], 'world.html').size is 200
+PASS new File([new Uint32Array(100)], 'world.html').size is 400
+PASS new File([new Int8Array(100)], 'world.html').size is 100
+PASS new File([new Int16Array(100)], 'world.html').size is 200
+PASS new File([new Int32Array(100)], 'world.html').size is 400
+PASS new File([new Float32Array(100)], 'world.html').size is 400
+PASS new File([new Float64Array(100)], 'world.html').size is 800
+PASS new File([new Float64Array(100), new Int32Array(100), new Uint8Array(100), new DataView(new ArrayBuffer(100))], 'world.html').size is 1400
+PASS new File([new Blob([new Int32Array(100)]), new Uint8Array(100), new Float32Array(100), new DataView(new ArrayBuffer(100))], 'world.html').size is 1000
+PASS new File([new Blob([new Int32Array(100)]), new File([new Uint16Array(100)], 'world.txt'), new Uint8Array(100), new Float32Array(100), new DataView(new ArrayBuffer(100))], 'world.html').size is 1200
+PASS new File([(new DataView(new ArrayBuffer(100))).buffer], 'world.html').size is 100
+PASS new File([(new Uint8Array(100)).buffer], 'world.html').size is 100
+PASS new File([(new Uint8ClampedArray(100)).buffer], 'world.html').size is 100
+PASS new File([(new Uint16Array(100)).buffer], 'world.html').size is 200
+PASS new File([(new Uint32Array(100)).buffer], 'world.html').size is 400
+PASS new File([(new Int8Array(100)).buffer], 'world.html').size is 100
+PASS new File([(new Int16Array(100)).buffer], 'world.html').size is 200
+PASS new File([(new Int32Array(100)).buffer], 'world.html').size is 400
+PASS new File([(new Float32Array(100)).buffer], 'world.html').size is 400
+PASS new File([(new Float64Array(100)).buffer], 'world.html').size is 800
+PASS new File([(new Float64Array(100)).buffer, (new Int32Array(100)).buffer, (new Uint8Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer], 'world.html').size is 1400
+PASS new File([new Blob([(new Int32Array(100)).buffer]), (new Uint8Array(100)).buffer, (new Float32Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer], 'world.html').size is 1000
+PASS new File([new Blob([(new Int32Array(100)).buffer]), new File([new Uint16Array(100).buffer], 'world.txt'), (new Uint8Array(100)).buffer, (new Float32Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer], 'world.html').size is 1200
+PASS new Blob([new Blob([new Int32Array(100)]), new File([new Uint16Array(100)], 'world.txt'), new Uint8Array(100), new Float32Array(100), new DataView(new ArrayBuffer(100))]).size is 1200
+PASS new Blob([new Blob([(new Int32Array(100)).buffer]), new File([new Uint16Array(100).buffer], 'world.txt'), (new Uint8Array(100)).buffer, (new Float32Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer]).size is 1200
+PASS new File({length: 0}, 'world.txt').size is 0
+PASS new File({length: 1, 0: 'string'}, 'world.txt').size is 6
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/files/file-constructor.html b/LayoutTests/fast/files/file-constructor.html
new file mode 100644 (file)
index 0000000..85f9e73
--- /dev/null
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+
+<script src="../../resources/js-test.js"></script>
+<script>
+description("Test the File constructor.");
+
+// Test the different ways you can construct a File.
+shouldBeTrue("(new File([], 'world.html')) instanceof window.File");
+shouldBeTrue("(new File(['hello'], 'world.html')) instanceof window.File");
+shouldBeTrue("(new File(['hello'], 'world.html', {})) instanceof window.File");
+shouldBeTrue("(new File(['hello'], 'world.html', {type:'text/html'})) instanceof window.File");
+shouldBeTrue("(new File(['hello'], 'world.html', {type:'text/html', endings:'native'})) instanceof window.File");
+shouldBeTrue("(new File(['hello'], 'world.html', {type:'text/html', endings:'transparent'})) instanceof window.File");
+
+// Test that File inherits from File.
+shouldBeTrue("(new File([], 'world.html')) instanceof window.File")
+
+// Verify that the file name argument is required.
+shouldThrow("(new File())", '"TypeError: First argument to File constructor must be a valid sequence, was undefined or null"');
+shouldThrow("(new File([]))", '"TypeError: Second argument to File constructor must be a valid string, was undefined"');
+
+// Test valid file names.
+shouldBeTrue("(new File([], null)) instanceof window.File");
+shouldBeTrue("(new File([], 1)) instanceof window.File");
+shouldBeTrue("(new File([], '')) instanceof window.File");
+shouldBeTrue("(new File([], document)) instanceof window.File");
+
+// Test invalid file parts.
+shouldThrow("new File('hello', 'world.html')", '"TypeError: Value is not a sequence"');
+shouldThrow("new File(0, 'world.html')", '"TypeError: Value is not a sequence"');
+shouldThrow("new File(null, 'world.html')", '"TypeError: First argument to File constructor must be a valid sequence, was undefined or null"');
+
+// Test valid file parts.
+shouldBeTrue("(new File([], 'world.html')) instanceof window.File");
+shouldBeTrue("(new File(['stringPrimitive'], 'world.html')) instanceof window.File");
+shouldBeTrue("(new File([String('stringObject')], 'world.html')) instanceof window.File");
+shouldBeTrue("(new File([new Blob], 'world.html')) instanceof window.File");
+shouldBeTrue("(new File([new Blob([new Blob])], 'world.html')) instanceof window.File");
+
+// Test File instances used as blob parts.
+shouldBeTrue("(new Blob([new File([], 'world.txt')])) instanceof window.Blob");
+shouldBeTrue("(new Blob([new Blob([new File([new Blob], 'world.txt')])])) instanceof window.Blob");
+shouldBeTrue("(new File([new File([], 'world.txt')], 'world.html')) instanceof window.File");
+shouldBeTrue("(new File([new Blob([new File([new Blob], 'world.txt')])], 'world.html')) instanceof window.File");
+
+// Test some conversions to string in the parts array.
+shouldBe("(new File([12], 'world.html')).size", "2");
+shouldBe("(new File([[]], 'world.html')).size", "0");         // [].toString() is the empty string
+shouldBe("(new File([{}], 'world.html')).size", "15");;       // {}.toString() is the string "[object Object]"
+shouldBe("(new File([document], 'world.html')).size", "21");  // document.toString() is the string "[object HTMLDocument]"
+
+var toStringingObj = { toString: function() { return "A string"; } };
+shouldBe("(new File([toStringingObj], 'world.html')).size", "8");
+
+var throwingObj = { toString: function() { throw "Error"; } };
+shouldThrow("new File([throwingObj], 'world.html')", "'Error'");
+
+// Test some conversions to string in the file name.
+shouldBe("(new File([], null)).name", "'null'");
+shouldBe("(new File([], 12)).name", "'12'");
+shouldBe("(new File([], '')).name", "''");
+shouldBe("(new File([], {})).name", "'[object Object]'");
+shouldBe("(new File([], document)).name", "'[object HTMLDocument]'");
+shouldBe("(new File([], toStringingObj)).name", "'A string'");
+shouldThrow("(new File([], throwingObj)).name", "'Error'");
+
+// Test some invalid property bags.
+shouldBeTrue("(new File([], 'world.html', {unknownKey:'value'})) instanceof window.File");
+shouldThrow("new File([], 'world.html', {type:throwingObj})", "'Error'");
+
+// Type with non-ascii characters should become the empty string.
+shouldBeTrue("(new File([], 'world.html', {type:'hello\u00EE'})) instanceof window.File");
+shouldBe("(new File([], 'world.html', {type:'hello\u00EE'})).type", "''");
+
+// FilePropertyBag  substeps: Type with non-ascii characters should prevent lastModified from being extracted.
+shouldBe("(new File([], 'world.html', {lastModified: 555, type:'goodbye'})).lastModified", "555"); 
+shouldNotBe("(new File([], 'world.html', {lastModified: 555, type:'goodbye\u00EE'})).lastModified", "555"); 
+
+// Test various non-object literals being used as property bags.
+shouldBeTrue("(new File([], 'world.html', null)) instanceof window.File");
+shouldBeTrue("(new File([], 'world.html', undefined)) instanceof window.File");
+shouldThrow("(new File([], 'world.html', 123)) instanceof window.File", "'TypeError: Third argument of the constructor is not of type Object'");
+shouldThrow("(new File([], 'world.html', 123.4)) instanceof window.File", "'TypeError: Third argument of the constructor is not of type Object'");
+shouldThrow("(new File([], 'world.html', true)) instanceof window.File", "'TypeError: Third argument of the constructor is not of type Object'");
+shouldThrow("(new File([], 'world.html', 'abc')) instanceof window.File", "'TypeError: Third argument of the constructor is not of type Object'");
+shouldBeTrue("(new File([], 'world.html', [])) instanceof window.File");
+shouldBeTrue("(new File([], 'world.html', /abc/)) instanceof window.File");
+shouldBeTrue("(new File([], 'world.html', function () {})) instanceof window.File");
+
+// Test that the name/type/size are correctly added to the File.
+shouldBe("(new File([], 'world.html')).name", "'world.html'");
+shouldBe("(new File([], 'w/orld/ht/m.l')).name", "'w:orld:ht:m.l'");
+shouldBe("(new File([], 'world.html', {type:'text/html'})).name", "'world.html'");
+shouldBe("(new File([], 'world.html', {type:'text/html'})).type", "'text/html'");
+shouldBe("(new File([], 'world.html', {type:'text/html'})).size", "0");
+shouldBe("(new File([], 'world.html', {type:'text/plain;charset=UTF-8'})).type", "'text/plain;charset=utf-8'");
+
+// Test that the lastModified is correctly added to the File.
+var aDate = new Date(441532800000);
+shouldBe("(new File([], 'world.html', {lastModified: 441532800000})).lastModified", "441532800000");
+// Unless specified, lastModified should default to now.
+shouldBeNow("(new File([], 'world.html')).lastModified", 20000);
+shouldBeNow("(new File([], 'world.html', {})).lastModified", 20000);
+shouldBeNow("(new File([], 'world.html', {type: 'text/plain'})).lastModified", 20000);
+// Test that Date instances are implicitly converted to numbers.
+shouldBe("(new File([], 'world.html', {lastModified: new Date(441532800000)})).lastModified", "441532800000");
+
+// Test the number of expected arguments in the File constructor.
+shouldBe("window.File.length", "2");
+
+// Test ArrayBufferView parameters.
+shouldBe("new File([new DataView(new ArrayBuffer(100))], 'world.html').size", "100");
+shouldBe("new File([new Uint8Array(100)], 'world.html').size", "100");
+shouldBe("new File([new Uint8ClampedArray(100)], 'world.html').size", "100");
+shouldBe("new File([new Uint16Array(100)], 'world.html').size", "200");
+shouldBe("new File([new Uint32Array(100)], 'world.html').size", "400");
+shouldBe("new File([new Int8Array(100)], 'world.html').size", "100");
+shouldBe("new File([new Int16Array(100)], 'world.html').size", "200");
+shouldBe("new File([new Int32Array(100)], 'world.html').size", "400");
+shouldBe("new File([new Float32Array(100)], 'world.html').size", "400");
+shouldBe("new File([new Float64Array(100)], 'world.html').size", "800");
+shouldBe("new File([new Float64Array(100), new Int32Array(100), new Uint8Array(100), new DataView(new ArrayBuffer(100))], 'world.html').size", "1400");
+shouldBe("new File([new Blob([new Int32Array(100)]), new Uint8Array(100), new Float32Array(100), new DataView(new ArrayBuffer(100))], 'world.html').size", "1000");
+shouldBe("new File([new Blob([new Int32Array(100)]), new File([new Uint16Array(100)], 'world.txt'), new Uint8Array(100), new Float32Array(100), new DataView(new ArrayBuffer(100))], 'world.html').size", "1200");
+
+// Test ArrayBuffer parameters.
+shouldBe("new File([(new DataView(new ArrayBuffer(100))).buffer], 'world.html').size", "100");
+shouldBe("new File([(new Uint8Array(100)).buffer], 'world.html').size", "100");
+shouldBe("new File([(new Uint8ClampedArray(100)).buffer], 'world.html').size", "100");
+shouldBe("new File([(new Uint16Array(100)).buffer], 'world.html').size", "200");
+shouldBe("new File([(new Uint32Array(100)).buffer], 'world.html').size", "400");
+shouldBe("new File([(new Int8Array(100)).buffer], 'world.html').size", "100");
+shouldBe("new File([(new Int16Array(100)).buffer], 'world.html').size", "200");
+shouldBe("new File([(new Int32Array(100)).buffer], 'world.html').size", "400");
+shouldBe("new File([(new Float32Array(100)).buffer], 'world.html').size", "400");
+shouldBe("new File([(new Float64Array(100)).buffer], 'world.html').size", "800");
+shouldBe("new File([(new Float64Array(100)).buffer, (new Int32Array(100)).buffer, (new Uint8Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer], 'world.html').size", "1400");
+shouldBe("new File([new Blob([(new Int32Array(100)).buffer]), (new Uint8Array(100)).buffer, (new Float32Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer], 'world.html').size", "1000");
+shouldBe("new File([new Blob([(new Int32Array(100)).buffer]), new File([new Uint16Array(100).buffer], 'world.txt'), (new Uint8Array(100)).buffer, (new Float32Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer], 'world.html').size", "1200");
+
+// Test building Blobs with ArrayBuffer / ArrayBufferView parts enclosed in files.
+shouldBe("new Blob([new Blob([new Int32Array(100)]), new File([new Uint16Array(100)], 'world.txt'), new Uint8Array(100), new Float32Array(100), new DataView(new ArrayBuffer(100))]).size", "1200");
+shouldBe("new Blob([new Blob([(new Int32Array(100)).buffer]), new File([new Uint16Array(100).buffer], 'world.txt'), (new Uint8Array(100)).buffer, (new Float32Array(100)).buffer, (new DataView(new ArrayBuffer(100))).buffer]).size", "1200");
+
+// Test passing blob parts in objects with indexed properties.
+// (This depends on the bindings code handling of sequence<T>)
+shouldBe("new File({length: 0}, 'world.txt').size", "0");
+shouldBe("new File({length: 1, 0: 'string'}, 'world.txt').size", "6");
+</script>
index 973ccdf..d03ba0b 100644 (file)
@@ -4,9 +4,9 @@ On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE
 
 
 PASS event.dataTransfer contains a File object on drop.
-PASS lastModifiedDate is not null
-PASS lastModifiedDate is >= testStartTime
-PASS (new Date()).getTime() is >= lastModifiedDate
+PASS lastModified is not null
+PASS lastModified is >= testStartTime.getTime()
+PASS (new Date()).getTime() is >= lastModified
 PASS successfullyParsed is true
 
 TEST COMPLETE
index 1ff107a..5ba1f6e 100644 (file)
@@ -4,7 +4,7 @@ On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE
 
 
 PASS event.dataTransfer contains a File object on drop.
-PASS file.lastModifiedDate verified
+PASS file.lastModified verified
 PASS successfullyParsed is true
 
 TEST COMPLETE
index 8566f06..66c686a 100644 (file)
@@ -10,13 +10,13 @@ function onFileDrop(file)
     // Remove the temp file.
     removeTempFile(tempFileName);
 
-    // This synchronosly queries the file's lastModifiedDate (which should fail) until/unless we start capturing the file metadata at File construction time.
-    lastModifiedDate = file.lastModifiedDate;
+    // This synchronosly queries the file's lastModified (which should fail) until/unless we start capturing the file metadata at File construction time.
+    lastModified = file.lastModified;
 
     // The returned value should be equal to the current date/time since the file's modified date/time is not available.
-    shouldNotBe('lastModifiedDate', 'null');
-    shouldBeGreaterThanOrEqual('lastModifiedDate', 'testStartTime');
-    shouldBeGreaterThanOrEqual('(new Date()).getTime()', 'lastModifiedDate');
+    shouldNotBe('lastModified', 'null');
+    shouldBeGreaterThanOrEqual('lastModified', 'testStartTime.getTime()');
+    shouldBeGreaterThanOrEqual('(new Date()).getTime()', 'lastModified');
 }
 
 function runTest()
index e104737..78be2a1 100644 (file)
@@ -6,11 +6,11 @@ function onFileDrop(file, filePath)
     xhr.open("GET", "http://127.0.0.1:8000/resources/file-last-modified.php?path=../local/fileapi/" + filePath, false);
     xhr.send();
     var expectedDate = new Date(parseInt(xhr.responseText) * 1000);
-    var actualDate = file.lastModifiedDate;
-    if (expectedDate.toString() == actualDate.toString())
-        testPassed("file.lastModifiedDate verified");
+    var actualDate = file.lastModified;
+    if (expectedDate.getTime() == actualDate)
+        testPassed("file.lastModified verified");
     else
-        testFailed("file.lastModifiedDate incorrect");
+        testFailed("file.lastModified incorrect");
 }
 
 function runTest()
index 43a0ed2..9f612eb 100644 (file)
@@ -39,18 +39,16 @@ PASS event.target.result.name == fileInput.files[0].name is true
 
 testNewFile():
 newFile = new File([test_content], 'filename', {type:'text/plain'})
-FAIL newFile = new File([test_content], 'filename', {type:'text/plain'}) threw exception TypeError: function is not a constructor (evaluating 'new File([test_content], 'filename', {type:'text/plain'})')
 
 validateResult(newFile):
-FAIL newFile.name == newFile.name should be true. Threw exception ReferenceError: Can't find variable: newFile
+PASS newFile.name == newFile.name is true
 transaction = db.transaction('storeName', 'readwrite')
 store = transaction.objectStore('storeName')
 store.put(newFile, 'newFilekey')
-FAIL store.put(newFile, 'newFilekey') threw exception ReferenceError: Can't find variable: newFile
 transaction = db.transaction('storeName', 'readwrite')
 store = transaction.objectStore('storeName')
 request = store.get('newFilekey')
-FAIL event.target.result.name == newFile.name should be true. Threw exception TypeError: undefined is not an object (evaluating 'event.target.result.name')
+PASS event.target.result.name == newFile.name is true
 
 testFileList():
 filelist = fileInput.files
@@ -77,10 +75,10 @@ PASS cursor.value[1].name == fileInput.files[1].name is true
 cursor.continue();
 PASS cursor.value == 'value' is true
 cursor.continue();
-FAIL cursor.value.name == newFile.name should be true. Threw exception TypeError: null is not an object (evaluating 'cursor.value')
+PASS cursor.value.name == newFile.name is true
 cursor.continue();
-FAIL Unexpected error: TypeError: null is not an object (evaluating 'cursor.continue')
-FAIL successfullyParsed should be true. Was false.
+PASS cursor is null
+PASS successfullyParsed is true
 
 TEST COMPLETE
 
index c5829f6..8c1cbf2 100644 (file)
@@ -1143,6 +1143,7 @@ set(WebCore_SOURCES
     bindings/js/JSEventTargetCustom.cpp
     bindings/js/JSExceptionBase.cpp
     bindings/js/JSFetchResponseCustom.cpp
+    bindings/js/JSFileCustom.cpp
     bindings/js/JSFileReaderCustom.cpp
     bindings/js/JSGeolocationCustom.cpp
     bindings/js/JSHTMLAllCollectionCustom.cpp
index b74449d..46e709c 100644 (file)
@@ -1,3 +1,29 @@
+2016-04-25  Brady Eidson  <beidson@apple.com>
+
+        Implement latest File object spec (including its constructor).
+        https://bugs.webkit.org/show_bug.cgi?id=156511
+
+        Reviewed by Darin Adler.
+
+        Test: fast/files/file-constructor.html
+
+        * CMakeLists.txt:
+        * WebCore.xcodeproj/project.pbxproj:
+
+        * bindings/js/JSDictionary.cpp:
+        (WebCore::JSDictionary::convertValue):
+        * bindings/js/JSDictionary.h:
+
+        * bindings/js/JSFileCustom.cpp: Added.
+        (WebCore::constructJSFile):
+
+        * fileapi/File.cpp:
+        (WebCore::File::File):
+        (WebCore::File::lastModified):
+        (WebCore::File::lastModifiedDate): Deleted.
+        * fileapi/File.h:
+        * fileapi/File.idl:
+
 2016-04-25  Antti Koivisto  <antti@apple.com>
 
         REGRESSION(r156846): Crashes with guard malloc
index 8ac5432..96bed15 100644 (file)
                516D7D701BB5F0BD00AF7C77 /* IDBConnectionToServerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 5185FCBD1BB5CB770012898F /* IDBConnectionToServerDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; };
                516D7D711BB5F0BD00AF7C77 /* IDBConnectionToClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 516D7D6D1BB5F06500AF7C77 /* IDBConnectionToClient.cpp */; };
                516D7D721BB5F0BD00AF7C77 /* IDBConnectionToClientDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 516D7D6E1BB5F06500AF7C77 /* IDBConnectionToClientDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; };
+               516E54FA1CCB2EA80040D954 /* JSFileCustom.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 516E54F91CCB27FF0040D954 /* JSFileCustom.cpp */; };
                516F7F6D1C31E39A00F111DC /* ServerOpenDBRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 516F7F6C1C31C79D00F111DC /* ServerOpenDBRequest.h */; settings = {ATTRIBUTES = (Private, ); }; };
                516F7F6E1C31E39C00F111DC /* ServerOpenDBRequest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 516F7F6B1C31C79D00F111DC /* ServerOpenDBRequest.cpp */; };
                517138EF1BED1D1A000D5F01 /* IndexKey.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 517138ED1BED1D17000D5F01 /* IndexKey.cpp */; };
                516C62241950E2B900337E75 /* JSGamepadEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSGamepadEvent.h; sourceTree = "<group>"; };
                516D7D6D1BB5F06500AF7C77 /* IDBConnectionToClient.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IDBConnectionToClient.cpp; sourceTree = "<group>"; };
                516D7D6E1BB5F06500AF7C77 /* IDBConnectionToClientDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IDBConnectionToClientDelegate.h; sourceTree = "<group>"; };
+               516E54F91CCB27FF0040D954 /* JSFileCustom.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSFileCustom.cpp; sourceTree = "<group>"; };
                516F7F6B1C31C79D00F111DC /* ServerOpenDBRequest.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ServerOpenDBRequest.cpp; sourceTree = "<group>"; };
                516F7F6C1C31C79D00F111DC /* ServerOpenDBRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ServerOpenDBRequest.h; sourceTree = "<group>"; };
                517138ED1BED1D17000D5F01 /* IndexKey.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IndexKey.cpp; sourceTree = "<group>"; };
                                BC2ED5540C6B9BD300920BFF /* JSElementCustom.cpp */,
                                ADEC78F718EE5308001315C2 /* JSElementCustom.h */,
                                BCEFAF4D0C317E6900FA81F6 /* JSEventCustom.cpp */,
+                               516E54F91CCB27FF0040D954 /* JSFileCustom.cpp */,
                                2E7582ED12764F260062628B /* JSFileReaderCustom.cpp */,
                                C28083411C6DC96A001451B6 /* JSFontFaceCustom.cpp */,
                                1C24EEAA1C72AA0A0080F8FC /* JSFontFaceSetCustom.cpp */,
                                2E0888E6114884E200AF4265 /* JSDOMFormDataCustom.cpp in Sources */,
                                E1C36CBD0EB08062007410BC /* JSDOMGlobalObject.cpp in Sources */,
                                7C2BDD3D17C7F98C0038FF15 /* JSDOMGlobalObjectTask.cpp in Sources */,
+                               516E54FA1CCB2EA80040D954 /* JSFileCustom.cpp in Sources */,
                                65DF31F709D1CC60000BE325 /* JSDOMImplementation.cpp in Sources */,
                                A9D248060D757E7D00FDF959 /* JSDOMMimeType.cpp in Sources */,
                                A9D248080D757E7D00FDF959 /* JSDOMMimeTypeArray.cpp in Sources */,
index 81f81d0..51e045c 100644 (file)
@@ -98,6 +98,11 @@ void JSDictionary::convertValue(ExecState* exec, JSValue value, int& result)
     result = value.toInt32(exec);
 }
 
+void JSDictionary::convertValue(ExecState* exec, JSValue value, long int& result)
+{
+    result = value.toInt32(exec);
+}
+
 void JSDictionary::convertValue(ExecState* exec, JSValue value, unsigned& result)
 {
     result = value.toUInt32(exec);
@@ -119,6 +124,12 @@ void JSDictionary::convertValue(ExecState* exec, JSValue value, unsigned long lo
     doubleToInteger(d, result);
 }
 
+void JSDictionary::convertValue(ExecState* exec, JSValue value, long long& result)
+{
+    double d = value.toNumber(exec);
+    result = llrint(d);
+}
+
 void JSDictionary::convertValue(ExecState* exec, JSValue value, double& result)
 {
     result = value.toNumber(exec);
index bc68209..0c7ea05 100644 (file)
@@ -117,10 +117,12 @@ private:
     
     static void convertValue(JSC::ExecState*, JSC::JSValue, bool& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, int& result);
+    static void convertValue(JSC::ExecState*, JSC::JSValue, long int& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, unsigned& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, unsigned short& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, unsigned long& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, unsigned long long& result);
+    static void convertValue(JSC::ExecState*, JSC::JSValue, long long& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, double& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, Dictionary& result);
     static void convertValue(JSC::ExecState*, JSC::JSValue, String& result);
diff --git a/Source/WebCore/bindings/js/JSFileCustom.cpp b/Source/WebCore/bindings/js/JSFileCustom.cpp
new file mode 100644 (file)
index 0000000..925a09d
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 Apple Inc. 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, 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
+ */
+
+#include "config.h"
+#include "JSFile.h"
+
+#include "JSDOMBinding.h"
+#include "JSDictionary.h"
+#include "WebKitBlobBuilder.h"
+#include <runtime/Error.h>
+#include <runtime/JSArray.h>
+#include <runtime/JSArrayBuffer.h>
+#include <runtime/JSArrayBufferView.h>
+#include <wtf/Assertions.h>
+#include <wtf/CurrentTime.h>
+
+using namespace JSC;
+
+namespace WebCore {
+
+EncodedJSValue JSC_HOST_CALL constructJSFile(ExecState* exec)
+{
+    auto* constructor = jsCast<DOMConstructorObject*>(exec->callee());
+    ScriptExecutionContext* context = constructor->scriptExecutionContext();
+    if (!context)
+        return throwVMError(exec, createReferenceError(exec, "File constructor associated document is unavailable"));
+
+    JSValue arg = exec->argument(0);
+    if (arg.isUndefinedOrNull())
+        return throwVMError(exec, createTypeError(exec, "First argument to File constructor must be a valid sequence, was undefined or null"));
+
+    unsigned blobPartsLength = 0;
+    JSObject* blobParts = toJSSequence(exec, arg, blobPartsLength);
+    if (exec->hadException())
+        return JSValue::encode(jsUndefined());
+    ASSERT(blobParts);
+
+    arg = exec->argument(1);
+    if (arg.isUndefined())
+        return throwVMError(exec, createTypeError(exec, "Second argument to File constructor must be a valid string, was undefined"));
+
+    String filename = arg.toWTFString(exec).replace('/', ':');
+    if (exec->hadException())
+        return JSValue::encode(jsUndefined());
+
+    String normalizedType;
+    Optional<int64_t> lastModified;
+
+    arg = exec->argument(2);
+    if (!arg.isUndefinedOrNull()) {
+        JSObject* filePropertyBagObject = arg.getObject();
+        if (!filePropertyBagObject)
+            return throwVMError(exec, createTypeError(exec, "Third argument of the constructor is not of type Object"));
+
+        // 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());
+
+        normalizedType = Blob::normalizedContentType(type);
+
+        // Only try to parse the lastModified date if there was not an invalid type argument.
+        if (type.isEmpty() ||  !normalizedType.isEmpty()) {
+            dictionary.get("lastModified", lastModified);
+            if (exec->hadException())
+                return JSValue::encode(jsUndefined());
+        }
+    }
+
+    if (!lastModified)
+        lastModified = currentTimeMS();
+
+    BlobBuilder blobBuilder;
+
+    for (unsigned i = 0; i < blobPartsLength; ++i) {
+        JSValue item = blobParts->get(exec, i);
+        if (exec->hadException())
+            return JSValue::encode(jsUndefined());
+
+        if (ArrayBuffer* arrayBuffer = toArrayBuffer(item))
+            blobBuilder.append(arrayBuffer);
+        else if (RefPtr<ArrayBufferView> arrayBufferView = toArrayBufferView(item))
+            blobBuilder.append(WTFMove(arrayBufferView));
+        else if (Blob* blob = JSBlob::toWrapped(item))
+            blobBuilder.append(blob);
+        else {
+            String string = item.toWTFString(exec);
+            if (exec->hadException())
+                return JSValue::encode(jsUndefined());
+            blobBuilder.append(string, ASCIILiteral("transparent"));
+        }
+    }
+
+    auto file = File::create(blobBuilder.finalize(), filename, normalizedType, lastModified.value());
+
+    return JSValue::encode(CREATE_DOM_WRAPPER(constructor->globalObject(), File, &file.get()));
+}
+
+} // namespace WebCore
+
index 8bba87f..7702a4d 100644 (file)
@@ -64,8 +64,21 @@ File::File(DeserializationContructor, const String& path, const URL& url, const
 {
 }
 
-double File::lastModifiedDate() const
+File::File(Vector<BlobPart>&& blobParts, const String& filename, const String& contentType, int64_t lastModified)
+    : Blob(WTFMove(blobParts), contentType)
+    , m_name(filename)
+    , m_overrideLastModifiedDate(lastModified)
 {
+}
+
+double File::lastModified() const
+{
+    if (m_overrideLastModifiedDate)
+        return m_overrideLastModifiedDate.value();
+
+    // 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.
     time_t modificationTime;
     if (getFileModificationTime(m_path, modificationTime) && isValidFileTime(modificationTime))
         return modificationTime * msPerSecond;
index 268fc7d..c1735d0 100644 (file)
@@ -27,6 +27,7 @@
 #define File_h
 
 #include "Blob.h"
+#include <wtf/Optional.h>
 #include <wtf/Ref.h>
 #include <wtf/TypeCasts.h>
 #include <wtf/text/WTFString.h>
@@ -42,6 +43,12 @@ public:
         return adoptRef(*new File(path));
     }
 
+    // Create a File using the 'new File' constructor.
+    static Ref<File> create(Vector<BlobPart> blobParts, const String& filename, const String& contentType, int64_t lastModified)
+    {
+        return adoptRef(*new File(WTFMove(blobParts), filename, contentType, lastModified));
+    }
+
     static Ref<File> deserialize(const String& path, const URL& srcURL, const String& type, const String& name)
     {
         return adoptRef(*new File(deserializationContructor, path, srcURL, type, name));
@@ -59,9 +66,7 @@ public:
 
     const String& path() const { return m_path; }
     const String& name() const { return m_name; }
-
-    // This returns the current date and time if the file's last modification date is not known (per spec: http://www.w3.org/TR/FileAPI/#dfn-lastModifiedDate).
-    double lastModifiedDate() const;
+    double lastModified() const;
 
     static String contentTypeForFile(const String& path);
 
@@ -72,6 +77,7 @@ public:
 private:
     WEBCORE_EXPORT explicit File(const String& path);
     File(const String& path, const String& nameOverride);
+    File(Vector<BlobPart>&& blobParts, const String& filename, const String& contentType, int64_t lastModified);
 
     File(DeserializationContructor, const String& path, const URL& srcURL, const String& type, const String& name);
 
@@ -82,6 +88,9 @@ private:
 
     String m_path;
     String m_name;
+
+    Optional<String> m_overrideFilename;
+    Optional<int64_t> m_overrideLastModifiedDate;
 };
 
 } // namespace WebCore
index 8eb9a1c..b961686 100644 (file)
  */
 
 [
+    Exposed=(Window),
     JSGenerateToNativeObject,
     JSGenerateToJSObject,
     ExportMacro=WEBCORE_EXPORT,
+    CustomConstructor(sequence<any> fileBits, DOMString fileName, optional FilePropertyBag options),
 ] interface File : Blob {
     readonly attribute DOMString name;
-#if !defined(LANGUAGE_GOBJECT) || !LANGUAGE_GOBJECT
-    readonly attribute Date lastModifiedDate;
-#endif
+    readonly attribute long long lastModified;
 };