Remove |double| timestamp code from blink::File

This CL changes:
- all timestamp arguments of File,
- the return value of LastModifiedMS(), and
- the type of snapshot_modification_time_ms_
from double to base::Optional<base::Time>.

This renames
 - LastModifiedMS() to LastModifiedTime() and
 - snapshot_modification_time_ms_ to snapshot_modification_time_
and removes
 - File::LastModifiedDate(),
 - InvalidFileTime() in file_metadata.h,
 - IsValidFileTime() in file_metadata.h,
 - ToJsTimeOrNaN() in file_metadata.h, and
 - JsTimeToOptionalTime() in file_metadata.h.

This CL fixes File.lastModified behavior for timestamps later than
275760-09-13T00:00Z by removing unnecessary double-base:Time
conversions.

* v8_script_value_serializer_test.cc
 Do not wrap test cases with |namespace { ... }| because
FRIEND_TEST_ALL_PREFIXES() doesn't work with it.

Change-Id: I630d869641ea033b68534f17178e78b8fc2d1953
Bug: 988343
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1942671
Commit-Queue: Kent Tamura <tkent@chromium.org>
Reviewed-by: Joshua Bell <jsbell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#720806}
diff --git a/third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value_test.cc b/third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value_test.cc
index 4034540..d9688ad0 100644
--- a/third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value_test.cc
+++ b/third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value_test.cc
@@ -4,6 +4,7 @@
 
 #include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
 
+#include "base/time/time.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value_factory.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
@@ -151,8 +152,8 @@
 TEST(SerializedScriptValueTest, FileConstructorFile) {
   V8TestingScope scope;
   scoped_refptr<BlobDataHandle> blob_data_handle = BlobDataHandle::Create();
-  auto* original_file =
-      MakeGarbageCollected<File>("hello.txt", 12345678.0, blob_data_handle);
+  auto* original_file = MakeGarbageCollected<File>(
+      "hello.txt", base::Time::FromJsTime(12345678.0), blob_data_handle);
   ASSERT_FALSE(original_file->HasBackingFile());
   ASSERT_EQ(File::kIsNotUserVisible, original_file->GetUserVisibility());
   ASSERT_EQ("hello.txt", original_file->name());
diff --git a/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_deserializer.cc b/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_deserializer.cc
index 1c5ebe0..d08e5e12 100644
--- a/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_deserializer.cc
+++ b/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_deserializer.cc
@@ -5,6 +5,8 @@
 #include "third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_deserializer.h"
 
 #include "base/numerics/checked_math.h"
+#include "base/optional.h"
+#include "base/time/time.h"
 #include "third_party/blink/public/platform/web_blob_info.h"
 #include "third_party/blink/renderer/bindings/core/v8/serialization/unpacked_serialized_script_value.h"
 #include "third_party/blink/renderer/bindings/core/v8/to_v8_for_core.h"
@@ -628,9 +630,12 @@
   auto blob_handle = GetOrCreateBlobDataHandle(uuid, type, kSizeForDataHandle);
   if (!blob_handle)
     return nullptr;
-  return File::CreateFromSerialization(
-      path, name, relative_path, user_visibility, has_snapshot, size,
-      last_modified_ms, std::move(blob_handle));
+  base::Optional<base::Time> last_modified;
+  if (has_snapshot && std::isfinite(last_modified_ms))
+    last_modified = base::Time::FromJsTime(last_modified_ms);
+  return File::CreateFromSerialization(path, name, relative_path,
+                                       user_visibility, has_snapshot, size,
+                                       last_modified, std::move(blob_handle));
 }
 
 File* V8ScriptValueDeserializer::ReadFileIndex() {
@@ -647,9 +652,9 @@
   }
   if (!blob_handle)
     return nullptr;
-  return File::CreateFromIndexedSerialization(
-      info.FilePath(), info.FileName(), info.size(),
-      ToJsTimeOrNaN(info.LastModified()), blob_handle);
+  return File::CreateFromIndexedSerialization(info.FilePath(), info.FileName(),
+                                              info.size(), info.LastModified(),
+                                              blob_handle);
 }
 
 DOMRectReadOnly* V8ScriptValueDeserializer::ReadDOMRectReadOnly() {
diff --git a/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_serializer_test.cc b/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_serializer_test.cc
index 39433ca4..7308bc6 100644
--- a/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_serializer_test.cc
+++ b/third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_serializer_test.cc
@@ -4,6 +4,7 @@
 
 #include "third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_serializer.h"
 
+#include "base/time/time.h"
 #include "build/build_config.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -145,8 +146,6 @@
   return testing::AssertionSuccess();
 }
 
