Skip to content

Commit

Permalink
Bug 1158399 - Expose the [[DateValue]] field in Date objects only thr…
Browse files Browse the repository at this point in the history
…ough a ClippedTime class that enforces prior TimeClip-ing on the given value. r=evilpie, r=bz, r=dhylands, r=mt, r=froydnj, r=khuey, r=baku, r=smaug

--HG--
extra : rebase_source : 803fe39f338b6b32cb0fe2be6cf4056bab57283a
  • Loading branch information
jswalden committed May 2, 2015
1 parent 0cacb70 commit 1b62a9d
Show file tree
Hide file tree
Showing 18 changed files with 175 additions and 80 deletions.
2 changes: 1 addition & 1 deletion dom/base/File.cpp
Expand Up @@ -526,7 +526,7 @@ File::GetLastModifiedDate(ErrorResult& aRv)
return Date();
}

return Date(value);
return Date(JS::TimeClip(value));
}

int64_t
Expand Down
21 changes: 7 additions & 14 deletions dom/bindings/Date.cpp
Expand Up @@ -6,39 +6,32 @@

#include "mozilla/dom/Date.h"

#include "jsapi.h" // for JS_ObjectIsDate, JS_NewDateObjectMsec
#include "jsapi.h" // for JS_ObjectIsDate
#include "jsfriendapi.h" // for DateGetMsecSinceEpoch
#include "js/Date.h" // for JS::NewDateObject, JS::ClippedTime, JS::TimeClip
#include "js/RootingAPI.h" // for Rooted, MutableHandle
#include "js/Value.h" // for Value
#include "mozilla/FloatingPoint.h" // for IsNaN, UnspecifiedNaN

