diff --git a/.gitignore b/.gitignore index bd7d5b2..7bda526 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ jstester/node_modules +encoding/tmp +pdu/tmp diff --git a/encoding/.air.toml b/encoding/.air.toml new file mode 100644 index 0000000..cdbafef --- /dev/null +++ b/encoding/.air.toml @@ -0,0 +1,51 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "tmp\\main.exe" + cmd = "go test ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = [] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 100 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = true + +[proxy] + app_port = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = true + keep_scroll = true diff --git a/encoding/ascii.go b/encoding/ascii.go new file mode 100644 index 0000000..552a7b2 --- /dev/null +++ b/encoding/ascii.go @@ -0,0 +1,22 @@ +package encoding + +import "bytes" + +type ASCIICoder struct{} + +func (c *ASCIICoder) Encode(s *string, buf *bytes.Buffer) error { + // These should be ASCII but UTF8 is a superset of ASCII so hopefully this'll be fine + buf.WriteString(*s) + return nil +} + +func (c *ASCIICoder) Decode(buf *bytes.Buffer) (string, error) { + return buf.String(), nil +} + +func (c ASCIICoder) EncodesInto(s *string) int { + return len(*s) +} +func (c ASCIICoder) DecodesInto(buf *bytes.Buffer) int { + return buf.Len() +} diff --git a/encoding/ascii_test.go b/encoding/ascii_test.go new file mode 100644 index 0000000..631d29e --- /dev/null +++ b/encoding/ascii_test.go @@ -0,0 +1,74 @@ +package encoding + +import ( + "bytes" + "testing" +) + +func TestASCIIEncodeSimpleASCIIString(t *testing.T) { + coder := &ASCIICoder{} + var buf bytes.Buffer + input := "Hello, World!" + + expected := []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33} + err := coder.Encode(&input, &buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if !bytes.Equal(buf.Bytes(), expected) { + t.Errorf("Expected %v, but got %v", expected, buf.Bytes()) + } +} + +func TestASCIIDecodeSimpleASCIIString(t *testing.T) { + coder := &ASCIICoder{} + var buf bytes.Buffer + input := []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33} + + expected := "Hello, World!" + buf.Write(input) + output, err := coder.Decode(&buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if output != expected { + t.Errorf("Expected %v, but got %v", expected, output) + } +} + +func TestASCIIEncodeEmptyString(t *testing.T) { + coder := &ASCIICoder{} + var buf bytes.Buffer + input := "" + + expected := []byte{} + err := coder.Encode(&input, &buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if !bytes.Equal(buf.Bytes(), expected) { + t.Errorf("Expected %v, but got %v", expected, buf.Bytes()) + } +} + +func TestASCIIDecodeEmptyString(t *testing.T) { + coder := &ASCIICoder{} + buf := bytes.NewBuffer([]byte{}) + + expected := "" + output, err := coder.Decode(buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if output != expected { + t.Errorf("Expected %v, but got %v", expected, output) + } +} diff --git a/encoding/charset.json b/encoding/charset.json new file mode 100644 index 0000000..60c72aa --- /dev/null +++ b/encoding/charset.json @@ -0,0 +1,5 @@ +{ + "ucs2": { + "link": "http://www.columbia.edu/kermit/ucs2.html" + } +} \ No newline at end of file diff --git a/encoding/coder.go b/encoding/coder.go new file mode 100644 index 0000000..2b3d69b --- /dev/null +++ b/encoding/coder.go @@ -0,0 +1,10 @@ +package encoding + +import "bytes" + +type Coder interface { + Encode(s *string, buf *bytes.Buffer) error + Decode(buf *bytes.Buffer) (string, error) + EncodesInto(s *string) int + DecodesInto(buf *bytes.Buffer) int +} diff --git a/encoding/gsm7.go b/encoding/gsm7.go new file mode 100644 index 0000000..8beadcd --- /dev/null +++ b/encoding/gsm7.go @@ -0,0 +1,136 @@ +package encoding + +import ( + "bytes" + "fmt" +) + +type GSM7Coder struct{} + +// Make sure buffer can fit EncodesInto bytes +// Otherwise Encode will allocate memory as it sees fit +// Which is fine but not optimal +// Preallocate the buffer with the size of EncodesInto bytes +func (c *GSM7Coder) Encode(s *string, buf *bytes.Buffer) error { + // utf8 := *(*[]byte)(unsafe.Pointer(&s)) + utf8 := []byte(*s) + var ( + offset int = 1 + bitshift byte = 0 + leap, shift bool + ) + encodedSize := c.EncodesInto(s) + cap := buf.Cap() + if cap < encodedSize { + buf.Grow(encodedSize - cap) + } + + for index, septet := range utf8 { + if septet > 0b01111111 { + return fmt.Errorf("invalid character at index %d", index) + } + if index == 0 { + continue + } + bitshift++ + // log.Printf("Index:%-3d Offset:%-3d Bitshift:%-3d CurrentByte:%08b (%-3d) OffsetByte:%08b (%-3d) Leap:%5v", index, offset, bitshift, utf8[index], utf8[index], utf8[index-offset], utf8[index-offset], leap) + mask := byte(255 >> (8 - bitshift)) + masked := (mask & septet) << (8 - bitshift) + // log.Printf("Index:%-3d Offset:%-3d Bitshift:%-3d Mask:%08b Masked:%08b", index, offset, bitshift, mask, masked) + if leap { + masked >>= 1 + } + utf8[index-offset] |= masked + utf8[index] >>= bitshift + + if !leap { + buf.WriteByte(utf8[index-offset]) + } + if index == len(utf8)-1 && utf8[index] > 0 { + buf.WriteByte(utf8[index]) + } + // log.Printf("Index:%-3d Offset:%-3d Bitshift:%-3d CurrentByte:%08b (%-3d) OffsetByte:%08b (%-3d) Leap:%5v", index, offset, bitshift, utf8[index], utf8[index], utf8[index-offset], utf8[index-offset], leap) + if bitshift >= 7 { + if leap { + // log.Printf("Shift at Index:%-3d Offset:%-3d Bitshift:%-3d", index, offset, bitshift) + leap = false + bitshift = 0 + offset++ + shift = true + continue + } + // log.Printf("Leap at Index:%-3d Offset:%-3d Bitshift:%-3d", index, offset, bitshift) + leap = true + bitshift = 6 + } + if shift { + offset = 1 + } + } + return nil +} + +func (c *GSM7Coder) Decode(buf *bytes.Buffer) (string, error) { + gsm7 := buf.Bytes() + var ( + offset int + bitshift byte = 0 + leap bool + ) + outLength := c.DecodesInto(buf) + lengthDiff := outLength - len(gsm7) + gsm7 = append(gsm7, make([]byte, lengthDiff)...) + start := len(gsm7) - 2 + + // We don't care about the last byte + // Unless it's the %8....... + // We'll deal with that later + for index := start; index >= 0; index-- { + octet := gsm7[index] + bitshift = byte((index % 7) + 1) + if bitshift == 7 { + leap = true + } + offset = 1 + // log.Println(offset, index, index+offset) + // log.Printf("Index:%-3d Offset:%-3d Bitshift:%-3d CurrentByte:%08b (%-3d) OffsetByte(%-3d):%08b (%-3d) Leap:%5v", index, offset, bitshift, gsm7[index], gsm7[index], index+offset, gsm7[index+offset], gsm7[index+offset], leap) + + mask := byte(255 << (8 - bitshift)) + masked := (mask & octet) >> (8 - bitshift) + // log.Printf("Index:%-3d Offset:%-3d Bitshift:%-3d Mask:%08b Masked:%08b", index, offset, bitshift, mask, masked) + if leap { + InsertAt(&gsm7, index+offset, masked) + } else { + gsm7[index+offset] |= masked + } + // Remove last bitshift bits + gsm7[index] <<= bitshift + // Move the remaining bit once to the right to form septet instead of octet + gsm7[index] >>= 1 + + // log.Printf("Index:%-3d Offset:%-3d Bitshift:%-3d CurrentByte:%08b (%-3d) OffsetByte(%-3d):%08b (%-3d) Leap:%5v", index, offset, bitshift, gsm7[index], gsm7[index], index+offset, gsm7[index+offset], gsm7[index+offset], leap) + leap = false + } + return string(gsm7), nil +} + +// Allocation free +// Which means data MUST have space for value +func InsertAt(data *[]byte, index int, value byte) { + copy((*data)[index+1:], (*data)[index:]) + (*data)[index] = value +} + +func (c GSM7Coder) EncodesInto(s *string) int { + slen := len(*s) + enclen := slen * 7 / 8 + if slen%8 != 0 { + enclen++ + } + return enclen +} +func (c GSM7Coder) DecodesInto(buf *bytes.Buffer) int { + blen := buf.Len() + declen := blen * 8 / 7 + return declen +} diff --git a/encoding/gsm7_test.go b/encoding/gsm7_test.go new file mode 100644 index 0000000..fe9731a --- /dev/null +++ b/encoding/gsm7_test.go @@ -0,0 +1,369 @@ +package encoding + +import ( + "bytes" + "testing" +) + +var ( + short8nString = "Sunshine" + longNot8nString = "Golden rays play, Chasing night away." + long8nString = "Ducks are fucking great, they quacks, O quackers, what the fuck." + + short8nStringEncodedBytes = []byte{0b11010011, 0b10111010, 0b01111011, 0b10001110, 0b01001110, 0b10111011, 0b11001011} + longNot8nStringEncodedBytes = []byte{0b11000111, 0b00110111, 0b10011011, 0b01011100, 0b01110110, 0b10000011, 0b11100100, 0b11100001, 0b11111100, 0b00011100, 0b00000100, 0b01100111, 0b10000111, 0b11110011, 0b00101100, 0b11010000, 0b00010000, 0b00011101, 0b10011110, 0b10100111, 0b11011101, 0b01100111, 0b10010000, 0b00111011, 0b01111101, 0b01000110, 0b11010011, 0b01000001, 0b11100001, 0b01111011, 0b00111000, 0b11101111, 0b00000010} + long8nStringEncodedBytes = []byte{0b11000100, 0b11111010, 0b01111000, 0b00111101, 0b00000111, 0b10000101, 0b11100101, 0b01100101, 0b10010000, 0b10111001, 0b00111110, 0b01011110, 0b10100111, 0b11011101, 0b01100111, 0b11010000, 0b01011001, 0b01011110, 0b00001110, 0b11010011, 0b01011001, 0b00100000, 0b00111010, 0b10111010, 0b10011100, 0b00000111, 0b11000101, 0b11101011, 0b11100001, 0b11110001, 0b01111010, 0b11001110, 0b00000010, 0b00111101, 0b01000001, 0b11110001, 0b01111010, 0b01111000, 0b10111100, 0b00101110, 0b11001011, 0b11100111, 0b00101100, 0b11010000, 0b00011101, 0b00011101, 0b10100110, 0b10000011, 0b11101000, 0b11101000, 0b00110010, 0b11001000, 0b01011100, 0b00011111, 0b10101111, 0b01011101} +) + +// region encode +func TestGSM7EncodeSimpleASCIIString(t *testing.T) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := short8nString + + expected := short8nStringEncodedBytes + err := coder.Encode(&input, &buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if !bytes.Equal(buf.Bytes(), expected) { + t.Errorf("Expected '%v', but got '%v'", expected, buf.Bytes()) + } +} + +func TestGSM7EncodeComplexASCIIString(t *testing.T) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := longNot8nString + + expected := longNot8nStringEncodedBytes + err := coder.Encode(&input, &buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if !bytes.Equal(buf.Bytes(), expected) { + t.Errorf("Expected '%v', but got '%v'", expected, buf.Bytes()) + } +} + +func TestGSM7EncodeComplex8nASCIIString(t *testing.T) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := "Ducks are fucking great, they quacks, O quackers, what the fuck." + + expected := long8nStringEncodedBytes + err := coder.Encode(&input, &buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if !bytes.Equal(buf.Bytes(), expected) { + t.Errorf("Expected '%v', but got '%v'", expected, buf.Bytes()) + } +} + +func TestGSM7EncodeDoesNotAllocateMoreThanNecessary(t *testing.T) { + coder := &GSM7Coder{} + input := "Ducks are fucking great, they quacks, O quackers, what the fuck." + buf := bytes.NewBuffer(make([]byte, coder.EncodesInto(&input))) + buf.Reset() + + expected := buf.Cap() + err := coder.Encode(&input, buf) + actual := buf.Cap() + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if actual != expected { + t.Errorf("Expected buffer of size %v, but got %v", expected, actual) + } +} + +func TestGSM7EncodeDoesAllocateWhenNecessary(t *testing.T) { + coder := &GSM7Coder{} + input := "Ducks are fucking great, they quacks, O quackers, what the fuck." + var buf bytes.Buffer + buf.Reset() + + original := buf.Cap() + err := coder.Encode(&input, &buf) + modified := buf.Cap() + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if !(modified > original) { + t.Errorf("Expected buffer to grow but buffer changed from %v to %v", original, modified) + } +} + +func TestGSM7EncodeEmptyString(t *testing.T) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := "" + + expected := []byte{} + err := coder.Encode(&input, &buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if !bytes.Equal(buf.Bytes(), expected) { + t.Errorf("Expected '%v', but got '%v'", expected, buf.Bytes()) + } +} + +// region decode +func TestGSM7DecodeSimpleASCIIString(t *testing.T) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := short8nStringEncodedBytes + + expected := short8nString + buf.Write(input) + actual, err := coder.Decode(&buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if actual != expected { + t.Errorf("Expected '%v', but got '%v'", expected, actual) + } +} + +func TestGSM7DecodeComplexASCIIString(t *testing.T) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := longNot8nStringEncodedBytes + + expected := longNot8nString + buf.Write(input) + actual, err := coder.Decode(&buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if actual != expected { + t.Errorf("Expected '%v', but got '%v'", expected, actual) + } +} + +func TestGSM7DecodeComplex8nASCIIString(t *testing.T) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := long8nStringEncodedBytes + + expected := long8nString + buf.Write(input) + actual, err := coder.Decode(&buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if actual != expected { + t.Errorf("Expected '%v', but got '%v'", expected, actual) + } +} + +func TestGSM7DecodeEmptyString(t *testing.T) { + coder := &GSM7Coder{} + buf := bytes.NewBuffer([]byte{}) + + expected := "" + actual, err := coder.Decode(buf) + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + + if actual != expected { + t.Errorf("Expected '%v', but got '%v'", expected, actual) + } +} + +// region insertat +func TestInsertAtBeginning(t *testing.T) { + data := []byte{2, 3, 4, 0} + InsertAt(&data, 0, 1) + expected := []byte{1, 2, 3, 4} + if !bytes.Equal(data, expected) { + t.Errorf("expected %v, got %v", expected, data) + } +} + +func TestInsertInMiddle(t *testing.T) { + data := []byte{2, 3, 4, 0} + InsertAt(&data, 1, 5) + expected := []byte{2, 5, 3, 4} + if !bytes.Equal(data, expected) { + t.Errorf("expected %v, got %v", expected, data) + } +} + +func TestInsertAtEnd(t *testing.T) { + data := []byte{1, 2, 3, 0} + InsertAt(&data, 3, 4) + expected := []byte{1, 2, 3, 4} + if !bytes.Equal(data, expected) { + t.Errorf("expected %v, got %v", expected, data) + } +} + +func TestIndexOutOfBounds(t *testing.T) { + data := []byte{2, 3, 4} + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + InsertAt(&data, 4, 5) +} + +func TestNegativeIndex(t *testing.T) { + data := []byte{2, 3, 4} + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + InsertAt(&data, -1, 1) +} + +func TestMaintainsOrderAfterInsertion(t *testing.T) { + data := []byte{2, 3, 4, 0} + InsertAt(&data, 1, 1) + expected := []byte{2, 1, 3, 4} + if !bytes.Equal(data, expected) { + t.Errorf("expected %v, got %v", expected, data) + } +} + +func TestMaintainsLength(t *testing.T) { + data := []byte{2, 3, 4, 0} + InsertAt(&data, 1, 1) + if len(data) != 4 { + t.Errorf("expected length 4, got %d", len(data)) + } +} + +func TestDeletesLastValue(t *testing.T) { + data := []byte{2, 3, 4, 5} + InsertAt(&data, 0, 1) + expected := []byte{1, 2, 3, 4} + if !bytes.Equal(data, expected) { + t.Errorf("expected %v, got %v", expected, data) + } +} + +// region misc tests +func TestGSM7EncodesIntoSmallString(t *testing.T) { + input := short8nString + expected := 7 + actual := GSM7Coder{}.EncodesInto(&input) + if actual != expected { + t.Errorf("Expected %d, but got %d", expected, actual) + } +} + +func TestGSM7EncodesIntoLargerNot8nString(t *testing.T) { + input := longNot8nString + expected := 33 + actual := GSM7Coder{}.EncodesInto(&input) + if actual != expected { + t.Errorf("Expected %d, but got %d", expected, actual) + } +} + +func TestGSM7EncodesIntoLarger8nString(t *testing.T) { + input := long8nString + expected := 56 + actual := GSM7Coder{}.EncodesInto(&input) + if actual != expected { + t.Errorf("Expected %d, but got %d", expected, actual) + } +} + +func TestGSM7DecodesIntoSmallString(t *testing.T) { + input := short8nStringEncodedBytes + expected := 8 + actual := GSM7Coder{}.DecodesInto(bytes.NewBuffer(input)) + if actual != expected { + t.Errorf("Expected %d, but got %d", expected, actual) + } +} + +func TestGSM7DecodesIntoLargerNot8nString(t *testing.T) { + input := longNot8nStringEncodedBytes + expected := 37 + actual := GSM7Coder{}.DecodesInto(bytes.NewBuffer(input)) + if actual != expected { + t.Errorf("Expected %d, but got %d", expected, actual) + } +} + +func TestGSM7DecodesIntoLarger8nString(t *testing.T) { + input := long8nStringEncodedBytes + expected := 64 + actual := GSM7Coder{}.DecodesInto(bytes.NewBuffer(input)) + if actual != expected { + t.Errorf("Expected %d, but got %d", expected, actual) + } +} + +// region benchmark +func BenchmarkGSM7EncodeSimpleASCIIString(b *testing.B) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := short8nString + b.ResetTimer() + + for i := 0; i < b.N; i++ { + coder.Encode(&input, &buf) + } +} + +func BenchmarkGSM7EncodeComplexASCIIString(b *testing.B) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := longNot8nString + b.ResetTimer() + + for i := 0; i < b.N; i++ { + coder.Encode(&input, &buf) + } +} + +func BenchmarkGSM7EncodeComplex8nASCIIString(b *testing.B) { + coder := &GSM7Coder{} + var buf bytes.Buffer + input := long8nString + b.ResetTimer() + + for i := 0; i < b.N; i++ { + coder.Encode(&input, &buf) + } +} + +func BenchmarkGSM7EncodeComplex8nASCIIStringPrealloc(b *testing.B) { + coder := &GSM7Coder{} + input := long8nString + b.ResetTimer() + + for i := 0; i < b.N; i++ { + buf := bytes.NewBuffer(make([]byte, coder.EncodesInto(&input))) + buf.Reset() + coder.Encode(&input, buf) + } +} diff --git a/encoding/ucs2.go b/encoding/ucs2.go new file mode 100644 index 0000000..34883fb --- /dev/null +++ b/encoding/ucs2.go @@ -0,0 +1,19 @@ +package encoding + +import "bytes" + +type UCS2Coder struct{} + +func (c *UCS2Coder) Encode(s *string, buf *bytes.Buffer) error { + panic("UCS2 not implemented yet") +} +func (c *UCS2Coder) Decode(buf *bytes.Buffer) (string, error) { + panic("UCS2 not implemented yet") +} + +func (c UCS2Coder) EncodesInto(s *string) int { + return len(*s) * 2 +} +func (c UCS2Coder) DecodesInto(buf *bytes.Buffer) int { + return buf.Len() / 2 +} diff --git a/pdu/.air.toml b/pdu/.air.toml new file mode 100644 index 0000000..267b2d1 --- /dev/null +++ b/pdu/.air.toml @@ -0,0 +1,51 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "tmp\\main.exe" + cmd = "go test ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = [] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/pdu/global.go b/pdu/global.go index d06cdb6..a0c8beb 100644 --- a/pdu/global.go +++ b/pdu/global.go @@ -1,3 +1,5 @@ package pdu var ByteBufferPool = NewBufferPoolManager() +const NULL = byte(0x00) +var NULL_ARR = []byte{NULL} \ No newline at end of file diff --git a/pdu/pdu.go b/pdu/pdu.go index 2511a1e..cc2a301 100644 --- a/pdu/pdu.go +++ b/pdu/pdu.go @@ -12,6 +12,7 @@ type ( Decode(*bytes.Buffer) error // Size in bytes Size() int + UpdateSize() } PDU_HEADER struct { @@ -80,3 +81,6 @@ func (p *PDU_HEADER) Decode(buf *bytes.Buffer) error { func (p *PDU_HEADER) Size() int { return 16 } +func (p *PDU_HEADER) UpdateSize() { + p.command_length = uint32(p.Size()) +} \ No newline at end of file diff --git a/pdu/submit.go b/pdu/submit.go index a2b8c9e..033702d 100644 --- a/pdu/submit.go +++ b/pdu/submit.go @@ -2,9 +2,10 @@ package pdu import ( "bytes" - "encoding/ascii85" "encoding/binary" "fmt" + + "smpptester/encoding" ) type ( @@ -64,37 +65,43 @@ type ( SUBMIT_MULTI struct{} SUBMIT_MULTI_RESP struct{} ) +// See https://www.codeproject.com/Tips/470755/Encoding-Decoding-7-bit-User-Data-for-SMS-PDU-PDU +// Another great site: https://doubleblak.com/blogPost.php?k=7bitpdu func (p *SUBMIT_SM) Encode(buf *bytes.Buffer) error { if buf == nil { return fmt.Errorf("cannot encode into nil buffer") } + messageEncoder := p.GetEncoder() + p.header.Encode(buf) - n := ascii85.Encode(buf.Bytes(), []byte(p.service_type)) - buf.Truncate(n) + // These should be ASCII but UTF8 is a superset of ASCII so hopefully this'll be fine + buf.WriteString(p.service_type) + buf.Write(NULL_ARR) binary.Write(buf, binary.BigEndian, p.source_addr_ton) binary.Write(buf, binary.BigEndian, p.source_addr_npi) - n = ascii85.Encode(buf.Bytes(), []byte(p.source_addr)) - buf.Truncate(n) + buf.WriteString(p.source_addr) + buf.Write(NULL_ARR) binary.Write(buf, binary.BigEndian, p.dest_addr_ton) binary.Write(buf, binary.BigEndian, p.dest_addr_npi) - n = ascii85.Encode(buf.Bytes(), []byte(p.destination_addr)) - buf.Truncate(n) + buf.WriteString(p.destination_addr) + buf.Write(NULL_ARR) binary.Write(buf, binary.BigEndian, p.esm_class) binary.Write(buf, binary.BigEndian, p.protocol_id) binary.Write(buf, binary.BigEndian, p.priority_flag) - n = ascii85.Encode(buf.Bytes(), []byte(p.schedule_delivery_time)) - buf.Truncate(n) - n = ascii85.Encode(buf.Bytes(), []byte(p.validity_period)) - buf.Truncate(n) + buf.WriteString(p.schedule_delivery_time) + buf.Write(NULL_ARR) + buf.WriteString(p.validity_period) + buf.Write(NULL_ARR) binary.Write(buf, binary.BigEndian, p.registered_delivery) binary.Write(buf, binary.BigEndian, p.replace_if_present) binary.Write(buf, binary.BigEndian, p.data_coding) binary.Write(buf, binary.BigEndian, p.sm_default_msg_id) binary.Write(buf, binary.BigEndian, p.sm_length) - // TODO: Implement encodings bsed on p.data_coding - n = ascii85.Encode(buf.Bytes(), []byte(p.short_message)) - buf.Truncate(n) + err := messageEncoder.Encode(&p.short_message, buf) + if err != nil { + return err + } return nil } func (p *SUBMIT_SM) Decode(buf *bytes.Buffer) error { @@ -106,23 +113,41 @@ func (p *SUBMIT_SM) Decode(buf *bytes.Buffer) error { func (p *SUBMIT_SM) Size() int { var size int size += p.header.Size() - size += len(p.service_type) * 1 + size += 1 + len(p.service_type) size += 1 // source_addr_ton size += 1 // source_addr_npi - size += len(p.source_addr) * 1 + size += 1 + len(p.source_addr) size += 1 // dest_addr_ton size += 1 // dest_addr_npi - size += len(p.destination_addr) * 1 + size += 1 + len(p.destination_addr) size += 1 // esm_class size += 1 // protocol_id size += 1 // priority_flag - size += len(p.schedule_delivery_time) * 1 - size += len(p.validity_period) * 1 + size += 1 + len(p.schedule_delivery_time) + size += 1 + len(p.validity_period) size += 1 // registered_delivery size += 1 // replace_if_present size += 1 // data_coding size += 1 // sm_default_msg_id size += 1 // sm_length - size += len(p.short_message) * 1 + size += p.GetEncoder().EncodesInto(&p.short_message) return size } +func (p *SUBMIT_SM) UpdateSize() { + p.header.command_length = uint32(p.Size()) + p.sm_length = byte(p.GetEncoder().EncodesInto(&p.short_message)) +} +func (p *SUBMIT_SM) GetEncoder() encoding.Coder { + switch p.data_coding { + case 0b00000000: // GSM7 + return &encoding.GSM7Coder{} + case 0b00000001: // ASCII + return &encoding.ASCIICoder{} + // case 0b00000011: // LATIN1 + // return &encoding.LATIN1Coder{} + case 0b00001000: // UCS2 + return &encoding.UCS2Coder{} + default: + return &encoding.ASCIICoder{} + } +} diff --git a/pdu/submit_test.go b/pdu/submit_test.go index 57decaa..6949051 100644 --- a/pdu/submit_test.go +++ b/pdu/submit_test.go @@ -1,54 +1,87 @@ package pdu import ( + "bytes" "testing" ) // region encode +// See examples: https://www.openmarket.com/docs/Content/apis/v4smpp/mt-examples.htm +func TestEncodeFunctionCorrectlyEncodesAllFields(t *testing.T) { + p := &SUBMIT_SM{ + header: &PDU_HEADER{ + command_length: 0, + command_id: 4, + command_status: 0, + sequence_number: 378019, + }, + service_type: "OMV4", + source_addr_ton: 3, + source_addr_npi: 1, + source_addr: "80362", + dest_addr_ton: 1, + dest_addr_npi: 1, + destination_addr: "812345001000", + esm_class: 0, + protocol_id: 0, + priority_flag: 0, + schedule_delivery_time: "", + validity_period: "180105120000004+", + registered_delivery: 1, + data_coding: 1, // The example uses 0 and claims to use GSM but the message is encoded as ASCII... + sm_default_msg_id: 0, + short_message: "Reply Yes to opt in or No to opt out.", + } + p.UpdateSize() + buf := ByteBufferPool.Get(p.Size()) + err := p.Encode(buf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } -// func TestRealScenario(t *testing.T) { -// expected := []byte{0, 0, 0, 54, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 67, 77, 84, 0, 1, 1, 49, 50, 51, 52, 53, 0, 1, 1, 54, 55, 56, 57, 48, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 72, 101, 108, 108, 111, 44, 32, 83, 77, 80, 80, 33} -// p := &SUBMIT_SM{ -// header: PDU_HEADER{ -// command_length: 0, -// command_id: 4, -// command_status: 0, -// sequence_number: 1, -// }, -// service_type: "CMT", -// source_addr_ton: 1, -// source_addr_npi: 1, -// source_addr: "12345", -// dest_addr_ton: 1, -// dest_addr_npi: 1, -// destination_addr: "67890", -// esm_class: 0, -// protocol_id: 0, -// priority_flag: 0, -// schedule_delivery_time: "", -// validity_period: "", -// registered_delivery: 1, -// data_coding: 0, -// sm_default_msg_id: 0, -// short_message: "Hello, SMPP!", -// } -// p.header.command_length = uint32(p.Size()) -// p.sm_length = byte(len(p.short_message)) -// buf := make([]byte, p.Size()) -// err := p.EncodeInto(&buf) -// if err != nil { -// t.Errorf("Expected no error, got %v", err) -// } -// if len(buf) != len(expected) { -// t.Errorf("Expected byte slice of length %d, got %d", len(expected), len(buf)) -// } -// for i, v := range buf { -// if v != expected[i] { -// t.Errorf("Expected byte slice with values %v, got %v", expected, buf) -// break -// } -// } -// } + expected := []byte{0, 0, 0, 107, 0, 0, 0, 4, 0, 0, 0, 0, 0, 5, 196, 163, 79, 77, 86, 52, 0, 3, 1, 56, 48, 51, 54, 50, 0, 1, 1, 56, 49, 50, 51, 52, 53, 48, 48, 49, 48, 48, 48, 0, 0, 0, 0, 0, 49, 56, 48, 49, 48, 53, 49, 50, 48, 48, 48, 48, 48, 48, 52, 43, 0, 1, 0, 1, 0, 37, 82, 101, 112, 108, 121, 32, 89, 101, 115, 32, 116, 111, 32, 111, 112, 116, 32, 105, 110, 32, 111, 114, 32, 78, 111, 32, 116, 111, 32, 111, 112, 116, 32, 111, 117, 116, 46} + if !bytes.Equal(buf.Bytes(), expected) { + t.Fatalf("expected %v, got %v", expected, buf.Bytes()) + } +} + +func TestEncodeFunctionCorrectlyEncodesAllFieldsGSM7Message(t *testing.T) { + p := &SUBMIT_SM{ + header: &PDU_HEADER{ + command_length: 0, + command_id: 4, + command_status: 0, + sequence_number: 378019, + }, + service_type: "OMV4", + source_addr_ton: 3, + source_addr_npi: 1, + source_addr: "80362", + dest_addr_ton: 1, + dest_addr_npi: 1, + destination_addr: "812345001000", + esm_class: 0, + protocol_id: 0, + priority_flag: 0, + schedule_delivery_time: "", + validity_period: "180105120000004+", + registered_delivery: 1, + data_coding: 0, // The example uses 0 and claims to use GSM but the message is encoded as ASCII... + sm_default_msg_id: 0, + short_message: "Reply Yes to opt in or No to opt out.", + } + p.UpdateSize() + buf := ByteBufferPool.Get(p.Size()) + err := p.Encode(buf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{0, 0, 0, 103, 0, 0, 0, 4, 0, 0, 0, 0, 0, 5, 196, 163, 79, 77, 86, 52, 0, 3, 1, 56, 48, 51, 54, 50, 0, 1, 1, 56, 49, 50, 51, 52, 53, 48, 48, 49, 48, 48, 48, 0, 0, 0, 0, 0, 49, 56, 48, 49, 48, 53, 49, 50, 48, 48, 48, 48, 48, 48, 52, 43, 0, 1, 0, 0, 0, 33, 210, 50, 156, 157, 7, 101, 203, 115, 16, 253, 13, 122, 195, 233, 160, 180, 27, 244, 150, 131, 156, 111, 16, 253, 13, 122, 195, 233, 160, 119, 157, 238, 2} + if !bytes.Equal(buf.Bytes(), expected) { + t.Fatalf("expected %v, got %v", expected, buf.Bytes()) + } +} // region decode @@ -61,8 +94,26 @@ func TestCalculateSizeTypicalInstance(t *testing.T) { schedule_delivery_time: "", validity_period: "", short_message: "Hello, World!", + data_coding: 1, } - expectedSize := 16 + len(p.service_type) + 1 + 1 + len(p.source_addr) + 1 + 1 + len(p.destination_addr) + 1 + 1 + 1 + len(p.schedule_delivery_time) + len(p.validity_period) + 1 + 1 + 1 + 1 + 1 + len(p.short_message) + expectedSize := 68 + actualSize := p.Size() + if actualSize != expectedSize { + t.Errorf("Expected size %d, but got %d", expectedSize, actualSize) + } +} + +func TestCalculateSizeTypicalGSM7Instance(t *testing.T) { + p := &SUBMIT_SM{ + service_type: "test_service", + source_addr: "12345", + destination_addr: "67890", + schedule_delivery_time: "", + validity_period: "", + short_message: "Hello, World!", + data_coding: 0, + } + expectedSize := 67 actualSize := p.Size() if actualSize != expectedSize { t.Errorf("Expected size %d, but got %d", expectedSize, actualSize) @@ -78,8 +129,9 @@ func TestCalculateSizeMaxLengths(t *testing.T) { schedule_delivery_time: string(make([]byte, maxLen)), validity_period: string(make([]byte, maxLen)), short_message: string(make([]byte, maxLen)), + data_coding: 1, } - expectedSize := 16 + maxLen + 1 + 1 + maxLen + 1 + 1 + maxLen + 1 + 1 + 1 + maxLen + maxLen + 1 + 1 + 1 + 1 + 1 + maxLen + expectedSize := 1563 actualSize := p.Size() if actualSize != expectedSize { t.Errorf("Expected size %d, but got %d", expectedSize, actualSize) @@ -95,11 +147,11 @@ func TestHandlesEmptyStringsForAllStringFields(t *testing.T) { validity_period: "", short_message: "", } - expectedSize := 16 + len(p.service_type) + 1 + 1 + len(p.source_addr) + 1 + 1 + len(p.destination_addr) + 1 + 1 + 1 + len(p.schedule_delivery_time) + len(p.validity_period) + 1 + 1 + 1 + 1 + 1 + len(p.short_message) + expectedSize := 33 actualSize := p.Size() if actualSize != expectedSize { t.Errorf("Expected size %d, but got %d", expectedSize, actualSize) } } -// region benchmark \ No newline at end of file +// region benchmark