-namespace {
-
 TEST(V8ScriptValueSerializerTest, RoundTripJSONLikeValue) {
   // Ensure that simple JavaScript objects work.
   // There are more exhaustive tests of JavaScript objects in V8.
@@ -1450,7 +1449,7 @@
 
 TEST(V8ScriptValueSerializerTest, RoundTripFileBackedByBlob) {
   V8TestingScope scope;
-  const double kModificationTime = 0.0;
+  const base::Time kModificationTime = base::Time::UnixEpoch();
   scoped_refptr<BlobDataHandle> blob_data_handle = BlobDataHandle::Create();
   auto* file = MakeGarbageCollected<File>("/native/path", kModificationTime,
                                           blob_data_handle);
@@ -1494,17 +1493,22 @@
 }
 
 // Used for checking that times provided are between now and the current time
-// when the checker was constructed, according to WTF::currentTime.
+// when the checker was constructed, according to base::Time::Now.
 class TimeIntervalChecker {
  public:
-  TimeIntervalChecker() : start_time_(base::Time::Now()) {}
-  bool WasAliveAt(double time_in_milliseconds) {
-    base::Time time = base::Time::FromJsTime(time_in_milliseconds);
-    return start_time_ <= time && time <= base::Time::Now();
+  TimeIntervalChecker() : start_time_(NowInMilliseconds()) {}
+
+  bool WasAliveAt(int64_t time_in_milliseconds) {
+    return start_time_ <= time_in_milliseconds &&
+           time_in_milliseconds <= NowInMilliseconds();
   }
 
  private:
-  const base::Time start_time_;
+  static int64_t NowInMilliseconds() {
+    return (base::Time::Now() - base::Time::UnixEpoch()).InMilliseconds();
+  }
+
+  const int64_t start_time_;
 };
 
 TEST(V8ScriptValueSerializerTest, DecodeFileV3) {
@@ -1525,7 +1529,7 @@
   EXPECT_EQ("text/plain", new_file->type());
   EXPECT_FALSE(new_file->HasValidSnapshotMetadata());
   EXPECT_EQ(0u, new_file->size());
-  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->LastModifiedDate()));
+  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->lastModified()));
   EXPECT_EQ(File::kIsUserVisible, new_file->GetUserVisibility());
 }
 
@@ -1550,7 +1554,7 @@
   EXPECT_EQ("text/plain", new_file->type());
   EXPECT_FALSE(new_file->HasValidSnapshotMetadata());
   EXPECT_EQ(0u, new_file->size());
-  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->LastModifiedDate()));
+  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->lastModified()));
   EXPECT_EQ(File::kIsUserVisible, new_file->GetUserVisibility());
 }
 
@@ -1577,7 +1581,9 @@
   EXPECT_EQ(512u, new_file->size());
   // From v4 to v7, the last modified time is written in seconds.
   // So -0.25 represents 250 ms before the Unix epoch.
-  EXPECT_EQ(-250.0, new_file->LastModifiedDate());
+  EXPECT_EQ(-250, new_file->lastModified());
+  EXPECT_EQ(base::TimeDelta::FromMillisecondsD(-250.0),
+            new_file->LastModifiedTime() - base::Time::UnixEpoch());
 }
 
 TEST(V8ScriptValueSerializerTest, DecodeFileV7) {
@@ -1601,7 +1607,7 @@
   EXPECT_EQ("text/plain", new_file->type());
   EXPECT_FALSE(new_file->HasValidSnapshotMetadata());
   EXPECT_EQ(0u, new_file->size());
-  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->LastModifiedDate()));
+  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->lastModified()));
   EXPECT_EQ(File::kIsNotUserVisible, new_file->GetUserVisibility());
 }
 