namespace mozilla {
namespace dom {

Date::Date()
: mMsecSinceEpoch(UnspecifiedNaN<double>())
{
}

bool
Date::IsUndefined() const
{
return IsNaN(mMsecSinceEpoch);
}

bool
Date::SetTimeStamp(JSContext* aCx, JSObject* aObject)
{
JS::Rooted<JSObject*> obj(aCx, aObject);
MOZ_ASSERT(JS_ObjectIsDate(aCx, obj));
mMsecSinceEpoch = js::DateGetMsecSinceEpoch(aCx, obj);
double msecs = js::DateGetMsecSinceEpoch(aCx, obj);
JS::ClippedTime time = JS::TimeClip(msecs);
MOZ_ASSERT(NumbersAreIdentical(msecs, time.toDouble()));
mMsecSinceEpoch = time;
return true;
}

bool
Date::ToDateObject(JSContext* aCx, JS::MutableHandle<JS::Value> aRval) const
{
JSObject* obj = JS_NewDateObjectMsec(aCx, mMsecSinceEpoch);
JSObject* obj = JS::NewDateObject(aCx, mMsecSinceEpoch);
if (!obj) {
return false;
}
Expand Down
27 changes: 20 additions & 7 deletions dom/bindings/Date.h
Expand Up @@ -9,6 +9,7 @@
#ifndef mozilla_dom_Date_h
#define mozilla_dom_Date_h

#include "js/Date.h"
#include "js/TypeDecls.h"

namespace mozilla {
Expand All @@ -17,29 +18,41 @@ namespace dom {
class Date
{
public:
// Not inlining much here to avoid the includes we'd need.
Date();
explicit Date(double aMilliseconds)
Date() {}
explicit Date(JS::ClippedTime aMilliseconds)
: mMsecSinceEpoch(aMilliseconds)
{}

bool IsUndefined() const;
double TimeStamp() const
bool IsUndefined() const
{
return !mMsecSinceEpoch.isValid();
}

JS::ClippedTime TimeStamp() const
{
return mMsecSinceEpoch;
}
void SetTimeStamp(double aMilliseconds)

// Returns an integer in the range [-8.64e15, +8.64e15] (-0 excluded), *or*
// returns NaN. DO NOT ASSUME THIS IS FINITE!
double ToDouble() const
{
return mMsecSinceEpoch.toDouble();
}

void SetTimeStamp(JS::ClippedTime aMilliseconds)
{
mMsecSinceEpoch = aMilliseconds;
}

// Can return false if CheckedUnwrap fails. This will NOT throw;
// callers should do it as needed.
bool SetTimeStamp(JSContext* aCx, JSObject* aObject);

bool ToDateObject(JSContext* aCx, JS::MutableHandle<JS::Value> aRval) const;

private:
double mMsecSinceEpoch;
JS::ClippedTime mMsecSinceEpoch;
};

} // namespace dom
Expand Down
2 changes: 1 addition & 1 deletion dom/devicestorage/nsDeviceStorage.cpp
Expand Up @@ -4319,7 +4319,7 @@ nsDOMDeviceStorage::EnumerateInternal(const nsAString& aPath,

PRTime since = 0;
if (aOptions.mSince.WasPassed() && !aOptions.mSince.Value().IsUndefined()) {
since = PRTime(aOptions.mSince.Value().TimeStamp());
since = PRTime(aOptions.mSince.Value().TimeStamp().toDouble());
}

nsRefPtr<DeviceStorageFile> dsf = new DeviceStorageFile(mStorageType,
Expand Down
3 changes: 2 additions & 1 deletion dom/filehandle/MetadataHelper.cpp
Expand Up @@ -10,6 +10,7 @@
#include "js/Value.h"
#include "js/RootingAPI.h"
#include "jsapi.h"
#include "js/Date.h"
#include "mozilla/dom/FileModeBinding.h"
#include "nsDebug.h"
#include "nsIFileStreams.h"
Expand Down Expand Up @@ -49,7 +50,7 @@ MetadataHelper::GetSuccessResult(JSContext* aCx,

if (mParams->LastModifiedRequested()) {
double msec = mParams->LastModified();
JSObject *date = JS_NewDateObjectMsec(aCx, msec);
JSObject *date = JS::NewDateObject(aCx, JS::TimeClip(msec));
NS_ENSURE_TRUE(date, NS_ERROR_OUT_OF_MEMORY);

JS::Rooted<JS::Value> dateRoot(aCx, JS::ObjectValue(*date));
Expand Down
17 changes: 11 additions & 6 deletions dom/html/HTMLInputElement.cpp
Expand Up @@ -1514,12 +1514,12 @@ HTMLInputElement::ConvertStringToNumber(nsAString& aValue,
return false;
}

double date = JS::MakeDate(year, month - 1, day);
if (IsNaN(date)) {
JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day));
if (!time.isValid()) {
return false;
}

aResultValue = Decimal::fromDouble(date);
aResultValue = Decimal::fromDouble(time.toDouble());
return true;
}
case NS_FORM_INPUT_TIME:
Expand Down Expand Up @@ -1762,7 +1762,8 @@ HTMLInputElement::GetValueAsDate(ErrorResult& aRv)
return Nullable<Date>();
}

return Nullable<Date>(Date(JS::MakeDate(year, month - 1, day)));
JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day));
return Nullable<Date>(Date(time));
}
case NS_FORM_INPUT_TIME:
{
Expand All @@ -1773,7 +1774,11 @@ HTMLInputElement::GetValueAsDate(ErrorResult& aRv)
return Nullable<Date>();
}

return Nullable<Date>(Date(millisecond));
JS::ClippedTime time = JS::TimeClip(millisecond);
MOZ_ASSERT(time.toDouble() == millisecond,
"HTML times are restricted to the day after the epoch and "
"never clip");
return Nullable<Date>(Date(time));
}
}

Expand All @@ -1795,7 +1800,7 @@ HTMLInputElement::SetValueAsDate(Nullable<Date> aDate, ErrorResult& aRv)
return;
}

SetValue(Decimal::fromDouble(aDate.Value().TimeStamp()));
SetValue(Decimal::fromDouble(aDate.Value().TimeStamp().toDouble()));
}

