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: make ReadStream throw error on NaN #19732

Closed
wants to merge 8 commits into from

Conversation

ryzokuken
Copy link
Contributor

This test makes fs.ReadStream throw an error when either start or
end is NaN.

Fixes: #19715

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

/cc @lpinca

@nodejs-github-bot nodejs-github-bot added the fs Issues and PRs related to the fs subsystem / file system. label Apr 1, 2018
@ryzokuken
Copy link
Contributor Author

@lpinca should I also add a regression test for it?

@BridgeAR
Copy link
Member

BridgeAR commented Apr 1, 2018

@ryzokuken yes please. Any code change should ideally come with a test.

lib/fs.js Outdated
throw new ERR_INVALID_ARG_TYPE('start', 'number', this.start);
}
if (this.end === undefined) {
this.end = Infinity;
} else if (typeof this.end !== 'number') {
} else if (typeof this.end !== 'number' || Number.isNaN(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.

Can you add this for the other typeof this.end !== 'number' check below as well? The one that is supposed to be merged with this one? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@addaleax Sure! On 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.

@addaleax done.

This test makes fs.ReadStream throw an error when either start or
end is NaN.

Fixes: nodejs#19715
@ryzokuken
Copy link
Contributor Author

@BridgeAR I'd love to add the test, but because this would be my first bugfix and thus the first test I'd be writing from scratch, I'd need some help with it. For starters, does it need to be a parallel test or a sequential one? Or perhaps it's neither?

@addaleax
Copy link
Member

addaleax commented Apr 1, 2018

https://github.com/nodejs/node/blob/master/doc/guides/writing-tests.md :)

parallel should work fine, I think.

Add regression tests for nodejs#19732,
making sure appropraite TypeErrors are thrown for NaN values.

Refs: nodejs#19732
@ryzokuken
Copy link
Contributor Author

@addaleax there was an existing test, so I added assertions to that instead. Let me know if that doesn't work, and I'd be happy to make a separate test file.

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

In particular, I think this is semver-patch because the issue shows that this would never actually work to begin with, and break creation of other ReadStream instances.

@ryzokuken
Copy link
Contributor Author

@addaleax Thanks. That's a relief. semver-major sounded scary.

@@ -18,10 +22,16 @@ const createReadStreamErr = (path, opt) => {
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError
});
Copy link
Member

Choose a reason for hiding this comment

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

Nit: unnecessary change.

lib/fs.js Outdated
@@ -2008,12 +2008,12 @@ function ReadStream(path, options) {
this.closed = false;

if (this.start !== undefined) {
if (typeof this.start !== 'number') {
if (typeof this.start !== 'number' || Number.isNaN(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.

As suggested by @anliting we can probably use just Number.isInteger().

@lpinca
Copy link
Member

lpinca commented Apr 1, 2018

};

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

// Should also throw on NaN (for https://github.com/nodejs/node/pull/19732)
createReadStreamErr(example, { start: NaN });
Copy link

Choose a reason for hiding this comment

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

I think we can leave only createReadStreamErr(example, { start: NaN }), because if start is not passed, end is not defined (https://github.com/ryzokuken/node/blob/473dd3d03fe4d08445518f9ab8e2b78892c22e27/lib/fs.js#L2010).

And at least we need to delete createReadStreamErr(example, { end: NaN }) to pass node-test-commit-linux.

Copy link
Member

Choose a reason for hiding this comment

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

@anliting If start is not passed but end is, then the value is still set and type-checked: https://github.com/ryzokuken/node/blob/473dd3d03fe4d08445518f9ab8e2b78892c22e27/lib/fs.js#L2031-L2032

Copy link

Choose a reason for hiding this comment

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

If we are going to test the end part, we need have start to pass the range test first: createReadStreamErr(example, { start: 0, end: NaN })

Copy link
Member

Choose a reason for hiding this comment

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

@anliting That would take a different branch, though?

Copy link

@anliting anliting Apr 1, 2018

Choose a reason for hiding this comment

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

@addaleax

Oh, I see that line (which makes end is still set and type-checked) now, my fault.

"If we are going to test the end part ..." means if we are going to test if this line work:
https://github.com/ryzokuken/node/blob/473dd3d03fe4d08445518f9ab8e2b78892c22e27/lib/fs.js#L2016

Copy link

@anliting anliting Apr 1, 2018

Choose a reason for hiding this comment

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

We might need to change

  if (typeof this.end !== 'number' || Number.isNaN(this.end))
    this.end = Infinity;

to

  if (typeof this.end !== 'number')
    this.end = Infinity;
  else if (Number.isNaN(this.end))
    throw new ERR_INVALID_ARG_TYPE('end', 'integer', this.end);

to pass node-test-commit-linux.

Copy link
Member

Choose a reason for hiding this comment

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

@anliting Ah, right! Yes, we should probably do that.

lib/fs.js Outdated
@@ -2008,12 +2008,12 @@ function ReadStream(path, options) {
this.closed = false;

if (this.start !== undefined) {
if (typeof this.start !== 'number') {
if (typeof this.start !== 'number' || Number.isNaN(this.start)) {
throw new ERR_INVALID_ARG_TYPE('start', 'number', this.start);
Copy link

Choose a reason for hiding this comment

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

Would throw new ERR_INVALID_ARG_TYPE('start', 'integer', this.start); be more appropriate? Since typeof NaN=='number'.

@ryzokuken
Copy link
Contributor Author

@anliting @addaleax thanks for figuring this out for me, pushing the required changes in a minute.

@ryzokuken
Copy link
Contributor Author

looks better now?

lib/fs.js Outdated
@@ -2008,13 +2008,13 @@ function ReadStream(path, options) {
this.closed = false;

if (this.start !== undefined) {
if (typeof this.start !== 'number') {
throw new ERR_INVALID_ARG_TYPE('start', 'number', this.start);
if (typeof this.start !== 'number' || !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.

Nit: the condition can be just !Number.isInteger() as that returns true when passing something that is not a number.

Copy link

Choose a reason for hiding this comment

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

@ryzokuken
Copy link
Contributor Author

@lpinca @anliting done, take a look.

@anliting
Copy link

anliting commented Apr 2, 2018

@ryzokuken
LGTM. Besides, I think neither ERR_INVALID_ARG_TYPE nor ERR_OUT_OF_RANGE fits this situation, maybe we can just choose ERR_OUT_OF_RANGE.

@anliting
Copy link

anliting commented Apr 2, 2018

Oops, I think I found another problem. It seems to throw when we pass {start:0} as options.

Change

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

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

    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')
    this.end = Infinity;
  else if (!Number.isInteger(this.end))
    throw new ERR_INVALID_ARG_TYPE('end', 'integer', this.end);

to

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

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

    this.pos = this.start;
  }else{

    // 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')
      this.end = Infinity;
    else if (!Number.isInteger(this.end))
      throw new ERR_INVALID_ARG_TYPE('end', 'integer', this.end);
  }

.

@ryzokuken
Copy link
Contributor Author

@anliting better now?

@anliting
Copy link

anliting commented Apr 2, 2018

Better ^^

@anliting
Copy link

anliting commented Apr 7, 2018

@ryzokuken

Yes, that understanding is right. What I like more would be "until end bytes and reads end-start bytes".

If you define the behavior as "read from start to end", end<start isn't valid. But if you define it as I said (#19732 (comment)), there isn't a problem (like [0,1,2,3].slice(3,1).length==0), user can just code the start and end independently.

@anliting
Copy link

anliting commented Apr 7, 2018

@ryzokuken

I think the both behaviors make a sense of its own; so I have no idea to choose.

@ryzokuken
Copy link
Contributor Author

Hmm, so I'd consult either @addaleax or someone else from the TSC regarding that particular behavior. Regarding fractions, Anna didn't want to throw on them either, because while it might not make a lot of sense, a lot of people do intricate math to calculate the values of both these parameters, and they might not get an integer all the time.

@anliting
Copy link

anliting commented Apr 7, 2018

@ryzokuken

"Exception" means those situations which we cannot handle; "exception handling" means we throw them back and hope if someone else handles.

If we don't throw on fractions, we will have to handle them. For example:

options.start=Math.ceil(options.start)
options.end=Math.floor(options.end)

or

options.start=Math.floor(options.start)
options.end=Math.floor(options.end)

If we current have no idea how to handle it, we can throw on them first, and wait for the idea comes. However, that makes another semver-major.

@ryzokuken
Copy link
Contributor Author

Again, I'd defer to @addaleax regarding that.

@addaleax
Copy link
Member

addaleax commented Apr 7, 2018

I think what’s currently happening is that both values are being rounded down? That sounds okay, especially because we have this very weird behaviour where end is inclusive, contrary to all conventions in JS and similar languages. (It’s not a harmful thing, just odd.) Rounding up for end also seems okay, or actually rounding the values to the nearest integer (like, when start is 12.99999998 for some reason).

I mean, honestly, I don’t think it’d be the end of the world if we threw on non-integer numbers, and my opinion isn’t always gold either. I’m just thinking, JS is a language where the math just happens to be defined in terms of floats, so it’s probably better to be on the safe side and implement something that handles that somewhat gracefully.

@ryzokuken
Copy link
Contributor Author

As long as we are "handling" fractions internally in a predictable manner, I think @anliting's very helpful suggestion is satisfied as well. I mean, we only need to throw where we aren't in a position to handle it ourselves, right? Here we can.

My suggestion: let's make this behavior of handling fractional arguments well-documented though, so that anyone doing complex math that might return a fraction (our target audience for this, basically), are well-informed and can reason about this behavior rationally.

@addaleax so, should we just round down on both arguments? Or maybe round down on start and up on end? Rounding to the nearest integer doesn't sound like something reliable, predictable to even reasonable IMO. What are your thoughts on this?

@addaleax
Copy link
Member

addaleax commented Apr 7, 2018

Rounding to the nearest integer doesn't sound like something reliable, predictable to even reasonable IMO. What are your thoughts on this?

When your math gives you 12.9999998, which should have turned out to 13 if it weren’t for floating-point inaccuracies, and we round down to 12, then all your offsets into the returned buffers are wrong.

Honestly, I don’t think many people would be affected by this either way, so it’s probably more important to pick some consistent behaviour than determining which exactly it should be?

@ryzokuken
Copy link
Contributor Author

When your math gives you 12.9999998, which should have turned out to 13 if it weren’t for floating-point inaccuracies, and we round down to 12, then all your offsets into the returned buffers are wrong.

Agreed. That makes sense.

it’s probably more important to pick some consistent behaviour than determining which exactly it should be?

You're right, but we'll still need to pick one anyway. Let me know whichever seems more reasonable to you.

@anliting
Copy link

anliting commented Apr 8, 2018

After reading your conversation, I would vote for "Math.round for both start and end". In contrary of floating-point inaccuracies, it is rare that the user provides numbers which are far away from integers and it still doesn't want to round it itself.

@ryzokuken
Copy link
Contributor Author

ryzokuken commented Apr 8, 2018

Updated table:

So, for the course of this pull request:

Type Current Behavior Proposed Behavior
undefined (0, Infinity)
Anything other than Number ERR_INVALID_ARG_TYPE ERR_INVALID_ARG_TYPE
NaN ERR_INVALID_ARG_TYPE ERR_INVALID_ARG_TYPE
Negative Numbers ERR_OUT_OF_RANGE
Fractional Numbers Math.round
Integers Work Perfectly Still work perfectly
Invalid Arguments ERR_OUT_OF_RANGE ERR_OUT_OF_RANGE

@addaleax @anliting I think we all agree on this now, right?

@anliting
Copy link

anliting commented Apr 8, 2018

@ryzokuken

I prefer ERR_OUT_OF_RANGE on NaN (#19732 (comment)), and I guess it will still be (0, Infinity) on undefined.

@ryzokuken
Copy link
Contributor Author

I prefer ERR_OUT_OF_RANGE on NaN

Let me guess, because it's not the wrong type (still a Number), but the wrong value of the correct type?

That does make a lot of sense.

@ryzokuken
Copy link
Contributor Author

So, finally:

Type Current Behavior Proposed Behavior
undefined (0, Infinity) (0, Infinity)
Anything other than Number ERR_INVALID_ARG_TYPE ERR_INVALID_ARG_TYPE
NaN ERR_INVALID_ARG_TYPE ERR_OUT_OF_RANGE
Negative Numbers ERR_OUT_OF_RANGE
Fractional Numbers Math.round
Integers Work Perfectly Still work perfectly
Invalid Arguments ERR_OUT_OF_RANGE ERR_OUT_OF_RANGE

@addaleax @anliting this looks good? I've been thinking of making a new PR so that others don't get bogged down by the conversation in here.

@addaleax
Copy link
Member

addaleax commented Apr 8, 2018

@ryzokuken Yes, I think that seems fine, assuming that “Invalid Arguments” means end < start?

@ryzokuken
Copy link
Contributor Author

@addaleax they do. Great. Making another PR for this today.

@anliting
Copy link

anliting commented Apr 9, 2018

@ryzokuken

LGTM. ^^

ryzokuken added a commit to ryzokuken/node that referenced this pull request Apr 16, 2018
Improve handling of erratic arguments in fs.ReadStream

Refs: nodejs#19732
@ryzokuken
Copy link
Contributor Author

Closing in favor of the other two PRs.

@ryzokuken ryzokuken closed this Apr 23, 2018
addaleax pushed a commit that referenced this pull request May 14, 2018
Improve handling of erratic arguments in fs.ReadStream

Refs: #19732
PR-URL: #19898
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blocked PRs that are blocked by other issues or PRs. fs Issues and PRs related to the fs subsystem / file system. semver-major PRs that contain breaking changes and should be released in the next major version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

fs.createReadStream doesn't throw with options={start:NaN,end:NaN}
6 participants