@@ -1628,7 +1634,10 @@
   EXPECT_EQ(512u, new_file->size());
   // From v8, the last modified time is written in milliseconds.
   // So -0.25 represents 0.25 ms before the Unix epoch.
-  EXPECT_EQ(-0.25, new_file->LastModifiedDate());
+  EXPECT_EQ(base::TimeDelta::FromMillisecondsD(-0.25),
+            new_file->LastModifiedTime() - base::Time::UnixEpoch());
+  // lastModified IDL attribute can't represent -0.25 ms.
+  EXPECT_EQ(INT64_C(0), new_file->lastModified());
   EXPECT_EQ(File::kIsUserVisible, new_file->GetUserVisibility());
 }
 
@@ -1758,7 +1767,7 @@
   EXPECT_EQ("text/plain", new_file->type());
   EXPECT_FALSE(new_file->HasValidSnapshotMetadata());
   EXPECT_EQ(0u, new_file->size());
-  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->LastModifiedDate()));
+  EXPECT_TRUE(time_interval_checker.WasAliveAt(new_file->lastModified()));
   EXPECT_EQ(File::kIsNotUserVisible, new_file->GetUserVisibility());
 }
 
@@ -1889,5 +1898,4 @@
   EXPECT_FALSE(transferred->locked(script_state, ASSERT_NO_EXCEPTION));
 }
 
-}  // namespace
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/clipboard/data_object_item.cc b/third_party/blink/renderer/core/clipboard/data_object_item.cc
index 4392c2a..5016284 100644
--- a/third_party/blink/renderer/core/clipboard/data_object_item.cc
+++ b/third_party/blink/renderer/core/clipboard/data_object_item.cc
@@ -30,6 +30,7 @@
 
 #include "third_party/blink/renderer/core/clipboard/data_object_item.h"
 
+#include "base/time/time.h"
 #include "third_party/blink/public/platform/platform.h"
 #include "third_party/blink/renderer/core/clipboard/clipboard_mime_types.h"
 #include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
@@ -156,8 +157,8 @@
     data->AppendBytes(png_data.data(), png_data.size());
     const uint64_t length = data->length();
     auto blob = BlobDataHandle::Create(std::move(data), length);
-    return MakeGarbageCollected<File>(
-        "image.png", base::Time::Now().ToDoubleT() * 1000.0, std::move(blob));
+    return MakeGarbageCollected<File>("image.png", base::Time::Now(),
+                                      std::move(blob));
   }
 
   return nullptr;
diff --git a/third_party/blink/renderer/core/exported/web_drag_data_test.cc b/third_party/blink/renderer/core/exported/web_drag_data_test.cc
index bbf6a69..41e2150 100644
--- a/third_party/blink/renderer/core/exported/web_drag_data_test.cc
+++ b/third_party/blink/renderer/core/exported/web_drag_data_test.cc
@@ -4,6 +4,7 @@
 
 #include "third_party/blink/public/platform/web_drag_data.h"
 
+#include "base/time/time.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/platform/web_vector.h"
 #include "third_party/blink/renderer/core/clipboard/data_object.h"
@@ -17,8 +18,9 @@
 
   // Native file.
   data_object->Add(MakeGarbageCollected<File>("/native/path"));
