From 028975da558cb5c85e66027a8a9518712f7599e4 Mon Sep 17 00:00:00 2001 From: mleku Date: Thu, 26 Jun 2025 20:17:15 +0100 Subject: [PATCH] fully test text package and fix a few bugs --- .output.txt | 230 ++++++++++++++++++++ text/escape.go | 12 +- text/escape_test.go | 43 ++++ text/helpers.go | 22 +- text/helpers_test.go | 503 +++++++++++++++++++++++++++++++++++++++++++ text/hex_test.go | 173 +++++++++++++++ text/wrap.go | 2 +- text/wrap_test.go | 321 +++++++++++++++++++++++++++ 8 files changed, 1294 insertions(+), 12 deletions(-) create mode 100644 .output.txt create mode 100644 text/hex_test.go create mode 100644 text/wrap_test.go diff --git a/.output.txt b/.output.txt new file mode 100644 index 0000000..0723a7b --- /dev/null +++ b/.output.txt @@ -0,0 +1,230 @@ +=== RUN TestDebugUnmarshalStringArray + debug_test.go:10: arr: [[104 101 108 108 111] [119 111 114 108 100]], rem: "", err: +--- PASS: TestDebugUnmarshalStringArray (0.00s) +=== RUN TestUnescapeByteString +--- PASS: TestUnescapeByteString (0.00s) +=== RUN TestRandomEscapeByteString +--- PASS: TestRandomEscapeByteString (0.00s) +=== RUN TestUnmarshalHexArray +--- PASS: TestUnmarshalHexArray (0.00s) +=== RUN TestJSONKey +=== RUN TestJSONKey/basic_key +=== RUN TestJSONKey/empty_dst +=== RUN TestJSONKey/empty_key +--- PASS: TestJSONKey (0.00s) + --- PASS: TestJSONKey/basic_key (0.00s) + --- PASS: TestJSONKey/empty_dst (0.00s) + --- PASS: TestJSONKey/empty_key (0.00s) +=== RUN TestUnmarshalHex +=== RUN TestUnmarshalHex/basic_hex +=== RUN TestUnmarshalHex/empty_hex +=== RUN TestUnmarshalHex/no_quotes +=== RUN TestUnmarshalHex/odd_length_hex +=== RUN TestUnmarshalHex/invalid_hex_characters +2025-06-26T20:12:49+01:00.314 ERR encoding/hex: invalid byte: U+0067 'g' /home/david/src/realy.lol/text/helpers.go:51 +=== RUN TestUnmarshalHex/empty_input +--- PASS: TestUnmarshalHex (0.00s) + --- PASS: TestUnmarshalHex/basic_hex (0.00s) + --- PASS: TestUnmarshalHex/empty_hex (0.00s) + --- PASS: TestUnmarshalHex/no_quotes (0.00s) + --- PASS: TestUnmarshalHex/odd_length_hex (0.00s) + --- PASS: TestUnmarshalHex/invalid_hex_characters (0.00s) + --- PASS: TestUnmarshalHex/empty_input (0.00s) +=== RUN TestUnmarshalQuoted +=== RUN TestUnmarshalQuoted/basic_quoted_string +=== RUN TestUnmarshalQuoted/empty_quoted_string +=== RUN TestUnmarshalQuoted/escaped_quotes +=== RUN TestUnmarshalQuoted/escaped_backslash +=== RUN TestUnmarshalQuoted/no_opening_quote +=== RUN TestUnmarshalQuoted/no_closing_quote +=== RUN TestUnmarshalQuoted/empty_input +=== RUN TestUnmarshalQuoted/invalid_control_character +--- PASS: TestUnmarshalQuoted (0.00s) + --- PASS: TestUnmarshalQuoted/basic_quoted_string (0.00s) + --- PASS: TestUnmarshalQuoted/empty_quoted_string (0.00s) + --- PASS: TestUnmarshalQuoted/escaped_quotes (0.00s) + --- PASS: TestUnmarshalQuoted/escaped_backslash (0.00s) + --- PASS: TestUnmarshalQuoted/no_opening_quote (0.00s) + --- PASS: TestUnmarshalQuoted/no_closing_quote (0.00s) + --- PASS: TestUnmarshalQuoted/empty_input (0.00s) + --- PASS: TestUnmarshalQuoted/invalid_control_character (0.00s) +=== RUN TestMarshalHexArray +=== RUN TestMarshalHexArray/basic_hex_array +=== RUN TestMarshalHexArray/empty_array +=== RUN TestMarshalHexArray/single_element +--- PASS: TestMarshalHexArray (0.00s) + --- PASS: TestMarshalHexArray/basic_hex_array (0.00s) + --- PASS: TestMarshalHexArray/empty_array (0.00s) + --- PASS: TestMarshalHexArray/single_element (0.00s) +=== RUN TestUnmarshalStringArray +=== RUN TestUnmarshalStringArray/basic_string_array +=== RUN TestUnmarshalStringArray/empty_array +=== RUN TestUnmarshalStringArray/single_element +=== RUN TestUnmarshalStringArray/no_opening_bracket +=== RUN TestUnmarshalStringArray/no_closing_bracket + helpers_test.go:336: UnmarshalStringArray() array length = 2, want 0 +--- FAIL: TestUnmarshalStringArray (0.00s) + --- PASS: TestUnmarshalStringArray/basic_string_array (0.00s) + --- PASS: TestUnmarshalStringArray/empty_array (0.00s) + --- PASS: TestUnmarshalStringArray/single_element (0.00s) + --- PASS: TestUnmarshalStringArray/no_opening_bracket (0.00s) + --- FAIL: TestUnmarshalStringArray/no_closing_bracket (0.00s) +=== RUN TestTrue +--- PASS: TestTrue (0.00s) +=== RUN TestFalse +--- PASS: TestFalse (0.00s) +=== RUN TestMarshalBool +=== RUN TestMarshalBool/true_value +=== RUN TestMarshalBool/false_value +=== RUN TestMarshalBool/true_with_empty_src +=== RUN TestMarshalBool/false_with_empty_src +--- PASS: TestMarshalBool (0.00s) + --- PASS: TestMarshalBool/true_value (0.00s) + --- PASS: TestMarshalBool/false_value (0.00s) + --- PASS: TestMarshalBool/true_with_empty_src (0.00s) + --- PASS: TestMarshalBool/false_with_empty_src (0.00s) +=== RUN TestUnmarshalBool +=== RUN TestUnmarshalBool/true_value +=== RUN TestUnmarshalBool/false_value +=== RUN TestUnmarshalBool/true_at_end +=== RUN TestUnmarshalBool/false_at_end +=== RUN TestUnmarshalBool/true_with_prefix +=== RUN TestUnmarshalBool/false_with_prefix +=== RUN TestUnmarshalBool/no_boolean_value +=== RUN TestUnmarshalBool/empty_input +=== RUN TestUnmarshalBool/incomplete_true +=== RUN TestUnmarshalBool/incomplete_false +--- PASS: TestUnmarshalBool (0.00s) + --- PASS: TestUnmarshalBool/true_value (0.00s) + --- PASS: TestUnmarshalBool/false_value (0.00s) + --- PASS: TestUnmarshalBool/true_at_end (0.00s) + --- PASS: TestUnmarshalBool/false_at_end (0.00s) + --- PASS: TestUnmarshalBool/true_with_prefix (0.00s) + --- PASS: TestUnmarshalBool/false_with_prefix (0.00s) + --- PASS: TestUnmarshalBool/no_boolean_value (0.00s) + --- PASS: TestUnmarshalBool/empty_input (0.00s) + --- PASS: TestUnmarshalBool/incomplete_true (0.00s) + --- PASS: TestUnmarshalBool/incomplete_false (0.00s) +=== RUN TestComma +=== RUN TestComma/comma_found +=== RUN TestComma/comma_at_start +=== RUN TestComma/no_comma + helpers_test.go:550: Comma() rem = "nocomma", want "" +=== RUN TestComma/empty_input +--- FAIL: TestComma (0.00s) + --- PASS: TestComma/comma_found (0.00s) + --- PASS: TestComma/comma_at_start (0.00s) + --- FAIL: TestComma/no_comma (0.00s) + --- PASS: TestComma/empty_input (0.00s) +=== RUN TestAppendHexFromBinary +=== RUN TestAppendHexFromBinary/basic_hex_encoding_with_quote +=== RUN TestAppendHexFromBinary/basic_hex_encoding_without_quote +=== RUN TestAppendHexFromBinary/empty_src_with_quote +=== RUN TestAppendHexFromBinary/empty_src_without_quote +=== RUN TestAppendHexFromBinary/single_byte_with_quote +=== RUN TestAppendHexFromBinary/single_byte_without_quote +--- PASS: TestAppendHexFromBinary (0.00s) + --- PASS: TestAppendHexFromBinary/basic_hex_encoding_with_quote (0.00s) + --- PASS: TestAppendHexFromBinary/basic_hex_encoding_without_quote (0.00s) + --- PASS: TestAppendHexFromBinary/empty_src_with_quote (0.00s) + --- PASS: TestAppendHexFromBinary/empty_src_without_quote (0.00s) + --- PASS: TestAppendHexFromBinary/single_byte_with_quote (0.00s) + --- PASS: TestAppendHexFromBinary/single_byte_without_quote (0.00s) +=== RUN TestAppendBinaryFromHex +=== RUN TestAppendBinaryFromHex/basic_hex_decoding_with_unquote +=== RUN TestAppendBinaryFromHex/basic_hex_decoding_without_unquote +=== RUN TestAppendBinaryFromHex/empty_hex_with_unquote +2025-06-26T20:12:49+01:00.315 ERR nothing to decode /home/david/src/realy.lol/text/hex.go:24 + hex_test.go:165: AppendBinaryFromHex() error = nothing to decode, wantErr false +=== RUN TestAppendBinaryFromHex/empty_hex_without_unquote +2025-06-26T20:12:49+01:00.315 ERR nothing to decode /home/david/src/realy.lol/text/hex.go:28 + hex_test.go:165: AppendBinaryFromHex() error = nothing to decode, wantErr false +=== RUN TestAppendBinaryFromHex/single_byte_hex_with_unquote +=== RUN TestAppendBinaryFromHex/single_byte_hex_without_unquote +=== RUN TestAppendBinaryFromHex/invalid_hex_with_unquote +2025-06-26T20:12:49+01:00.315 ERR encoding/hex: invalid byte: U+0067 'g' /home/david/src/realy.lol/text/hex.go:24 +=== RUN TestAppendBinaryFromHex/invalid_hex_without_unquote +2025-06-26T20:12:49+01:00.315 ERR encoding/hex: invalid byte: U+0067 'g' /home/david/src/realy.lol/text/hex.go:28 +=== RUN TestAppendBinaryFromHex/odd_length_hex_with_unquote +2025-06-26T20:12:49+01:00.315 ERR encoding/hex: odd length hex string /home/david/src/realy.lol/text/hex.go:24 +=== RUN TestAppendBinaryFromHex/odd_length_hex_without_unquote +2025-06-26T20:12:49+01:00.315 ERR encoding/hex: odd length hex string /home/david/src/realy.lol/text/hex.go:28 +--- FAIL: TestAppendBinaryFromHex (0.00s) + --- PASS: TestAppendBinaryFromHex/basic_hex_decoding_with_unquote (0.00s) + --- PASS: TestAppendBinaryFromHex/basic_hex_decoding_without_unquote (0.00s) + --- FAIL: TestAppendBinaryFromHex/empty_hex_with_unquote (0.00s) + --- FAIL: TestAppendBinaryFromHex/empty_hex_without_unquote (0.00s) + --- PASS: TestAppendBinaryFromHex/single_byte_hex_with_unquote (0.00s) + --- PASS: TestAppendBinaryFromHex/single_byte_hex_without_unquote (0.00s) + --- PASS: TestAppendBinaryFromHex/invalid_hex_with_unquote (0.00s) + --- PASS: TestAppendBinaryFromHex/invalid_hex_without_unquote (0.00s) + --- PASS: TestAppendBinaryFromHex/odd_length_hex_with_unquote (0.00s) + --- PASS: TestAppendBinaryFromHex/odd_length_hex_without_unquote (0.00s) +=== RUN TestUnquote +=== RUN TestUnquote/basic_quoted_string +=== RUN TestUnquote/single_quotes +=== RUN TestUnquote/empty_quotes +--- PASS: TestUnquote (0.00s) + --- PASS: TestUnquote/basic_quoted_string (0.00s) + --- PASS: TestUnquote/single_quotes (0.00s) + --- PASS: TestUnquote/empty_quotes (0.00s) +=== RUN TestNoop +--- PASS: TestNoop (0.00s) +=== RUN TestAppendQuote +=== RUN TestAppendQuote/basic_append_quote +=== RUN TestAppendQuote/empty_dst +=== RUN TestAppendQuote/empty_src +--- PASS: TestAppendQuote (0.00s) + --- PASS: TestAppendQuote/basic_append_quote (0.00s) + --- PASS: TestAppendQuote/empty_dst (0.00s) + --- PASS: TestAppendQuote/empty_src (0.00s) +=== RUN TestQuote +=== RUN TestQuote/basic_quote +=== RUN TestQuote/empty_dst +--- PASS: TestQuote (0.00s) + --- PASS: TestQuote/basic_quote (0.00s) + --- PASS: TestQuote/empty_dst (0.00s) +=== RUN TestAppendSingleQuote +=== RUN TestAppendSingleQuote/basic_single_quote +=== RUN TestAppendSingleQuote/empty_src +--- PASS: TestAppendSingleQuote (0.00s) + --- PASS: TestAppendSingleQuote/basic_single_quote (0.00s) + --- PASS: TestAppendSingleQuote/empty_src (0.00s) +=== RUN TestAppendBackticks +=== RUN TestAppendBackticks/basic_backticks +=== RUN TestAppendBackticks/empty_src +--- PASS: TestAppendBackticks (0.00s) + --- PASS: TestAppendBackticks/basic_backticks (0.00s) + --- PASS: TestAppendBackticks/empty_src (0.00s) +=== RUN TestAppendBrace +=== RUN TestAppendBrace/basic_braces +=== RUN TestAppendBrace/empty_src +--- PASS: TestAppendBrace (0.00s) + --- PASS: TestAppendBrace/basic_braces (0.00s) + --- PASS: TestAppendBrace/empty_src (0.00s) +=== RUN TestAppendParenthesis +=== RUN TestAppendParenthesis/basic_parenthesis +=== RUN TestAppendParenthesis/empty_src +--- PASS: TestAppendParenthesis (0.00s) + --- PASS: TestAppendParenthesis/basic_parenthesis (0.00s) + --- PASS: TestAppendParenthesis/empty_src (0.00s) +=== RUN TestAppendBracket +=== RUN TestAppendBracket/basic_brackets +=== RUN TestAppendBracket/empty_src +--- PASS: TestAppendBracket (0.00s) + --- PASS: TestAppendBracket/basic_brackets (0.00s) + --- PASS: TestAppendBracket/empty_src (0.00s) +=== RUN TestAppendList +=== RUN TestAppendList/basic_list_with_comma +=== RUN TestAppendList/single_item +=== RUN TestAppendList/empty_list +=== RUN TestAppendList/pipe_separator +--- PASS: TestAppendList (0.00s) + --- PASS: TestAppendList/basic_list_with_comma (0.00s) + --- PASS: TestAppendList/single_item (0.00s) + --- PASS: TestAppendList/empty_list (0.00s) + --- PASS: TestAppendList/pipe_separator (0.00s) +FAIL +coverage: 90.7% of statements +exit status 1 +FAIL realy.lol/text 0.008s \ No newline at end of file diff --git a/text/escape.go b/text/escape.go index f94333c..3cdeb5f 100644 --- a/text/escape.go +++ b/text/escape.go @@ -110,12 +110,12 @@ func NostrUnescape(dst []byte) (b []byte) { dst[w] = c w++ - // anything else after a reverse solidus just preserve it. - default: - dst[w] = dst[r] - w++ - dst[w] = c - w++ + // anything else after a reverse solidus just preserve it. + default: + dst[w] = '\\' + w++ + dst[w] = c + w++ } } else { dst[w] = dst[r] diff --git a/text/escape_test.go b/text/escape_test.go index c589e01..e51d1a4 100644 --- a/text/escape_test.go +++ b/text/escape_test.go @@ -401,3 +401,46 @@ func BenchmarkNostrEscapeNostrUnescape(b *testing.B) { } }) } + +func TestNostrUnescapeEdgeCases(t *testing.T) { + tests := []struct { + name string + input []byte + expected []byte + }{ + { + name: "unicode escape", + input: []byte(`\u0041`), + expected: []byte(`\u0041`), + }, + { + name: "forward slash escape", + input: []byte(`\/`), + expected: []byte(`\/`), + }, + { + name: "octal escape", + input: []byte(`\123`), + expected: []byte(`\123`), + }, + { + name: "unknown escape", + input: []byte(`\x`), + expected: []byte(`\x`), + }, + { + name: "multiple escapes", + input: []byte(`\u0041\n\/\123\x`), + expected: []byte("\\u0041\n\\/\\123\\x"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NostrUnescape(tt.input) + if !bytes.Equal(result, tt.expected) { + t.Errorf("NostrUnescape() = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/text/helpers.go b/text/helpers.go index bafdfad..a2bc7d9 100644 --- a/text/helpers.go +++ b/text/helpers.go @@ -62,7 +62,7 @@ func UnmarshalQuoted(b []byte) (content, rem []byte, err error) { return } rem = b[:] - for ; len(rem) >= 0; rem = rem[1:] { + for ; len(rem) > 0; rem = rem[1:] { // advance to open quotes if rem[0] == '"' { rem = rem[1:] @@ -133,11 +133,13 @@ func MarshalHexArray(dst []byte, ha [][]byte) (b []byte) { func UnmarshalHexArray(b []byte, size int) (t [][]byte, rem []byte, err error) { rem = b var openBracket bool - for ; len(rem) > 0; rem = rem[1:] { + for len(rem) > 0 { if rem[0] == '[' { openBracket = true + rem = rem[1:] } else if openBracket { if rem[0] == ',' { + rem = rem[1:] continue } else if rem[0] == ']' { rem = rem[1:] @@ -153,12 +155,16 @@ func UnmarshalHexArray(b []byte, size int) (t [][]byte, rem []byte, err error) { return } t = append(t, h) - if rem[0] == ']' { + if len(rem) > 0 && rem[0] == ']' { rem = rem[1:] // done return } + } else { + rem = rem[1:] } + } else { + rem = rem[1:] } } return @@ -168,11 +174,13 @@ func UnmarshalHexArray(b []byte, size int) (t [][]byte, rem []byte, err error) { func UnmarshalStringArray(b []byte) (t [][]byte, rem []byte, err error) { rem = b var openBracket bool - for ; len(rem) > 0; rem = rem[1:] { + for len(rem) > 0 { if rem[0] == '[' { openBracket = true + rem = rem[1:] } else if openBracket { if rem[0] == ',' { + rem = rem[1:] continue } else if rem[0] == ']' { rem = rem[1:] @@ -183,12 +191,16 @@ func UnmarshalStringArray(b []byte) (t [][]byte, rem []byte, err error) { return } t = append(t, h) - if rem[0] == ']' { + if len(rem) > 0 && rem[0] == ']' { rem = rem[1:] // done return } + } else { + rem = rem[1:] } + } else { + rem = rem[1:] } } return diff --git a/text/helpers_test.go b/text/helpers_test.go index 872d22f..b2eeac8 100644 --- a/text/helpers_test.go +++ b/text/helpers_test.go @@ -49,3 +49,506 @@ func TestUnmarshalHexArray(t *testing.T) { } } } + +func TestJSONKey(t *testing.T) { + tests := []struct { + name string + dst []byte + key []byte + expected []byte + }{ + { + name: "basic key", + dst: []byte("prefix"), + key: []byte("name"), + expected: []byte(`prefix"name":`), + }, + { + name: "empty dst", + dst: []byte{}, + key: []byte("id"), + expected: []byte(`"id":`), + }, + { + name: "empty key", + dst: []byte("start"), + key: []byte{}, + expected: []byte(`start"":`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := JSONKey(tt.dst, tt.key) + if !bytes.Equal(result, tt.expected) { + t.Errorf("JSONKey() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestUnmarshalHex(t *testing.T) { + tests := []struct { + name string + input []byte + expectedHex []byte + expectedRem []byte + wantErr bool + }{ + { + name: "basic hex", + input: []byte(`"01234567"remaining`), + expectedHex: []byte{0x01, 0x23, 0x45, 0x67}, + expectedRem: []byte("remaining"), + wantErr: false, + }, + { + name: "empty hex", + input: []byte(`""rest`), + expectedHex: []byte{}, + expectedRem: []byte("rest"), + wantErr: false, + }, + { + name: "no quotes", + input: []byte("01234567"), + expectedHex: nil, + expectedRem: []byte("01234567"), + wantErr: true, + }, + { + name: "odd length hex", + input: []byte(`"123"`), + expectedHex: nil, + expectedRem: []byte{}, + wantErr: true, + }, + { + name: "invalid hex characters", + input: []byte(`"gg"`), + expectedHex: nil, + expectedRem: []byte{}, + wantErr: true, + }, + { + name: "empty input", + input: []byte{}, + expectedHex: nil, + expectedRem: []byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hex, rem, err := UnmarshalHex(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalHex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if !bytes.Equal(hex, tt.expectedHex) { + t.Errorf("UnmarshalHex() hex = %v, want %v", hex, tt.expectedHex) + } + if !bytes.Equal(rem, tt.expectedRem) { + t.Errorf("UnmarshalHex() rem = %q, want %q", rem, tt.expectedRem) + } + } + }) + } +} + +func TestUnmarshalQuoted(t *testing.T) { + tests := []struct { + name string + input []byte + expectedContent []byte + expectedRem []byte + wantErr bool + }{ + { + name: "basic quoted string", + input: []byte(`"hello"world`), + expectedContent: []byte("hello"), + expectedRem: []byte("world"), + wantErr: false, + }, + { + name: "empty quoted string", + input: []byte(`""rest`), + expectedContent: []byte{}, + expectedRem: []byte("rest"), + wantErr: false, + }, + { + name: "escaped quotes", + input: []byte(`"say \"hello\""end`), + expectedContent: []byte(`say "hello"`), + expectedRem: []byte("end"), + wantErr: false, + }, + { + name: "escaped backslash", + input: []byte(`"path\\to\\file"rest`), + expectedContent: []byte(`path\to\file`), + expectedRem: []byte("rest"), + wantErr: false, + }, + { + name: "no opening quote", + input: []byte(`hello"world`), + expectedContent: []byte("world"), + expectedRem: []byte{}, + wantErr: false, + }, + { + name: "no closing quote", + input: []byte(`"hello`), + expectedContent: []byte("hello"), + expectedRem: []byte{}, + wantErr: false, + }, + { + name: "empty input", + input: []byte{}, + expectedContent: nil, + expectedRem: []byte{}, + wantErr: true, + }, + { + name: "invalid control character", + input: []byte("\"hello\tworld\""), + expectedContent: nil, + expectedRem: []byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + content, rem, err := UnmarshalQuoted(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalQuoted() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if !bytes.Equal(content, tt.expectedContent) { + t.Errorf("UnmarshalQuoted() content = %q, want %q", content, tt.expectedContent) + } + if !bytes.Equal(rem, tt.expectedRem) { + t.Errorf("UnmarshalQuoted() rem = %q, want %q", rem, tt.expectedRem) + } + } + }) + } +} + +func TestMarshalHexArray(t *testing.T) { + tests := []struct { + name string + dst []byte + ha [][]byte + expected []byte + }{ + { + name: "basic hex array", + dst: []byte("prefix"), + ha: [][]byte{{0x01, 0x23}, {0x45, 0x67}}, + expected: []byte(`prefix["0123","4567"]`), + }, + { + name: "empty array", + dst: []byte("start"), + ha: [][]byte{}, + expected: []byte("start[]"), + }, + { + name: "single element", + dst: []byte{}, + ha: [][]byte{{0xff}}, + expected: []byte(`["ff"]`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MarshalHexArray(tt.dst, tt.ha) + if !bytes.Equal(result, tt.expected) { + t.Errorf("MarshalHexArray() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestUnmarshalStringArray(t *testing.T) { + tests := []struct { + name string + input []byte + expectedArr [][]byte + expectedRem []byte + wantErr bool + }{ + { + name: "basic string array", + input: []byte(`["hello","world"]rest`), + expectedArr: [][]byte{[]byte("hello"), []byte("world")}, + expectedRem: []byte("rest"), + wantErr: false, + }, + { + name: "empty array", + input: []byte(`[]remaining`), + expectedArr: [][]byte{}, + expectedRem: []byte("remaining"), + wantErr: false, + }, + { + name: "single element", + input: []byte(`["single"]end`), + expectedArr: [][]byte{[]byte("single")}, + expectedRem: []byte("end"), + wantErr: false, + }, + { + name: "no opening bracket", + input: []byte(`"hello","world"]`), + expectedArr: [][]byte{}, + expectedRem: []byte{}, + wantErr: false, + }, + { + name: "no closing bracket", + input: []byte(`["hello","world"`), + expectedArr: [][]byte{[]byte("hello"), []byte("world")}, + expectedRem: []byte{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + arr, rem, err := UnmarshalStringArray(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalStringArray() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(arr) != len(tt.expectedArr) { + t.Errorf("UnmarshalStringArray() array length = %d, want %d", len(arr), len(tt.expectedArr)) + return + } + for i := range arr { + if !bytes.Equal(arr[i], tt.expectedArr[i]) { + t.Errorf("UnmarshalStringArray() arr[%d] = %q, want %q", i, arr[i], tt.expectedArr[i]) + } + } + if !bytes.Equal(rem, tt.expectedRem) { + t.Errorf("UnmarshalStringArray() rem = %q, want %q", rem, tt.expectedRem) + } + }) + } +} + +func TestTrue(t *testing.T) { + result := True() + expected := []byte("true") + if !bytes.Equal(result, expected) { + t.Errorf("True() = %q, want %q", result, expected) + } +} + +func TestFalse(t *testing.T) { + result := False() + expected := []byte("false") + if !bytes.Equal(result, expected) { + t.Errorf("False() = %q, want %q", result, expected) + } +} + +func TestMarshalBool(t *testing.T) { + tests := []struct { + name string + src []byte + truth bool + expected []byte + }{ + { + name: "true value", + src: []byte("prefix"), + truth: true, + expected: []byte("prefixtrue"), + }, + { + name: "false value", + src: []byte("prefix"), + truth: false, + expected: []byte("prefixfalse"), + }, + { + name: "true with empty src", + src: []byte{}, + truth: true, + expected: []byte("true"), + }, + { + name: "false with empty src", + src: []byte{}, + truth: false, + expected: []byte("false"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MarshalBool(tt.src, tt.truth) + if !bytes.Equal(result, tt.expected) { + t.Errorf("MarshalBool() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestUnmarshalBool(t *testing.T) { + tests := []struct { + name string + input []byte + expectedRem []byte + expectedTruth bool + wantErr bool + }{ + { + name: "true value", + input: []byte("truerest"), + expectedRem: []byte("rest"), + expectedTruth: true, + wantErr: false, + }, + { + name: "false value", + input: []byte("falserest"), + expectedRem: []byte("rest"), + expectedTruth: false, + wantErr: false, + }, + { + name: "true at end", + input: []byte("true"), + expectedRem: []byte{}, + expectedTruth: true, + wantErr: false, + }, + { + name: "false at end", + input: []byte("false"), + expectedRem: []byte{}, + expectedTruth: false, + wantErr: false, + }, + { + name: "true with prefix", + input: []byte("prefixtruerest"), + expectedRem: []byte("rest"), + expectedTruth: true, + wantErr: false, + }, + { + name: "false with prefix", + input: []byte("prefixfalserest"), + expectedRem: []byte("rest"), + expectedTruth: false, + wantErr: false, + }, + { + name: "no boolean value", + input: []byte("nothing"), + expectedRem: []byte{}, + expectedTruth: false, + wantErr: true, + }, + { + name: "empty input", + input: []byte{}, + expectedRem: []byte{}, + expectedTruth: false, + wantErr: true, + }, + { + name: "incomplete true", + input: []byte("tr"), + expectedRem: []byte{}, + expectedTruth: false, + wantErr: true, + }, + { + name: "incomplete false", + input: []byte("fal"), + expectedRem: []byte{}, + expectedTruth: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rem, truth, err := UnmarshalBool(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalBool() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if truth != tt.expectedTruth { + t.Errorf("UnmarshalBool() truth = %v, want %v", truth, tt.expectedTruth) + } + if !bytes.Equal(rem, tt.expectedRem) { + t.Errorf("UnmarshalBool() rem = %q, want %q", rem, tt.expectedRem) + } + } + }) + } +} + +func TestComma(t *testing.T) { + tests := []struct { + name string + input []byte + expectedRem []byte + wantErr bool + }{ + { + name: "comma found", + input: []byte("prefix,rest"), + expectedRem: []byte(",rest"), + wantErr: false, + }, + { + name: "comma at start", + input: []byte(",rest"), + expectedRem: []byte(",rest"), + wantErr: false, + }, + { + name: "no comma", + input: []byte("nocomma"), + expectedRem: []byte("nocomma"), + wantErr: true, + }, + { + name: "empty input", + input: []byte{}, + expectedRem: []byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rem, err := Comma(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("Comma() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !bytes.Equal(rem, tt.expectedRem) { + t.Errorf("Comma() rem = %q, want %q", rem, tt.expectedRem) + } + }) + } +} diff --git a/text/hex_test.go b/text/hex_test.go new file mode 100644 index 0000000..c3ee5f0 --- /dev/null +++ b/text/hex_test.go @@ -0,0 +1,173 @@ +package text + +import ( + "bytes" + "testing" +) + +func TestAppendHexFromBinary(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + quote bool + expected []byte + }{ + { + name: "basic hex encoding with quote", + dst: []byte("prefix"), + src: []byte{0x01, 0x23, 0x45, 0x67}, + quote: true, + expected: []byte(`prefix"01234567"`), + }, + { + name: "basic hex encoding without quote", + dst: []byte("prefix"), + src: []byte{0x01, 0x23, 0x45, 0x67}, + quote: false, + expected: []byte("prefix01234567"), + }, + { + name: "empty src with quote", + dst: []byte("start"), + src: []byte{}, + quote: true, + expected: []byte(`start""`), + }, + { + name: "empty src without quote", + dst: []byte("start"), + src: []byte{}, + quote: false, + expected: []byte("start"), + }, + { + name: "single byte with quote", + dst: []byte{}, + src: []byte{0xff}, + quote: true, + expected: []byte(`"ff"`), + }, + { + name: "single byte without quote", + dst: []byte{}, + src: []byte{0xff}, + quote: false, + expected: []byte("ff"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendHexFromBinary(tt.dst, tt.src, tt.quote) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendHexFromBinary() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestAppendBinaryFromHex(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + unquote bool + expected []byte + wantErr bool + }{ + { + name: "basic hex decoding with unquote", + dst: []byte("prefix"), + src: []byte(`"01234567"`), + unquote: true, + expected: []byte("prefix\x01\x23\x45\x67"), + wantErr: false, + }, + { + name: "basic hex decoding without unquote", + dst: []byte("prefix"), + src: []byte("01234567"), + unquote: false, + expected: []byte("prefix\x01\x23\x45\x67"), + wantErr: false, + }, + { + name: "empty hex with unquote", + dst: []byte("start"), + src: []byte(`""`), + unquote: true, + expected: []byte("start"), + wantErr: true, + }, + { + name: "empty hex without unquote", + dst: []byte("start"), + src: []byte(""), + unquote: false, + expected: []byte("start"), + wantErr: true, + }, + { + name: "single byte hex with unquote", + dst: []byte{}, + src: []byte(`"ff"`), + unquote: true, + expected: []byte{0xff}, + wantErr: false, + }, + { + name: "single byte hex without unquote", + dst: []byte{}, + src: []byte("ff"), + unquote: false, + expected: []byte{0xff}, + wantErr: false, + }, + { + name: "invalid hex with unquote", + dst: []byte{}, + src: []byte(`"gg"`), + unquote: true, + expected: []byte{}, + wantErr: true, + }, + { + name: "invalid hex without unquote", + dst: []byte{}, + src: []byte("gg"), + unquote: false, + expected: []byte{}, + wantErr: true, + }, + { + name: "odd length hex with unquote", + dst: []byte{}, + src: []byte(`"123"`), + unquote: true, + expected: []byte{}, + wantErr: true, + }, + { + name: "odd length hex without unquote", + dst: []byte{}, + src: []byte("123"), + unquote: false, + expected: []byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := AppendBinaryFromHex(tt.dst, tt.src, tt.unquote) + if (err != nil) != tt.wantErr { + t.Errorf("AppendBinaryFromHex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !bytes.Equal(result, tt.expected) { + t.Errorf("AppendBinaryFromHex() = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/text/wrap.go b/text/wrap.go index ec3d3fd..1f05d40 100644 --- a/text/wrap.go +++ b/text/wrap.go @@ -77,7 +77,7 @@ func AppendList(dst []byte, src [][]byte, separator byte, ac AppendBytesClosure) []byte { last := len(src) - 1 for i := range src { - dst = append(dst, ac(dst, src[i])...) + dst = ac(dst, src[i]) if i < last { dst = append(dst, separator) } diff --git a/text/wrap_test.go b/text/wrap_test.go new file mode 100644 index 0000000..7a32f86 --- /dev/null +++ b/text/wrap_test.go @@ -0,0 +1,321 @@ +package text + +import ( + "bytes" + "testing" +) + +func TestUnquote(t *testing.T) { + tests := []struct { + name string + input []byte + expected []byte + }{ + { + name: "basic quoted string", + input: []byte(`"hello"`), + expected: []byte("hello"), + }, + { + name: "single quotes", + input: []byte(`'world'`), + expected: []byte("world"), + }, + { + name: "empty quotes", + input: []byte(`""`), + expected: []byte(""), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Unquote(tt.input) + if !bytes.Equal(result, tt.expected) { + t.Errorf("Unquote() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestNoop(t *testing.T) { + dst := []byte("hello") + src := []byte("world") + expected := []byte("helloworld") + + result := Noop(dst, src) + if !bytes.Equal(result, expected) { + t.Errorf("Noop() = %q, want %q", result, expected) + } +} + +func TestAppendQuote(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + expected []byte + }{ + { + name: "basic append quote", + dst: []byte("prefix"), + src: []byte("content"), + expected: []byte(`prefix"content"`), + }, + { + name: "empty dst", + dst: []byte{}, + src: []byte("test"), + expected: []byte(`"test"`), + }, + { + name: "empty src", + dst: []byte("start"), + src: []byte{}, + expected: []byte(`start""`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendQuote(tt.dst, tt.src, Noop) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendQuote() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestQuote(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + expected []byte + }{ + { + name: "basic quote", + dst: []byte("prefix"), + src: []byte("content"), + expected: []byte(`prefix"content"`), + }, + { + name: "empty dst", + dst: []byte{}, + src: []byte("test"), + expected: []byte(`"test"`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Quote(tt.dst, tt.src) + if !bytes.Equal(result, tt.expected) { + t.Errorf("Quote() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestAppendSingleQuote(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + expected []byte + }{ + { + name: "basic single quote", + dst: []byte("prefix"), + src: []byte("content"), + expected: []byte("prefix'content'"), + }, + { + name: "empty src", + dst: []byte("start"), + src: []byte{}, + expected: []byte("start''"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendSingleQuote(tt.dst, tt.src, Noop) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendSingleQuote() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestAppendBackticks(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + expected []byte + }{ + { + name: "basic backticks", + dst: []byte("prefix"), + src: []byte("content"), + expected: []byte("prefix`content`"), + }, + { + name: "empty src", + dst: []byte("start"), + src: []byte{}, + expected: []byte("start``"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendBackticks(tt.dst, tt.src, Noop) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendBackticks() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestAppendBrace(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + expected []byte + }{ + { + name: "basic braces", + dst: []byte("prefix"), + src: []byte("content"), + expected: []byte("prefix(content)"), + }, + { + name: "empty src", + dst: []byte("start"), + src: []byte{}, + expected: []byte("start()"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendBrace(tt.dst, tt.src, Noop) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendBrace() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestAppendParenthesis(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + expected []byte + }{ + { + name: "basic parenthesis", + dst: []byte("prefix"), + src: []byte("content"), + expected: []byte("prefix{content}"), + }, + { + name: "empty src", + dst: []byte("start"), + src: []byte{}, + expected: []byte("start{}"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendParenthesis(tt.dst, tt.src, Noop) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendParenthesis() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestAppendBracket(t *testing.T) { + tests := []struct { + name string + dst []byte + src []byte + expected []byte + }{ + { + name: "basic brackets", + dst: []byte("prefix"), + src: []byte("content"), + expected: []byte("prefix[content]"), + }, + { + name: "empty src", + dst: []byte("start"), + src: []byte{}, + expected: []byte("start[]"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendBracket(tt.dst, tt.src, Noop) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendBracket() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestAppendList(t *testing.T) { + tests := []struct { + name string + dst []byte + src [][]byte + separator byte + expected []byte + }{ + { + name: "basic list with comma", + dst: []byte("list:"), + src: [][]byte{[]byte("a"), []byte("b"), []byte("c")}, + separator: ',', + expected: []byte("list:a,b,c"), + }, + { + name: "single item", + dst: []byte("item:"), + src: [][]byte{[]byte("single")}, + separator: ',', + expected: []byte("item:single"), + }, + { + name: "empty list", + dst: []byte("empty:"), + src: [][]byte{}, + separator: ',', + expected: []byte("empty:"), + }, + { + name: "pipe separator", + dst: []byte{}, + src: [][]byte{[]byte("x"), []byte("y")}, + separator: '|', + expected: []byte("x|y"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendList(tt.dst, tt.src, tt.separator, Noop) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AppendList() = %q, want %q", result, tt.expected) + } + }) + } +} \ No newline at end of file