Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: improve argument handling for ReadStream #19898

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e507f93
fs: improve argument handling for ReadStream
ryzokuken Apr 9, 2018
30c285f
test,fs: update tests for improvements in ReadStream
ryzokuken Apr 10, 2018
915d77c
Remove inconsistency
ryzokuken Apr 10, 2018
17d6d55
Separate testing for start and end
ryzokuken Apr 10, 2018
36574f4
Remove invalid check
ryzokuken Apr 10, 2018
e4415d5
Address comments regarding handling undefined
ryzokuken Apr 10, 2018
b946616
Refactor conditions
ryzokuken Apr 11, 2018
d00b46a
fs,doc: add documentation regarding changes to createReadStream
ryzokuken Apr 13, 2018
93b0915
Add a changes block and move new documentation towards the end
ryzokuken Apr 14, 2018
5d03199
Replace version by REPLACEME
ryzokuken Apr 14, 2018
047529b
Minimize comments and documentation
ryzokuken Apr 15, 2018
a90457b
Address nit by adding parens
ryzokuken Apr 15, 2018
c3be59a
Resolve merge conflict
ryzokuken Apr 16, 2018
41b3a2e
Thow RangeError on fractional values
ryzokuken Apr 20, 2018
3530baa
Cleanup fs.md
ryzokuken Apr 23, 2018
4863dba
Fix error
ryzokuken Apr 24, 2018
1cf1814
Fix failures
ryzokuken Apr 25, 2018
2c35628
Address nit
ryzokuken Apr 26, 2018
a24f115
Use isSafeInteger over isInteger
ryzokuken Apr 27, 2018
0368397
Change error message
ryzokuken Apr 29, 2018
b6fa515
Improve error handling
ryzokuken May 5, 2018
7bda63c
Refactor fs tests for brevity
ryzokuken May 5, 2018
c34ba43
Address nits
ryzokuken May 8, 2018
30e36c2
Update RangeError error message
ryzokuken May 9, 2018
9af225c
Update RangeError error message v2
ryzokuken May 9, 2018
c76e75a
Fix lint errors
ryzokuken May 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,11 @@ fs.copyFileSync('source.txt', 'destination.txt', COPYFILE_EXCL);
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/19898
description: Impose new restrictions on `start` and `end`, throwing
more appropriate errors in cases when we cannot reasonably
handle the input values.
- version: v7.6.0
pr-url: https://github.com/nodejs/node/pull/10739
description: The `path` parameter can be a WHATWG `URL` object using
Expand Down
47 changes: 33 additions & 14 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2014,30 +2014,49 @@ function ReadStream(path, options) {
this.closed = false;

if (this.start !== undefined) {
if (typeof this.start !== 'number' || Number.isNaN(this.start)) {
if (typeof this.start !== 'number') {
throw new ERR_INVALID_ARG_TYPE('start', 'number', this.start);
}
if (this.end === undefined) {
this.end = Infinity;
} else if (typeof this.end !== 'number' || Number.isNaN(this.end)) {
throw new ERR_INVALID_ARG_TYPE('end', 'number', this.end);

if (Number.isNaN(this.start)) {
Copy link
Contributor

@ronkorving ronkorving Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With it throwing if not an integer, this check technically doesn't have to happen. NaN is not an integer according to Number.isInteger. The typeof this.start !== 'number' test can also be removed for the same reason. Perhaps that keeps it simpler?

The same applies to this.end below.

As far as error codes go, I think it would be fair to just use ERR_INVALID_ARG_TYPE if not an integer, and ERR_OUT_OF_RANGE if < 0. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather throw a little more descriptively, let the user know what went wrong.

That said, I'd love to hear everyone's opinions on this, and would love to change this accordingly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple PRs that consolidate validation functions. It is possible to improve the check here performance and code wise but functionality wise it is fine and I believe it is best to land it as is. It can still be improved later on.

Copy link
Contributor

@ronkorving ronkorving Apr 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BridgeAR The only problem with "improving later" may be that some cases at one point may report out-of-range and after an update would report invalid-arg-type. For example, at this point I was suggesting to make the entire test the following:

if (this.start !== undefined) {
  if (!Number.isInteger(this.start)) {
    throw new ERR_INVALID_ARG_TYPE('start', 'integer', this.start);
  }

  if (this.start < 0) {
    throw new ERR_OUT_OF_RANGE('start', 'positive', this.start);
  }

  this.pos = this.start;
}

So that changes NaN and floats to invalid-arg-type from the code as it is now. If that were to ever happen in the future, that would constitute another semver-major, wouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if you think about it, NaN and floats aren't the wrong type, are they? They're still Numbers.

They are invalid arguments because they lie outside of the rational input range.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronkorving we only throw ERR_INVALID_ARG_TYPE in case the type is really a mismatch. As @ryzokuken pointed out NaN and floats are of type number. So they should be ERR_OUT_OF_RANGE.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize the type was only allowed to be a JS type, not a type in the broader sense (like integer).

throw new ERR_OUT_OF_RANGE('start', 'not NaN', this.start);
}

if (this.start > this.end) {
const errVal = `{start: ${this.start}, end: ${this.end}}`;
throw new ERR_OUT_OF_RANGE('start', '<= "end"', errVal);
if (this.start < 0) {
throw new ERR_OUT_OF_RANGE('start', 'positive', this.start);
}

if (!Number.isInteger(this.start)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would instead use Number.isSafeInteger. this.start may be unreliable if it is higher than Number.MAX_SAFE_INTEGER.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Made the change.

throw new ERR_OUT_OF_RANGE('start', 'integer', this.start);
}

this.pos = this.start;
}

// Backwards compatibility: Make sure `end` is a number regardless of `start`.
// TODO(addaleax): Make the above typecheck not depend on `start` instead.
// (That is a semver-major change).
if (typeof this.end !== 'number')
if (this.end === undefined) {
this.end = Infinity;
else if (Number.isNaN(this.end))
throw new ERR_INVALID_ARG_TYPE('end', 'number', this.end);
} else {
if (typeof this.end !== 'number') {
throw new ERR_INVALID_ARG_TYPE('end', 'number', this.end);
}

if (Number.isNaN(this.end)) {
throw new ERR_OUT_OF_RANGE('end', 'not NaN', this.end);
}

if (this.end < 0) {
throw new ERR_OUT_OF_RANGE('end', 'positive', this.end);
}

if (!Number.isInteger(this.end)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

throw new ERR_OUT_OF_RANGE('start', 'integer', this.end);
}

if (this.start !== undefined && this.start > this.end) {
const errVal = `{start: ${this.start}, end: ${this.end}}`;
throw new ERR_OUT_OF_RANGE('start', '<= "end"', errVal);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again I would also change this to:

throw new ERR_OUT_OF_RANGE('start', `<= ${this.end}`, this.start);

This ends up as:

The value of "start" is out of range. It must be <= ${this.end}. Received ${this.start}

instead of:

The value of "start" is out of range. It must be <= "end". Received {start: ${this.start}, end: ${this.end}}

It just seems better to read for me. It is also the way how it is done in e.g. the buffer module.

But that is just a suggestion.

Copy link
Contributor Author

@ryzokuken ryzokuken May 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not as inclined regarding this one.

Let's assume that the passed value of end is 4, and start is 6. The error message would be: The value of "start" is out of range. It must be <= 4. Received 6.

I bet there would be a section of developers, already coping with a huge language barrier, who would might to the conclusion that there's some magic value (4 in this case) which "start" needs to be smaller than.

What about: The value of "start" is out of range. It must be <= "end" (here: 4). Received 6 instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 that works as well.

}
}

if (typeof this.fd !== 'number')
this.open();
Expand Down
65 changes: 48 additions & 17 deletions test/parallel/test-fs-read-stream-throw-type-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,54 @@ fs.createReadStream(example, null);
fs.createReadStream(example, 'utf8');
fs.createReadStream(example, { encoding: 'utf8' });

const createReadStreamErr = (path, opt) => {
common.expectsError(
() => {
fs.createReadStream(path, opt);
},
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError
});
const createReadStreamErr = (path, opt, error) => {
common.expectsError(() => {
fs.createReadStream(path, opt);
}, error);
};

createReadStreamErr(example, 123);
createReadStreamErr(example, 0);
createReadStreamErr(example, true);
createReadStreamErr(example, false);
const typeError = {
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError
};

const rangeError = {
code: 'ERR_OUT_OF_RANGE',
type: RangeError
};

createReadStreamErr(example, 123, typeError);
createReadStreamErr(example, 0, typeError);
createReadStreamErr(example, true, typeError);
createReadStreamErr(example, false, typeError);

// Case 0: Should not throw if either start or end is undefined
fs.createReadStream(example, {});
fs.createReadStream(example, { start: 0 });
fs.createReadStream(example, { end: Infinity });

// Case 1: Should throw TypeError if either start or end is not of type 'number'
createReadStreamErr(example, { start: 'invalid' }, typeError);
createReadStreamErr(example, { end: 'invalid' }, typeError);
createReadStreamErr(example, { start: 'invalid', end: 'invalid' }, typeError);

// Case 2: Should throw RangeError if either start or end is NaN
createReadStreamErr(example, { start: NaN }, rangeError);
createReadStreamErr(example, { end: NaN }, rangeError);
createReadStreamErr(example, { start: NaN, end: NaN }, rangeError);

// Case 3: Should throw RangeError if either start or end is negative
createReadStreamErr(example, { start: -1 }, rangeError);
createReadStreamErr(example, { end: -1 }, rangeError);
createReadStreamErr(example, { start: -1, end: -1 }, rangeError);

// Case 4: Should throw RangeError if either start or end is fractional
createReadStreamErr(example, { start: 0.1 }, rangeError);
createReadStreamErr(example, { end: 0.1 }, rangeError);
createReadStreamErr(example, { start: 0.1, end: 0.1 }, rangeError);

// Case 5: Should not throw if both start and end are whole numbers
fs.createReadStream(example, { start: 1, end: 5 });

// createReadSteam _should_ throw on NaN
createReadStreamErr(example, { start: NaN });
createReadStreamErr(example, { end: NaN });
createReadStreamErr(example, { start: NaN, end: NaN });
// Case 6: Should throw RangeError if start is greater than end
createReadStreamErr(example, { start: 5, end: 1 }, rangeError);