-  data_object->Add(
-      MakeGarbageCollected<File>("name", 0.0, BlobDataHandle::Create()));
+  // Blob file.
+  data_object->Add(MakeGarbageCollected<File>("name", base::Time::UnixEpoch(),
+                                              BlobDataHandle::Create()));
 
   // User visible snapshot file.
   {
diff --git a/third_party/blink/renderer/core/fetch/fetch_data_loader.cc b/third_party/blink/renderer/core/fetch/fetch_data_loader.cc
index f1806d0d..04b0599 100644
--- a/third_party/blink/renderer/core/fetch/fetch_data_loader.cc
+++ b/third_party/blink/renderer/core/fetch/fetch_data_loader.cc
@@ -7,6 +7,7 @@
 #include <memory>
 #include <utility>
 
+#include "base/optional.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
 #include "third_party/blink/renderer/core/fetch/multipart_parser.h"
 #include "third_party/blink/renderer/core/fileapi/file.h"
@@ -422,7 +423,7 @@
         DCHECK(!string_builder_);
         const auto size = blob_data_->length();
         auto* file = MakeGarbageCollected<File>(
-            filename_, InvalidFileTime(),
+            filename_, base::nullopt,
             BlobDataHandle::Create(std::move(blob_data_), size));
         form_data->append(name_, file, filename_);
         return true;
diff --git a/third_party/blink/renderer/core/fileapi/file.cc b/third_party/blink/renderer/core/fileapi/file.cc
index a925d34e..b94b64e 100644
--- a/third_party/blink/renderer/core/fileapi/file.cc
+++ b/third_party/blink/renderer/core/fileapi/file.cc
@@ -132,11 +132,16 @@
     const FilePropertyBag* options) {
   DCHECK(options->hasType());
 
-  double last_modified;
-  if (options->hasLastModified())
-    last_modified = static_cast<double>(options->lastModified());
-  else
-    last_modified = base::Time::Now().ToDoubleT() * 1000.0;
+  base::Time last_modified;
+  if (options->hasLastModified()) {
+    // We don't use base::Time::FromJsTime(double) here because
+    // options->lastModified() is a 64-bit integer, and casting it to
+    // double is lossy.
+    last_modified = base::Time::UnixEpoch() +
+                    base::TimeDelta::FromMilliseconds(options->lastModified());
+  } else {
+    last_modified = base::Time::Now();
+  }
   DCHECK(options->hasEndings());
   bool normalize_line_endings_to_native = options->endings() == "native";
   if (normalize_line_endings_to_native)
@@ -194,8 +199,7 @@
       has_backing_file_(true),
       user_visibility_(user_visibility),
       path_(path),
-      name_(FilePathToWebString(WebStringToFilePath(path).BaseName())),
-      snapshot_modification_time_ms_(InvalidFileTime()) {}
+      name_(FilePathToWebString(WebStringToFilePath(path).BaseName())) {}
 
 File::File(const String& path,
            const String& name,
@@ -207,8 +211,7 @@
       has_backing_file_(true),
       user_visibility_(user_visibility),
       path_(path),
-      name_(name),
-      snapshot_modification_time_ms_(InvalidFileTime()) {}
+      name_(name) {}
 
 File::File(const String& path,
            const String& name,
@@ -216,29 +219,28 @@
            UserVisibility user_visibility,
            bool has_snapshot_data,
            uint64_t size,
-           double last_modified,
+           const base::Optional<base::Time>& last_modified,
            scoped_refptr<BlobDataHandle> blob_data_handle)
     : Blob(std::move(blob_data_handle)),
       has_backing_file_(!path.IsEmpty() || !relative_path.IsEmpty()),
       user_visibility_(user_visibility),
       path_(path),
       name_(name),
-      snapshot_modification_time_ms_(has_snapshot_data ? last_modified
-                                                       : InvalidFileTime()),
+      snapshot_modification_time_(last_modified),
       relative_path_(relative_path) {
   if (has_snapshot_data)
     snapshot_size_ = size;
 }
 
 File::File(const String& name,
-           double modification_time_ms,
+           const base::Optional<base::Time>& modification_time,
            scoped_refptr<BlobDataHandle> blob_data_handle)
     : Blob(std::move(blob_data_handle)),
       has_backing_file_(false),
       user_visibility_(File::kIsNotUserVisible),
       name_(name),
       snapshot_size_(Blob::size()),
-      snapshot_modification_time_ms_(modification_time_ms) {
+      snapshot_modification_time_(modification_time) {
   uint64_t size = Blob::size();
   if (size != std::numeric_limits<uint64_t>::max())
     snapshot_size_ = size;
@@ -254,8 +256,7 @@
       user_visibility_(user_visibility),
       path_(metadata.platform_path),
       name_(name),
-      snapshot_modification_time_ms_(
-          ToJsTimeOrNaN(metadata.modification_time)) {
+      snapshot_modification_time_(metadata.modification_time) {
   if (metadata.length >= 0)
     snapshot_size_ = metadata.length;
 }
@@ -271,8 +272,7 @@
       name_(DecodeURLEscapeSequences(file_system_url.LastPathComponent(),
                                      DecodeURLMode::kUTF8OrIsomorphic)),
       file_system_url_(file_system_url),
-      snapshot_modification_time_ms_(
-          ToJsTimeOrNaN(metadata.modification_time)) {
+      snapshot_modification_time_(metadata.modification_time) {
   if (metadata.length >= 0)
     snapshot_size_ = metadata.length;
 }
@@ -285,7 +285,7 @@
       name_(other.name_),
       file_system_url_(other.file_system_url_),
       snapshot_size_(other.snapshot_size_),
-      snapshot_modification_time_ms_(other.snapshot_modification_time_ms_),
+      snapshot_modification_time_(other.snapshot_modification_time_),
       relative_path_(other.relative_path_) {}
 
 File* File::Clone(const String& name) const {
@@ -295,49 +295,31 @@
   return file;
 }
 
-double File::LastModifiedMS() const {
-  if (HasValidSnapshotMetadata() &&
-      IsValidFileTime(snapshot_modification_time_ms_))
-    return snapshot_modification_time_ms_;
+base::Time File::LastModifiedTime() const {
+  if (HasValidSnapshotMetadata() && snapshot_modification_time_)
+    return *snapshot_modification_time_;
 
   base::Optional<base::Time> modification_time;
   if (HasBackingFile() && GetFileModificationTime(path_, modification_time) &&
       modification_time)
-    return modification_time->ToJsTimeIgnoringNull();
+    return *modification_time;
 
-  return base::Time::Now().ToDoubleT() * 1000.0;
+  // lastModified / lastModifiedDate getters should return the current time
+  // when the last modification time isn't known.
+  return base::Time::Now();
 }
 
 int64_t File::lastModified() const {
-  double modified_date = LastModifiedMS();
-
-  // The getter should return the current time when the last modification time
-  // isn't known.
-  if (!IsValidFileTime(modified_date))
-    modified_date = base::Time::Now().ToDoubleT() * 1000.0;
-
   // lastModified returns a number, not a Date instance,
   // http://dev.w3.org/2006/webapi/FileAPI/#file-attrs
-  return floor(modified_date);
+  return (LastModifiedTime() - base::Time::UnixEpoch()).InMilliseconds();
 }
 
 ScriptValue File::lastModifiedDate(ScriptState* script_state) const {
   // lastModifiedDate returns a Date instance,
   // http://www.w3.org/TR/FileAPI/#dfn-lastModifiedDate
-  return ScriptValue(
-      script_state->GetIsolate(),
-      ToV8(base::Time::FromJsTime(LastModifiedDate()), script_state));
-}
-
-double File::LastModifiedDate() const {
-  double modified_date = LastModifiedMS();
-
-  // The getter should return the current time when the last modification time
-  // isn't known.
-  if (!IsValidFileTime(modified_date))
-    modified_date = base::Time::Now().ToDoubleT() * 1000.0;
-
-  return modified_date;
+  return ScriptValue(script_state->GetIsolate(),
+                     ToV8(LastModifiedTime(), script_state));
 }
 
 uint64_t File::size() const {
@@ -380,8 +362,7 @@
     base::Optional<base::Time>& snapshot_modification_time) const {
   if (HasValidSnapshotMetadata()) {
     snapshot_size = *snapshot_size_;
-    snapshot_modification_time =
-        JsTimeToOptionalTime(snapshot_modification_time_ms_);
+    snapshot_modification_time = snapshot_modification_time_;
     return;
   }
 
diff --git a/third_party/blink/renderer/core/fileapi/file.h b/third_party/blink/renderer/core/fileapi/file.h
index acd4d5ad..93aae77 100644
--- a/third_party/blink/renderer/core/fileapi/file.h
+++ b/third_party/blink/renderer/core/fileapi/file.h
@@ -26,7 +26,10 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_FILEAPI_FILE_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_FILEAPI_FILE_H_
 
+#include "base/gtest_prod_util.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/optional.h"
+#include "base/time/time.h"
 #include "third_party/blink/renderer/bindings/core/v8/array_buffer_or_array_buffer_view_or_blob_or_usv_string.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/fileapi/blob.h"
@@ -74,7 +77,7 @@
       UserVisibility user_visibility,
       bool has_snapshot_data,
       uint64_t size,
-      double last_modified,
+      const base::Optional<base::Time>& last_modified,
       scoped_refptr<BlobDataHandle> blob_data_handle) {
     return MakeGarbageCollected<File>(
         path, name, relative_path, user_visibility, has_snapshot_data, size,
@@ -84,7 +87,7 @@
       const String& path,
       const String& name,
       uint64_t size,
-      double last_modified,
+      const base::Optional<base::Time>& last_modified,
       scoped_refptr<BlobDataHandle> blob_data_handle) {
     return MakeGarbageCollected<File>(path, name, String(), kIsNotUserVisible,
                                       true, size, last_modified,
@@ -132,10 +135,10 @@
        UserVisibility,
        bool has_snapshot_data,
        uint64_t size,
-       double last_modified,
+       const base::Optional<base::Time>& last_modified,
        scoped_refptr<BlobDataHandle>);
   File(const String& name,
-       double modification_time,
+       const base::Optional<base::Time>& modification_time,
        scoped_refptr<BlobDataHandle>);
   File(const String& name, const FileMetadata&, UserVisibility);
   File(const KURL& file_system_url, const FileMetadata&, UserVisibility);
@@ -198,8 +201,6 @@
   // Getter for the lastModifiedDate IDL attribute,
   // http://www.w3.org/TR/FileAPI/#dfn-lastModifiedDate
   ScriptValue lastModifiedDate(ScriptState* script_state) const;
-  // Returns milliseconds from the Unix epoch.
-  double LastModifiedDate() const;
 
   UserVisibility GetUserVisibility() const { return user_visibility_; }
 
@@ -214,7 +215,7 @@
       base::Optional<base::Time>& snapshot_modification_time) const;
 
   // Returns true if this has a valid snapshot metadata
-  // (i.e. m_snapshotSize >= 0).
+  // (i.e. snapshot_size_.has_value()).
   bool HasValidSnapshotMetadata() const { return snapshot_size_.has_value(); }
 
   // Returns true if the sources (file path, file system URL, or blob handler)
@@ -227,9 +228,9 @@
  private:
   void InvalidateSnapshotMetadata() { snapshot_size_.reset(); }
 
-  // Returns File's last modified time (in MS since Epoch.)
+  // Returns File's last modified time.
   // If the modification time isn't known, the current time is returned.
-  double LastModifiedMS() const;
+  base::Time LastModifiedTime() const;
 
 #if DCHECK_IS_ON()
   // Instances backed by a file must have an empty file system URL.
@@ -247,14 +248,22 @@
 
   KURL file_system_url_;
 
-  // If m_snapshotSize is negative (initialized to -1 by default), the snapshot
-  // metadata is invalid and we retrieve the latest metadata synchronously in
-  // size(), lastModifiedTime() and slice().
+  // If snapshot_size_ has no value, the snapshot metadata is invalid and
+  // we retrieve the latest metadata synchronously in size(),
+  // LastModifiedTime() and slice().
   // Otherwise, the snapshot metadata are used directly in those methods.
   base::Optional<uint64_t> snapshot_size_;
-  const double snapshot_modification_time_ms_;
+  const base::Optional<base::Time> snapshot_modification_time_;
 
   String relative_path_;
+
+  FRIEND_TEST_ALL_PREFIXES(FileTest, NativeFileWithoutTimestamp);
+  FRIEND_TEST_ALL_PREFIXES(FileTest, NativeFileWithUnixEpochTimestamp);
+  FRIEND_TEST_ALL_PREFIXES(FileTest, NativeFileWithApocalypseTimestamp);
+  FRIEND_TEST_ALL_PREFIXES(V8ScriptValueSerializerTest,
+                           DecodeFileV4WithSnapshot);
+  FRIEND_TEST_ALL_PREFIXES(V8ScriptValueSerializerTest,
+                           DecodeFileV8WithSnapshot);
 };
 
 template <>
diff --git a/third_party/blink/renderer/core/fileapi/file_list_test.cc b/third_party/blink/renderer/core/fileapi/file_list_test.cc
index a616fce..6190cc6 100644
--- a/third_party/blink/renderer/core/fileapi/file_list_test.cc
+++ b/third_party/blink/renderer/core/fileapi/file_list_test.cc
@@ -4,6 +4,7 @@
 
 #include "third_party/blink/renderer/core/fileapi/file_list.h"
 
+#include "base/time/time.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/renderer/platform/file_metadata.h"
 
@@ -18,7 +19,8 @@
   // Blob file.
   const scoped_refptr<BlobDataHandle> blob_data_handle =
       BlobDataHandle::Create();
-  file_list->Append(MakeGarbageCollected<File>("name", 0.0, blob_data_handle));
+  file_list->Append(MakeGarbageCollected<File>("name", base::Time::UnixEpoch(),
+                                               blob_data_handle));
 
   // User visible snapshot file.
   {
diff --git a/third_party/blink/renderer/core/fileapi/file_test.cc b/third_party/blink/renderer/core/fileapi/file_test.cc
index e787a5e..e9ecaf11 100644
--- a/third_party/blink/renderer/core/fileapi/file_test.cc
+++ b/third_party/blink/renderer/core/fileapi/file_test.cc
@@ -53,16 +53,6 @@
   base::File::Info file_info_;
 };
 
-void ExpectLastModifiedIsNow(const File& file) {
-  const base::Time now = base::Time::Now();
-  const base::Time epoch = base::Time::UnixEpoch();
-  // Because lastModified() applies floor() internally, it can be smaller than
-  // |now|. |+ 1| adjusts it.
-  EXPECT_GE(epoch + base::TimeDelta::FromMilliseconds(file.lastModified() + 1),
-            now);
-  EXPECT_GE(file.LastModifiedDate() + 1, now.ToJsTime());
-}
-
 }  // namespace
 
 TEST(FileTest, NativeFileWithoutTimestamp) {
@@ -75,7 +65,13 @@
   EXPECT_TRUE(file->HasBackingFile());
   EXPECT_EQ("/native/path", file->GetPath());
   EXPECT_TRUE(file->FileSystemURL().IsEmpty());
-  ExpectLastModifiedIsNow(*file);
+
+  const base::Time now = base::Time::Now();
+  const base::TimeDelta delta_now = now - base::Time::UnixEpoch();
+  // Because lastModified() applies floor() internally, we should compare
+  // integral millisecond values.
+  EXPECT_GE(file->lastModified(), delta_now.InMilliseconds());
+  EXPECT_GE(file->LastModifiedTime(), now);
 }
 
 TEST(FileTest, NativeFileWithUnixEpochTimestamp) {
@@ -87,7 +83,7 @@
   auto* const file = MakeGarbageCollected<File>("/native/path");
   EXPECT_TRUE(file->HasBackingFile());
   EXPECT_EQ(0, file->lastModified());
-  EXPECT_EQ(0.0, file->LastModifiedDate());
+  EXPECT_EQ(base::Time::UnixEpoch(), file->LastModifiedTime());
 }
 
 TEST(FileTest, NativeFileWithApocalypseTimestamp) {
@@ -99,16 +95,16 @@
   auto* const file = MakeGarbageCollected<File>("/native/path");
   EXPECT_TRUE(file->HasBackingFile());
 
-  ExpectLastModifiedIsNow(*file);
-  // Actually, the timestamp should not be |now|.
-  // EXPECT_EQ(base::Time::Max() - base::Time::UnixEpoch(),
-  //           base::TimeDelta::FromMilliseconds(file->lastModified()));
+  EXPECT_EQ((base::Time::Max() - base::Time::UnixEpoch()).InMilliseconds(),
+            file->lastModified());
+  EXPECT_EQ(base::Time::Max(), file->LastModifiedTime());
 }
 
 TEST(FileTest, blobBackingFile) {
   const scoped_refptr<BlobDataHandle> blob_data_handle =
       BlobDataHandle::Create();
-  auto* const file = MakeGarbageCollected<File>("name", 0.0, blob_data_handle);
+  auto* const file = MakeGarbageCollected<File>("name", base::Time::UnixEpoch(),
+                                                blob_data_handle);
   EXPECT_FALSE(file->HasBackingFile());
   EXPECT_TRUE(file->GetPath().IsEmpty());
   EXPECT_TRUE(file->FileSystemURL().IsEmpty());
@@ -152,12 +148,13 @@
 
   const scoped_refptr<BlobDataHandle> blob_data_a = BlobDataHandle::Create();
   const scoped_refptr<BlobDataHandle> blob_data_b = BlobDataHandle::Create();
+  const base::Time kEpoch = base::Time::UnixEpoch();
   auto* const blob_file_a1 =
-      MakeGarbageCollected<File>("name", 0.0, blob_data_a);
+      MakeGarbageCollected<File>("name", kEpoch, blob_data_a);
   auto* const blob_file_a2 =
-      MakeGarbageCollected<File>("name", 0.0, blob_data_a);
+      MakeGarbageCollected<File>("name", kEpoch, blob_data_a);
   auto* const blob_file_b =
-      MakeGarbageCollected<File>("name", 0.0, blob_data_b);
+      MakeGarbageCollected<File>("name", kEpoch, blob_data_b);
 
   KURL url_a("filesystem:http://example.com/isolated/hash/non-native-file-A");
   KURL url_b("filesystem:http://example.com/isolated/hash/non-native-file-B");
diff --git a/third_party/blink/renderer/core/html/forms/form_data.cc b/third_party/blink/renderer/core/html/forms/form_data.cc
index 8083925..4ef6dba 100644
--- a/third_party/blink/renderer/core/html/forms/form_data.cc
+++ b/third_party/blink/renderer/core/html/forms/form_data.cc
@@ -360,8 +360,7 @@
   String filename = filename_;
   if (filename.IsNull())
     filename = "blob";
-  return MakeGarbageCollected<File>(filename,
-                                    base::Time::Now().ToDoubleT() * 1000.0,
+  return MakeGarbageCollected<File>(filename, base::Time::Now(),
                                     GetBlob()->GetBlobDataHandle());
 }
 
diff --git a/third_party/blink/renderer/modules/native_file_system/native_file_system_file_handle.cc b/third_party/blink/renderer/modules/native_file_system/native_file_system_file_handle.cc
index 535c082..369ebd2a 100644
--- a/third_party/blink/renderer/modules/native_file_system/native_file_system_file_handle.cc
+++ b/third_party/blink/renderer/modules/native_file_system/native_file_system_file_handle.cc
@@ -83,11 +83,8 @@
           native_file_system_error::Reject(resolver, *result);
           return;
         }
-        double last_modified = info.last_modified.is_null()
-                                   ? InvalidFileTime()
-                                   : info.last_modified.ToJsTime();
-        resolver->Resolve(
-            MakeGarbageCollected<File>(name, last_modified, blob));
+        resolver->Resolve(MakeGarbageCollected<File>(
+            name, NullableTimeToOptionalTime(info.last_modified), blob));
       },
       WrapPersistent(resolver), name()));
 
diff --git a/third_party/blink/renderer/platform/file_metadata.h b/third_party/blink/renderer/platform/file_metadata.h
index 162b95c..08c52dd 100644
--- a/third_party/blink/renderer/platform/file_metadata.h
+++ b/third_party/blink/renderer/platform/file_metadata.h
@@ -41,15 +41,6 @@
 
 namespace blink {
 
-// TODO(crbug.com/988343): InvalidFileTime() and IsValidFileTime() should be
-// removed. File timestamps should be represented as base::Optional<base::Time>.
-inline double InvalidFileTime() {
-  return std::numeric_limits<double>::quiet_NaN();
-}
-inline bool IsValidFileTime(double time) {
-  return std::isfinite(time);
-}
-
 class FileMetadata {
   DISALLOW_NEW();
 
@@ -81,20 +72,6 @@
 
 PLATFORM_EXPORT void RebindFileUtilitiesForTesting();
 
-// TODO(crbug.com/988343): Temporary conversion function. This should be
-// removed.
-inline double ToJsTimeOrNaN(base::Optional<base::Time> time) {
-  return time ? time->ToJsTimeIgnoringNull() : InvalidFileTime();
-}
-
-// TODO(crbug.com/988343): Temporary conversion function. This should be
-// removed.
-inline base::Optional<base::Time> JsTimeToOptionalTime(double ms) {
-  if (!IsValidFileTime(ms))
-    return base::nullopt;
-  return base::Time::FromJsTime(ms);
-}
-
 inline base::Optional<base::Time> NullableTimeToOptionalTime(base::Time time) {
   if (time.is_null())
     return base::nullopt;