NS_IMETHODIMP
Expand Down
5 changes: 3 additions & 2 deletions dom/indexedDB/IDBObjectStore.cpp
Expand Up @@ -20,6 +20,7 @@
#include "IndexedDatabaseInlines.h"
#include "IndexedDatabaseManager.h"
#include "js/Class.h"
#include "js/Date.h"
#include "js/StructuredClone.h"
#include "KeyPath.h"
#include "mozilla/Endian.h"
Expand Down Expand Up @@ -749,8 +750,8 @@ class IndexDeserializationHelper
return false;
}

JS::Rooted<JSObject*> date(aCx,
JS_NewDateObjectMsec(aCx, aData.lastModifiedDate));
JS::ClippedTime time = JS::TimeClip(aData.lastModifiedDate);
JS::Rooted<JSObject*> date(aCx, JS::NewDateObject(aCx, time));
if (NS_WARN_IF(!date)) {
return false;
}
Expand Down
7 changes: 6 additions & 1 deletion dom/indexedDB/Key.cpp
Expand Up @@ -8,6 +8,7 @@
#include "Key.h"

#include <algorithm>
#include "js/Date.h"
#include "js/Value.h"
#include "jsfriendapi.h"
#include "mozilla/Endian.h"
Expand Down Expand Up @@ -237,7 +238,11 @@ Key::DecodeJSValInternal(const unsigned char*& aPos, const unsigned char* aEnd,
}
else if (*aPos - aTypeOffset == eDate) {
double msec = static_cast<double>(DecodeNumber(aPos, aEnd));
JSObject* date = JS_NewDateObjectMsec(aCx, msec);
JS::ClippedTime time = JS::TimeClip(msec);
MOZ_ASSERT(msec == time.toDouble(),
"encoding from a Date object not containing an invalid date "
"means we should always have clipped values");
JSObject* date = JS::NewDateObject(aCx, time);
if (!date) {
IDB_WARNING("Failed to make date!");
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
Expand Down
6 changes: 5 additions & 1 deletion dom/media/webrtc/RTCCertificate.h
Expand Up @@ -21,6 +21,7 @@
#include "mozilla/dom/Date.h"
#include "mozilla/dom/CryptoKey.h"
#include "mtransport/dtlsidentity.h"
#include "js/Date.h"
#include "js/StructuredClone.h"
#include "js/TypeDecls.h"

Expand Down Expand Up @@ -54,7 +55,10 @@ class RTCCertificate final

// WebIDL expires attribute. Note: JS dates are milliseconds since epoch;
// NSPR PRTime is in microseconds since the same epoch.
int64_t Expires() const { return mExpires / PR_USEC_PER_MSEC; }
JS::ClippedTime Expires() const
{
return JS::TimeClip(mExpires / PR_USEC_PER_MSEC);
}

// Accessors for use by PeerConnectionImpl.
RefPtr<DtlsIdentity> CreateDtlsIdentity() const;
Expand Down
2 changes: 1 addition & 1 deletion dom/time/TimeManager.cpp
Expand Up @@ -34,7 +34,7 @@ TimeManager::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
void
TimeManager::Set(Date& aDate)
{
Set(aDate.TimeStamp());
Set(aDate.ToDouble());
}

void
Expand Down
107 changes: 87 additions & 20 deletions js/public/Date.h
Expand Up @@ -3,51 +3,118 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* JavaScript date/time computation and creation functions. */

#ifndef js_Date_h
#define js_Date_h

/*
* Dates in JavaScript are defined by IEEE-754 double precision numbers from
* the set:
*
* { t ∈ ℕ : -8.64e15 ≤ t ≤ +8.64e15 } ∪ { NaN }
*
* The single NaN value represents any invalid-date value. All other values
* represent idealized durations in milliseconds since the UTC epoch. (Leap
* seconds are ignored; leap days are not.) +0 is the only zero in this set.
* The limit represented by 8.64e15 milliseconds is 100 million days either
* side of 00:00 January 1, 1970 UTC.
*
* Dates in the above set are represented by the |ClippedTime| class. The
* double type is a superset of the above set, so it *may* (but need not)
* represent a date. Use ECMAScript's |TimeClip| method to produce a date from
* a double.
*
* Date *objects* are simply wrappers around |TimeClip|'d numbers, with a bunch
* of accessor methods to the various aspects of the represented date.
*/

#include "mozilla/FloatingPoint.h"
#include "mozilla/MathAlgorithms.h"

#include "jstypes.h"

#include "js/Conversions.h"
#include "js/Value.h"

struct JSContext;

namespace JS {

class ClippedTime;
inline ClippedTime TimeClip(double time);

/*
* |ClippedTime| represents the limited subset of dates/times described above.
*
* An invalid date/time may be created through the |ClippedTime::invalid|
* method. Otherwise, a |ClippedTime| may be created using the |TimeClip|
* method.
*
* In typical use, the user might wish to manipulate a timestamp. The user
* performs a series of operations on it, but the final value might not be a
* date as defined above -- it could have overflowed, acquired a fractional
* component, &c. So as a *final* step, the user passes that value through
* |TimeClip| to produce a number restricted to JavaScript's date range.
*
* APIs that accept a JavaScript date value thus accept a |ClippedTime|, not a
* double. This ensures that date/time APIs will only ever receive acceptable
* JavaScript dates. This also forces users to perform any desired clipping,
* as only the user knows what behavior is desired when clipping occurs.
*/
class ClippedTime
{
double t;

/* ES5 15.9.1.14. */
double timeClip(double time) {
/* Steps 1-2. */
const double MaxTimeMagnitude = 8.64e15;
if (!mozilla::IsFinite(time) || mozilla::Abs(time) > MaxTimeMagnitude)
return JS::GenericNaN();

/* Step 3. */
return JS::ToInteger(time) + (+0.0);
}
explicit ClippedTime(double time) : t(time) {}
friend ClippedTime TimeClip(double time);

public:
ClippedTime() : t(JS::GenericNaN()) {}
explicit ClippedTime(double time) : t(timeClip(time)) {}
// Create an invalid date.
ClippedTime() : t(mozilla::UnspecifiedNaN<double>()) {}

static ClippedTime NaN() { return ClippedTime(); }
// Create an invalid date/time, more explicitly; prefer this to the default
// constructor.
static ClippedTime invalid() { return ClippedTime(); }

double value() const { return t; }
double toDouble() const { return t; }

bool isValid() const { return !mozilla::IsNaN(t); }
};

// ES6 20.3.1.15.
//
// Clip a double to JavaScript's date range (or to an invalid date) using the
// ECMAScript TimeClip algorithm.
inline ClippedTime
TimeClip(double d)
TimeClip(double time)
{
// Steps 1-2.
const double MaxTimeMagnitude = 8.64e15;
if (!mozilla::IsFinite(time) || mozilla::Abs(time) > MaxTimeMagnitude)
return ClippedTime(mozilla::UnspecifiedNaN<double>());

// Step 3.
return ClippedTime(ToInteger(time) + (+0.0));
}

// Produce a double Value from the given time. Because times may be NaN,
// prefer using this to manual canonicalization.
inline Value
TimeValue(ClippedTime time)
{
return ClippedTime(d);
return DoubleValue(JS::CanonicalizeNaN(time.toDouble()));
}

// Year is a year, month is 0-11, day is 1-based. The return value is
// a number of milliseconds since the epoch. Can return NaN.
// Create a new Date object whose [[DateValue]] internal slot contains the
// clipped |time|. (Users who must represent times outside that range must use
// another representation.)
extern JS_PUBLIC_API(JSObject*)
NewDateObject(JSContext* cx, ClippedTime time);

// Year is a year, month is 0-11, day is 1-based. The return value is a number
// of milliseconds since the epoch.
//
// Consistent with the MakeDate algorithm defined in ECMAScript, this value is
// *not* clipped! Use JS::TimeClip if you need a clipped date.
JS_PUBLIC_API(double)
MakeDate(double year, unsigned month, unsigned day);

Expand Down

0 comments on commit 1b62a9d

Please sign in to comment.