-
Notifications
You must be signed in to change notification settings - Fork 23
Fix overflow when reading negative s8 #16
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
base: master
Are you sure you want to change the base?
Conversation
When doing bitwise operations in JS the values are always converted to a signed 32-bit integer and the result is also always a signed 32-bit integer. The calculation that converts two `u4`s to one `s8` assumes only positive inputs, this change makes sure of that.
@cherue, can you clarify which test shows the problem in JS right now? |
Right now no test shows the problem. I am writing a test called The ksy for this test currently looks like this: meta:
id: js_overflow
seq:
- id: signed_negative_be
type: s8be
- id: signed_negative_le
type: s8le
- id: signed_positive_be
type: s8be
- id: signed_positive_le
type: s8le
- id: unsigned_be
type: u8be
- id: unsigned_le
type: u8le
instances:
overflow_signed_negative_be:
pos: 48
type: s8be
overflow_signed_negative_le:
pos: 56
type: s8le
overflow_signed_positive_be:
pos: 64
type: s8be
overflow_signed_positive_le:
pos: 72
type: s8le
overflow_unsigned_be:
pos: 80
type: u8be
overflow_unsigned_le:
pos: 88
type: u8le While writing this test I noticed that Also I misnamed my branch, it is the low bytes that overflow. The high bytes can't ever overflow because the sign bit is always set. I'll fix that and then write up a better explanation of the problem. |
Because the high bytes always have the sign bit set XORing them can never result in a negative number.
Let's look at First, the var high = this.readU4be(); // 0x ff e0 00 00
var low = this.readU4be(); // 0x 00 00 00 01 Then high = high ^ 0xffffffff; // 0x 00 1f ff ff
low = low ^ 0xffffffff; // 0x ff ff ff fe And finally they are combined to create the return -(0x100000000 * high + low) - 1; The expected result is This happens because the result of XOR is defined to be a signed 32-bit integer. Which means the final step, which should be: return -(0x100000000 * 2097151 + 4294967294) - 1;
// -9007199254740991 currently is: return -(0x100000000 * 2097151 + -2) - 1;
// -9007194959773695 |
@GreyCat do you want a test for this first? How about an |
@@ -802,6 +790,18 @@ KaitaiStream.createStringFromArray = function(array) { | |||
return chunks.join(""); | |||
}; | |||
|
|||
KaitaiStream.twoU4sToS8 = function(high, low) { | |||
if ((high & 0x80000000) != 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always use strict equality operator ===
(or !==
negated) in JavaScript unless you have a very good reason to use loose equality op with type juggling (==
/!=
).
if ((high & 0x80000000) != 0) { | |
if ((high & 0x80000000) !== 0) { |
The loose equality op ==
knows several ways how to surprise you: at random "" == false
, "" == 0
, [] == false
(whereas ![] === false
actually) , [] == ''
, [] == 0
, [0] == false
, [1] == true
, [null, null] == ","
etc. Thus it's rarely a good idea to use the loose equality.
It doesn't matter whether you think you know the type of the operands, because it's JavaScript. Unless you are doing typeof X === ...
everywhere, you don't know the types.
// negative number | ||
high = high ^ 0xffffffff; | ||
low = low ^ 0xffffffff; | ||
low = low < 0 ? 2**32 + low : low; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't use exponentiation operator in JavaScript, because this would noticeably impair the KS runtime compatibility with JavaScript environments (compare compatibility table on MDN of exponentiation **
and Uint8Array
, which is probably the latest JS feature the runtime uses).
Chrome | Edge | Firefox | Internet Explorer | Opera | Safari | Android webview | Chrome for Android | Firefox for Android | Opera for Android | Safari on iOS | Samsung Internet | Node.js | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Uint8Array |
7 | 12 | 4 | 10 | 11.6 | 5.1 | 4 | 18 | 4 | 12 | 4.2 | 1.0 | 0.10 |
Exponentiation (** ) |
52 | 14 | 52 | No | 39 | 10.1 | 51 | 52 | 52 | 41 | 10.3 | 6.0 | 7.0.0 |
Moreover, environments that don't know about exponential operator **
treat its usage anywhere in the code as a syntax error, regardless of whether the code using it would run or not. So it's impossible to polyfill it in such environments, because it's a syntax thing. This is a significant difference from Uint8Array
, which can be polyfilled.
I believe that in theory you can bring the current KS runtime up and running even in ancient browsers like IE 5 🙂, if you link a few polyfills.
I suggest a cleaner (and probably also faster) way how to convert the 32-bit signed integer to an unsigned one - using the unsigned right shift operator. From MDN:
Unlike the other bitwise operators, zero-fill right shift returns an unsigned 32-bit integer.
So it's enough to do >>> 0
, which is guaranteed to yield a 32-bit unsigned integer. You don't even have to check if the original number is positive or negative.
low = low < 0 ? 2**32 + low : low; | |
low >>>= 0; // convert to unsigned 32-bit integer |
When doing bitwise operations in JS the values are always converted to a
signed(edit: not always signed, see here) 32-bit integer and the result is also always a signed 32-bit integer.The calculation that converts two
u4
s to ones8
assumes only positive inputs, this change makes sure of that.I discovered this when writing a test for JS 53-bit integer overflows, but that test and the error on overflow aren't part of this.