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

Convert byte array to string #266

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

sharmaashish13
Copy link

While using a base64 polyfill, encountered an issue where passing binary string from Go to JS didnt work as expected.

  1. If binary string has a null character in middle of string, CString() function just terminates the value at that null character resulting into a truncated value.
  2. v8go.cc's NewValueString function uses NewFromUtf8 which returns invalid response.

Code sample can be found here:
kuoruan/v8go-polyfills#4

Hence, added a new API that uses v8::String::NewFromOneByte to solve this scenario.

value.go Show resolved Hide resolved
value.go Outdated
@@ -53,6 +53,15 @@ func Null(iso *Isolate) *Value {
return iso.null
}

func NewStringFromByteArray(iso *Isolate, val []byte) (*Value, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We will probably want the inverse to be supported as well (from the v8 value in JS back to Go)

Copy link
Author

Choose a reason for hiding this comment

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

As mentioned in another comment, the reason behind implementing byte array to string is to overcome the challenges with null character in binary strings. I'm not sure for which scenario we would want to implement the reverse.

Copy link
Contributor

Choose a reason for hiding this comment

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

When v8go converts a *Value to a Go string (e.g. with .String()), it truncates the string at the first null character it encounters.

Copy link
Author

Choose a reason for hiding this comment

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

I added a testcase in value_test.go where i am validating the same. If you print val.String(), it doesnt truncate the data even though there is a 0 byte in the input array.
Having said that, do you have any specific test-case? I can look into that scenario.

Copy link

Choose a reason for hiding this comment

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

I guess we could introduce a (v *Value) BinaryString() []byte or (v *Value) BinaryString() BinaryString depending on this outcome.

Copy link

Choose a reason for hiding this comment

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

When v8go converts a *Value to a Go string (e.g. with .String()), it truncates the string at the first null character it encounters.

Note that we're not trying to solve #192 here. We may do that in a separate PR.

Copy link
Collaborator

@dylanahsmith dylanahsmith left a comment

Choose a reason for hiding this comment

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

Although the test could be improved and documentation could be added, the implementation looks good to me.

value_test.go Outdated
Comment on lines 482 to 485
inputString := "CN7ySxWjjWNpbYPB1n/TBR8avujZS2cHdXRR5ZM7Fi6QBjlVzBqPu0CI9pQXcW9fvbMXdqU+57XY/QdGozJT19+gbQDM0ZQVzjwqtpyLorcPHjMqum+7dHD6XF5cXo3NZlKGsxcnvSVyClBDU5M1dUCe8bB9yV0wVM6ge+0WAmTX2GYbncilTjDw0bSJI1Z+71NT8UQCfmimKhVxJiKrnkaTrTw2Ma/1I2w4Dny3cRlFtCtob9cvNOeeIm8HtQoi/7HXoE0uFr1C39OL2hCC1TJsxX94djtNFqd9aUOPYrwT+zErSokSvbNYS5WpEjEpRJze9+TCV9NLmqCnARK4Bw"
byts, _ := base64.RawStdEncoding.DecodeString(inputString)

expected := "ÞòK£cimƒÁÖÓ¾èÙKgutQå“;.9Ȕ»@ˆö”qo_½³v¥>çµØýF£2S×ß m"
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems more complicated than it needs to be, which makes it hard to see the actual connection between the input and output if it fails and makes it harder to see what is actually being tested.

It seems like we just need the input to have the following for testing different edge cases:

  • a null byte ('\x00') doesn't terminate the string
  • bytes encoding a valid multi-byte UTF-8 encoding for a character should not be combined into a single character (e.g. []byte("ç") should have length 2 in the JS string)
  • an invalid UTF-8 byte sequence (e.g. '\xff') gets preserved
Suggested change
inputString := "CN7ySxWjjWNpbYPB1n/TBR8avujZS2cHdXRR5ZM7Fi6QBjlVzBqPu0CI9pQXcW9fvbMXdqU+57XY/QdGozJT19+gbQDM0ZQVzjwqtpyLorcPHjMqum+7dHD6XF5cXo3NZlKGsxcnvSVyClBDU5M1dUCe8bB9yV0wVM6ge+0WAmTX2GYbncilTjDw0bSJI1Z+71NT8UQCfmimKhVxJiKrnkaTrTw2Ma/1I2w4Dny3cRlFtCtob9cvNOeeIm8HtQoi/7HXoE0uFr1C39OL2hCC1TJsxX94djtNFqd9aUOPYrwT+zErSokSvbNYS5WpEjEpRJze9+TCV9NLmqCnARK4Bw"
byts, _ := base64.RawStdEncoding.DecodeString(inputString)
expected := "�ÞòK�£�cim�ÁÖ�Ó���¾èÙKg�utQå�;�.��9UÌ��»@�ö��qo_½³�v¥>çµØý�F£2S×ß m"
expected := "a\x00ç\xffe"
input = []byte(expected)

value_test.go Outdated
Comment on lines 490 to 571
if !val.IsString() {
t.Errorf("expected string but got %s", reflect.TypeOf(val))
}
if val.String() != expected {
t.Errorf("expected not same as actual")
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this also work for v8.NewValue(expected), in which case this isn't testing the distinguishing behaviour of this new method, which is that the bytes aren't interpreted as UTF-8 in the JS string. Since there isn't a v8go.String type, a workaround would be to test this in JS (e.g. getting the length and making sure it counts each byte in a UTF-8 encoded byte sequence)

value.go Outdated
@@ -53,6 +53,15 @@ func Null(iso *Isolate) *Value {
return iso.null
}

func NewStringFromByteArray(iso *Isolate, val []byte) (*Value, error) {
if iso == nil {
return nil, errors.New("v8go: failed to create new Value: Isolate cannot be <nil>")
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be treated as a bug, so should instead panic to make sure the error is quickly noticed during testing..

value.go Outdated Show resolved Hide resolved
@codecov
Copy link

codecov bot commented Jan 14, 2022

Codecov Report

Merging #266 (41fcfd3) into master (5e91d3d) will decrease coverage by 0.30%.
The diff coverage is 60.00%.

❗ Current head 41fcfd3 differs from pull request most recent head 184e508. Consider uploading reports for the commit 184e508 to get more accurate results
Impacted file tree graph

@@            Coverage Diff             @@
##           master     #266      +/-   ##
==========================================
- Coverage   95.86%   95.55%   -0.31%     
==========================================
  Files          17       17              
  Lines         580      585       +5     
==========================================
+ Hits          556      559       +3     
- Misses         15       16       +1     
- Partials        9       10       +1     
Impacted Files Coverage Δ
value.go 93.85% <60.00%> (-0.98%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5e91d3d...184e508. Read the comment docs.

v8go.h Outdated
@@ -195,6 +195,7 @@ extern ValuePtr NewValueUndefined(IsolatePtr iso_ptr);
extern ValuePtr NewValueInteger(IsolatePtr iso_ptr, int32_t v);
extern ValuePtr NewValueIntegerFromUnsigned(IsolatePtr iso_ptr, uint32_t v);
extern RtnValue NewValueString(IsolatePtr iso_ptr, const char* v, int v_length);
extern RtnValue NewValueStringFromByteArray(IsolatePtr iso, const uint8_t* v, int len);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The Value in the name doesn't seem necessary and doesn't match the function it is used from.

args := info.Args()
input := args[0].String()
if len(input) == 0 {
//String() terminates input at null character. hence hardcoding for 3rd input
Copy link
Collaborator

Choose a reason for hiding this comment

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

A workaround is no longer needed for this.

Copy link
Author

Choose a reason for hiding this comment

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

.String() not working as expected. for input string a\x00\xffe, it should return 4 bytes instead getting 5.

value.go Outdated
@@ -53,6 +53,16 @@ func Null(iso *Isolate) *Value {
return iso.null
}

// NewStringFromByteArray returns V8::string from byte array. Converts each byte into equivalent character.
func NewStringFromByteArray(iso *Isolate, val []byte) (*Value, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

NewStringFromBytes would be a shorter name. Referring to the argument as a ByteArray doesn't make it any clearer and could even seem to refer to a JS array before looking at the parameter type.

The parameter name could be named bytes to be consistent with what it is referred to as in the function name.

Also, I've opened #277 since this should actually be returning a String type. We should add that type before adding a constructor for a string, so we don't need a breaking change to change it from Value to String later.

Suggested change
func NewStringFromByteArray(iso *Isolate, val []byte) (*Value, error) {
func NewStringFromBytes(iso *Isolate, bytes []byte) (*String, error) {

v8go.cc Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
@noelcarl
Copy link

@dylanahsmith @genevieve is this pull request something we can still move forward with? If not is there another known workaround for supporting the passing of a binary string from Go to JavaScript? Thanks.

cc @sharmaashish13

@dylanahsmith
Copy link
Collaborator

genevieve and I had been exploring using this library while working at Shopify, which didn't end up adopting this for production use and neither of us work at Shopify anymore. I think you will need help from someone else with merge access who has been more recently active on this project.

@noelcarl
Copy link

Ok, sounds good. Thanks for the update and context @dylanahsmith.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants