nodejs / node-v0.x-archive

Moved to https://github.com/nodejs/node
34.42k stars 7.31k forks source link

Buffer inconsistent throws/coercion #5323

Closed trevnorris closed 9 years ago

trevnorris commented 11 years ago

Working on #4964 and noticed that Buffers have inconsistencies of when arguments are thrown and when they're coerced. This is a proposal to solidify how they should be handled. It has taken care to follow existing practices by browsers in regards to generic argument handling and that of the Typed Array specification:

As an example of how to handle this, here are a couple verification functions:

inline uint32_t GetOptionalUintArg(Handle<Value> arg) {
  int32_t val_i = arg->Int32Value();
  if (val_i < 0)
    return ThrowRangeError("index is less than zero");
  return static_cast<uint32_t>(val_i);
}

inline uint32_t GetRequiredUintArg(Handle<Value> arg) {
  if (arg->IsUndefined())
    return ThrowTypeError("argument is undefined");
  return GetOptionalUintArg(arg);
}
bnoordhuis commented 11 years ago

I second this motion.

Arguments expected to be numeric will coerce all other values.

My beef with this would be that it could hide silly type errors in core, i.e. that it could mask expensive type coercions. Not insurmountable: if all type checking logic is centralized, I can always patch my tree to throw instead of coerce and run the tests (and hope they excise that particular behavior.)

isaacs commented 11 years ago

My beef with this would be that it could hide silly type errors in core

I'm inclined to agree with this. Where would we be coercing to a numeric type?

trevnorris commented 11 years ago

@bnoordhuis I feel the same way. My take on it is that type coercion in js is usually cheap, and if an unexpected value goes through it'll show up as a DEOPT while testing. I understand this isn't optimal, but can't think of a much better way.

For cc-land, not completely sure. The best idea I've had is to create a global set of argument parsing functions, like the two above, which would make it easy to patch for testing.

@isaacs For any functions that users aren't supposed to touch, no need for the coercion. It's geared towards the public API. And it's not just for numbers specifically (though I forgot to mention that). It's for any primitive. Remember 0468861? It's pretty standard to auto-coerce any expected primitive argument to be what is required.

Here's an example:

var dv = new DataView(new ArrayBuffer(8));
dv.setFloat64('0','1.1');
dv.getFloat64('0');
// output: 1.1
trevnorris commented 11 years ago

These are easy primitive coercion methods that follow ECMA 262.

Here is an example using several of the above:

// use: fillRange(val, [start][, end][, noAssert]);
function fillRange(val, start, end, noAssert) {
  val = +val;
  // null-ish will default to 0
  start = ~~start;
  // null-ish will default to "data_length"
  end = end == null ? data_length : ~~end;

  // js api's generally fix this for developers
  if (end > start)
    end = start;

  // important to throw for index check when js is trying to
  // access raw memory (e.g. Buffers/Typed Arrays)
  if (!noAssert && (start < 0 || end > total_length))
    throw new RangeError("out of range index");

  // ...
}

Now as far as the c++ side, definitely want @bnoordhuis opinion on that. I'd like to be able to replicate the above for consistency, but value coercion is much more expensive on the native side. What about making a few argument value parsing functions included in node.h. Possibly setting a #define for debug mode so it'll throw instead of auto-coercing the values?

bnoordhuis commented 11 years ago

What about making a few argument value parsing functions included in node.h. Possibly setting a #define for debug mode so it'll throw instead of auto-coercing the values?

I'm okay with that but said functions should go into node_internals.h, not node.h.

trevnorris commented 11 years ago

Sounds good to me. Other than that, have any comments on any other arguments that should be thrown?

trevnorris commented 11 years ago

@isaacs Want your opinion on one thing. Right now the Buffer .slice() will coerce all arguments. This sort of make sense because it follows the same pattern as Array. Though since the user isn't just getting a copy back, imho, out of range indexes should throw. So, change tha API, or make an exception for that case?

trevnorris commented 11 years ago

Ok. Consensus has been reached.

General:

General Numeric rules:

Other general rules:

Exception:

General Buffer method rules:

One TBD:

Other notes:

Have a hard time believing it's come out this complicated, but this should address most, if not all, argument parsing concerns.

/cc @isaacs @bnoordhuis just for one last review

bnoordhuis commented 11 years ago

I'm not sure if your comment addresses this but:

Expected Int/Uint values are coerced:

Can lead to unexpected results (where 'unexpected' === 'most people won't realize that', not 'undefined'.)

> 0x7fffffff === ~~0x7fffffff  // 2147483647
true
> 0x80000000 === ~~0x80000000  // -2147483648
false

This is more of a general observation because you can't have buffers that big anyway.

trevnorris commented 11 years ago

Was thinking about that myself, but then realized that no one else does it as well:

new ArrayBuffer(~~(0xffffffff+3))
trevnorris commented 11 years ago

But I did make an exception for Buffer instantiation. I floor the Number which will leave it in floating point range. So in that one case it'll throw regardless of the size of n.

domenic commented 11 years ago

Sorry to jump in here, but

Optional arguments are set to their defaults if == null.

Is not really the way to go. It should be === undefined to match ES6 semantics of default arguments and all built-in ES5 functions.

Otherwise the general movement toward coercion is great and matches the ES5 built-ins.

trevnorris commented 11 years ago

Good assessment. There are parts of the API where null is a valid but not default parameter. Oversight on my part. Thanks.

trevnorris commented 11 years ago

@isaacs I'd suggest during v0.13 cleanup we make this standard across all of core as much as possible.

jasnell commented 9 years ago

@trevnorris ... is this still an issue?

jasnell commented 9 years ago

@trevnorris ... ping :-)

trevnorris commented 9 years ago

going to say no.