Skip to content

Commit f90bcc3

Browse files
authored
feat!(NODE-4706): validate Timestamp ctor argument (#536)
1 parent 8511225 commit f90bcc3

19 files changed

+397
-150
lines changed

.evergreen/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ tasks:
139139
- func: "run typescript"
140140
vars:
141141
TS_VERSION: "4.0.2"
142-
TRY_COMPILING_LIBRARY: "true"
142+
TRY_COMPILING_LIBRARY: "false"
143143
- name: check-typescript-current
144144
commands:
145145
- func: fetch source

docs/upgrade-to-v5.md

+33
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,36 @@ BSON.deserialize(BSON.serialize({ d: -0 }))
134134
### Capital "D" ObjectID export removed
135135

136136
For clarity the deprecated and duplicate export `ObjectID` has been removed. `ObjectId` matches the class name and is equal in every way to the capital "D" export.
137+
138+
### Timestamp constructor validation
139+
140+
The `Timestamp` type no longer accepts two number arguments for the low and high bits of the int64 value.
141+
142+
Supported constructors are as follows:
143+
144+
```typescript
145+
class Timestamp {
146+
constructor(int: bigint);
147+
constructor(long: Long);
148+
constructor(value: { t: number; i: number });
149+
}
150+
```
151+
152+
Any code that use the two number argument style of constructing a Timestamp will need to be migrated to one of the supported constructors. We recommend using the `{ t: number; i: number }` style input, representing the timestamp and increment respectively.
153+
154+
```typescript
155+
// in 4.x BSON
156+
new Timestamp(1, 2); // as an int64: 8589934593
157+
// in 5.x BSON
158+
new Timestamp({ t: 2, i: 1 }); // as an int64: 8589934593
159+
```
160+
161+
Additionally, the `t` and `i` fields of `{ t: number; i: number }` are now validated more strictly to ensure your Timestamps are being constructed as expected.
162+
163+
For example:
164+
```typescript
165+
new Timestamp({ t: -2, i: 1 });
166+
// Will throw, both fields need to be positive
167+
new Timestamp({ t: 2, i: 0xFFFF_FFFF + 1 });
168+
// Will throw, both fields need to be less than or equal to the unsigned int32 max value
169+
```

package-lock.json

+41-22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@babel/plugin-external-helpers": "^7.18.6",
3131
"@babel/preset-env": "^7.19.4",
3232
"@istanbuljs/nyc-config-typescript": "^1.0.2",
33-
"@microsoft/api-extractor": "^7.33.5",
33+
"@microsoft/api-extractor": "^7.33.6",
3434
"@rollup/plugin-babel": "^6.0.2",
3535
"@rollup/plugin-commonjs": "^23.0.2",
3636
"@rollup/plugin-json": "^5.0.1",
@@ -63,7 +63,7 @@
6363
"standard-version": "^9.5.0",
6464
"ts-node": "^10.9.1",
6565
"tsd": "^0.24.1",
66-
"typescript": "^4.8.4",
66+
"typescript": "^4.9.3",
6767
"typescript-cached-transpile": "0.0.6",
6868
"uuid": "^9.0.0",
6969
"v8-profiler-next": "^1.9.0"

src/db_ref.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Document } from './bson';
22
import type { EJSONOptions } from './extended_json';
33
import type { ObjectId } from './objectid';
4-
import { isObjectLike } from './parser/utils';
54

65
/** @public */
76
export interface DBRefLike {
@@ -13,10 +12,14 @@ export interface DBRefLike {
1312
/** @internal */
1413
export function isDBRefLike(value: unknown): value is DBRefLike {
1514
return (
16-
isObjectLike(value) &&
15+
value != null &&
16+
typeof value === 'object' &&
17+
'$id' in value &&
1718
value.$id != null &&
19+
'$ref' in value &&
1820
typeof value.$ref === 'string' &&
19-
(value.$db == null || typeof value.$db === 'string')
21+
// If '$db' is defined it MUST be a string, otherwise it should be absent
22+
(!('$db' in value) || ('$db' in value && typeof value.$db === 'string'))
2023
);
2124
}
2225

src/extended_json.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Long } from './long';
1010
import { MaxKey } from './max_key';
1111
import { MinKey } from './min_key';
1212
import { ObjectId } from './objectid';
13-
import { isDate, isObjectLike, isRegExp } from './parser/utils';
13+
import { isDate, isRegExp } from './parser/utils';
1414
import { BSONRegExp } from './regexp';
1515
import { BSONSymbol } from './symbol';
1616
import { Timestamp } from './timestamp';
@@ -36,7 +36,10 @@ type BSONType =
3636

3737
export function isBSONType(value: unknown): value is BSONType {
3838
return (
39-
isObjectLike(value) && Reflect.has(value, '_bsontype') && typeof value._bsontype === 'string'
39+
value != null &&
40+
typeof value === 'object' &&
41+
'_bsontype' in value &&
42+
typeof value._bsontype === 'string'
4043
);
4144
}
4245

src/long.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { EJSONOptions } from './extended_json';
2-
import { isObjectLike } from './parser/utils';
32
import type { Timestamp } from './timestamp';
43

54
interface LongWASMHelpers {
@@ -327,7 +326,12 @@ export class Long {
327326
* Tests if the specified object is a Long.
328327
*/
329328
static isLong(value: unknown): value is Long {
330-
return isObjectLike(value) && value['__isLong__'] === true;
329+
return (
330+
value != null &&
331+
typeof value === 'object' &&
332+
'__isLong__' in value &&
333+
value.__isLong__ === true
334+
);
331335
}
332336

333337
/**

src/parser/deserializer.ts

+15-12
Original file line numberDiff line numberDiff line change
@@ -530,18 +530,21 @@ function deserializeObject(
530530
value = promoteValues ? symbol : new BSONSymbol(symbol);
531531
index = index + stringSize;
532532
} else if (elementType === constants.BSON_DATA_TIMESTAMP) {
533-
const lowBits =
534-
buffer[index++] |
535-
(buffer[index++] << 8) |
536-
(buffer[index++] << 16) |
537-
(buffer[index++] << 24);
538-
const highBits =
539-
buffer[index++] |
540-
(buffer[index++] << 8) |
541-
(buffer[index++] << 16) |
542-
(buffer[index++] << 24);
543-
544-
value = new Timestamp(lowBits, highBits);
533+
// We intentionally **do not** use bit shifting here
534+
// Bit shifting in javascript coerces numbers to **signed** int32s
535+
// We need to keep i, and t unsigned
536+
const i =
537+
buffer[index++] +
538+
buffer[index++] * (1 << 8) +
539+
buffer[index++] * (1 << 16) +
540+
buffer[index++] * (1 << 24);
541+
const t =
542+
buffer[index++] +
543+
buffer[index++] * (1 << 8) +
544+
buffer[index++] * (1 << 16) +
545+
buffer[index++] * (1 << 24);
546+
547+
value = new Timestamp({ i, t });
545548
} else if (elementType === constants.BSON_DATA_MIN_KEY) {
546549
value = new MinKey();
547550
} else if (elementType === constants.BSON_DATA_MAX_KEY) {

src/parser/utils.ts

+1-11
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,6 @@ export function isMap(d: unknown): d is Map<unknown, unknown> {
3232
return Object.prototype.toString.call(d) === '[object Map]';
3333
}
3434

35-
// To ensure that 0.4 of node works correctly
3635
export function isDate(d: unknown): d is Date {
37-
return isObjectLike(d) && Object.prototype.toString.call(d) === '[object Date]';
38-
}
39-
40-
/**
41-
* @internal
42-
* this is to solve the `'someKey' in x` problem where x is unknown.
43-
* https://github.com/typescript-eslint/typescript-eslint/issues/1071#issuecomment-541955753
44-
*/
45-
export function isObjectLike(candidate: unknown): candidate is Record<string, unknown> {
46-
return typeof candidate === 'object' && candidate !== null;
36+
return Object.prototype.toString.call(d) === '[object Date]';
4737
}

0 commit comments

Comments
 (0)