├── .travis.yml ├── LICENSE ├── README.md ├── cmd ├── charsets │ └── charsets.go ├── smscounter │ ├── smscounter.go │ └── smscounter_test.go ├── smsdecode │ ├── smsdecode.go │ └── smsdecode_test.go ├── smsdeliver │ └── smsdeliver.go └── smssubmit │ └── smssubmit.go ├── collect.go ├── collect_test.go ├── encode.go ├── encode_test.go ├── encoding ├── bcd │ ├── bcd.go │ └── bcd_test.go ├── gsm7 │ ├── 7bit.go │ ├── 7bit_test.go │ ├── charset │ │ ├── bengali.go │ │ ├── charset.go │ │ ├── charset_test.go │ │ ├── default.go │ │ ├── gujarati.go │ │ ├── hindi.go │ │ ├── kannada.go │ │ ├── malayalam.go │ │ ├── oriya.go │ │ ├── portuguese.go │ │ ├── punjabi.go │ │ ├── spanish.go │ │ ├── tamil.go │ │ ├── telugu.go │ │ ├── turkish.go │ │ └── urdu.go │ ├── gsm7.go │ └── gsm7_test.go ├── pdumode │ ├── doc.go │ ├── pdu.go │ ├── pdu_test.go │ ├── smscaddress.go │ └── smscaddress_test.go ├── semioctet │ ├── semioctet.go │ └── semioctet_test.go ├── tpdu │ ├── address.go │ ├── address_test.go │ ├── dcs.go │ ├── dcs_test.go │ ├── error.go │ ├── error_test.go │ ├── firstoctet.go │ ├── firstoctet_test.go │ ├── options.go │ ├── options_test.go │ ├── parameterindicator.go │ ├── parameterindicator_test.go │ ├── timestamp.go │ ├── timestamp_test.go │ ├── tpdu.go │ ├── tpdu_internal_test.go │ ├── tpdu_test.go │ ├── userdata.go │ ├── userdata_test.go │ ├── validityperiod.go │ └── validityperiod_test.go └── ucs2 │ ├── ucs2.go │ └── ucs2_test.go ├── error.go ├── go.mod ├── go.sum ├── internal └── readme │ └── readme.go ├── options.go ├── sms.go └── sms_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.x" 5 | - "1.11" 6 | - tip 7 | 8 | os: 9 | - linux 10 | - osx 11 | 12 | matrix: 13 | allow_failures: 14 | - go: tip 15 | fast_finish: true 16 | 17 | before_install: 18 | - go get github.com/mattn/goveralls 19 | 20 | script: 21 | - go test $(go list ./... | grep -v /cmd/) -coverprofile=gover.coverprofile 22 | - $GOPATH/bin/goveralls -coverprofile gover.coverprofile -service=travis-ci 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Kent Gibson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /cmd/charsets/charsets.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/warthog618/sms/encoding/gsm7/charset" 11 | ) 12 | 13 | var charsetName = []string{ 14 | "Basic (default)", 15 | "Turkish", 16 | "Spanish", 17 | "Portuguese", 18 | "Bengali", 19 | "Gujaranti", 20 | "Hindi", 21 | "Kannada", 22 | "Malayalam", 23 | "Oriya", 24 | "Punjabi", 25 | "Tamil", 26 | "Telugu", 27 | "Urdu", 28 | } 29 | 30 | func main() { 31 | for nli := charset.Default; nli <= charset.Urdu; nli++ { 32 | fmt.Printf("%s Locking (NLI=%d)\n", charsetName[nli], nli) 33 | Display(charset.NewDecoder(nli)) 34 | fmt.Println() 35 | fmt.Printf("%s Shift (NLI=%d)\n", charsetName[nli], nli) 36 | Display(charset.NewExtDecoder(nli)) 37 | fmt.Println() 38 | } 39 | } 40 | 41 | // Display prints the character set for a given character set decoder. 42 | func Display(m charset.Decoder) { 43 | specials := map[rune]string{ 44 | '\n': "LF", 45 | '\r': "CR", 46 | '\f': "FF", 47 | ' ': "SP", 48 | 0x1b: "ESC", 49 | 0x20ac: " €", 50 | } 51 | fmt.Printf(" ") 52 | for c := 0; c < 8; c++ { 53 | fmt.Printf("0x%d_ ", c) 54 | } 55 | fmt.Println("") 56 | for r := 0; r < 0x10; r++ { 57 | fmt.Printf("0x_%x: ", r) 58 | for c := 0; c < 8; c++ { 59 | k := byte(c*0x10 + r) 60 | if v, ok := m[k]; ok { 61 | if s, ok := specials[v]; ok { 62 | fmt.Printf("%3s ", s) 63 | } else if v >= 0x400 { 64 | fmt.Printf("%04x ", v) 65 | } else { 66 | fmt.Printf(" %c ", v) 67 | } 68 | } else { 69 | fmt.Printf(" ") 70 | } 71 | } 72 | fmt.Println() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmd/smscounter/smscounter.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2019 Kent Gibson . 4 | 5 | // smscounter provides an example of generating output similar to that 6 | // generated by github.com/danxexe/sms-counter. 7 | // 8 | // This is non-optimal as it encodes the required Submit TPDUs, and calculates 9 | // the output from them rather than just performing the minimal calculations 10 | // required for the output. OTOH CPU is cheap so I've not bothered to add 11 | // suitably optimised methods to the library. YMMV. 12 | package main 13 | 14 | import ( 15 | "flag" 16 | "fmt" 17 | "log" 18 | "os" 19 | 20 | "github.com/warthog618/sms" 21 | "github.com/warthog618/sms/encoding/tpdu" 22 | ) 23 | 24 | func main() { 25 | var msg string 26 | var nli int 27 | flag.StringVar(&msg, "message", "", "The message to encode") 28 | flag.IntVar(&nli, "language", 0, "The NLI of a character set to use in addition to the default") 29 | flag.Usage = usage 30 | flag.Parse() 31 | if msg == "" { 32 | flag.Usage() 33 | os.Exit(1) 34 | } 35 | c, err := NewCount(msg, nli) 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | fmt.Print(c) 40 | } 41 | 42 | // NewCount creates the SMS count statistics for a message. 43 | func NewCount(msg string, nli int) (Count, error) { 44 | options := []sms.EncoderOption(nil) 45 | if nli != 0 { 46 | options = append(options, sms.WithCharset(nli)) 47 | } 48 | c := Count{} 49 | pdus, err := sms.Encode([]byte(msg), options...) 50 | if err != nil { 51 | return c, err 52 | } 53 | alpha, _ := pdus[0].Alphabet() 54 | lastLen := len(pdus[len(pdus)-1].UD) // valid for 7bit as it is unpacked into octets. 55 | pm := pdus[0].UDBlockSize() 56 | switch alpha { 57 | case tpdu.Alpha7Bit: 58 | if hasEscapes(pdus) { 59 | c.Encoding = "7BIT_EX" 60 | } else { 61 | c.Encoding = "7BIT" 62 | } 63 | case tpdu.Alpha8Bit: 64 | c.Encoding = "8BIT" 65 | case tpdu.AlphaUCS2: 66 | c.Encoding = "UCS-2" 67 | lastLen /= 2 // UCS-2 code points 68 | pm /= 2 // UCS-2 code points 69 | } 70 | c.Pdulen = pm 71 | c.Llen = lastLen 72 | c.Remaining = c.Pdulen - c.Llen 73 | c.Messages = len(pdus) 74 | c.Tlen = (pm * (c.Messages - 1)) + lastLen 75 | return c, nil 76 | } 77 | 78 | func hasEscapes(pdus []tpdu.TPDU) bool { 79 | for _, pdu := range pdus { 80 | for _, d := range pdu.UD { 81 | if d == 0x1b { 82 | return true 83 | } 84 | } 85 | } 86 | return false 87 | } 88 | 89 | func usage() { 90 | fmt.Fprintf(os.Stderr, "smscounter determimes the number of SMS-Submit TPDUs "+ 91 | "required to encode a given message.\n"+ 92 | "The message is encoded using the GSM7 default alphabet, or if necessary\n"+ 93 | "an optionally specified character set, or failing those as UCS-2.\n"+ 94 | "If the message is too long for a single PDU then it is split into several.\n\n"+ 95 | "Usage: smscounter -message \n") 96 | flag.PrintDefaults() 97 | } 98 | 99 | // Count contains the statistics related to encoding a message in SMS-SUBMIT TPDUs. 100 | type Count struct { 101 | Encoding string 102 | Messages int 103 | Tlen int 104 | Llen int 105 | Pdulen int 106 | Remaining int 107 | } 108 | 109 | func (c Count) String() string { 110 | return fmt.Sprintf("encoding: %s\n", c.Encoding) + 111 | fmt.Sprintf("messages: %d\n", c.Messages) + 112 | fmt.Sprintf("total length: %d\n", c.Tlen) + 113 | fmt.Sprintf("last PDU length: %d\n", c.Llen) + 114 | fmt.Sprintf("per_message: %d\n", c.Pdulen) + 115 | fmt.Sprintf("remaining: %d\n", c.Remaining) 116 | } 117 | -------------------------------------------------------------------------------- /cmd/smscounter/smscounter_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2019 Kent Gibson .package main 4 | 5 | package main 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNewCount(t *testing.T) { 14 | patterns := []struct { 15 | name string 16 | msg string 17 | nli int 18 | out Count 19 | err error 20 | }{ 21 | { 22 | "std", 23 | "content of the SMS", 24 | 0, 25 | Count{"7BIT", 1, 18, 18, 160, 142}, 26 | nil, 27 | }, 28 | { 29 | "grin", 30 | "hello 😁", 31 | 0, 32 | Count{"UCS-2", 1, 8, 8, 70, 62}, 33 | nil, 34 | }, 35 | { 36 | "urdu locking", 37 | "hi ت", 38 | 13, 39 | Count{"7BIT", 1, 4, 4, 155, 151}, 40 | nil, 41 | }, 42 | { 43 | "urdu extended", 44 | "hi ؎", 45 | 13, 46 | Count{"7BIT_EX", 1, 5, 5, 155, 150}, 47 | nil, 48 | }, 49 | { 50 | "urdu locking and extended", 51 | "hi ت؎", 52 | 13, 53 | Count{"7BIT_EX", 1, 6, 6, 152, 146}, 54 | nil, 55 | }, 56 | } 57 | 58 | for _, p := range patterns { 59 | f := func(t *testing.T) { 60 | out, err := NewCount(p.msg, p.nli) 61 | assert.Equal(t, p.err, err) 62 | assert.Equal(t, p.out, out) 63 | } 64 | t.Run(p.name, f) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/smsdecode/smsdecode.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // smsdecode provides an example of unmarshalling and displaying a SMS TPDU. 6 | package main 7 | 8 | import ( 9 | "encoding/hex" 10 | "flag" 11 | "fmt" 12 | "io" 13 | "log" 14 | "os" 15 | "strings" 16 | 17 | "github.com/warthog618/sms" 18 | "github.com/warthog618/sms/encoding/pdumode" 19 | "github.com/warthog618/sms/encoding/tpdu" 20 | ) 21 | 22 | func main() { 23 | pm := flag.Bool("p", false, "PDU is prefixed with SCA (PDU mode)") 24 | orig := flag.Bool("o", false, "PDU is mobile originated") 25 | flag.Usage = usage 26 | flag.Parse() 27 | if flag.NArg() != 1 { 28 | flag.Usage() 29 | os.Exit(1) 30 | } 31 | tp, smsc, err := decode(flag.Arg(0), *pm, *orig) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | if smsc != nil { 36 | dumpSMSC(os.Stdout, smsc) 37 | } 38 | dumpTPDU(os.Stdout, tp) 39 | } 40 | 41 | func decode(s string, pm, mo bool) (p *tpdu.TPDU, a *pdumode.SMSCAddress, err error) { 42 | b, err := hex.DecodeString(s) 43 | if err != nil { 44 | return 45 | } 46 | if pm { 47 | var pdu *pdumode.PDU 48 | pdu, err = pdumode.UnmarshalHexString(s) 49 | if err != nil { 50 | return 51 | } 52 | a = &pdu.SMSC 53 | b = pdu.TPDU 54 | } 55 | if mo { 56 | p, err = sms.Unmarshal(b, sms.AsMO) 57 | return 58 | } 59 | p, err = sms.Unmarshal(b) 60 | return 61 | } 62 | 63 | func dumpSMSC(w io.Writer, smsc *pdumode.SMSCAddress) { 64 | n := smsc.Number() 65 | fmt.Fprintf(w, "SMSC: %s\n", n) 66 | } 67 | 68 | func dumpTPDU(w io.Writer, t *tpdu.TPDU) { 69 | var st string 70 | var dump func(w io.Writer, t *tpdu.TPDU) 71 | switch t.SmsType() { 72 | case tpdu.SmsCommand: 73 | st = "SMS-COMMAND" 74 | dump = dumpCommand 75 | case tpdu.SmsDeliver: 76 | st = "SMS-DELIVER" 77 | dump = dumpDeliver 78 | case tpdu.SmsDeliverReport: 79 | st = "SMS-DELIVER-REPORT" 80 | dump = dumpDeliverReport 81 | case tpdu.SmsStatusReport: 82 | st = "SMS-STATUS-REPORT" 83 | dump = dumpStatusReport 84 | case tpdu.SmsSubmit: 85 | st = "SMS-SUBMIT" 86 | dump = dumpSubmit 87 | case tpdu.SmsSubmitReport: 88 | st = "SMS-SUBMIT-REPORT" 89 | dump = dumpSubmitReport 90 | } 91 | fmt.Fprintf(w, "TPDU: %s\n", st) 92 | dump(w, t) 93 | } 94 | 95 | func dumpCommand(w io.Writer, t *tpdu.TPDU) { 96 | fmt.Fprintf(w, "TP-MTI: 0x%02x %s\n", int(t.SmsType().MTI()), t.SmsType().MTI()) 97 | fmt.Fprintf(w, "TP-UDHI: %t\n", t.FirstOctet.UDHI()) 98 | fmt.Fprintf(w, "TP-SRR: %t\n", t.FirstOctet.SRR()) 99 | fmt.Fprintf(w, "TP-MR: %d\n", t.MR) 100 | fmt.Fprintf(w, "TP-PID: 0x%02x\n", t.PID) 101 | fmt.Fprintf(w, "TP-CT: 0x%02x\n", t.CT) 102 | fmt.Fprintf(w, "TP-MN: %d\n", t.MN) 103 | fmt.Fprintf(w, "TP-DA: %s\n", t.DA.Number()) 104 | fmt.Fprintf(w, "TP-SCTS: %s\n", t.SCTS) 105 | fmt.Fprintf(w, "TP-CDL: %d\n", len(t.UD)) 106 | dumpCD(w, t.UD) 107 | } 108 | 109 | func dumpDeliver(w io.Writer, t *tpdu.TPDU) { 110 | fmt.Fprintf(w, "TP-MTI: 0x%02x %s\n", int(t.SmsType().MTI()), t.SmsType().MTI()) 111 | fmt.Fprintf(w, "TP-MMS: %t\n", t.FirstOctet.MMS()) 112 | fmt.Fprintf(w, "TP-LP: %t\n", t.FirstOctet.LP()) 113 | fmt.Fprintf(w, "TP-RP: %t\n", t.FirstOctet.RP()) 114 | fmt.Fprintf(w, "TP-UDHI: %t\n", t.FirstOctet.UDHI()) 115 | fmt.Fprintf(w, "TP-SRI: %t\n", t.FirstOctet.SRI()) 116 | fmt.Fprintf(w, "TP-OA: %s\n", t.OA.Number()) 117 | fmt.Fprintf(w, "TP-PID: 0x%02x\n", t.PID) 118 | fmt.Fprintf(w, "TP-DCS: %s\n", t.DCS) 119 | fmt.Fprintf(w, "TP-SCTS: %s\n", t.SCTS) 120 | if t.UDH != nil { 121 | dumpUDH(w, t.UDH) 122 | } 123 | dumpUD(w, t.UD) 124 | } 125 | 126 | func dumpDeliverReport(w io.Writer, t *tpdu.TPDU) { 127 | fmt.Fprintf(w, "TP-MTI: 0x%02x %s\n", int(t.SmsType().MTI()), t.SmsType().MTI()) 128 | fmt.Fprintf(w, "TP-UDHI: %t\n", t.FirstOctet.UDHI()) 129 | fmt.Fprintf(w, "TP-FCS: 0x%02x\n", t.FCS) 130 | fmt.Fprintf(w, "TP-PI: %s\n", t.PI) 131 | if t.PI.PID() { 132 | fmt.Fprintf(w, "TP-PID: 0x%02x\n", t.PID) 133 | } 134 | if t.PI.DCS() { 135 | fmt.Fprintf(w, "TP-DCS: %s\n", t.DCS) 136 | } 137 | if t.UDH != nil { 138 | dumpUDH(w, t.UDH) 139 | } 140 | dumpUD(w, t.UD) 141 | } 142 | 143 | func dumpStatusReport(w io.Writer, t *tpdu.TPDU) { 144 | fmt.Fprintf(w, "TP-MTI: 0x%02x %s\n", int(t.SmsType().MTI()), t.SmsType().MTI()) 145 | fmt.Fprintf(w, "TP-UDHI: %t\n", t.FirstOctet.UDHI()) 146 | fmt.Fprintf(w, "TP-MMS: %t\n", t.FirstOctet.MMS()) 147 | fmt.Fprintf(w, "TP-LP: %t\n", t.FirstOctet.LP()) 148 | fmt.Fprintf(w, "TP-SRQ: %t\n", t.FirstOctet.SRQ()) 149 | fmt.Fprintf(w, "TP-MR: %d\n", t.MR) 150 | fmt.Fprintf(w, "TP-RA: %s\n", t.RA.Number()) 151 | fmt.Fprintf(w, "TP-SCTS: %s\n", t.SCTS) 152 | fmt.Fprintf(w, "TP-DT: %s\n", t.DT) 153 | fmt.Fprintf(w, "TP-ST: 0x%02x\n", t.ST) 154 | fmt.Fprintf(w, "TP-PI: %s\n", t.PI) 155 | if t.PI.PID() { 156 | fmt.Fprintf(w, "TP-PID: 0x%02x\n", t.PID) 157 | } 158 | if t.PI.DCS() { 159 | fmt.Fprintf(w, "TP-DCS: %s\n", t.DCS) 160 | } 161 | if t.UDH != nil { 162 | dumpUDH(w, t.UDH) 163 | } 164 | dumpUD(w, t.UD) 165 | } 166 | 167 | func dumpSubmit(w io.Writer, t *tpdu.TPDU) { 168 | fmt.Fprintf(w, "TP-MTI: 0x%02x %s\n", int(t.SmsType().MTI()), t.SmsType().MTI()) 169 | fmt.Fprintf(w, "TP-RD: %t\n", t.FirstOctet.RD()) 170 | fmt.Fprintf(w, "TP-VPF: 0x%02x %s\n", int(t.FirstOctet.VPF()), t.FirstOctet.VPF()) 171 | fmt.Fprintf(w, "TP-RP: %t\n", t.FirstOctet.RP()) 172 | fmt.Fprintf(w, "TP-UDHI: %t\n", t.FirstOctet.UDHI()) 173 | fmt.Fprintf(w, "TP-SRR: %t\n", t.FirstOctet.SRR()) 174 | fmt.Fprintf(w, "TP-MR: %d\n", t.MR) 175 | fmt.Fprintf(w, "TP-DA: %s\n", t.DA.Number()) 176 | fmt.Fprintf(w, "TP-PID: 0x%02x\n", t.PID) 177 | fmt.Fprintf(w, "TP-DCS: %s\n", t.DCS) 178 | dumpVP(w, t.VP) 179 | if t.UDH != nil { 180 | dumpUDH(w, t.UDH) 181 | } 182 | dumpUD(w, t.UD) 183 | } 184 | 185 | func dumpSubmitReport(w io.Writer, t *tpdu.TPDU) { 186 | fmt.Fprintf(w, "TP-MTI: 0x%02x %s\n", int(t.SmsType().MTI()), t.SmsType().MTI()) 187 | fmt.Fprintf(w, "TP-UDHI: %t\n", t.FirstOctet.UDHI()) 188 | fmt.Fprintf(w, "TP-FCS: 0x%02x\n", t.FCS) 189 | fmt.Fprintf(w, "TP-PI: %s\n", t.PI) 190 | fmt.Fprintf(w, "TP-SCTS: %s\n", t.SCTS) 191 | if t.PI.PID() { 192 | fmt.Fprintf(w, "TP-PID: 0x%02x\n", t.PID) 193 | } 194 | if t.PI.DCS() { 195 | fmt.Fprintf(w, "TP-DCS: %s\n", t.DCS) 196 | } 197 | if t.UDH != nil { 198 | dumpUDH(w, t.UDH) 199 | } 200 | dumpUD(w, t.UD) 201 | } 202 | 203 | func dumpCD(w io.Writer, ud []byte) { 204 | lines := strings.Split(hex.Dump(ud), "\n") 205 | fmt.Fprintf(w, "TP-CD: %s\n", lines[0]) 206 | for _, l := range lines[1:] { 207 | fmt.Fprintf(w, " %s\n", l) 208 | } 209 | } 210 | 211 | func dumpVP(w io.Writer, vp tpdu.ValidityPeriod) { 212 | switch vp.Format { 213 | case tpdu.VpfNotPresent: 214 | fmt.Fprintf(w, "TP-VP: Not Present\n") 215 | case tpdu.VpfAbsolute: 216 | fmt.Fprintf(w, "TP-VP: Absolute - %s\n", vp.Time) 217 | case tpdu.VpfEnhanced: 218 | fmt.Fprintf(w, "TP-VP: Enhanced %s - %s\n", 219 | tpdu.EnhancedFormat(vp.EFI), vp.Duration) 220 | case tpdu.VpfRelative: 221 | fmt.Fprintf(w, "TP-VP: Relative - %s\n", vp.Duration) 222 | } 223 | } 224 | 225 | func dumpUDH(w io.Writer, udh tpdu.UserDataHeader) { 226 | ie := udh[0] 227 | fmt.Fprintf(w, "TP-UDH: ID: %d Data: %v\n", ie.ID, ie.Data) 228 | for _, ie = range udh[1:] { 229 | fmt.Fprintf(w, " ID: %d Data: %v\n", ie.ID, ie.Data) 230 | } 231 | } 232 | 233 | func dumpUD(w io.Writer, ud []byte) { 234 | lines := strings.Split(strings.TrimSpace(hex.Dump(ud)), "\n") 235 | fmt.Fprintf(w, "TP-UD: %s\n", lines[0]) 236 | for _, l := range lines[1:] { 237 | fmt.Fprintf(w, " %s\n", l) 238 | } 239 | } 240 | 241 | func usage() { 242 | fmt.Fprintf(os.Stderr, "Usage: smsdecode [-p] [-o] \n") 243 | flag.PrintDefaults() 244 | } 245 | -------------------------------------------------------------------------------- /cmd/smsdecode/smsdecode_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2019 Kent Gibson .package main 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/hex" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/warthog618/sms/encoding/tpdu" 13 | ) 14 | 15 | func TestDecode(t *testing.T) { 16 | patterns := []struct { 17 | name string 18 | pdu string 19 | pm bool 20 | mo bool 21 | out *tpdu.TPDU 22 | err error 23 | }{ 24 | { 25 | "invalid hex", 26 | "bad hex", 27 | false, 28 | false, 29 | nil, 30 | hex.InvalidByteError(' '), 31 | }, 32 | { 33 | "decode fail", 34 | "010005912143f500000bc8329bfd06dddf7236", 35 | false, 36 | true, 37 | nil, 38 | tpdu.DecodeError{ 39 | Field: "SmsSubmit.ud.sm", 40 | Offset: 10, 41 | Err: tpdu.ErrUnderflow, 42 | }, 43 | }, 44 | { 45 | "pdumode decode fail", 46 | "07911614220991", 47 | true, 48 | true, 49 | nil, 50 | tpdu.DecodeError{ 51 | Field: "addr", 52 | Offset: 2, 53 | Err: tpdu.ErrUnderflow, 54 | }, 55 | }, 56 | { 57 | "submit", 58 | "010005912143f500000bc8329bfd06dddf723619", 59 | false, 60 | true, 61 | &tpdu.TPDU{ 62 | Direction: 1, 63 | FirstOctet: 1, 64 | DA: tpdu.Address{TOA: 0x91, Addr: "12345"}, 65 | UD: []byte{ 66 | 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 67 | }, 68 | }, 69 | nil, 70 | }, 71 | { 72 | "submit pdumode", 73 | "07911614220991f1010005912143f500000bc8329bfd06dddf723619", 74 | true, 75 | true, 76 | &tpdu.TPDU{ 77 | Direction: 1, 78 | FirstOctet: 1, 79 | DA: tpdu.Address{TOA: 0x91, Addr: "12345"}, 80 | UD: []byte{ 81 | 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 82 | }, 83 | }, 84 | nil, 85 | }, 86 | } 87 | 88 | for _, p := range patterns { 89 | f := func(t *testing.T) { 90 | out, _, err := decode(p.pdu, p.pm, p.mo) 91 | assert.Equal(t, p.err, err) 92 | assert.Equal(t, p.out, out) 93 | } 94 | t.Run(p.name, f) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /cmd/smsdeliver/smsdeliver.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2019 Kent Gibson . 4 | 5 | // smsdeliver provides an example of extracting a message from a set of 6 | // SMS-DELIVER TPDUs. 7 | package main 8 | 9 | import ( 10 | "encoding/hex" 11 | "flag" 12 | "fmt" 13 | "log" 14 | "os" 15 | 16 | "github.com/warthog618/sms" 17 | "github.com/warthog618/sms/encoding/pdumode" 18 | ) 19 | 20 | func main() { 21 | var pm bool 22 | flag.BoolVar(&pm, "p", false, "PDU is prefixed with SCA (PDU mode)") 23 | flag.Usage = usage 24 | flag.Parse() 25 | if flag.NArg() == 0 { 26 | flag.Usage() 27 | os.Exit(1) 28 | } 29 | c := sms.NewCollector() 30 | defer c.Close() 31 | for _, a := range flag.Args() { 32 | b, err := hex.DecodeString(a) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | tb := b 37 | if pm { 38 | pdu, err := pdumode.UnmarshalBinary(b) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | tb = pdu.TPDU 43 | } 44 | t, err := sms.Unmarshal(tb) 45 | if err != nil { 46 | log.Printf("unmarshal error: %v", err) 47 | continue 48 | } 49 | pdus, err := c.Collect(*t) 50 | if err != nil { 51 | log.Printf("collect error: %v", err) 52 | } 53 | if pdus == nil { 54 | continue 55 | } 56 | msg, err := sms.Decode(pdus) 57 | if err != nil { 58 | log.Printf("decode error: %v", err) 59 | } 60 | if msg != nil { 61 | fmt.Printf("%s: %s\n", pdus[0].OA.Number(), msg) 62 | } 63 | } 64 | // report active collect pipes 65 | pipes := c.Pipes() 66 | for k, v := range pipes { 67 | fmt.Println("incomplete reassembly: ", k) 68 | fmt.Println(v) 69 | } 70 | } 71 | 72 | func usage() { 73 | fmt.Fprintf(os.Stderr, "smsdeliver decodes and displays the message from one or more SMS Deliver TPDUs.\n"+ 74 | "Usage: smsdeliver [-p] [pdu...]\n") 75 | flag.PrintDefaults() 76 | } 77 | -------------------------------------------------------------------------------- /cmd/smssubmit/smssubmit.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // smssubmit provides an example of encoding a message into a set of SMS-SUBMIT 6 | // TPDUs. 7 | package main 8 | 9 | import ( 10 | "encoding/hex" 11 | "flag" 12 | "fmt" 13 | "log" 14 | "os" 15 | 16 | "github.com/warthog618/sms" 17 | ) 18 | 19 | func main() { 20 | var number, msg string 21 | var nli int 22 | flag.StringVar(&number, "number", "", "Destination number in international format") 23 | flag.StringVar(&msg, "message", "", "The message to encode") 24 | flag.IntVar(&nli, "language", 0, "The NLI of a character set to use in addition to the default") 25 | flag.Usage = usage 26 | flag.Parse() 27 | if number == "" || msg == "" { 28 | flag.Usage() 29 | os.Exit(1) 30 | } 31 | 32 | options := []sms.EncoderOption{sms.AsSubmit, sms.To(number)} 33 | if nli != 0 { 34 | options = append(options, sms.WithCharset(nli)) 35 | } 36 | pdus, err := sms.Encode([]byte(msg), options...) 37 | if err != nil { 38 | log.Println(err) 39 | return 40 | } 41 | if len(pdus) == 1 { 42 | b, _ := pdus[0].MarshalBinary() 43 | fmt.Printf("Submit TPDU:\n%s\n", hex.EncodeToString(b)) 44 | return 45 | } 46 | for i, p := range pdus { 47 | b, _ := p.MarshalBinary() 48 | fmt.Printf("Submit TPDU %d:\n%s\n", i+1, hex.EncodeToString(b)) 49 | } 50 | } 51 | 52 | func usage() { 53 | fmt.Fprintf(os.Stderr, "smssubmit encodes a message into a SMS Submit TPDU.\n"+ 54 | "The message is encoded using the GSM7 default alphabet, or if necessary\n"+ 55 | "an optionally specified character set, or failing those as UCS-2.\n"+ 56 | "If the message is too long for a single PDU then it is split into several.\n\n"+ 57 | "Usage: smssubmit -number -message \n") 58 | flag.PrintDefaults() 59 | } 60 | -------------------------------------------------------------------------------- /collect.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package sms 6 | 7 | import ( 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/warthog618/sms/encoding/tpdu" 13 | ) 14 | 15 | // Collector contains reassembly pipes that buffer concatenated TPDUs until a 16 | // full set is available to be concatenated. 17 | type Collector struct { 18 | sync.Mutex // covers pipes and closing closed 19 | pipes map[string]*pipe 20 | closed bool 21 | duration time.Duration 22 | expiryHandler func([]*tpdu.TPDU) 23 | } 24 | 25 | // CollectorOption alters the behaviour of a Collector. 26 | type CollectorOption interface { 27 | ApplyCollectorOption(*Collector) 28 | } 29 | 30 | type reassemblyTimeoutOption struct { 31 | d time.Duration 32 | eh func([]*tpdu.TPDU) 33 | } 34 | 35 | func (o reassemblyTimeoutOption) ApplyCollectorOption(c *Collector) { 36 | c.duration = o.d 37 | c.expiryHandler = o.eh 38 | } 39 | 40 | // WithReassemblyTimeout limits the time allowed for a collection of TPDUs to 41 | // be collected. 42 | // 43 | // If the timer expires before the collection is complete then the collected 44 | // TPDUs are passed to the expiryHandler. The expiry handler can be nil in 45 | // which case the collected TPDUs are simply discarded. 46 | // 47 | // A zero duration disables the timeout. 48 | func WithReassemblyTimeout(d time.Duration, eh func([]*tpdu.TPDU)) CollectorOption { 49 | return reassemblyTimeoutOption{d, eh} 50 | } 51 | 52 | // NewCollector creates a Collector. 53 | func NewCollector(options ...CollectorOption) *Collector { 54 | c := Collector{ 55 | pipes: make(map[string]*pipe), 56 | } 57 | for _, o := range options { 58 | o.ApplyCollectorOption(&c) 59 | } 60 | return &c 61 | } 62 | 63 | // Close shuts down the Collector and all active pipes. 64 | func (c *Collector) Close() { 65 | c.Lock() 66 | defer c.Unlock() 67 | if c.closed { 68 | return 69 | } 70 | c.closed = true 71 | for _, p := range c.pipes { 72 | if p.cleanup != nil { 73 | p.cleanup.Stop() 74 | } 75 | } 76 | } 77 | 78 | // Pipes returns a snapshot of the reassembly pipes. 79 | // 80 | // This is intended for diagnostics. 81 | func (c *Collector) Pipes() map[string][]*tpdu.TPDU { 82 | c.Lock() 83 | m := map[string][]*tpdu.TPDU{} 84 | for k, v := range c.pipes { 85 | m[k] = v.segments 86 | } 87 | c.Unlock() 88 | return m 89 | } 90 | 91 | // Collect adds a TPDU to the collection. 92 | // 93 | // If all the components of a concatenated TPDU are available then they are 94 | // returned. 95 | func (c *Collector) Collect(pdu tpdu.TPDU) (d []*tpdu.TPDU, err error) { 96 | c.Lock() 97 | defer c.Unlock() 98 | if c.closed { 99 | return nil, ErrClosed 100 | } 101 | segments, seqno, concatRef, ok := pdu.ConcatInfo() 102 | if !ok || segments < 2 { 103 | // short circuit single segment - no need for a pipe 104 | return []*tpdu.TPDU{&pdu}, nil 105 | } 106 | if seqno < 1 || seqno > segments { 107 | return nil, ErrReassemblyInconsistency 108 | } 109 | key, err := pduKey(pdu, segments, concatRef) 110 | p, ok := c.pipes[key] 111 | if ok { 112 | if p.segments[seqno-1] != nil { 113 | return nil, ErrDuplicateSegment 114 | } 115 | if p.cleanup != nil && !p.cleanup.Stop() { 116 | // timer has fired, but cleanup hasn't been performed yet - so need 117 | // a new pipe 118 | ok = false 119 | } 120 | } 121 | if !ok { 122 | p = &pipe{nil, make([]*tpdu.TPDU, segments), 0} 123 | c.pipes[key] = p 124 | } 125 | p.segments[seqno-1] = &pdu 126 | p.frags++ 127 | if p.frags == segments { 128 | delete(c.pipes, key) 129 | return p.segments, nil 130 | } 131 | if c.duration != 0 { 132 | p.cleanup = time.AfterFunc(c.duration, func() { 133 | c.Lock() 134 | m := c.pipes[key] 135 | if m == p { 136 | delete(c.pipes, key) 137 | } 138 | c.Unlock() 139 | if c.expiryHandler != nil { 140 | c.expiryHandler(p.segments) 141 | } 142 | }) 143 | } 144 | return nil, err 145 | } 146 | 147 | func pduKey(pdu tpdu.TPDU, segments, concatRef int) (string, error) { 148 | st := pdu.SmsType() 149 | var key string 150 | switch st { 151 | case tpdu.SmsSubmit: 152 | key = fmt.Sprintf("%d:%02x:%s:%d:%d", 153 | st, 154 | pdu.DA.TOA, 155 | pdu.DA.Addr, 156 | concatRef, 157 | segments) 158 | case tpdu.SmsDeliver: 159 | key = fmt.Sprintf("%d:%02x:%s:%d:%d", 160 | st, 161 | pdu.OA.TOA, 162 | pdu.OA.Addr, 163 | concatRef, 164 | segments) 165 | default: 166 | return "", tpdu.ErrUnsupportedSmsType(st) 167 | } 168 | return key, nil 169 | } 170 | 171 | // pipe is a buffer that contains the individual TPDUs in a concatenation set 172 | // until the complete set is available or the reassembly times out. 173 | type pipe struct { 174 | cleanup *time.Timer 175 | segments []*tpdu.TPDU 176 | frags int 177 | } 178 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package sms 6 | 7 | import ( 8 | "sync/atomic" 9 | 10 | "github.com/warthog618/sms/encoding/tpdu" 11 | ) 12 | 13 | // Encode builds a set of TPDUs containing the message. 14 | // 15 | // Long messages are split into multiple concatenated TPDUs, while short 16 | // messages may fit in one. 17 | // 18 | // By default messages are encoded into SMS-SUBMIT TPDUs. This behaviour may 19 | // be overridden via options. 20 | // 21 | // For 8-bit encoding the message is encoded as is. 22 | // 23 | // For 7-bit encoding the message is assumed to contain UTF-8. 24 | // 25 | // For explicit UCS-2 encoding the message is assumed to contain UTF-16, 26 | // encoded as an array of bytes. This can be created from an array of UTF-16 27 | // runes using ucs2.Encode. 28 | // 29 | // For implicit UCS-2 encoding (the fallback with 7-bit fails) the message is 30 | // assumed to contain UTF-8. 31 | func Encode(msg []byte, options ...EncoderOption) ([]tpdu.TPDU, error) { 32 | options = append([]EncoderOption{AsSubmit}, options...) 33 | e := NewEncoder(options...) 34 | return e.Encode(msg) 35 | } 36 | 37 | // Encoder builds SMS TPDUs from simple inputs such as the destination number 38 | // and the message in a UTF8 form. 39 | type Encoder struct { 40 | // options for encoding UD 41 | eopts []tpdu.UDEncodeOption 42 | 43 | // options for segmentation 44 | sopts []tpdu.SegmentationOption 45 | 46 | // The template TPDU for encoding. 47 | pdu tpdu.TPDU 48 | 49 | // MsgCount is the number of TPDUs encoded. 50 | MsgCount tpdu.Counter 51 | 52 | // ConcatRef is the number of multi-segment messages encoded. 53 | ConcatRef tpdu.Counter 54 | } 55 | 56 | // NewEncoder creates an Encoder. 57 | func NewEncoder(options ...EncoderOption) *Encoder { 58 | e := Encoder{} 59 | for _, option := range options { 60 | option.ApplyEncoderOption(&e) 61 | } 62 | if e.MsgCount == nil { 63 | e.MsgCount = &Counter{} 64 | } 65 | if e.ConcatRef == nil { 66 | e.ConcatRef = &Counter{} 67 | } 68 | return &e 69 | } 70 | 71 | // Encode builds a set of TPDUs containing the message. 72 | // 73 | // Long messages are split into multiple concatenated TPDUs, while short 74 | // messages may fit in one. 75 | // 76 | // By default messages are encoded into SMS-DELIVER TPDUs. This behaviour may 77 | // be overridden via options, either to NewEncoder or Encode. 78 | // 79 | // For 8-bit encoding the message is encoded as is. 80 | // 81 | // For 7-bit encoding the message is assumed to contain UTF-8. 82 | // 83 | // For explicit UCS-2 encoding the message is assumed to contain UTF-16, 84 | // encoded as an array of bytes. This can be created from an array of UTF-16 85 | // runes using ucs2.Encode. 86 | // 87 | // For implicit UCS-2 encoding (the fallback with 7-bit fails) the message is 88 | // assumed to contain UTF-8. 89 | func (e Encoder) Encode(msg []byte, options ...EncoderOption) ([]tpdu.TPDU, error) { 90 | for _, option := range options { 91 | option.ApplyEncoderOption(&e) 92 | } 93 | sopts := append(e.sopts, tpdu.WithMR(e.MsgCount), tpdu.WithConcatRef(e.ConcatRef)) 94 | // take the DCS in the template TPDU as a hint... 95 | alpha, _ := e.pdu.DCS.Alphabet() 96 | switch alpha { 97 | case tpdu.Alpha8Bit, tpdu.AlphaUCS2: 98 | return e.pdu.Segment(msg, sopts...), nil 99 | default: 100 | // encode as GSM7, or failing that UCS2... 101 | d, udh, alpha := tpdu.EncodeUserData(msg, e.eopts...) 102 | dcs, err := e.pdu.DCS.WithAlphabet(alpha) 103 | if err != nil { 104 | return nil, ErrDcsConflict 105 | } 106 | if dcs != e.pdu.DCS { 107 | e.pdu.SetDCS(byte(dcs)) 108 | } 109 | if udh != nil { 110 | e.pdu.SetUDH(append(e.pdu.UDH[:0:0], udh...)) 111 | } 112 | return e.pdu.Segment(d, sopts...), nil 113 | } 114 | } 115 | 116 | // Counter is an implementation of the tpdu.Counter interface. 117 | // 118 | // It also provides a Read method on the current value for diagnostic purposes. 119 | type Counter struct { 120 | c int64 121 | } 122 | 123 | // Count increments and returns the counter. 124 | func (c *Counter) Count() int { 125 | return int(atomic.AddInt64(&c.c, 1)) 126 | } 127 | 128 | // Read returns the counter. 129 | func (c *Counter) Read() int { 130 | return int(atomic.LoadInt64(&c.c)) 131 | } 132 | -------------------------------------------------------------------------------- /encoding/bcd/bcd.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // Package bcd provides conversions to and from BCD format. 6 | package bcd 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | // Decode decodes a BCD encoded octet into the equivalent integer. 13 | // 14 | // The lowest nibble is taken as being the most significant. 15 | func Decode(bcd byte) (int, error) { 16 | msn := bcd & 0x0f 17 | lsn := bcd >> 4 18 | if msn > 9 || lsn > 9 { 19 | return 0, ErrInvalidOctet(bcd) 20 | } 21 | return int(msn*10 + lsn), nil 22 | } 23 | 24 | // DecodeSigned decodes a BCD encoded octet where bit 3 of the msn indicates 25 | // the sign of the encoded integer. 26 | func DecodeSigned(bcd byte) (int, error) { 27 | msn := bcd & 0x07 28 | lsn := bcd >> 4 29 | if lsn > 9 { 30 | return 0, ErrInvalidOctet(bcd) 31 | } 32 | retval := int(msn*10 + lsn) 33 | if bcd&0x08 != 0 { 34 | retval = -retval 35 | } 36 | return retval, nil 37 | } 38 | 39 | // Encode converts an integer in the range 0..99 into two BCD digits. 40 | // 41 | // The return value is the two BCD digits encoded into a byte in big endian, 42 | // and any error detected during conversion. 43 | func Encode(u int) (byte, error) { 44 | if u < 0 || u > 99 { 45 | return 0, ErrInvalidInteger(u) 46 | } 47 | msn := u % 10 48 | lsn := u / 10 49 | b := (msn << 4) | lsn 50 | return byte(b), nil 51 | } 52 | 53 | // EncodeSigned converts an integer in the range -79..79 into two BCD digits. 54 | // 55 | // The return value is the two BCD digits encoded into a byte, with the most 56 | // significant digit stored in the lowest nibble, and any error detected during 57 | // conversion. If the integer is negative then bit 3 of the byte is set to 1 . 58 | func EncodeSigned(s int) (byte, error) { 59 | if s < -79 || s > 79 { 60 | return 0, ErrInvalidInteger(s) 61 | } 62 | b := 0 63 | if s < 0 { 64 | b = 0x08 65 | s = -s 66 | } 67 | msn := s % 10 68 | lsn := s / 10 69 | b = b | (msn << 4) | lsn 70 | return byte(b), nil 71 | } 72 | 73 | // ErrInvalidOctet indicates that at least one of the nibbles in the BCD octet 74 | // is invalid, i.e. greater than 9. 75 | // 76 | // For DecodeSigned only the upper (least significant) nibble can be invalid. 77 | type ErrInvalidOctet byte 78 | 79 | func (e ErrInvalidOctet) Error() string { 80 | return fmt.Sprintf("bcd: invalid octet: 0x%02x", byte(e)) 81 | } 82 | 83 | // ErrInvalidInteger indicates that the integer is outside the range that can 84 | // be encoded. 85 | type ErrInvalidInteger int 86 | 87 | func (e ErrInvalidInteger) Error() string { 88 | return fmt.Sprintf("bcd: invalid integer: %d", int(e)) 89 | } 90 | -------------------------------------------------------------------------------- /encoding/bcd/bcd_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package bcd_test 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/warthog618/sms/encoding/bcd" 13 | ) 14 | 15 | type decodePattern struct { 16 | name string 17 | in byte 18 | out int 19 | err error 20 | } 21 | 22 | func TestDecode(t *testing.T) { 23 | patterns := []decodePattern{ 24 | { 25 | "zero", 26 | 0x0, 27 | 0, 28 | nil, 29 | }, 30 | { 31 | "thirteen", 32 | 0x31, 33 | 13, 34 | nil, 35 | }, 36 | { 37 | "thirty one", 38 | 0x13, 39 | 31, 40 | nil, 41 | }, 42 | { 43 | "ninety nine", 44 | 0x99, 45 | 99, 46 | nil, 47 | }, 48 | { 49 | "a9", 50 | 0xa9, 51 | 0, 52 | bcd.ErrInvalidOctet(0xa9), 53 | }, 54 | { 55 | "9a", 56 | 0x9a, 57 | 0, 58 | bcd.ErrInvalidOctet(0x9a), 59 | }, 60 | { 61 | "ff", 62 | 0xff, 63 | 0, 64 | bcd.ErrInvalidOctet(0xff), 65 | }, 66 | } 67 | for _, p := range patterns { 68 | f := func(t *testing.T) { 69 | i, err := bcd.Decode(p.in) 70 | assert.Equal(t, p.err, err) 71 | assert.Equal(t, p.out, i) 72 | } 73 | t.Run(p.name, f) 74 | } 75 | } 76 | 77 | func TestDecodeSigned(t *testing.T) { 78 | patterns := []decodePattern{ 79 | { 80 | "zero", 81 | 0x0, 82 | 0, 83 | nil, 84 | }, 85 | { 86 | "thirteen", 87 | 0x31, 88 | 13, 89 | nil, 90 | }, 91 | { 92 | "thirty one", 93 | 0x13, 94 | 31, 95 | nil, 96 | }, 97 | { 98 | "seventy nine", 99 | 0x97, 100 | 79, 101 | nil, 102 | }, 103 | { 104 | "negative zero", 105 | 0x08, 106 | 0, 107 | nil, 108 | }, 109 | { 110 | "negative one", 111 | 0x18, 112 | -1, 113 | nil, 114 | }, 115 | { 116 | "negative 19", 117 | 0x99, 118 | -19, 119 | nil, 120 | }, 121 | { 122 | "a9", 123 | 0xa9, 124 | 0, 125 | bcd.ErrInvalidOctet(0xa9), 126 | }, 127 | { 128 | "negative 29", 129 | 0x9a, 130 | -29, 131 | nil, 132 | }, 133 | { 134 | "negative 79", 135 | 0x9f, 136 | -79, 137 | nil, 138 | }, 139 | { 140 | "ff", 141 | 0xff, 142 | 0, 143 | bcd.ErrInvalidOctet(0xff), 144 | }, 145 | } 146 | for _, p := range patterns { 147 | f := func(t *testing.T) { 148 | i, err := bcd.DecodeSigned(p.in) 149 | assert.Equal(t, p.err, err) 150 | assert.Equal(t, p.out, i) 151 | } 152 | t.Run(p.name, f) 153 | } 154 | } 155 | 156 | type encodePattern struct { 157 | name string 158 | in int 159 | out byte 160 | err error 161 | } 162 | 163 | func TestEncode(t *testing.T) { 164 | patterns := []encodePattern{ 165 | { 166 | "zero", 167 | 0, 168 | 0x0, 169 | nil, 170 | }, 171 | { 172 | "thirteen", 173 | 13, 174 | 0x31, 175 | nil, 176 | }, 177 | { 178 | "thirty one", 179 | 31, 180 | 0x13, 181 | nil, 182 | }, 183 | { 184 | "ninety nine", 185 | 99, 186 | 0x99, 187 | nil, 188 | }, 189 | { 190 | "negative", 191 | -1, 192 | 0, 193 | bcd.ErrInvalidInteger(-1), 194 | }, 195 | { 196 | "hundred", 197 | 100, 198 | 0, 199 | bcd.ErrInvalidInteger(100), 200 | }, 201 | { 202 | "a9", 203 | 0xa9, 204 | 0, 205 | bcd.ErrInvalidInteger(0xa9), 206 | }, 207 | { 208 | "9a", 209 | 0x9a, 210 | 0, 211 | bcd.ErrInvalidInteger(0x9a), 212 | }, 213 | { 214 | "ff", 215 | 0xff, 216 | 0, 217 | bcd.ErrInvalidInteger(0xff), 218 | }, 219 | } 220 | for _, p := range patterns { 221 | f := func(t *testing.T) { 222 | b, err := bcd.Encode(p.in) 223 | assert.Equal(t, p.err, err) 224 | assert.Equal(t, p.out, b) 225 | } 226 | t.Run(p.name, f) 227 | } 228 | } 229 | 230 | func TestEncodeSigned(t *testing.T) { 231 | patterns := []encodePattern{ 232 | { 233 | "zero", 234 | 0, 235 | 0x0, 236 | nil, 237 | }, 238 | { 239 | "thirteen", 240 | 13, 241 | 0x31, 242 | nil, 243 | }, 244 | { 245 | "thirty one", 246 | 31, 247 | 0x13, 248 | nil, 249 | }, 250 | { 251 | "seventy nine", 252 | 79, 253 | 0x97, 254 | nil, 255 | }, 256 | { 257 | "negative one", 258 | -1, 259 | 0x18, 260 | nil, 261 | }, 262 | { 263 | "negative 19", 264 | -19, 265 | 0x99, 266 | nil, 267 | }, 268 | { 269 | "a9", 270 | 0xa9, 271 | 0, 272 | bcd.ErrInvalidInteger(0xa9), 273 | }, 274 | { 275 | "negative 29", 276 | -29, 277 | 0x9a, 278 | nil, 279 | }, 280 | { 281 | "negative 79", 282 | -79, 283 | 0x9f, 284 | nil, 285 | }, 286 | { 287 | "ff", 288 | 0xff, 289 | 0, 290 | bcd.ErrInvalidInteger(0xff), 291 | }, 292 | } 293 | for _, p := range patterns { 294 | f := func(t *testing.T) { 295 | b, err := bcd.EncodeSigned(p.in) 296 | assert.Equal(t, p.err, err) 297 | assert.Equal(t, p.out, b) 298 | } 299 | t.Run(p.name, f) 300 | } 301 | } 302 | 303 | // TestErrInvalidOctet tests that the errors can be stringified. 304 | // 305 | // It is fragile, as it compares the strings exactly, but its main purpose is 306 | // to confirm the Error function doesn't recurse, as that is bad. 307 | func TestErrInvalidOctet(t *testing.T) { 308 | patterns := []byte{0x00, 0xa0, 0x0a, 0x9a, 0xa9, 0xff} 309 | for _, p := range patterns { 310 | f := func(t *testing.T) { 311 | err := bcd.ErrInvalidOctet(p) 312 | expected := fmt.Sprintf("bcd: invalid octet: 0x%02x", p) 313 | s := err.Error() 314 | assert.Equal(t, expected, s) 315 | } 316 | t.Run(fmt.Sprintf("%x", p), f) 317 | } 318 | } 319 | 320 | // TestErrInvalidInteger tests that the errors can be stringified. 321 | // 322 | // It is fragile, as it compares the strings exactly, but its main purpose is 323 | // to confirm the Error function doesn't recurse, as that is bad. 324 | func TestErrInvalidInteger(t *testing.T) { 325 | patterns := []int{0, 20, -80, 100} 326 | for _, p := range patterns { 327 | f := func(t *testing.T) { 328 | err := bcd.ErrInvalidInteger(p) 329 | expected := fmt.Sprintf("bcd: invalid integer: %d", p) 330 | s := err.Error() 331 | assert.Equal(t, expected, s) 332 | } 333 | t.Run(fmt.Sprintf("%x", p), f) 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /encoding/gsm7/7bit.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package gsm7 6 | 7 | const cr byte = 0x0d 8 | 9 | // Pack7Bit packs an array of septets into an 8bit array as per the packing 10 | // rules defined in 3GPP TS 23.038 Section 6.1.2.1 11 | // 12 | // The padBits is the number of bits of pad to place at the beginning of the 13 | // packed array, as the packed septets may not start on an octet boundary. 14 | // 15 | // Packed arrays containing 8n or 8n-1 digits both return 8n septets. The 16 | // caller must be aware of the number of expected digits in order to 17 | // distinguish between a 0 septet ending the sequence in the 8n case, and 0 18 | // padding in the 8n-1 case. 19 | func Pack7Bit(u []byte, fillBits int) []byte { 20 | if len(u) == 0 { 21 | return append(u[:0:0], u...) 22 | } 23 | p := make([]byte, 0, (len(u)*7+7+fillBits)/8) 24 | var r, s byte 25 | rbits := uint(fillBits) 26 | for _, s = range u { 27 | if rbits == 0 { 28 | // no residual bits so not enough for a full octet 29 | r = s 30 | rbits = 7 31 | continue 32 | } 33 | r = (r | s<> (8 - rbits) 36 | rbits-- 37 | } 38 | if rbits != 0 { 39 | p = append(p, r) 40 | } 41 | return p 42 | } 43 | 44 | // Unpack7Bit unpacks septets, packed into an 8bit array as per the packing 45 | // rules defined in 3GPP TS 23.038 Section 6.1.2.1, into an array of septets. 46 | // 47 | // The fillBits is the number of bits of pad at the beginning of the src, as 48 | // the packed septets may not start on an octet boundary. 49 | func Unpack7Bit(p []byte, fillBits int) []byte { 50 | if len(p) == 0 { 51 | return append(p[:0:0], p...) 52 | } 53 | u := make([]byte, 0, (len(p)*8+6+fillBits)/7) 54 | var r byte 55 | var rbits uint 56 | if fillBits != 0 { 57 | rbits = uint(7 - fillBits) 58 | } 59 | for _, o := range p { 60 | r = (r | o<>1) 65 | rbits = 0 66 | r = 0 67 | } else { 68 | // each octet provides one extra residual bit 69 | rbits++ 70 | r = o >> (8 - rbits) 71 | } 72 | } 73 | if fillBits > 0 { 74 | u = u[1:] 75 | } 76 | return u 77 | } 78 | 79 | // Pack7BitUSSD packs an array of septets into an 8bit array as per the packing 80 | // rules defined in 3GPP TS 23.038 Section 6.1.2.3 81 | // 82 | // The padBits is the number of bits of pad to place at the beginning of the 83 | // packed array, as the packed septets may not start on an octet boundary. 84 | // 85 | // A filler CR is added to the final octet if there are 7 bits unused (to 86 | // distinguish from the 0x00 septet), or if the last septet is CR and ends on 87 | // an octet boundary (so it wont be considered filler). 88 | func Pack7BitUSSD(u []byte, fillBits int) []byte { 89 | b := Pack7Bit(u, fillBits) 90 | if len(b) == 0 { 91 | return append(b[:0:0], b...) 92 | } 93 | last := len(b) - 1 94 | if b[last]&^0x1 == 0 && u[len(u)-1] != 0 { 95 | b[last] = b[last] | (cr << 1) 96 | } else if len(u)&0x7 == 0 && u[len(u)-1] == cr { 97 | b = append(b, cr) 98 | } 99 | return b 100 | } 101 | 102 | // Unpack7BitUSSD unpacks septets, packed into an 8bit array, as per the 103 | // packing rules defined in 3GPP TS 23.038 Section 6.1.2.3, into an array of 104 | // septets. 105 | // 106 | // The fillBits is the number of bits of pad at the beginning of the src, as 107 | // the packed septets may not start on an octet boundary. 108 | // 109 | // Any trailing CR is assumed to be filler if it ends on an octet boundary, or 110 | // if it starts on an octet boundary and the previous character is also CR. 111 | func Unpack7BitUSSD(p []byte, fillBits int) []byte { 112 | u := Unpack7Bit(p, fillBits) 113 | // remove any trailing filler 114 | if len(p) > 1 && ((p[len(p)-1]>>1 == cr) || (p[len(p)-1] == cr && p[len(p)-2]>>1 == cr)) { 115 | u = u[:len(u)-1] 116 | } 117 | return u 118 | } 119 | -------------------------------------------------------------------------------- /encoding/gsm7/7bit_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package gsm7_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/warthog618/sms/encoding/gsm7" 12 | ) 13 | 14 | type testPattern struct { 15 | name string 16 | padBits int 17 | p []byte 18 | u []byte 19 | } 20 | 21 | var ( 22 | testPatterns = []testPattern{ 23 | { 24 | "nil", 25 | 0, 26 | nil, 27 | nil, 28 | }, 29 | { 30 | "empty", 31 | 0, 32 | []byte{}, 33 | []byte{}, 34 | }, 35 | { 36 | "empty fill", 37 | 1, 38 | []byte{}, 39 | []byte{}, 40 | }, 41 | { 42 | "cr", 43 | 0, 44 | []byte{13}, 45 | []byte("\r"), 46 | }, 47 | { 48 | "one", 49 | 0, 50 | []byte{49}, 51 | []byte("1"), 52 | }, 53 | { 54 | "two", 55 | 0, 56 | []byte{48, 25}, 57 | []byte("02"), 58 | }, 59 | { 60 | "message", 61 | 0, 62 | []byte{0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0x01}, 63 | []byte("message\x00"), 64 | }, 65 | { 66 | "seven", 67 | 0, 68 | []byte{240, 48, 157, 94, 150, 187, 1}, 69 | []byte("pattern\x00"), // packed is zero filled 70 | }, 71 | { 72 | "eight", 73 | 0, 74 | []byte{240, 48, 157, 94, 150, 187, 127}, 75 | []byte("pattern?"), 76 | }, 77 | { 78 | "nine", 79 | 0, 80 | []byte{240, 48, 157, 94, 150, 187, 67, 33}, 81 | []byte("pattern!!"), 82 | }, 83 | { 84 | "long", 85 | 0, 86 | []byte{ 87 | 97, 144, 189, 44, 207, 131, 216, 111, 247, 25, 68, 47, 207, 88 | 233, 32, 120, 152, 78, 47, 203, 221, 89 | }, 90 | []byte("a very long test pattern"), 91 | }, 92 | { 93 | "filler", 94 | 0, 95 | []byte{230, 52, 155, 93, 150, 255, 0}, 96 | []byte("filler?\x00"), 97 | }, 98 | { 99 | "fill1", 100 | 1, 101 | []byte{0xfe}, 102 | []byte{0x7f}, 103 | }, 104 | { 105 | "fill2", 106 | 2, 107 | []byte{0xfc, 1}, 108 | []byte{0x7f, 0x00}, 109 | }, 110 | { 111 | "fill3", 112 | 3, 113 | []byte{0xf8, 3}, 114 | []byte{0x7f}, 115 | }, 116 | { 117 | "fill4", 118 | 4, 119 | []byte{0xf0, 7}, 120 | []byte{0x7f}, 121 | }, 122 | { 123 | "fill5", 124 | 5, 125 | []byte{0xe0, 0xf}, 126 | []byte{0x7f}, 127 | }, 128 | { 129 | "fill6", 130 | 6, 131 | []byte{0xc0, 0x1f}, 132 | []byte{0x7f}, 133 | }, 134 | } 135 | ussdTestPatterns = []testPattern{ 136 | { 137 | "nil", 138 | 0, 139 | nil, 140 | nil, 141 | }, 142 | { 143 | "empty", 144 | 0, 145 | []byte{}, 146 | []byte{}, 147 | }, 148 | { 149 | "cr", 150 | 0, 151 | []byte{13}, 152 | []byte("\r"), 153 | }, 154 | { 155 | "one", 156 | 0, 157 | []byte{49}, 158 | []byte("1"), 159 | }, 160 | { 161 | "two", 162 | 0, 163 | []byte{48, 25}, 164 | []byte("02"), 165 | }, 166 | { 167 | "message0", 168 | 0, 169 | []byte{0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0x01}, 170 | []byte("message\x00"), 171 | }, 172 | { 173 | "message", 174 | 0, 175 | []byte{0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0x1b}, 176 | []byte("message"), 177 | }, 178 | { 179 | "seven", 180 | 0, 181 | []byte{240, 48, 157, 94, 150, 187, 27}, // packed is filled with CR 182 | []byte("pattern"), 183 | }, 184 | { 185 | "eight", 186 | 0, 187 | []byte{240, 48, 157, 94, 150, 187, 127}, 188 | []byte("pattern?"), 189 | }, 190 | { 191 | "nine", 192 | 0, 193 | []byte{240, 48, 157, 94, 150, 187, 67, 33}, 194 | []byte("pattern!!"), 195 | }, 196 | { 197 | "long", 198 | 0, 199 | []byte{ 200 | 97, 144, 189, 44, 207, 131, 216, 111, 247, 25, 68, 47, 207, 233, 201 | 32, 120, 152, 78, 47, 203, 221, 202 | }, 203 | []byte("a very long test pattern"), 204 | }, 205 | { 206 | "octet cr", 207 | 0, 208 | []byte{240, 48, 157, 94, 150, 187, 27, 13}, 209 | []byte("pattern\r"), 210 | }, 211 | { 212 | "filler", 213 | 0, 214 | []byte{230, 52, 155, 93, 150, 255, 26}, 215 | []byte("filler?"), 216 | }, 217 | { 218 | "null filler", 219 | 0, 220 | []byte{230, 52, 155, 93, 150, 255, 0}, 221 | []byte("filler?\x00"), 222 | }, 223 | { 224 | "fill1", 225 | 1, 226 | []byte{0xfe}, 227 | []byte{0x7f}, 228 | }, 229 | { 230 | "fill2", 231 | 2, 232 | []byte{0xfc, 27}, // packed is filled with CR 233 | []byte{0x7f}, 234 | }, 235 | { 236 | "fill3", 237 | 3, 238 | []byte{0xf8, 3}, 239 | []byte{0x7f}, 240 | }, 241 | { 242 | "fill4", 243 | 4, 244 | []byte{0xf0, 7}, 245 | []byte{0x7f}, 246 | }, 247 | { 248 | "fill5", 249 | 5, 250 | []byte{0xe0, 0xf}, 251 | []byte{0x7f}, 252 | }, 253 | { 254 | "fill6", 255 | 6, 256 | []byte{0xc0, 0x1f}, 257 | []byte{0x7f}, 258 | }, 259 | } 260 | ) 261 | 262 | func TestUnpack7Bit(t *testing.T) { 263 | for _, p := range testPatterns { 264 | f := func(t *testing.T) { 265 | u := gsm7.Unpack7Bit(p.p, p.padBits) 266 | assert.Equal(t, p.u, u) 267 | } 268 | t.Run(p.name, f) 269 | } 270 | } 271 | 272 | func TestPack7Bit(t *testing.T) { 273 | for _, p := range testPatterns { 274 | f := func(t *testing.T) { 275 | d := gsm7.Pack7Bit(p.u, p.padBits) 276 | assert.Equal(t, p.p, d) 277 | } 278 | t.Run(p.name, f) 279 | } 280 | } 281 | 282 | func TestUnpack7BitUSSD(t *testing.T) { 283 | for _, p := range ussdTestPatterns { 284 | f := func(t *testing.T) { 285 | u := gsm7.Unpack7BitUSSD(p.p, p.padBits) 286 | assert.Equal(t, p.u, u) 287 | } 288 | t.Run(p.name, f) 289 | } 290 | } 291 | 292 | func TestPack7BitUSSD(t *testing.T) { 293 | for _, p := range ussdTestPatterns { 294 | f := func(t *testing.T) { 295 | d := gsm7.Pack7BitUSSD(p.u, p.padBits) 296 | assert.Equal(t, p.p, d) 297 | } 298 | t.Run(p.name, f) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/bengali.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | bengaliDecoder = Decoder{ 9 | 0x00: '\u0981', 10 | 0x01: '\u0982', 11 | 0x02: '\u0983', 12 | 0x03: '\u0985', 13 | 0x04: '\u0986', 14 | 0x05: '\u0987', 15 | 0x06: '\u0988', 16 | 0x07: '\u0989', 17 | 0x08: '\u098a', 18 | 0x09: '\u098b', 19 | 0x0a: '\n', 20 | 0x0b: '\u098c', 21 | 0x0d: '\r', 22 | 0x0f: '\u098f', 23 | 0x10: '\u0990', 24 | 0x13: '\u0993', 25 | 0x14: '\u0994', 26 | 0x15: '\u0995', 27 | 0x16: '\u0996', 28 | 0x17: '\u0997', 29 | 0x18: '\u0998', 30 | 0x19: '\u0999', 31 | 0x1a: '\u099a', 32 | 0x1b: 0x1b, 33 | 0x1c: '\u099b', 34 | 0x1d: '\u099c', 35 | 0x1e: '\u099d', 36 | 0x1f: '\u099e', 37 | 0x20: 0x20, 38 | 0x21: '!', 39 | 0x22: '\u099f', 40 | 0x23: '\u09a0', 41 | 0x24: '\u09a1', 42 | 0x25: '\u09a2', 43 | 0x26: '\u09a3', 44 | 0x27: '\u09a4', 45 | 0x28: ')', 46 | 0x29: '(', 47 | 0x2a: '\u09a5', 48 | 0x2b: '\u09a6', 49 | 0x2c: ',', 50 | 0x2d: '\u09a7', 51 | 0x2e: '.', 52 | 0x2f: '\u09a8', 53 | 0x30: '0', 54 | 0x31: '1', 55 | 0x32: '2', 56 | 0x33: '3', 57 | 0x34: '4', 58 | 0x35: '5', 59 | 0x36: '6', 60 | 0x37: '7', 61 | 0x38: '8', 62 | 0x39: '9', 63 | 0x3a: ':', 64 | 0x3b: ';', 65 | 0x3d: '\u09aa', 66 | 0x3e: '\u09ab', 67 | 0x3f: '?', 68 | 0x40: '\u09ac', 69 | 0x41: '\u09ad', 70 | 0x42: '\u09ae', 71 | 0x43: '\u09af', 72 | 0x44: '\u09b0', 73 | 0x46: '\u09b2', 74 | 0x4a: '\u09b6', 75 | 0x4b: '\u09b7', 76 | 0x4c: '\u09b8', 77 | 0x4d: '\u09b9', 78 | 0x4e: '\u09bc', 79 | 0x4f: '\u09bd', 80 | 0x50: '\u09be', 81 | 0x51: '\u09bf', 82 | 0x52: '\u09c0', 83 | 0x53: '\u09c1', 84 | 0x54: '\u09c2', 85 | 0x55: '\u09c3', 86 | 0x56: '\u09c4', 87 | 0x59: '\u09c7', 88 | 0x5a: '\u09c8', 89 | 0x5d: '\u09cb', 90 | 0x5e: '\u09cc', 91 | 0x5f: '\u09cd', 92 | 0x60: '\u09ce', 93 | 0x61: 'a', 94 | 0x62: 'b', 95 | 0x63: 'c', 96 | 0x64: 'd', 97 | 0x65: 'e', 98 | 0x66: 'f', 99 | 0x67: 'g', 100 | 0x68: 'h', 101 | 0x69: 'i', 102 | 0x6a: 'j', 103 | 0x6b: 'k', 104 | 0x6c: 'l', 105 | 0x6d: 'm', 106 | 0x6e: 'n', 107 | 0x6f: 'o', 108 | 0x70: 'p', 109 | 0x71: 'q', 110 | 0x72: 'r', 111 | 0x73: 's', 112 | 0x74: 't', 113 | 0x75: 'u', 114 | 0x76: 'v', 115 | 0x77: 'w', 116 | 0x78: 'x', 117 | 0x79: 'y', 118 | 0x7a: 'z', 119 | 0x7b: '\u09d7', 120 | 0x7c: '\u09dc', 121 | 0x7d: '\u09dd', 122 | 0x7e: '\u09f0', 123 | 0x7f: '\u09f1', 124 | } 125 | bengaliExtDecoder = Decoder{ 126 | 0x00: '@', 127 | 0x01: '£', 128 | 0x02: '$', 129 | 0x03: '¥', 130 | 0x04: '¿', 131 | 0x05: '"', 132 | 0x06: '¤', 133 | 0x07: '%', 134 | 0x08: '&', 135 | 0x09: '\'', 136 | 0x0a: '\f', 137 | 0x0b: '*', 138 | 0x0c: '+', 139 | 0x0d: '\r', 140 | 0x0e: '-', 141 | 0x0f: '/', 142 | 0x10: '<', 143 | 0x11: '=', 144 | 0x12: '>', 145 | 0x13: '¡', 146 | 0x14: '^', 147 | 0x15: '¡', 148 | 0x16: '_', 149 | 0x17: '#', 150 | 0x18: '*', 151 | 0x19: '\u09e6', 152 | 0x1a: '\u09e7', 153 | 0x1b: 0x1b, 154 | 0x1c: '\u09e8', 155 | 0x1d: '\u09e9', 156 | 0x1e: '\u09ea', 157 | 0x1f: '\u09eb', 158 | 0x20: '\u09ec', 159 | 0x21: '\u09ed', 160 | 0x22: '\u09ee', 161 | 0x23: '\u09ef', 162 | 0x24: '\u09df', 163 | 0x25: '\u09e0', 164 | 0x26: '\u09e1', 165 | 0x27: '\u09e2', 166 | 0x28: '{', 167 | 0x29: '}', 168 | 0x2a: '\u09e3', 169 | 0x2b: '\u09f2', 170 | 0x2c: '\u09f3', 171 | 0x2d: '\u09f4', 172 | 0x2e: '\u09f5', 173 | 0x2f: '\\', 174 | 0x30: '\u09f6', 175 | 0x31: '\u09f7', 176 | 0x32: '\u09f8', 177 | 0x33: '\u09f9', 178 | 0x34: '\u09fa', 179 | 0x3c: '[', 180 | 0x3d: '~', 181 | 0x3e: ']', 182 | 0x40: '|', 183 | 0x41: 'A', 184 | 0x42: 'B', 185 | 0x43: 'C', 186 | 0x44: 'D', 187 | 0x45: 'E', 188 | 0x46: 'F', 189 | 0x47: 'G', 190 | 0x48: 'H', 191 | 0x49: 'I', 192 | 0x4a: 'J', 193 | 0x4b: 'K', 194 | 0x4c: 'L', 195 | 0x4d: 'M', 196 | 0x4e: 'N', 197 | 0x4f: 'O', 198 | 0x50: 'P', 199 | 0x51: 'Q', 200 | 0x52: 'R', 201 | 0x53: 'S', 202 | 0x54: 'T', 203 | 0x55: 'U', 204 | 0x56: 'V', 205 | 0x57: 'W', 206 | 0x58: 'X', 207 | 0x59: 'Y', 208 | 0x5a: 'Z', 209 | 0x65: '€', 210 | } 211 | bengaliEncoder Encoder 212 | bengaliExtEncoder Encoder 213 | ) 214 | 215 | func generateBengaliEncoder() Encoder { 216 | return generateEncoder(bengaliDecoder) 217 | } 218 | 219 | func generateBengaliExtEncoder() Encoder { 220 | return generateEncoder(bengaliExtDecoder) 221 | } 222 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/charset.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // Package charset provides encoders and decoders for GSM character sets. 6 | package charset 7 | 8 | // DefaultDecoder returns the default mapping table from GSM7 to UTF8. 9 | func DefaultDecoder() Decoder { 10 | if defaultDecoder == nil { 11 | defaultDecoder = generateDefaultDecoder() 12 | } 13 | return defaultDecoder 14 | } 15 | 16 | // NewDecoder returns the mapping table from GSM7 to UTF8 for the given language. 17 | func NewDecoder(nli int) Decoder { 18 | if di, ok := decoder[nli]; ok { 19 | if *di.e == nil { 20 | *di.e = di.g() 21 | } 22 | return *di.e 23 | } 24 | return DefaultDecoder() 25 | } 26 | 27 | // DefaultExtDecoder returns the default extension mapping table from GSM7 to UTF8. 28 | func DefaultExtDecoder() Decoder { 29 | return defaultExtDecoder 30 | } 31 | 32 | // NewExtDecoder returns the extension mapping table from GSM7 to UTF8 for the given language. 33 | func NewExtDecoder(nli int) Decoder { 34 | if di, ok := extDecoder[nli]; ok { 35 | return *di.e 36 | } 37 | return DefaultExtDecoder() 38 | } 39 | 40 | // DefaultEncoder returns the default mapping table from UTF8 to GSM7. 41 | func DefaultEncoder() Encoder { 42 | if defaultEncoder == nil { 43 | defaultEncoder = generateDefaultEncoder() 44 | } 45 | return defaultEncoder 46 | } 47 | 48 | // NewEncoder returns the mapping table from UTF8 to GSM7 for the given language. 49 | func NewEncoder(nli int) Encoder { 50 | if ei, ok := encoder[nli]; ok { 51 | if *ei.e == nil { 52 | *ei.e = ei.g() 53 | } 54 | return *ei.e 55 | } 56 | return DefaultEncoder() 57 | } 58 | 59 | // DefaultExtEncoder returns the default extension mapping table from UTF8 to GSM7. 60 | func DefaultExtEncoder() Encoder { 61 | if defaultExtEncoder == nil { 62 | defaultExtEncoder = generateDefaultExtEncoder() 63 | } 64 | return defaultExtEncoder 65 | } 66 | 67 | // NewExtEncoder returns the extension mapping table from UTF8 to GSM7 for the given language. 68 | func NewExtEncoder(nli int) Encoder { 69 | if ei, ok := extEncoder[nli]; ok { 70 | if *ei.e == nil { 71 | *ei.e = ei.g() 72 | } 73 | return *ei.e 74 | } 75 | return DefaultExtEncoder() 76 | } 77 | 78 | // Decoder provides a mapping from GSM7 byte to UTF8 rune. 79 | type Decoder map[byte]rune 80 | 81 | // Encoder provides a mapping from UTF8 rune to GSM7 byte. 82 | type Encoder map[rune]byte 83 | 84 | // NationalLanguageIdentifier indicates the character set in use, as defined in 85 | // 3GPP TS 23.038 Section 6.2.1.2.4. 86 | type NationalLanguageIdentifier int 87 | 88 | const ( 89 | // Default character set. 90 | Default int = iota 91 | // Turkish character set. 92 | Turkish 93 | // Spanish character set 94 | Spanish 95 | // Portuguese character set 96 | Portuguese 97 | // Bengali character set 98 | Bengali 99 | // Gujaranti character set 100 | Gujaranti 101 | // Hindi character set 102 | Hindi 103 | // Kannada character set 104 | Kannada 105 | // Malayalam character set 106 | Malayalam 107 | // Oriya character set 108 | Oriya 109 | // Punjabi character set 110 | Punjabi 111 | // Tamil character set 112 | Tamil 113 | // Telugu character set 114 | Telugu 115 | // Urdu character set 116 | Urdu 117 | 118 | // Helper consts for creating and iterating over slices 119 | 120 | // End marker for loops (exclusive) 121 | End 122 | // Start point for loops (inclusive) 123 | Start = Turkish 124 | // Size is for array sizing (excluding default) 125 | Size = End - Start 126 | ) 127 | 128 | func generateEncoder(d Decoder) Encoder { 129 | e := make(Encoder, len(d)) 130 | for k, v := range d { 131 | if ko, ok := e[v]; !ok || ko > k { 132 | e[v] = k 133 | } 134 | } 135 | return e 136 | } 137 | 138 | func generateEncoderFromRunes(runes []rune) Encoder { 139 | e := make(Encoder, len(runes)) 140 | for i, r := range runes { 141 | e[r] = byte(i) 142 | } 143 | return e 144 | } 145 | 146 | func generateDecoderFromRunes(runes []rune) Decoder { 147 | dset := make(Decoder, len(runes)) 148 | for i, r := range runes { 149 | dset[byte(i)] = r 150 | } 151 | return dset 152 | } 153 | 154 | type decoderGenerator func() Decoder 155 | 156 | type decoderNGen struct { 157 | e *Decoder 158 | g decoderGenerator 159 | } 160 | 161 | type encoderGenerator func() Encoder 162 | 163 | type encoderNGen struct { 164 | e *Encoder 165 | g encoderGenerator 166 | } 167 | 168 | var ( 169 | decoder = map[int]decoderNGen{ 170 | Turkish: {&turkishDecoder, generateTurkishDecoder}, 171 | // Spanish uses default 172 | Portuguese: {&portugueseDecoder, generatePortugueseDecoder}, 173 | Bengali: {&bengaliDecoder, nil}, 174 | Gujaranti: {&gujaratiDecoder, nil}, 175 | Hindi: {&hindiDecoder, nil}, 176 | Kannada: {&kannadaDecoder, nil}, 177 | Malayalam: {&malayalamDecoder, nil}, 178 | Oriya: {&oriyaDecoder, nil}, 179 | Punjabi: {&punjabiDecoder, nil}, 180 | Tamil: {&tamilDecoder, nil}, 181 | Telugu: {&teluguDecoder, nil}, 182 | Urdu: {&urduDecoder, nil}, 183 | } 184 | extDecoder = map[int]decoderNGen{ 185 | Turkish: {&turkishExtDecoder, nil}, 186 | Spanish: {&spanishExtDecoder, nil}, 187 | Portuguese: {&portugueseExtDecoder, nil}, 188 | Bengali: {&bengaliExtDecoder, nil}, 189 | Gujaranti: {&gujaratiExtDecoder, nil}, 190 | Hindi: {&hindiExtDecoder, nil}, 191 | Kannada: {&kannadaExtDecoder, nil}, 192 | Malayalam: {&malayalamExtDecoder, nil}, 193 | Oriya: {&oriyaExtDecoder, nil}, 194 | Punjabi: {&punjabiExtDecoder, nil}, 195 | Tamil: {&tamilExtDecoder, nil}, 196 | Telugu: {&teluguExtDecoder, nil}, 197 | Urdu: {&urduExtDecoder, nil}, 198 | } 199 | encoder = map[int]encoderNGen{ 200 | Turkish: {&turkishEncoder, generateTurkishEncoder}, 201 | // Spanish uses default 202 | Portuguese: {&portugueseEncoder, generatePortugueseEncoder}, 203 | Bengali: {&bengaliEncoder, generateBengaliEncoder}, 204 | Gujaranti: {&gujaratiEncoder, generateGujaratiEncoder}, 205 | Hindi: {&hindiEncoder, generateHindiEncoder}, 206 | Kannada: {&kannadaEncoder, generateKannadaEncoder}, 207 | Malayalam: {&malayalamEncoder, generateMalayalamEncoder}, 208 | Oriya: {&oriyaEncoder, generateOriyaEncoder}, 209 | Punjabi: {&punjabiEncoder, generatePunjabiEncoder}, 210 | Tamil: {&tamilEncoder, generateTamilEncoder}, 211 | Telugu: {&teluguEncoder, generateTeluguEncoder}, 212 | Urdu: {&urduEncoder, generateUrduEncoder}, 213 | } 214 | extEncoder = map[int]encoderNGen{ 215 | Turkish: {&turkishExtEncoder, generateTurkishExtEncoder}, 216 | Spanish: {&spanishExtEncoder, generateSpanishExtEncoder}, 217 | Portuguese: {&portugueseExtEncoder, generatePortugueseExtEncoder}, 218 | Bengali: {&bengaliExtEncoder, generateBengaliExtEncoder}, 219 | Gujaranti: {&gujaratiExtEncoder, generateGujaratiExtEncoder}, 220 | Hindi: {&hindiExtEncoder, generateHindiExtEncoder}, 221 | Kannada: {&kannadaExtEncoder, generateKannadaExtEncoder}, 222 | Malayalam: {&malayalamExtEncoder, generateMalayalamExtEncoder}, 223 | Oriya: {&oriyaExtEncoder, generateOriyaExtEncoder}, 224 | Punjabi: {&punjabiExtEncoder, generatePunjabiExtEncoder}, 225 | Tamil: {&tamilExtEncoder, generateTamilExtEncoder}, 226 | Telugu: {&teluguExtEncoder, generateTeluguExtEncoder}, 227 | Urdu: {&urduExtEncoder, generateUrduExtEncoder}, 228 | } 229 | ) 230 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/default.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | defaultDecoder Decoder 9 | defaultExtDecoder = Decoder{ 10 | 0x0a: '\f', 11 | 0x0d: '\n', 12 | 0x14: '^', 13 | 0x28: '{', 14 | 0x29: '}', 15 | 0x2f: '\\', 16 | 0x3c: '[', 17 | 0x3d: '~', 18 | 0x3e: ']', 19 | 0x40: '|', 20 | 0x65: '€', 21 | } 22 | defaultEncoder Encoder 23 | defaultExtEncoder Encoder 24 | defaultRunes = []rune( 25 | "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?" + 26 | "¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà") 27 | ) 28 | 29 | func generateDefaultEncoder() Encoder { 30 | return generateEncoderFromRunes(defaultRunes) 31 | } 32 | 33 | func generateDefaultDecoder() Decoder { 34 | return generateDecoderFromRunes(defaultRunes) 35 | } 36 | 37 | func generateDefaultExtEncoder() Encoder { 38 | return generateEncoder(defaultExtDecoder) 39 | } 40 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/gujarati.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | gujaratiDecoder = Decoder{ 9 | 0x00: '\u0a81', 10 | 0x01: '\u0a82', 11 | 0x02: '\u0a83', 12 | 0x03: '\u0a85', 13 | 0x04: '\u0a86', 14 | 0x05: '\u0a87', 15 | 0x06: '\u0a88', 16 | 0x07: '\u0a89', 17 | 0x08: '\u0a8a', 18 | 0x09: '\u0a8b', 19 | 0x0a: '\n', 20 | 0x0b: '\u0a8c', 21 | 0x0c: '\u0a8d', 22 | 0x0d: '\r', 23 | 0x0f: '\u0a8f', 24 | 0x10: '\u0a90', 25 | 0x11: '\u0a91', 26 | 0x13: '\u0a93', 27 | 0x14: '\u0a94', 28 | 0x15: '\u0a95', 29 | 0x16: '\u0a96', 30 | 0x17: '\u0a97', 31 | 0x18: '\u0a98', 32 | 0x19: '\u0a99', 33 | 0x1a: '\u0a9a', 34 | 0x1b: 0x1b, 35 | 0x1c: '\u0a9b', 36 | 0x1d: '\u0a9c', 37 | 0x1e: '\u0a9d', 38 | 0x1f: '\u0a9e', 39 | 0x20: 0x20, 40 | 0x21: '!', 41 | 0x22: '\u0a9f', 42 | 0x23: '\u0aa0', 43 | 0x24: '\u0aa1', 44 | 0x25: '\u0aa2', 45 | 0x26: '\u0aa3', 46 | 0x27: '\u0aa4', 47 | 0x28: ')', 48 | 0x29: '(', 49 | 0x2a: '\u0aa5', 50 | 0x2b: '\u0aa6', 51 | 0x2c: ',', 52 | 0x2d: '\u0aa7', 53 | 0x2e: '.', 54 | 0x2f: '\u0aa8', 55 | 0x30: '0', 56 | 0x31: '1', 57 | 0x32: '2', 58 | 0x33: '3', 59 | 0x34: '4', 60 | 0x35: '5', 61 | 0x36: '6', 62 | 0x37: '7', 63 | 0x38: '8', 64 | 0x39: '9', 65 | 0x3a: ':', 66 | 0x3b: ';', 67 | 0x3d: '\u0aaa', 68 | 0x3e: '\u0aab', 69 | 0x3f: '?', 70 | 0x40: '\u0aac', 71 | 0x41: '\u0aad', 72 | 0x42: '\u0aae', 73 | 0x43: '\u0aaf', 74 | 0x44: '\u0ab0', 75 | 0x46: '\u0ab2', 76 | 0x47: '\u0ab3', 77 | 0x49: '\u0ab5', 78 | 0x4a: '\u0ab6', 79 | 0x4b: '\u0ab7', 80 | 0x4c: '\u0ab8', 81 | 0x4d: '\u0ab9', 82 | 0x4e: '\u0abc', 83 | 0x4f: '\u0abd', 84 | 0x50: '\u0abe', 85 | 0x51: '\u0abf', 86 | 0x52: '\u0ac0', 87 | 0x53: '\u0ac1', 88 | 0x54: '\u0ac2', 89 | 0x55: '\u0ac3', 90 | 0x56: '\u0ac4', 91 | 0x57: '\u0ac5', 92 | 0x59: '\u0ac7', 93 | 0x5a: '\u0ac8', 94 | 0x5b: '\u0ac9', 95 | 0x5d: '\u0acb', 96 | 0x5e: '\u0acc', 97 | 0x5f: '\u0acd', 98 | 0x60: '\u0ad0', 99 | 0x61: 'a', 100 | 0x62: 'b', 101 | 0x63: 'c', 102 | 0x64: 'd', 103 | 0x65: 'e', 104 | 0x66: 'f', 105 | 0x67: 'g', 106 | 0x68: 'h', 107 | 0x69: 'i', 108 | 0x6a: 'j', 109 | 0x6b: 'k', 110 | 0x6c: 'l', 111 | 0x6d: 'm', 112 | 0x6e: 'n', 113 | 0x6f: 'o', 114 | 0x70: 'p', 115 | 0x71: 'q', 116 | 0x72: 'r', 117 | 0x73: 's', 118 | 0x74: 't', 119 | 0x75: 'u', 120 | 0x76: 'v', 121 | 0x77: 'w', 122 | 0x78: 'x', 123 | 0x79: 'y', 124 | 0x7a: 'z', 125 | 0x7b: '\u0ae0', 126 | 0x7c: '\u0ae1', 127 | 0x7d: '\u0ae2', 128 | 0x7e: '\u0ae3', 129 | 0x7f: '\u0af1', 130 | } 131 | gujaratiExtDecoder = Decoder{ 132 | 0x00: '@', 133 | 0x01: '£', 134 | 0x02: '$', 135 | 0x03: '¥', 136 | 0x04: '¿', 137 | 0x05: '"', 138 | 0x06: '¤', 139 | 0x07: '%', 140 | 0x08: '&', 141 | 0x09: '\'', 142 | 0x0a: '\f', 143 | 0x0b: '*', 144 | 0x0c: '+', 145 | 0x0d: '\r', 146 | 0x0e: '-', 147 | 0x0f: '/', 148 | 0x10: '<', 149 | 0x11: '=', 150 | 0x12: '>', 151 | 0x13: '¡', 152 | 0x14: '^', 153 | 0x15: '¡', 154 | 0x16: '_', 155 | 0x17: '#', 156 | 0x18: '*', 157 | 0x19: '\u0964', 158 | 0x1a: '\u0965', 159 | 0x1b: 0x1b, 160 | 0x1c: '\u0ae6', 161 | 0x1d: '\u0ae7', 162 | 0x1e: '\u0ae8', 163 | 0x1f: '\u0ae9', 164 | 0x20: '\u0aea', 165 | 0x21: '\u0aeb', 166 | 0x22: '\u0aec', 167 | 0x23: '\u0aed', 168 | 0x24: '\u0aee', 169 | 0x25: '\u0aef', 170 | 0x28: '{', 171 | 0x29: '}', 172 | 0x2f: '\\', 173 | 0x3c: '[', 174 | 0x3d: '~', 175 | 0x3e: ']', 176 | 0x40: '|', 177 | 0x41: 'A', 178 | 0x42: 'B', 179 | 0x43: 'C', 180 | 0x44: 'D', 181 | 0x45: 'E', 182 | 0x46: 'F', 183 | 0x47: 'G', 184 | 0x48: 'H', 185 | 0x49: 'I', 186 | 0x4a: 'J', 187 | 0x4b: 'K', 188 | 0x4c: 'L', 189 | 0x4d: 'M', 190 | 0x4e: 'N', 191 | 0x4f: 'O', 192 | 0x50: 'P', 193 | 0x51: 'Q', 194 | 0x52: 'R', 195 | 0x53: 'S', 196 | 0x54: 'T', 197 | 0x55: 'U', 198 | 0x56: 'V', 199 | 0x57: 'W', 200 | 0x58: 'X', 201 | 0x59: 'Y', 202 | 0x5a: 'Z', 203 | 0x65: '€', 204 | } 205 | gujaratiEncoder Encoder 206 | gujaratiExtEncoder Encoder 207 | ) 208 | 209 | func generateGujaratiEncoder() Encoder { 210 | return generateEncoder(gujaratiDecoder) 211 | } 212 | 213 | func generateGujaratiExtEncoder() Encoder { 214 | return generateEncoder(gujaratiExtDecoder) 215 | } 216 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/hindi.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | hindiDecoder = Decoder{ 9 | 0x00: '\u0981', 10 | 0x01: '\u0982', 11 | 0x02: '\u0983', 12 | 0x03: 'अ', 13 | 0x04: 'आ', 14 | 0x05: 'इ', 15 | 0x06: 'ई', 16 | 0x07: 'उ', 17 | 0x08: 'ऊ', 18 | 0x09: 'ऋ', 19 | 0x0a: '\n', 20 | 0x0b: 'ऌ', 21 | 0x0c: 'ऍ', 22 | 0x0d: '\r', 23 | 0x0e: 'ऎ', 24 | 0x0f: 'ए', 25 | 0x10: 'ऐ', 26 | 0x11: 'ऑ', 27 | 0x12: 'ऒ', 28 | 0x13: 'ओ', 29 | 0x14: 'औ', 30 | 0x15: 'क', 31 | 0x16: 'ख', 32 | 0x17: 'ग', 33 | 0x18: 'घ', 34 | 0x19: 'ङ', 35 | 0x1a: 'च', 36 | 0x1b: 0x1b, 37 | 0x1c: 'छ', 38 | 0x1d: 'ज', 39 | 0x1e: 'झ', 40 | 0x1f: 'ञ', 41 | 0x20: 0x20, 42 | 0x21: '!', 43 | 0x22: 'ट', 44 | 0x23: 'ठ', 45 | 0x24: 'ड', 46 | 0x25: 'ढ', 47 | 0x26: 'ण', 48 | 0x27: 'त', 49 | 0x28: ')', 50 | 0x29: '(', 51 | 0x2a: 'थ', 52 | 0x2b: 'द', 53 | 0x2c: ',', 54 | 0x2d: 'ध', 55 | 0x2e: '.', 56 | 0x2f: 'न', 57 | 0x30: '0', 58 | 0x31: '1', 59 | 0x32: '2', 60 | 0x33: '3', 61 | 0x34: '4', 62 | 0x35: '5', 63 | 0x36: '6', 64 | 0x37: '7', 65 | 0x38: '8', 66 | 0x39: '9', 67 | 0x3a: ':', 68 | 0x3b: ';', 69 | 0x3c: 'ऩ', 70 | 0x3d: 'प', 71 | 0x3e: 'फ', 72 | 0x3f: '?', 73 | 0x40: 'ब', 74 | 0x41: 'भ', 75 | 0x42: 'म', 76 | 0x43: 'य', 77 | 0x44: 'र', 78 | 0x45: 'ऱ', 79 | 0x46: 'ल', 80 | 0x47: 'ळ', 81 | 0x48: 'ऴ', 82 | 0x49: 'व', 83 | 0x4a: 'श', 84 | 0x4b: 'ष', 85 | 0x4c: 'स', 86 | 0x4d: 'ह', 87 | 0x4e: '\u093c', 88 | 0x4f: 'ऽ', 89 | 0x50: '\u093e', 90 | 0x51: '\u093f', 91 | 0x52: '\u0940', 92 | 0x53: '\u0941', 93 | 0x54: '\u0942', 94 | 0x55: '\u0943', 95 | 0x56: '\u0944', 96 | 0x57: '\u0945', 97 | 0x58: '\u0946', 98 | 0x59: '\u0947', 99 | 0x5a: '\u0948', 100 | 0x5b: '\u0949', 101 | 0x5c: '\u094a', 102 | 0x5d: '\u094b', 103 | 0x5e: '\u094c', 104 | 0x5f: '\u094d', 105 | 0x60: 'ॐ', 106 | 0x61: 'a', 107 | 0x62: 'b', 108 | 0x63: 'c', 109 | 0x64: 'd', 110 | 0x65: 'e', 111 | 0x66: 'f', 112 | 0x67: 'g', 113 | 0x68: 'h', 114 | 0x69: 'i', 115 | 0x6a: 'j', 116 | 0x6b: 'k', 117 | 0x6c: 'l', 118 | 0x6d: 'm', 119 | 0x6e: 'n', 120 | 0x6f: 'o', 121 | 0x70: 'p', 122 | 0x71: 'q', 123 | 0x72: 'r', 124 | 0x73: 's', 125 | 0x74: 't', 126 | 0x75: 'u', 127 | 0x76: 'v', 128 | 0x77: 'w', 129 | 0x78: 'x', 130 | 0x79: 'y', 131 | 0x7a: 'z', 132 | 0x7b: 'ॲ', 133 | 0x7c: 'ॻ', 134 | 0x7d: 'ॼ', 135 | 0x7e: 'ॾ', 136 | 0x7f: 'ॿ', 137 | } 138 | hindiExtDecoder = Decoder{ 139 | 0x00: '@', 140 | 0x01: '£', 141 | 0x02: '$', 142 | 0x03: '¥', 143 | 0x04: '¿', 144 | 0x05: '"', 145 | 0x06: '¤', 146 | 0x07: '%', 147 | 0x08: '&', 148 | 0x09: '\'', 149 | 0x0a: '\f', 150 | 0x0b: '*', 151 | 0x0c: '+', 152 | 0x0d: '\r', 153 | 0x0e: '-', 154 | 0x0f: '/', 155 | 0x10: '<', 156 | 0x11: '=', 157 | 0x12: '>', 158 | 0x13: '¡', 159 | 0x14: '^', 160 | 0x15: '¡', 161 | 0x16: '_', 162 | 0x17: '#', 163 | 0x18: '*', 164 | 0x19: '।', 165 | 0x1a: '॥', 166 | 0x1b: 0x1b, 167 | 0x1c: '०', 168 | 0x1d: '१', 169 | 0x1e: '२', 170 | 0x1f: '३', 171 | 0x20: '४', 172 | 0x21: '५', 173 | 0x22: '६', 174 | 0x23: '७', 175 | 0x24: '८', 176 | 0x25: '९', 177 | 0x26: '\u0951', 178 | 0x27: '\u0952', 179 | 0x28: '{', 180 | 0x29: '}', 181 | 0x2a: '\u0953', 182 | 0x2b: '\u0954', 183 | 0x2c: 'क़', 184 | 0x2d: 'ख़', 185 | 0x2e: 'ग़', 186 | 0x2f: '\\', 187 | 0x30: 'ज़', 188 | 0x31: 'ड़', 189 | 0x32: 'ढ़', 190 | 0x33: 'फ़', 191 | 0x34: 'य़', 192 | 0x35: 'ॠ', 193 | 0x36: 'ॡ', 194 | 0x37: '\u0962', 195 | 0x38: '\u0963', 196 | 0x39: '॰', 197 | 0x3a: 'ॱ', 198 | 0x3c: '[', 199 | 0x3d: '~', 200 | 0x3e: ']', 201 | 0x40: '|', 202 | 0x41: 'A', 203 | 0x42: 'B', 204 | 0x43: 'C', 205 | 0x44: 'D', 206 | 0x45: 'E', 207 | 0x46: 'F', 208 | 0x47: 'G', 209 | 0x48: 'H', 210 | 0x49: 'I', 211 | 0x4a: 'J', 212 | 0x4b: 'K', 213 | 0x4c: 'L', 214 | 0x4d: 'M', 215 | 0x4e: 'N', 216 | 0x4f: 'O', 217 | 0x50: 'P', 218 | 0x51: 'Q', 219 | 0x52: 'R', 220 | 0x53: 'S', 221 | 0x54: 'T', 222 | 0x55: 'U', 223 | 0x56: 'V', 224 | 0x57: 'W', 225 | 0x58: 'X', 226 | 0x59: 'Y', 227 | 0x5a: 'Z', 228 | 0x65: '€', 229 | } 230 | hindiEncoder Encoder 231 | hindiExtEncoder Encoder 232 | ) 233 | 234 | func generateHindiEncoder() Encoder { 235 | return generateEncoder(hindiDecoder) 236 | } 237 | 238 | func generateHindiExtEncoder() Encoder { 239 | return generateEncoder(hindiExtDecoder) 240 | } 241 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/kannada.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | kannadaDecoder = Decoder{ 9 | 0x01: '\u0c82', 10 | 0x02: '\u0c83', 11 | 0x03: '\u0c85', 12 | 0x04: '\u0c86', 13 | 0x05: '\u0c87', 14 | 0x06: '\u0c88', 15 | 0x07: '\u0c89', 16 | 0x08: '\u0c8a', 17 | 0x09: '\u0c8b', 18 | 0x0a: '\n', 19 | 0x0b: '\u0c8c', 20 | 0x0d: '\r', 21 | 0x0e: '\u0c8e', 22 | 0x0f: '\u0c8f', 23 | 0x10: '\u0c90', 24 | 0x12: '\u0c92', 25 | 0x13: '\u0c93', 26 | 0x14: '\u0c94', 27 | 0x15: '\u0c95', 28 | 0x16: '\u0c96', 29 | 0x17: '\u0c97', 30 | 0x18: '\u0c98', 31 | 0x19: '\u0c99', 32 | 0x1a: '\u0c9a', 33 | 0x1b: 0x1b, 34 | 0x1c: '\u0c9b', 35 | 0x1d: '\u0c9c', 36 | 0x1e: '\u0c9d', 37 | 0x1f: '\u0c9e', 38 | 0x20: 0x20, 39 | 0x21: '!', 40 | 0x22: '\u0c9f', 41 | 0x23: '\u0ca0', 42 | 0x24: '\u0ca1', 43 | 0x25: '\u0ca2', 44 | 0x26: '\u0ca3', 45 | 0x27: '\u0ca4', 46 | 0x28: ')', 47 | 0x29: '(', 48 | 0x2a: '\u0ca5', 49 | 0x2b: '\u0ca6', 50 | 0x2c: ',', 51 | 0x2d: '\u0ca7', 52 | 0x2e: '.', 53 | 0x2f: '\u0ca8', 54 | 0x30: '0', 55 | 0x31: '1', 56 | 0x32: '2', 57 | 0x33: '3', 58 | 0x34: '4', 59 | 0x35: '5', 60 | 0x36: '6', 61 | 0x37: '7', 62 | 0x38: '8', 63 | 0x39: '9', 64 | 0x3a: ':', 65 | 0x3b: ';', 66 | 0x3d: '\u0caa', 67 | 0x3e: '\u0cab', 68 | 0x3f: '?', 69 | 0x40: '\u0cac', 70 | 0x41: '\u0cad', 71 | 0x42: '\u0cae', 72 | 0x43: '\u0caf', 73 | 0x44: '\u0cb0', 74 | 0x45: '\u0cb1', 75 | 0x46: '\u0cb2', 76 | 0x47: '\u0cb3', 77 | 0x49: '\u0cb5', 78 | 0x4a: '\u0cb6', 79 | 0x4b: '\u0cb7', 80 | 0x4c: '\u0cb8', 81 | 0x4d: '\u0cb9', 82 | 0x4e: '\u0cbc', 83 | 0x4f: '\u0cbd', 84 | 0x50: '\u0cbe', 85 | 0x51: '\u0cbf', 86 | 0x52: '\u0cc0', 87 | 0x53: '\u0cc1', 88 | 0x54: '\u0cc2', 89 | 0x55: '\u0cc3', 90 | 0x56: '\u0cc4', 91 | 0x58: '\u0cc6', 92 | 0x59: '\u0cc7', 93 | 0x5a: '\u0cc8', 94 | 0x5c: '\u0cca', 95 | 0x5d: '\u0ccb', 96 | 0x5e: '\u0ccc', 97 | 0x5f: '\u0ccd', 98 | 0x60: '\u0cd5', 99 | 0x61: 'a', 100 | 0x62: 'b', 101 | 0x63: 'c', 102 | 0x64: 'd', 103 | 0x65: 'e', 104 | 0x66: 'f', 105 | 0x67: 'g', 106 | 0x68: 'h', 107 | 0x69: 'i', 108 | 0x6a: 'j', 109 | 0x6b: 'k', 110 | 0x6c: 'l', 111 | 0x6d: 'm', 112 | 0x6e: 'n', 113 | 0x6f: 'o', 114 | 0x70: 'p', 115 | 0x71: 'q', 116 | 0x72: 'r', 117 | 0x73: 's', 118 | 0x74: 't', 119 | 0x75: 'u', 120 | 0x76: 'v', 121 | 0x77: 'w', 122 | 0x78: 'x', 123 | 0x79: 'y', 124 | 0x7a: 'z', 125 | 0x7b: '\u0cd6', 126 | 0x7c: '\u0ce0', 127 | 0x7d: '\u0ce1', 128 | 0x7e: '\u0ce2', 129 | 0x7f: '\u0ce3', 130 | } 131 | kannadaExtDecoder = Decoder{ 132 | 0x00: '@', 133 | 0x01: '£', 134 | 0x02: '$', 135 | 0x03: '¥', 136 | 0x04: '¿', 137 | 0x05: '"', 138 | 0x06: '¤', 139 | 0x07: '%', 140 | 0x08: '&', 141 | 0x09: '\'', 142 | 0x0a: '\f', 143 | 0x0b: '*', 144 | 0x0c: '+', 145 | 0x0d: '\r', 146 | 0x0e: '-', 147 | 0x0f: '/', 148 | 0x10: '<', 149 | 0x11: '=', 150 | 0x12: '>', 151 | 0x13: '¡', 152 | 0x14: '^', 153 | 0x15: '¡', 154 | 0x16: '_', 155 | 0x17: '#', 156 | 0x18: '*', 157 | 0x19: '\u0964', 158 | 0x1a: '\u0965', 159 | 0x1b: 0x1b, 160 | 0x1c: '\u0ce6', 161 | 0x1d: '\u0ce7', 162 | 0x1e: '\u0ce8', 163 | 0x1f: '\u0ce9', 164 | 0x20: '\u0cea', 165 | 0x21: '\u0ceb', 166 | 0x22: '\u0cec', 167 | 0x23: '\u0ced', 168 | 0x24: '\u0cee', 169 | 0x25: '\u0cef', 170 | 0x26: '\u0cde', 171 | 0x27: '\u0cf1', 172 | 0x28: '{', 173 | 0x29: '}', 174 | 0x2a: '\u0cf2', 175 | 0x2f: '\\', 176 | 0x3c: '[', 177 | 0x3d: '~', 178 | 0x3e: ']', 179 | 0x40: '|', 180 | 0x41: 'A', 181 | 0x42: 'B', 182 | 0x43: 'C', 183 | 0x44: 'D', 184 | 0x45: 'E', 185 | 0x46: 'F', 186 | 0x47: 'G', 187 | 0x48: 'H', 188 | 0x49: 'I', 189 | 0x4a: 'J', 190 | 0x4b: 'K', 191 | 0x4c: 'L', 192 | 0x4d: 'M', 193 | 0x4e: 'N', 194 | 0x4f: 'O', 195 | 0x50: 'P', 196 | 0x51: 'Q', 197 | 0x52: 'R', 198 | 0x53: 'S', 199 | 0x54: 'T', 200 | 0x55: 'U', 201 | 0x56: 'V', 202 | 0x57: 'W', 203 | 0x58: 'X', 204 | 0x59: 'Y', 205 | 0x5a: 'Z', 206 | 0x65: '€', 207 | } 208 | kannadaEncoder Encoder 209 | kannadaExtEncoder Encoder 210 | ) 211 | 212 | func generateKannadaEncoder() Encoder { 213 | return generateEncoder(kannadaDecoder) 214 | } 215 | 216 | func generateKannadaExtEncoder() Encoder { 217 | return generateEncoder(kannadaExtDecoder) 218 | } 219 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/malayalam.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | malayalamDecoder = Decoder{ 9 | 0x01: '\u0d02', 10 | 0x02: '\u0d03', 11 | 0x03: '\u0d05', 12 | 0x04: '\u0d06', 13 | 0x05: '\u0d07', 14 | 0x06: '\u0d08', 15 | 0x07: '\u0d09', 16 | 0x08: '\u0d0a', 17 | 0x09: '\u0d0b', 18 | 0x0a: '\n', 19 | 0x0b: '\u0d0c', 20 | 0x0d: '\r', 21 | 0x0e: '\u0d0e', 22 | 0x0f: '\u0d0f', 23 | 0x10: '\u0d10', 24 | 0x12: '\u0d12', 25 | 0x13: '\u0d13', 26 | 0x14: '\u0d14', 27 | 0x15: '\u0d15', 28 | 0x16: '\u0d16', 29 | 0x17: '\u0d17', 30 | 0x18: '\u0d18', 31 | 0x19: '\u0d19', 32 | 0x1a: '\u0d1a', 33 | 0x1b: 0x1b, 34 | 0x1c: '\u0d1b', 35 | 0x1d: '\u0d1c', 36 | 0x1e: '\u0d1d', 37 | 0x1f: '\u0d1e', 38 | 0x20: 0x20, 39 | 0x21: '!', 40 | 0x22: '\u0d1f', 41 | 0x23: '\u0d20', 42 | 0x24: '\u0d21', 43 | 0x25: '\u0d22', 44 | 0x26: '\u0d23', 45 | 0x27: '\u0d24', 46 | 0x28: ')', 47 | 0x29: '(', 48 | 0x2a: '\u0d25', 49 | 0x2b: '\u0d26', 50 | 0x2c: ',', 51 | 0x2d: '\u0d27', 52 | 0x2e: '.', 53 | 0x2f: '\u0d28', 54 | 0x30: '0', 55 | 0x31: '1', 56 | 0x32: '2', 57 | 0x33: '3', 58 | 0x34: '4', 59 | 0x35: '5', 60 | 0x36: '6', 61 | 0x37: '7', 62 | 0x38: '8', 63 | 0x39: '9', 64 | 0x3a: ':', 65 | 0x3b: ';', 66 | 0x3d: '\u0d2a', 67 | 0x3e: '\u0d2b', 68 | 0x3f: '?', 69 | 0x40: '\u0d2c', 70 | 0x41: '\u0d2d', 71 | 0x42: '\u0d2e', 72 | 0x43: '\u0d2f', 73 | 0x44: '\u0d30', 74 | 0x45: '\u0d31', 75 | 0x46: '\u0d32', 76 | 0x47: '\u0d33', 77 | 0x48: '\u0d34', 78 | 0x49: '\u0d35', 79 | 0x4a: '\u0d36', 80 | 0x4b: '\u0d37', 81 | 0x4c: '\u0d38', 82 | 0x4d: '\u0d39', 83 | 0x4f: '\u0d3d', 84 | 0x50: '\u0d3e', 85 | 0x51: '\u0d3f', 86 | 0x52: '\u0d40', 87 | 0x53: '\u0d41', 88 | 0x54: '\u0d42', 89 | 0x55: '\u0d43', 90 | 0x56: '\u0d44', 91 | 0x58: '\u0d46', 92 | 0x59: '\u0d47', 93 | 0x5a: '\u0d48', 94 | 0x5c: '\u0d4a', 95 | 0x5d: '\u0d4b', 96 | 0x5e: '\u0d4c', 97 | 0x5f: '\u0d4d', 98 | 0x60: '\u0d57', 99 | 0x61: 'a', 100 | 0x62: 'b', 101 | 0x63: 'c', 102 | 0x64: 'd', 103 | 0x65: 'e', 104 | 0x66: 'f', 105 | 0x67: 'g', 106 | 0x68: 'h', 107 | 0x69: 'i', 108 | 0x6a: 'j', 109 | 0x6b: 'k', 110 | 0x6c: 'l', 111 | 0x6d: 'm', 112 | 0x6e: 'n', 113 | 0x6f: 'o', 114 | 0x70: 'p', 115 | 0x71: 'q', 116 | 0x72: 'r', 117 | 0x73: 's', 118 | 0x74: 't', 119 | 0x75: 'u', 120 | 0x76: 'v', 121 | 0x77: 'w', 122 | 0x78: 'x', 123 | 0x79: 'y', 124 | 0x7a: 'z', 125 | 0x7b: '\u0d60', 126 | 0x7c: '\u0d61', 127 | 0x7d: '\u0d62', 128 | 0x7e: '\u0d63', 129 | 0x7f: '\u0d79', 130 | } 131 | malayalamExtDecoder = Decoder{ 132 | 0x00: '@', 133 | 0x01: '£', 134 | 0x02: '$', 135 | 0x03: '¥', 136 | 0x04: '¿', 137 | 0x05: '"', 138 | 0x06: '¤', 139 | 0x07: '%', 140 | 0x08: '&', 141 | 0x09: '\'', 142 | 0x0a: '\f', 143 | 0x0b: '*', 144 | 0x0c: '+', 145 | 0x0d: '\r', 146 | 0x0e: '-', 147 | 0x0f: '/', 148 | 0x10: '<', 149 | 0x11: '=', 150 | 0x12: '>', 151 | 0x13: '¡', 152 | 0x14: '^', 153 | 0x15: '¡', 154 | 0x16: '_', 155 | 0x17: '#', 156 | 0x18: '*', 157 | 0x19: '\u0964', 158 | 0x1a: '\u0965', 159 | 0x1b: 0x1b, 160 | 0x1c: '\u0d66', 161 | 0x1d: '\u0d67', 162 | 0x1e: '\u0d68', 163 | 0x1f: '\u0d69', 164 | 0x20: '\u0d6a', 165 | 0x21: '\u0d6b', 166 | 0x22: '\u0d6c', 167 | 0x23: '\u0d6d', 168 | 0x24: '\u0d6e', 169 | 0x25: '\u0d6f', 170 | 0x26: '\u0d70', 171 | 0x27: '\u0d71', 172 | 0x28: '{', 173 | 0x29: '}', 174 | 0x2a: '\u0d72', 175 | 0x2b: '\u0d73', 176 | 0x2c: '\u0d74', 177 | 0x2d: '\u0d75', 178 | 0x2e: '\u0d7a', 179 | 0x2f: '\\', 180 | 0x30: '\u0d7b', 181 | 0x31: '\u0d7c', 182 | 0x32: '\u0d7d', 183 | 0x33: '\u0d7e', 184 | 0x34: '\u0d7f', 185 | 0x3c: '[', 186 | 0x3d: '~', 187 | 0x3e: ']', 188 | 0x40: '|', 189 | 0x41: 'A', 190 | 0x42: 'B', 191 | 0x43: 'C', 192 | 0x44: 'D', 193 | 0x45: 'E', 194 | 0x46: 'F', 195 | 0x47: 'G', 196 | 0x48: 'H', 197 | 0x49: 'I', 198 | 0x4a: 'J', 199 | 0x4b: 'K', 200 | 0x4c: 'L', 201 | 0x4d: 'M', 202 | 0x4e: 'N', 203 | 0x4f: 'O', 204 | 0x50: 'P', 205 | 0x51: 'Q', 206 | 0x52: 'R', 207 | 0x53: 'S', 208 | 0x54: 'T', 209 | 0x55: 'U', 210 | 0x56: 'V', 211 | 0x57: 'W', 212 | 0x58: 'X', 213 | 0x59: 'Y', 214 | 0x5a: 'Z', 215 | 0x65: '€', 216 | } 217 | malayalamEncoder Encoder 218 | malayalamExtEncoder Encoder 219 | ) 220 | 221 | func generateMalayalamEncoder() Encoder { 222 | return generateEncoder(malayalamDecoder) 223 | } 224 | 225 | func generateMalayalamExtEncoder() Encoder { 226 | return generateEncoder(malayalamExtDecoder) 227 | } 228 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/oriya.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | oriyaDecoder = Decoder{ 9 | 0x00: '\u0b01', 10 | 0x01: '\u0b02', 11 | 0x02: '\u0b03', 12 | 0x03: '\u0b05', 13 | 0x04: '\u0b06', 14 | 0x05: '\u0b07', 15 | 0x06: '\u0b08', 16 | 0x07: '\u0b09', 17 | 0x08: '\u0b0a', 18 | 0x09: '\u0b0b', 19 | 0x0a: '\n', 20 | 0x0b: '\u0b0c', 21 | 0x0d: '\r', 22 | 0x0f: '\u0b0f', 23 | 0x10: '\u0b10', 24 | 0x13: '\u0b13', 25 | 0x14: '\u0b14', 26 | 0x15: '\u0b15', 27 | 0x16: '\u0b16', 28 | 0x17: '\u0b17', 29 | 0x18: '\u0b18', 30 | 0x19: '\u0b19', 31 | 0x1a: '\u0b1a', 32 | 0x1b: 0x1b, 33 | 0x1c: '\u0b1b', 34 | 0x1d: '\u0b1c', 35 | 0x1e: '\u0b1d', 36 | 0x1f: '\u0b1e', 37 | 0x20: 0x20, 38 | 0x21: '!', 39 | 0x22: '\u0b1f', 40 | 0x23: '\u0b20', 41 | 0x24: '\u0b21', 42 | 0x25: '\u0b22', 43 | 0x26: '\u0b23', 44 | 0x27: '\u0b24', 45 | 0x28: ')', 46 | 0x29: '(', 47 | 0x2a: '\u0b25', 48 | 0x2b: '\u0b26', 49 | 0x2c: ',', 50 | 0x2d: '\u0b27', 51 | 0x2e: '.', 52 | 0x2f: '\u0b28', 53 | 0x30: '0', 54 | 0x31: '1', 55 | 0x32: '2', 56 | 0x33: '3', 57 | 0x34: '4', 58 | 0x35: '5', 59 | 0x36: '6', 60 | 0x37: '7', 61 | 0x38: '8', 62 | 0x39: '9', 63 | 0x3a: ':', 64 | 0x3b: ';', 65 | 0x3d: '\u0b2a', 66 | 0x3e: '\u0b2b', 67 | 0x3f: '?', 68 | 0x40: '\u0b2c', 69 | 0x41: '\u0b2d', 70 | 0x42: '\u0b2e', 71 | 0x43: '\u0b2f', 72 | 0x44: '\u0b30', 73 | 0x46: '\u0b32', 74 | 0x47: '\u0b33', 75 | 0x49: '\u0b35', 76 | 0x4a: '\u0b36', 77 | 0x4b: '\u0b37', 78 | 0x4c: '\u0b38', 79 | 0x4d: '\u0b39', 80 | 0x4e: '\u0b3c', 81 | 0x4f: '\u0b3d', 82 | 0x50: '\u0b3e', 83 | 0x51: '\u0b3f', 84 | 0x52: '\u0b40', 85 | 0x53: '\u0b41', 86 | 0x54: '\u0b42', 87 | 0x55: '\u0b43', 88 | 0x56: '\u0b44', 89 | 0x59: '\u0b47', 90 | 0x5a: '\u0b48', 91 | 0x5d: '\u0b4b', 92 | 0x5e: '\u0b4c', 93 | 0x5f: '\u0b4d', 94 | 0x60: '\u0b56', 95 | 0x61: 'a', 96 | 0x62: 'b', 97 | 0x63: 'c', 98 | 0x64: 'd', 99 | 0x65: 'e', 100 | 0x66: 'f', 101 | 0x67: 'g', 102 | 0x68: 'h', 103 | 0x69: 'i', 104 | 0x6a: 'j', 105 | 0x6b: 'k', 106 | 0x6c: 'l', 107 | 0x6d: 'm', 108 | 0x6e: 'n', 109 | 0x6f: 'o', 110 | 0x70: 'p', 111 | 0x71: 'q', 112 | 0x72: 'r', 113 | 0x73: 's', 114 | 0x74: 't', 115 | 0x75: 'u', 116 | 0x76: 'v', 117 | 0x77: 'w', 118 | 0x78: 'x', 119 | 0x79: 'y', 120 | 0x7a: 'z', 121 | 0x7b: '\u0b57', 122 | 0x7c: '\u0b60', 123 | 0x7d: '\u0b61', 124 | 0x7e: '\u0b62', 125 | 0x7f: '\u0b63', 126 | } 127 | oriyaExtDecoder = Decoder{ 128 | 0x00: '@', 129 | 0x01: '£', 130 | 0x02: '$', 131 | 0x03: '¥', 132 | 0x04: '¿', 133 | 0x05: '"', 134 | 0x06: '¤', 135 | 0x07: '%', 136 | 0x08: '&', 137 | 0x09: '\'', 138 | 0x0a: '\f', 139 | 0x0b: '*', 140 | 0x0c: '+', 141 | 0x0d: '\r', 142 | 0x0e: '-', 143 | 0x0f: '/', 144 | 0x10: '<', 145 | 0x11: '=', 146 | 0x12: '>', 147 | 0x13: '¡', 148 | 0x14: '^', 149 | 0x15: '¡', 150 | 0x16: '_', 151 | 0x17: '#', 152 | 0x18: '*', 153 | 0x19: '\u0964', 154 | 0x1a: '\u0965', 155 | 0x1b: 0x1b, 156 | 0x1c: '\u0b66', 157 | 0x1d: '\u0b67', 158 | 0x1e: '\u0b68', 159 | 0x1f: '\u0b69', 160 | 0x20: '\u0b6a', 161 | 0x21: '\u0b6b', 162 | 0x22: '\u0b6c', 163 | 0x23: '\u0b6d', 164 | 0x24: '\u0b6e', 165 | 0x25: '\u0b6f', 166 | 0x26: '\u0b5c', 167 | 0x27: '\u0b5d', 168 | 0x28: '{', 169 | 0x29: '}', 170 | 0x2a: '\u0b5f', 171 | 0x2b: '\u0b70', 172 | 0x2c: '\u0b71', 173 | 0x2f: '\\', 174 | 0x3c: '[', 175 | 0x3d: '~', 176 | 0x3e: ']', 177 | 0x40: '|', 178 | 0x41: 'A', 179 | 0x42: 'B', 180 | 0x43: 'C', 181 | 0x44: 'D', 182 | 0x45: 'E', 183 | 0x46: 'F', 184 | 0x47: 'G', 185 | 0x48: 'H', 186 | 0x49: 'I', 187 | 0x4a: 'J', 188 | 0x4b: 'K', 189 | 0x4c: 'L', 190 | 0x4d: 'M', 191 | 0x4e: 'N', 192 | 0x4f: 'O', 193 | 0x50: 'P', 194 | 0x51: 'Q', 195 | 0x52: 'R', 196 | 0x53: 'S', 197 | 0x54: 'T', 198 | 0x55: 'U', 199 | 0x56: 'V', 200 | 0x57: 'W', 201 | 0x58: 'X', 202 | 0x59: 'Y', 203 | 0x5a: 'Z', 204 | 0x65: '€', 205 | } 206 | oriyaEncoder Encoder 207 | oriyaExtEncoder Encoder 208 | ) 209 | 210 | func generateOriyaEncoder() Encoder { 211 | return generateEncoder(oriyaDecoder) 212 | } 213 | 214 | func generateOriyaExtEncoder() Encoder { 215 | return generateEncoder(oriyaExtDecoder) 216 | } 217 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/portuguese.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | portugueseDecoder Decoder 9 | portugueseExtDecoder = Decoder{ 10 | 0x05: 'ê', 11 | 0x09: 'ç', 12 | 0x0a: '\f', 13 | 0x0b: 'Ô', 14 | 0x0c: 'ô', 15 | 0x0d: '\n', 16 | 0x0e: 'Á', 17 | 0x0f: 'á', 18 | 0x12: 'Φ', 19 | 0x13: 'Γ', 20 | 0x14: '^', 21 | 0x15: 'Ω', 22 | 0x16: 'Π', 23 | 0x17: 'Ψ', 24 | 0x18: 'Σ', 25 | 0x19: 'Θ', 26 | 0x1f: 'Ê', 27 | 0x28: '{', 28 | 0x29: '}', 29 | 0x2f: '\\', 30 | 0x3c: '[', 31 | 0x3d: '~', 32 | 0x3e: ']', 33 | 0x40: '|', 34 | 0x41: 'À', 35 | 0x49: 'Í', 36 | 0x4f: 'Ó', 37 | 0x55: 'Ú', 38 | 0x5b: 'Ã', 39 | 0x5c: 'Õ', 40 | 0x61: 'Â', 41 | 0x65: '€', 42 | 0x69: 'í', 43 | 0x6f: 'ó', 44 | 0x75: 'ú', 45 | 0x7b: 'ã', 46 | 0x7c: 'õ', 47 | 0x7f: 'â', 48 | } 49 | portugueseEncoder Encoder 50 | portugueseExtEncoder Encoder 51 | portugueseRunes = []rune( 52 | "@£$¥êéúíóç\nÔô\rÁáΔ_ªÇÀ∞^\\€Ó|\x1bÂâÊÉ !\"#º%&'()*+,-./0123456789:;<=>?" + 53 | "ÍABCDEFGHIJKLMNOPQRSTUVWXYZÃÕÚܧ~abcdefghijklmnopqrstuvwxyzãõ`üà") 54 | ) 55 | 56 | func generatePortugueseEncoder() Encoder { 57 | return generateEncoderFromRunes(portugueseRunes) 58 | } 59 | 60 | func generatePortugueseDecoder() Decoder { 61 | return generateDecoderFromRunes(portugueseRunes) 62 | } 63 | 64 | func generatePortugueseExtEncoder() Encoder { 65 | return generateEncoder(portugueseExtDecoder) 66 | } 67 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/punjabi.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | punjabiDecoder = Decoder{ 9 | 0x00: '\u0a01', 10 | 0x01: '\u0a02', 11 | 0x02: '\u0a03', 12 | 0x03: '\u0a05', 13 | 0x04: '\u0a06', 14 | 0x05: '\u0a07', 15 | 0x06: '\u0a08', 16 | 0x07: '\u0a09', 17 | 0x08: '\u0a0a', 18 | 0x0a: '\n', 19 | 0x0d: '\r', 20 | 0x0f: '\u0a0f', 21 | 0x10: '\u0a10', 22 | 0x13: '\u0a13', 23 | 0x14: '\u0a14', 24 | 0x15: '\u0a15', 25 | 0x16: '\u0a16', 26 | 0x17: '\u0a17', 27 | 0x18: '\u0a18', 28 | 0x19: '\u0a19', 29 | 0x1a: '\u0a1a', 30 | 0x1b: 0x1b, 31 | 0x1c: '\u0a1b', 32 | 0x1d: '\u0a1c', 33 | 0x1e: '\u0a1d', 34 | 0x1f: '\u0a1e', 35 | 0x20: 0x20, 36 | 0x21: '!', 37 | 0x22: '\u0a1f', 38 | 0x23: '\u0a20', 39 | 0x24: '\u0a21', 40 | 0x25: '\u0a22', 41 | 0x26: '\u0a23', 42 | 0x27: '\u0a24', 43 | 0x28: ')', 44 | 0x29: '(', 45 | 0x2a: '\u0a25', 46 | 0x2b: '\u0a26', 47 | 0x2c: ',', 48 | 0x2d: '\u0a27', 49 | 0x2e: '.', 50 | 0x2f: '\u0a28', 51 | 0x30: '0', 52 | 0x31: '1', 53 | 0x32: '2', 54 | 0x33: '3', 55 | 0x34: '4', 56 | 0x35: '5', 57 | 0x36: '6', 58 | 0x37: '7', 59 | 0x38: '8', 60 | 0x39: '9', 61 | 0x3a: ':', 62 | 0x3b: ';', 63 | 0x3d: '\u0a2a', 64 | 0x3e: '\u0a2b', 65 | 0x3f: '?', 66 | 0x40: '\u0a2c', 67 | 0x41: '\u0a2d', 68 | 0x42: '\u0a2e', 69 | 0x43: '\u0a2f', 70 | 0x44: '\u0a30', 71 | 0x46: '\u0a32', 72 | 0x47: '\u0a33', 73 | 0x49: '\u0a35', 74 | 0x4a: '\u0a36', 75 | 0x4c: '\u0a38', 76 | 0x4d: '\u0a39', 77 | 0x4e: '\u0a3c', 78 | 0x50: '\u0a3e', 79 | 0x51: '\u0a3f', 80 | 0x52: '\u0a40', 81 | 0x53: '\u0a41', 82 | 0x54: '\u0a42', 83 | 0x59: '\u0a47', 84 | 0x5a: '\u0a48', 85 | 0x5d: '\u0a4b', 86 | 0x5e: '\u0a4c', 87 | 0x5f: '\u0a4d', 88 | 0x60: '\u0a51', 89 | 0x61: 'a', 90 | 0x62: 'b', 91 | 0x63: 'c', 92 | 0x64: 'd', 93 | 0x65: 'e', 94 | 0x66: 'f', 95 | 0x67: 'g', 96 | 0x68: 'h', 97 | 0x69: 'i', 98 | 0x6a: 'j', 99 | 0x6b: 'k', 100 | 0x6c: 'l', 101 | 0x6d: 'm', 102 | 0x6e: 'n', 103 | 0x6f: 'o', 104 | 0x70: 'p', 105 | 0x71: 'q', 106 | 0x72: 'r', 107 | 0x73: 's', 108 | 0x74: 't', 109 | 0x75: 'u', 110 | 0x76: 'v', 111 | 0x77: 'w', 112 | 0x78: 'x', 113 | 0x79: 'y', 114 | 0x7a: 'z', 115 | 0x7b: '\u0a70', 116 | 0x7c: '\u0a71', 117 | 0x7d: '\u0a72', 118 | 0x7e: '\u0a73', 119 | 0x7f: '\u0a74', 120 | } 121 | punjabiExtDecoder = Decoder{ 122 | 0x00: '@', 123 | 0x01: '£', 124 | 0x02: '$', 125 | 0x03: '¥', 126 | 0x04: '¿', 127 | 0x05: '"', 128 | 0x06: '¤', 129 | 0x07: '%', 130 | 0x08: '&', 131 | 0x09: '\'', 132 | 0x0a: '\f', 133 | 0x0b: '*', 134 | 0x0c: '+', 135 | 0x0d: '\r', 136 | 0x0e: '-', 137 | 0x0f: '/', 138 | 0x10: '<', 139 | 0x11: '=', 140 | 0x12: '>', 141 | 0x13: '¡', 142 | 0x14: '^', 143 | 0x15: '¡', 144 | 0x16: '_', 145 | 0x17: '#', 146 | 0x18: '*', 147 | 0x19: '\u0964', 148 | 0x1a: '\u0965', 149 | 0x1b: 0x1b, 150 | 0x1c: '\u0a66', 151 | 0x1d: '\u0a67', 152 | 0x1e: '\u0a68', 153 | 0x1f: '\u0a69', 154 | 0x20: '\u0a6a', 155 | 0x21: '\u0a6b', 156 | 0x22: '\u0a6c', 157 | 0x23: '\u0a6d', 158 | 0x24: '\u0a6e', 159 | 0x25: '\u0a6f', 160 | 0x26: '\u0a59', 161 | 0x27: '\u0a5a', 162 | 0x28: '{', 163 | 0x29: '}', 164 | 0x2a: '\u0a5b', 165 | 0x2b: '\u0a5c', 166 | 0x2c: '\u0a5e', 167 | 0x2d: '\u0a75', 168 | 0x2f: '\\', 169 | 0x3c: '[', 170 | 0x3d: '~', 171 | 0x3e: ']', 172 | 0x40: '|', 173 | 0x41: 'A', 174 | 0x42: 'B', 175 | 0x43: 'C', 176 | 0x44: 'D', 177 | 0x45: 'E', 178 | 0x46: 'F', 179 | 0x47: 'G', 180 | 0x48: 'H', 181 | 0x49: 'I', 182 | 0x4a: 'J', 183 | 0x4b: 'K', 184 | 0x4c: 'L', 185 | 0x4d: 'M', 186 | 0x4e: 'N', 187 | 0x4f: 'O', 188 | 0x50: 'P', 189 | 0x51: 'Q', 190 | 0x52: 'R', 191 | 0x53: 'S', 192 | 0x54: 'T', 193 | 0x55: 'U', 194 | 0x56: 'V', 195 | 0x57: 'W', 196 | 0x58: 'X', 197 | 0x59: 'Y', 198 | 0x5a: 'Z', 199 | 0x65: '€', 200 | } 201 | punjabiEncoder Encoder 202 | punjabiExtEncoder Encoder 203 | ) 204 | 205 | func generatePunjabiEncoder() Encoder { 206 | return generateEncoder(punjabiDecoder) 207 | } 208 | 209 | func generatePunjabiExtEncoder() Encoder { 210 | return generateEncoder(punjabiExtDecoder) 211 | } 212 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/spanish.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | spanishExtDecoder = Decoder{ 9 | 0x09: 'ç', 10 | 0x0a: '\f', 11 | 0x0d: '\n', 12 | 0x14: '^', 13 | 0x28: '{', 14 | 0x29: '}', 15 | 0x2f: '\\', 16 | 0x3c: '[', 17 | 0x3d: '~', 18 | 0x3e: ']', 19 | 0x40: '|', 20 | 0x41: 'Á', 21 | 0x49: 'Í', 22 | 0x4f: 'Ó', 23 | 0x55: 'Ú', 24 | 0x61: 'á', 25 | 0x65: '€', 26 | 0x69: 'í', 27 | 0x6f: 'ó', 28 | 0x75: 'ú', 29 | } 30 | spanishExtEncoder Encoder 31 | ) 32 | 33 | func generateSpanishExtEncoder() Encoder { 34 | return generateEncoder(spanishExtDecoder) 35 | } 36 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/tamil.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | tamilDecoder = Decoder{ 9 | 0x01: '\u0b82', 10 | 0x02: '\u0b83', 11 | 0x03: '\u0b85', 12 | 0x04: '\u0b86', 13 | 0x05: '\u0b87', 14 | 0x06: '\u0b88', 15 | 0x07: '\u0b89', 16 | 0x08: '\u0b8a', 17 | 0x0a: '\n', 18 | 0x0d: '\r', 19 | 0x0e: '\u0b8e', 20 | 0x0f: '\u0b8f', 21 | 0x10: '\u0b90', 22 | 0x12: '\u0b92', 23 | 0x13: '\u0b93', 24 | 0x14: '\u0b94', 25 | 0x15: '\u0b95', 26 | 0x19: '\u0b99', 27 | 0x1a: '\u0b9a', 28 | 0x1b: 0x1b, 29 | 0x1d: '\u0b9c', 30 | 0x1f: '\u0b9e', 31 | 0x20: 0x20, 32 | 0x21: '!', 33 | 0x22: '\u0b9f', 34 | 0x26: '\u0ba3', 35 | 0x27: '\u0ba4', 36 | 0x28: ')', 37 | 0x29: '(', 38 | 0x2c: ',', 39 | 0x2e: '.', 40 | 0x2f: '\u0ba8', 41 | 0x30: '0', 42 | 0x31: '1', 43 | 0x32: '2', 44 | 0x33: '3', 45 | 0x34: '4', 46 | 0x35: '5', 47 | 0x36: '6', 48 | 0x37: '7', 49 | 0x38: '8', 50 | 0x39: '9', 51 | 0x3a: ':', 52 | 0x3b: ';', 53 | 0x3c: '\u0ba9', 54 | 0x3d: '\u0baa', 55 | 0x3f: '?', 56 | 0x42: '\u0bae', 57 | 0x43: '\u0baf', 58 | 0x44: '\u0bb0', 59 | 0x45: '\u0bb1', 60 | 0x46: '\u0bb2', 61 | 0x47: '\u0bb3', 62 | 0x48: '\u0bb4', 63 | 0x49: '\u0bb5', 64 | 0x4a: '\u0bb6', 65 | 0x4b: '\u0bb7', 66 | 0x4c: '\u0bb8', 67 | 0x4d: '\u0bb9', 68 | 0x50: '\u0bbe', 69 | 0x51: '\u0bbf', 70 | 0x52: '\u0bc0', 71 | 0x53: '\u0bc1', 72 | 0x54: '\u0bc2', 73 | 0x58: '\u0bc6', 74 | 0x59: '\u0bc7', 75 | 0x5a: '\u0bc8', 76 | 0x5c: '\u0bca', 77 | 0x5d: '\u0bcb', 78 | 0x5e: '\u0bcc', 79 | 0x5f: '\u0bcd', 80 | 0x60: '\u0bd0', 81 | 0x61: 'a', 82 | 0x62: 'b', 83 | 0x63: 'c', 84 | 0x64: 'd', 85 | 0x65: 'e', 86 | 0x66: 'f', 87 | 0x67: 'g', 88 | 0x68: 'h', 89 | 0x69: 'i', 90 | 0x6a: 'j', 91 | 0x6b: 'k', 92 | 0x6c: 'l', 93 | 0x6d: 'm', 94 | 0x6e: 'n', 95 | 0x6f: 'o', 96 | 0x70: 'p', 97 | 0x71: 'q', 98 | 0x72: 'r', 99 | 0x73: 's', 100 | 0x74: 't', 101 | 0x75: 'u', 102 | 0x76: 'v', 103 | 0x77: 'w', 104 | 0x78: 'x', 105 | 0x79: 'y', 106 | 0x7a: 'z', 107 | 0x7b: '\u0bd7', 108 | 0x7c: '\u0bf0', 109 | 0x7d: '\u0bf1', 110 | 0x7e: '\u0bf2', 111 | 0x7f: '\u0bf9', 112 | } 113 | tamilExtDecoder = Decoder{ 114 | 0x00: '@', 115 | 0x01: '£', 116 | 0x02: '$', 117 | 0x03: '¥', 118 | 0x04: '¿', 119 | 0x05: '"', 120 | 0x06: '¤', 121 | 0x07: '%', 122 | 0x08: '&', 123 | 0x09: '\'', 124 | 0x0a: '\f', 125 | 0x0b: '*', 126 | 0x0c: '+', 127 | 0x0d: '\r', 128 | 0x0e: '-', 129 | 0x0f: '/', 130 | 0x10: '<', 131 | 0x11: '=', 132 | 0x12: '>', 133 | 0x13: '¡', 134 | 0x14: '^', 135 | 0x15: '¡', 136 | 0x16: '_', 137 | 0x17: '#', 138 | 0x18: '*', 139 | 0x19: '\u0964', 140 | 0x1a: '\u0965', 141 | 0x1b: 0x1b, 142 | 0x1c: '\u0be6', 143 | 0x1d: '\u0be7', 144 | 0x1e: '\u0be8', 145 | 0x1f: '\u0be9', 146 | 0x20: '\u0bea', 147 | 0x21: '\u0beb', 148 | 0x22: '\u0bec', 149 | 0x23: '\u0bed', 150 | 0x24: '\u0bee', 151 | 0x25: '\u0bef', 152 | 0x26: '\u0bf3', 153 | 0x27: '\u0bf4', 154 | 0x28: '{', 155 | 0x29: '}', 156 | 0x2a: '\u0bf5', 157 | 0x2b: '\u0bf6', 158 | 0x2c: '\u0bf7', 159 | 0x2d: '\u0bf8', 160 | 0x2e: '\u0bfa', 161 | 0x2f: '\\', 162 | 0x3c: '[', 163 | 0x3d: '~', 164 | 0x3e: ']', 165 | 0x40: '|', 166 | 0x41: 'A', 167 | 0x42: 'B', 168 | 0x43: 'C', 169 | 0x44: 'D', 170 | 0x45: 'E', 171 | 0x46: 'F', 172 | 0x47: 'G', 173 | 0x48: 'H', 174 | 0x49: 'I', 175 | 0x4a: 'J', 176 | 0x4b: 'K', 177 | 0x4c: 'L', 178 | 0x4d: 'M', 179 | 0x4e: 'N', 180 | 0x4f: 'O', 181 | 0x50: 'P', 182 | 0x51: 'Q', 183 | 0x52: 'R', 184 | 0x53: 'S', 185 | 0x54: 'T', 186 | 0x55: 'U', 187 | 0x56: 'V', 188 | 0x57: 'W', 189 | 0x58: 'X', 190 | 0x59: 'Y', 191 | 0x5a: 'Z', 192 | 0x65: '€', 193 | } 194 | tamilEncoder Encoder 195 | tamilExtEncoder Encoder 196 | ) 197 | 198 | func generateTamilEncoder() Encoder { 199 | return generateEncoder(tamilDecoder) 200 | } 201 | 202 | func generateTamilExtEncoder() Encoder { 203 | return generateEncoder(tamilExtDecoder) 204 | } 205 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/telugu.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | teluguDecoder = Decoder{ 9 | 0x00: '\u0c01', 10 | 0x01: '\u0c02', 11 | 0x02: '\u0c03', 12 | 0x03: '\u0c05', 13 | 0x04: '\u0c06', 14 | 0x05: '\u0c07', 15 | 0x06: '\u0c08', 16 | 0x07: '\u0c09', 17 | 0x08: '\u0c0a', 18 | 0x09: '\u0c0b', 19 | 0x0a: '\n', 20 | 0x0b: '\u0c0c', 21 | 0x0d: '\r', 22 | 0x0e: '\u0c0e', 23 | 0x0f: '\u0c0f', 24 | 0x10: '\u0c10', 25 | 0x12: '\u0c12', 26 | 0x13: '\u0c13', 27 | 0x14: '\u0c14', 28 | 0x15: '\u0c15', 29 | 0x16: '\u0c16', 30 | 0x17: '\u0c17', 31 | 0x18: '\u0c18', 32 | 0x19: '\u0c19', 33 | 0x1a: '\u0c1a', 34 | 0x1b: 0x1b, 35 | 0x1c: '\u0c1b', 36 | 0x1d: '\u0c1c', 37 | 0x1e: '\u0c1d', 38 | 0x1f: '\u0c1e', 39 | 0x20: 0x20, 40 | 0x21: '!', 41 | 0x22: '\u0c1f', 42 | 0x23: '\u0c20', 43 | 0x24: '\u0c21', 44 | 0x25: '\u0c22', 45 | 0x26: '\u0c23', 46 | 0x27: '\u0c24', 47 | 0x28: ')', 48 | 0x29: '(', 49 | 0x2a: '\u0c25', 50 | 0x2b: '\u0c26', 51 | 0x2c: ',', 52 | 0x2d: '\u0c27', 53 | 0x2e: '.', 54 | 0x2f: '\u0c28', 55 | 0x30: '0', 56 | 0x31: '1', 57 | 0x32: '2', 58 | 0x33: '3', 59 | 0x34: '4', 60 | 0x35: '5', 61 | 0x36: '6', 62 | 0x37: '7', 63 | 0x38: '8', 64 | 0x39: '9', 65 | 0x3a: ':', 66 | 0x3b: ';', 67 | 0x3d: '\u0c2a', 68 | 0x3e: '\u0c2b', 69 | 0x3f: '?', 70 | 0x40: '\u0c2c', 71 | 0x41: '\u0c2d', 72 | 0x42: '\u0c2e', 73 | 0x43: '\u0c2f', 74 | 0x44: '\u0c30', 75 | 0x45: '\u0c31', 76 | 0x46: '\u0c32', 77 | 0x47: '\u0c33', 78 | 0x49: '\u0c35', 79 | 0x4a: '\u0c36', 80 | 0x4b: '\u0c37', 81 | 0x4c: '\u0c38', 82 | 0x4d: '\u0c39', 83 | 0x4f: '\u0c3d', 84 | 0x50: '\u0c3e', 85 | 0x51: '\u0c3f', 86 | 0x52: '\u0c40', 87 | 0x53: '\u0c41', 88 | 0x54: '\u0c42', 89 | 0x55: '\u0c43', 90 | 0x56: '\u0c44', 91 | 0x58: '\u0c46', 92 | 0x59: '\u0c47', 93 | 0x5a: '\u0c48', 94 | 0x5c: '\u0c4a', 95 | 0x5d: '\u0c4b', 96 | 0x5e: '\u0c4c', 97 | 0x5f: '\u0c4d', 98 | 0x60: '\u0c55', 99 | 0x61: 'a', 100 | 0x62: 'b', 101 | 0x63: 'c', 102 | 0x64: 'd', 103 | 0x65: 'e', 104 | 0x66: 'f', 105 | 0x67: 'g', 106 | 0x68: 'h', 107 | 0x69: 'i', 108 | 0x6a: 'j', 109 | 0x6b: 'k', 110 | 0x6c: 'l', 111 | 0x6d: 'm', 112 | 0x6e: 'n', 113 | 0x6f: 'o', 114 | 0x70: 'p', 115 | 0x71: 'q', 116 | 0x72: 'r', 117 | 0x73: 's', 118 | 0x74: 't', 119 | 0x75: 'u', 120 | 0x76: 'v', 121 | 0x77: 'w', 122 | 0x78: 'x', 123 | 0x79: 'y', 124 | 0x7a: 'z', 125 | 0x7b: '\u0c56', 126 | 0x7c: '\u0c60', 127 | 0x7d: '\u0c61', 128 | 0x7e: '\u0c62', 129 | 0x7f: '\u0c63', 130 | } 131 | teluguExtDecoder = Decoder{ 132 | 0x00: '@', 133 | 0x01: '£', 134 | 0x02: '$', 135 | 0x03: '¥', 136 | 0x04: '¿', 137 | 0x05: '"', 138 | 0x06: '¤', 139 | 0x07: '%', 140 | 0x08: '&', 141 | 0x09: '\'', 142 | 0x0a: '\f', 143 | 0x0b: '*', 144 | 0x0c: '+', 145 | 0x0d: '\r', 146 | 0x0e: '-', 147 | 0x0f: '/', 148 | 0x10: '<', 149 | 0x11: '=', 150 | 0x12: '>', 151 | 0x13: '¡', 152 | 0x14: '^', 153 | 0x15: '¡', 154 | 0x16: '_', 155 | 0x17: '#', 156 | 0x18: '*', 157 | 0x1b: 0x1b, 158 | 0x1c: '\u0c66', 159 | 0x1d: '\u0c67', 160 | 0x1e: '\u0c68', 161 | 0x1f: '\u0c69', 162 | 0x20: '\u0c6a', 163 | 0x21: '\u0c6b', 164 | 0x22: '\u0c6c', 165 | 0x23: '\u0c6d', 166 | 0x24: '\u0c6e', 167 | 0x25: '\u0c6f', 168 | 0x26: '\u0c58', 169 | 0x27: '\u0c59', 170 | 0x28: '{', 171 | 0x29: '}', 172 | 0x2a: '\u0c78', 173 | 0x2b: '\u0c79', 174 | 0x2c: '\u0c7a', 175 | 0x2d: '\u0c7b', 176 | 0x2e: '\u0c7c', 177 | 0x2f: '\\', 178 | 0x30: '\u0c7d', 179 | 0x31: '\u0c7e', 180 | 0x32: '\u0c7f', 181 | 0x3c: '[', 182 | 0x3d: '~', 183 | 0x3e: ']', 184 | 0x40: '|', 185 | 0x41: 'A', 186 | 0x42: 'B', 187 | 0x43: 'C', 188 | 0x44: 'D', 189 | 0x45: 'E', 190 | 0x46: 'F', 191 | 0x47: 'G', 192 | 0x48: 'H', 193 | 0x49: 'I', 194 | 0x4a: 'J', 195 | 0x4b: 'K', 196 | 0x4c: 'L', 197 | 0x4d: 'M', 198 | 0x4e: 'N', 199 | 0x4f: 'O', 200 | 0x50: 'P', 201 | 0x51: 'Q', 202 | 0x52: 'R', 203 | 0x53: 'S', 204 | 0x54: 'T', 205 | 0x55: 'U', 206 | 0x56: 'V', 207 | 0x57: 'W', 208 | 0x58: 'X', 209 | 0x59: 'Y', 210 | 0x5a: 'Z', 211 | 0x65: '€', 212 | } 213 | teluguEncoder Encoder 214 | teluguExtEncoder Encoder 215 | ) 216 | 217 | func generateTeluguEncoder() Encoder { 218 | return generateEncoder(teluguDecoder) 219 | } 220 | 221 | func generateTeluguExtEncoder() Encoder { 222 | return generateEncoder(teluguExtDecoder) 223 | } 224 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/turkish.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | turkishDecoder Decoder 9 | turkishExtDecoder = Decoder{ 10 | 0x0a: '\f', 11 | 0x0d: '\n', 12 | 0x14: '^', 13 | 0x28: '{', 14 | 0x29: '}', 15 | 0x2f: '\\', 16 | 0x3c: '[', 17 | 0x3d: '~', 18 | 0x3e: ']', 19 | 0x40: '|', 20 | 0x47: 'Ğ', 21 | 0x49: 'İ', 22 | 0x53: 'Ş', 23 | 0x63: 'ç', 24 | 0x65: '€', 25 | 0x67: 'ğ', 26 | 0x69: 'ı', 27 | 0x73: 'ş', 28 | } 29 | turkishEncoder Encoder 30 | turkishExtEncoder Encoder 31 | turkishRunes = []rune( 32 | "@£$¥€éùıòÇ\nĞğ\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bŞşßÉ !\"#¤%&'()*+,-./0123456789:;<=>?" + 33 | "İABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧçabcdefghijklmnopqrstuvwxyzäöñüà") 34 | ) 35 | 36 | func generateTurkishEncoder() Encoder { 37 | return generateEncoderFromRunes(turkishRunes) 38 | } 39 | 40 | func generateTurkishDecoder() Decoder { 41 | return generateDecoderFromRunes(turkishRunes) 42 | } 43 | 44 | func generateTurkishExtEncoder() Encoder { 45 | return generateEncoder(turkishExtDecoder) 46 | } 47 | -------------------------------------------------------------------------------- /encoding/gsm7/charset/urdu.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package charset 6 | 7 | var ( 8 | urduDecoder = Decoder{ 9 | // awkward layout as most editors have problems with these RTL characters. 10 | 0x00: 'ا', 11 | 0x01: 'آ', 12 | 0x02: 'ب', 13 | 0x03: 'ٻ', 14 | 0x04: 'ڀ', 15 | 0x05: 'پ', 16 | 0x06: 'ڦ', 17 | 0x07: 'ت', 18 | 0x08: 'ۂ', 19 | 0x09: 'ٿ', 20 | 0x0a: '\n', 21 | 0x0b: 'ٹ', 22 | 0x0c: 'ٽ', 23 | 0x0d: '\r', 24 | 0x0e: 'ٺ', 25 | 0x0f: 'ټ', 26 | 0x10: 'ث', 27 | 0x11: 'ج', 28 | 0x12: 'ځ', 29 | 0x13: 'ڄ', 30 | 0x14: 'ڃ', 31 | 0x15: 'څ', 32 | 0x16: 'چ', 33 | 0x17: 'ڇ', 34 | 0x18: 'ح', 35 | 0x19: 'خ', 36 | 0x1a: 'د', 37 | 0x1b: 0x1b, 38 | 0x1c: 'ڌ', 39 | 0x1d: 'ڈ', 40 | 0x1e: 'ډ', 41 | 0x1f: 'ڊ', 42 | 0x20: 0x20, 43 | 0x21: '!', 44 | 0x22: 'ڏ', 45 | 0x23: 'ڍ', 46 | 0x24: 'ذ', 47 | 0x25: 'ر', 48 | 0x26: 'ڑ', 49 | 0x27: 'ړ', 50 | 0x28: ')', 51 | 0x29: '(', 52 | 0x2a: 'ڙ', 53 | 0x2b: 'ز', 54 | 0x2c: ',', 55 | 0x2d: 'ږ', 56 | 0x2e: '.', 57 | 0x2f: 'ژ', 58 | 0x30: '0', 59 | 0x31: '1', 60 | 0x32: '2', 61 | 0x33: '3', 62 | 0x34: '4', 63 | 0x35: '5', 64 | 0x36: '6', 65 | 0x37: '7', 66 | 0x38: '8', 67 | 0x39: '9', 68 | 0x3a: ':', 69 | 0x3b: ';', 70 | 0x3c: 'ښ', 71 | 0x3d: 'س', 72 | 0x3e: 'ش', 73 | 0x3f: '?', 74 | 0x40: 'ص', 75 | 0x41: 'ض', 76 | 0x42: 'ط', 77 | 0x43: 'ظ', 78 | 0x44: 'ع', 79 | 0x45: 'ف', 80 | 0x46: 'ق', 81 | 0x47: 'ک', 82 | 0x48: 'ڪ', 83 | 0x49: 'ګ', 84 | 0x4a: 'گ', 85 | 0x4b: 'ڳ', 86 | 0x4c: 'ڱ', 87 | 0x4d: 'ل', 88 | 0x4e: 'م', 89 | 0x4f: 'ن', 90 | 0x50: 'ں', 91 | 0x51: 'ڻ', 92 | 0x52: 'ڼ', 93 | 0x53: 'و', 94 | 0x54: 'ۄ', 95 | 0x55: 'ە', 96 | 0x56: 'ہ', 97 | 0x57: 'ھ', 98 | 0x58: 'ء', 99 | 0x59: 'ی', 100 | 0x5a: 'ې', 101 | 0x5b: 'ے', 102 | 0x5c: '\u064D', 103 | 0x5d: '\u0650', 104 | 0x5e: '\u064f', 105 | 0x5f: '\u0657', 106 | 0x60: '\u0654', 107 | 0x61: 'a', 108 | 0x62: 'b', 109 | 0x63: 'c', 110 | 0x64: 'd', 111 | 0x65: 'e', 112 | 0x66: 'f', 113 | 0x67: 'g', 114 | 0x68: 'h', 115 | 0x69: 'i', 116 | 0x6a: 'j', 117 | 0x6b: 'k', 118 | 0x6c: 'l', 119 | 0x6d: 'm', 120 | 0x6e: 'n', 121 | 0x6f: 'o', 122 | 0x70: 'p', 123 | 0x71: 'q', 124 | 0x72: 'r', 125 | 0x73: 's', 126 | 0x74: 't', 127 | 0x75: 'u', 128 | 0x76: 'v', 129 | 0x77: 'w', 130 | 0x78: 'x', 131 | 0x79: 'y', 132 | 0x7a: 'z', 133 | 0x7b: '\u0655', 134 | 0x7c: '\u0651', 135 | 0x7d: '\u0653', 136 | 0x7e: '\u0656', 137 | 0x7f: '\u0670', 138 | } 139 | urduExtDecoder = Decoder{ 140 | 0x00: '@', 141 | 0x01: '£', 142 | 0x02: '$', 143 | 0x03: '¥', 144 | 0x04: '¿', 145 | 0x05: '"', 146 | 0x06: '¤', 147 | 0x07: '%', 148 | 0x08: '&', 149 | 0x09: '\'', 150 | 0x0a: '\f', 151 | 0x0b: '*', 152 | 0x0c: '+', 153 | 0x0d: '\r', 154 | 0x0e: '-', 155 | 0x0f: '/', 156 | 0x10: '<', 157 | 0x11: '=', 158 | 0x12: '>', 159 | 0x13: '¡', 160 | 0x14: '^', 161 | 0x15: '¡', 162 | 0x16: '_', 163 | 0x17: '#', 164 | 0x18: '*', 165 | 0x19: '؀', 166 | 0x1a: '؁', 167 | 0x1b: 0x1b, 168 | 0x1c: '۰', 169 | 0x1d: '۱', 170 | 0x1e: '۲', 171 | 0x1f: '۳', 172 | 0x20: '۴', 173 | 0x21: '۵', 174 | 0x22: '۶', 175 | 0x23: '۷', 176 | 0x24: '۸', 177 | 0x25: '۹', 178 | 0x26: '،', 179 | 0x27: '؍', 180 | 0x28: '{', 181 | 0x29: '}', 182 | 0x2a: '؎', 183 | 0x2b: '؏', 184 | 0x2c: '\u0610', 185 | 0x2d: '\u0611', 186 | 0x2e: '\u0612', 187 | 0x2f: '\\', 188 | 0x30: '\u0613', 189 | 0x31: '\u0614', 190 | 0x32: '\u061b', 191 | 0x33: '\u061f', 192 | 0x34: '\u0640', 193 | 0x35: '\u0652', 194 | 0x36: '\u0658', 195 | 0x37: '٫', 196 | 0x38: '٬', 197 | 0x39: 'ٲ', 198 | 0x3a: 'ٳ', 199 | 0x3b: 'ۍ', 200 | 0x3c: '[', 201 | 0x3d: '~', 202 | 0x3e: ']', 203 | 0x3f: '۔', 204 | 0x40: '|', 205 | 0x41: 'A', 206 | 0x42: 'B', 207 | 0x43: 'C', 208 | 0x44: 'D', 209 | 0x45: 'E', 210 | 0x46: 'F', 211 | 0x47: 'G', 212 | 0x48: 'H', 213 | 0x49: 'I', 214 | 0x4a: 'J', 215 | 0x4b: 'K', 216 | 0x4c: 'L', 217 | 0x4d: 'M', 218 | 0x4e: 'N', 219 | 0x4f: 'O', 220 | 0x50: 'P', 221 | 0x51: 'Q', 222 | 0x52: 'R', 223 | 0x53: 'S', 224 | 0x54: 'T', 225 | 0x55: 'U', 226 | 0x56: 'V', 227 | 0x57: 'W', 228 | 0x58: 'X', 229 | 0x59: 'Y', 230 | 0x5a: 'Z', 231 | 0x65: '€', 232 | } 233 | urduEncoder Encoder 234 | urduExtEncoder Encoder 235 | ) 236 | 237 | func generateUrduEncoder() Encoder { 238 | return generateEncoder(urduDecoder) 239 | } 240 | 241 | func generateUrduExtEncoder() Encoder { 242 | return generateEncoder(urduExtDecoder) 243 | } 244 | -------------------------------------------------------------------------------- /encoding/gsm7/gsm7.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // Package gsm7 provides conversions to and from 7bit packed user data. 6 | package gsm7 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/warthog618/sms/encoding/gsm7/charset" 12 | ) 13 | 14 | const ( 15 | esc byte = 0x1b 16 | sp byte = 0x20 17 | ) 18 | 19 | // Decoder converts from GSM7 to UTF-8 using a particular character set. 20 | type Decoder struct { 21 | set charset.Decoder 22 | ext charset.Decoder 23 | strict bool 24 | } 25 | 26 | // Encoder converts from UTF-8 to GSM7 using a particular character set. 27 | type Encoder struct { 28 | set charset.Encoder 29 | ext charset.Encoder 30 | } 31 | 32 | // DecoderOption applies an option to a Decoder. 33 | type DecoderOption interface { 34 | applyDecoderOption(*Decoder) 35 | } 36 | 37 | // EncoderOption applies an option to an Encoder. 38 | type EncoderOption interface { 39 | applyEncoderOption(*Encoder) 40 | } 41 | 42 | // NewDecoder returns a new GSM7 decoder which uses the default character set. 43 | func NewDecoder(options ...DecoderOption) Decoder { 44 | d := Decoder{} 45 | for _, option := range options { 46 | option.applyDecoderOption(&d) 47 | } 48 | if d.set == nil { 49 | d.set = charset.DefaultDecoder() 50 | } 51 | if d.ext == nil { 52 | d.ext = charset.DefaultExtDecoder() 53 | } 54 | return d 55 | } 56 | 57 | // NewEncoder returns a new GSM7 encoder which uses the default character set. 58 | func NewEncoder(options ...EncoderOption) Encoder { 59 | e := Encoder{} 60 | for _, option := range options { 61 | option.applyEncoderOption(&e) 62 | } 63 | if e.set == nil { 64 | e.set = charset.DefaultEncoder() 65 | } 66 | if e.ext == nil { 67 | e.ext = charset.DefaultExtEncoder() 68 | } 69 | return e 70 | } 71 | 72 | // Decode converts the src from unpacked GSM7 to UTF-8. 73 | func Decode(src []byte, options ...DecoderOption) ([]byte, error) { 74 | d := NewDecoder(options...) 75 | return d.Decode(src) 76 | } 77 | 78 | // Encode converts the src from UTF-8 to GSM7 and writes the result to dst. 79 | // 80 | // The return value includes the encoded GSM7 bytes, and any error that 81 | // occurred during encoding. 82 | func Encode(src []byte, options ...EncoderOption) ([]byte, error) { 83 | e := NewEncoder(options...) 84 | return e.Encode(src) 85 | } 86 | 87 | // Decode converts the src from unpacked GSM7 to UTF-8. 88 | func (d *Decoder) Decode(src []byte) ([]byte, error) { 89 | if len(src) == 0 { 90 | return nil, nil 91 | } 92 | dst := make([]byte, 0, len(src)) 93 | escaped := false 94 | for _, g := range src { 95 | if escaped { // must be first to deal with double escapes 96 | escaped = false 97 | if g == esc { 98 | dst = append(dst, sp) 99 | continue 100 | } 101 | if m, ok := d.ext[g]; ok { 102 | dst = append(dst, []byte(string(m))...) 103 | continue 104 | } 105 | if d.strict { 106 | return nil, ErrInvalidSeptet(g) 107 | } 108 | } else if g == esc { // then regular escapes 109 | escaped = true 110 | continue 111 | } 112 | if m, ok := d.set[g]; ok { 113 | dst = append(dst, []byte(string(m))...) 114 | continue 115 | } 116 | if d.strict { 117 | return nil, ErrInvalidSeptet(g) 118 | } 119 | dst = append(dst, sp) 120 | } 121 | // handle dangling escape 122 | if escaped { 123 | dst = append(dst, sp) 124 | } 125 | return dst, nil 126 | } 127 | 128 | // CharsetOption specifies the character set to be used for encoding and 129 | // decoding. 130 | type CharsetOption struct { 131 | nli int 132 | } 133 | 134 | func (o CharsetOption) applyDecoderOption(d *Decoder) { 135 | d.set = charset.NewDecoder(o.nli) 136 | } 137 | 138 | func (o CharsetOption) applyEncoderOption(e *Encoder) { 139 | e.set = charset.NewEncoder(o.nli) 140 | } 141 | 142 | // ExtCharsetOption specifies the extension character set to be used for 143 | // encoding and decoding. 144 | type ExtCharsetOption struct { 145 | nli int 146 | } 147 | 148 | func (o ExtCharsetOption) applyDecoderOption(d *Decoder) { 149 | d.ext = charset.NewExtDecoder(o.nli) 150 | } 151 | 152 | func (o ExtCharsetOption) applyEncoderOption(e *Encoder) { 153 | e.ext = charset.NewExtEncoder(o.nli) 154 | } 155 | 156 | // WithCharset specifies the character set map used for encoding or decoding. 157 | func WithCharset(nli int) CharsetOption { 158 | return CharsetOption{nli} 159 | } 160 | 161 | // WithExtCharset replaces the extension character set map used for encoding or 162 | // decoding. 163 | func WithExtCharset(nli int) ExtCharsetOption { 164 | return ExtCharsetOption{nli} 165 | } 166 | 167 | // NullDecoder fails to decode any characters. 168 | type NullDecoder struct{} 169 | 170 | func (o NullDecoder) applyDecoderOption(d *Decoder) { 171 | d.ext = make(charset.Decoder) 172 | } 173 | 174 | // StrictOption specifies that the decoder should return an error rather than 175 | // ignoring undecodable septets. 176 | type StrictOption struct{} 177 | 178 | func (o StrictOption) applyDecoderOption(d *Decoder) { 179 | d.strict = true 180 | } 181 | 182 | var ( 183 | // Strict specifies that the decoder should return an error rather than 184 | // ignoring undecodable septets. 185 | Strict = StrictOption{} 186 | 187 | // WithoutExtCharset specifies that no extension character set will be 188 | // available to decode escaped characters. 189 | WithoutExtCharset = NullDecoder{} 190 | ) 191 | 192 | // WithCharset replaces the character set map used by the Decoder. 193 | func (d Decoder) WithCharset(set charset.Decoder) Decoder { 194 | d.set = set 195 | return d 196 | } 197 | 198 | // WithExtCharset replaces the extension character set map used by the Decoder. 199 | func (d Decoder) WithExtCharset(ext charset.Decoder) Decoder { 200 | d.ext = ext 201 | return d 202 | } 203 | 204 | // Strict makes the Decoder return an error if an unknown character is detected 205 | // when looking up a septet in the character set (not the extension set). 206 | func (d Decoder) Strict() Decoder { 207 | d.strict = true 208 | return d 209 | } 210 | 211 | // Encode converts the src from UTF-8 to GSM7 and writes the result to dst. 212 | // 213 | // The return value includes the encoded GSM7 bytes, and any error that 214 | // occurred during encoding. 215 | func (e *Encoder) Encode(src []byte) ([]byte, error) { 216 | if len(src) == 0 { 217 | return nil, nil 218 | } 219 | dst := make([]byte, 0, len(src)) 220 | for _, u := range string(src) { 221 | g, ok := e.set[u] 222 | if ok { 223 | dst = append(dst, g) 224 | continue 225 | } 226 | g, ok = e.ext[u] 227 | if ok { 228 | dst = append(dst, esc, g) 229 | continue 230 | } 231 | return nil, ErrInvalidUTF8(u) 232 | } 233 | return dst, nil 234 | } 235 | 236 | // WithCharset replaces the character set map used by the Encoder. 237 | func (e Encoder) WithCharset(set charset.Encoder) Encoder { 238 | e.set = set 239 | return e 240 | } 241 | 242 | // WithExtCharset replaces the extension character set map used by the Encoder. 243 | func (e Encoder) WithExtCharset(ext charset.Encoder) Encoder { 244 | e.ext = ext 245 | return e 246 | } 247 | 248 | // ErrInvalidSeptet indicates a septet cannot be decoded. 249 | type ErrInvalidSeptet byte 250 | 251 | func (e ErrInvalidSeptet) Error() string { 252 | return fmt.Sprintf("gsm7: invalid septet 0x%02x", int(e)) 253 | } 254 | 255 | // ErrInvalidUTF8 indicates a rune cannot be converted to GSM7. 256 | type ErrInvalidUTF8 rune 257 | 258 | func (e ErrInvalidUTF8) Error() string { 259 | return fmt.Sprintf("gsm7: invalid utf8 '%c' (%U)", rune(e), int(e)) 260 | } 261 | -------------------------------------------------------------------------------- /encoding/gsm7/gsm7_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package gsm7_test 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/warthog618/sms/encoding/gsm7" 13 | "github.com/warthog618/sms/encoding/gsm7/charset" 14 | ) 15 | 16 | type decoderPattern struct { 17 | name string 18 | in []byte 19 | out []byte 20 | err error 21 | } 22 | 23 | type encoderPattern struct { 24 | name string 25 | in []byte 26 | out []byte 27 | err error 28 | } 29 | 30 | func testDecoder(t *testing.T, d gsm7.Decoder, patterns []decoderPattern) { 31 | for _, p := range patterns { 32 | f := func(t *testing.T) { 33 | out, err := d.Decode(p.in) 34 | assert.Equal(t, p.err, err) 35 | assert.Equal(t, p.out, out) 36 | } 37 | t.Run(p.name, f) 38 | } 39 | } 40 | 41 | func testEncoder(t *testing.T, e gsm7.Encoder, patterns []encoderPattern) { 42 | for _, p := range patterns { 43 | f := func(t *testing.T) { 44 | out, err := e.Encode(p.in) 45 | assert.Equal(t, p.err, err) 46 | assert.Equal(t, p.out, out) 47 | } 48 | t.Run(p.name, f) 49 | } 50 | } 51 | 52 | func TestDecode(t *testing.T) { 53 | d := gsm7.NewDecoder() 54 | p := []decoderPattern{ 55 | {"empty", nil, nil, nil}, 56 | {"base", []byte("message"), []byte("message"), nil}, 57 | {"ext", []byte("\x1b\x28\x1b\x29"), []byte("{}"), nil}, 58 | {"escaped", []byte("mes\x1b\x40sage"), []byte("mes|sage"), nil}, 59 | {"double escaped", []byte("mes\x1b\x1b\x40sage"), []byte("mes ¡sage"), nil}, 60 | {"dangling escape", []byte("message\x1b"), []byte("message "), nil}, 61 | } 62 | testDecoder(t, d, p) 63 | } 64 | 65 | func TestDecoderWithCharset(t *testing.T) { 66 | set := map[byte]rune{'m': 'M', 'e': 'E', 's': 'S', 'a': 'A', 'g': 'G'} 67 | d := gsm7.NewDecoder().WithCharset(set) 68 | p := []decoderPattern{ 69 | {"base", []byte("message"), []byte("MESSAGE"), nil}, 70 | {"ext", []byte("\x1b\x28\x1b\x29"), []byte("{}"), nil}, 71 | {"escaped", []byte("mes\x1b\x40sage"), []byte("MES|SAGE"), nil}, 72 | {"double escaped", []byte("mes\x1b\x1b\x40sage"), []byte("MES SAGE"), nil}, 73 | {"dangling escape", []byte("message\x1b"), []byte("MESSAGE "), nil}, 74 | {"unknown", []byte("mesMsage"), []byte("MES SAGE"), nil}, 75 | } 76 | testDecoder(t, d, p) 77 | } 78 | 79 | func TestDecoderWithExtCharset(t *testing.T) { 80 | ext := map[byte]rune{0x40: 'Q'} 81 | d := gsm7.NewDecoder().WithExtCharset(ext) 82 | p := []decoderPattern{ 83 | {"base", []byte("\x40"), []byte("¡"), nil}, 84 | {"ext", []byte("\x1b\x40"), []byte("Q"), nil}, 85 | } 86 | testDecoder(t, d, p) 87 | } 88 | 89 | func TestDecoderStrict(t *testing.T) { 90 | set := map[byte]rune{'m': 'M', 'e': 'E', 's': 'S', 'a': 'A', 'g': 'G'} 91 | ext := map[byte]rune{'e': 'E', 'x': 'X', 't': 'T'} 92 | d := gsm7.NewDecoder().Strict().WithCharset(set).WithExtCharset(ext) 93 | p := []decoderPattern{ 94 | {"known", []byte("message"), []byte("MESSAGE"), nil}, 95 | {"ext", []byte("\x1be\x1bx\x1bt"), []byte("EXT"), nil}, 96 | {"unknown", []byte("mesMsage"), nil, gsm7.ErrInvalidSeptet('M')}, 97 | {"unknown ext", []byte("mes\x1bmsage"), nil, gsm7.ErrInvalidSeptet('m')}, 98 | } 99 | testDecoder(t, d, p) 100 | } 101 | 102 | func TestEncode(t *testing.T) { 103 | e := gsm7.NewEncoder() 104 | p := []encoderPattern{ 105 | {"empty", nil, nil, nil}, 106 | {"base", []byte("message"), []byte("message"), nil}, 107 | {"ext", []byte("{}"), []byte("\x1b\x28\x1b\x29"), nil}, 108 | {"escaped", []byte("mes|sage"), []byte("mes\x1b\x40sage"), nil}, 109 | {"invalid", []byte("mesŞsage"), nil, gsm7.ErrInvalidUTF8('Ş')}, 110 | } 111 | testEncoder(t, e, p) 112 | } 113 | 114 | func TestEncoderWithCharset(t *testing.T) { 115 | set := map[rune]byte{'Ş': 0x40} 116 | e := gsm7.NewEncoder().WithCharset(set) 117 | p := []encoderPattern{ 118 | {"base", []byte("Ş"), []byte("\x40"), nil}, 119 | {"ext", []byte("|"), []byte("\x1b\x40"), nil}, 120 | } 121 | testEncoder(t, e, p) 122 | } 123 | 124 | func TestEncoderWithExtCharset(t *testing.T) { 125 | ext := map[rune]byte{'Ş': 0x40} 126 | e := gsm7.NewEncoder().WithExtCharset(ext) 127 | p := []encoderPattern{ 128 | {"base", []byte("¡"), []byte("\x40"), nil}, 129 | {"ext", []byte("Ş"), []byte("\x1b\x40"), nil}, 130 | } 131 | testEncoder(t, e, p) 132 | } 133 | 134 | // TestErrInvalidSeptet tests that the errors can be stringified. 135 | // It is fragile, as it compares the strings exactly, but its main purpose is 136 | // to confirm the Error function doesn't recurse, as that is bad. 137 | func TestErrInvalidSeptet(t *testing.T) { 138 | patterns := []byte{0x00, 0xa0, 0x0a, 0x9a, 0xa9, 0xff} 139 | for _, p := range patterns { 140 | f := func(t *testing.T) { 141 | err := gsm7.ErrInvalidSeptet(p) 142 | expected := fmt.Sprintf("gsm7: invalid septet 0x%02x", int(err)) 143 | s := err.Error() 144 | assert.Equal(t, expected, s) 145 | } 146 | t.Run(fmt.Sprintf("%x", p), f) 147 | } 148 | } 149 | 150 | // TestErrInvalidUTF8 tests that the errors can be stringified. 151 | // It is fragile, as it compares the strings exactly, but its main purpose is 152 | // to confirm the Error function doesn't recurse, as that is bad. 153 | func TestErrInvalidUTF8(t *testing.T) { 154 | patterns := []byte{0x00, 0xa0, 0x0a, 0x9a, 0xa9, 0xff} 155 | for _, p := range patterns { 156 | f := func(t *testing.T) { 157 | err := gsm7.ErrInvalidUTF8(p) 158 | expected := fmt.Sprintf("gsm7: invalid utf8 '%c' (%U)", rune(err), int(err)) 159 | s := err.Error() 160 | if s != expected { 161 | t.Errorf("failed to stringify %02x, expected '%s', got '%s'", p, expected, s) 162 | } 163 | } 164 | t.Run(fmt.Sprintf("%x", p), f) 165 | } 166 | } 167 | 168 | func TestWithCharset(t *testing.T) { 169 | out, err := gsm7.Encode([]byte("ıĞ"), gsm7.WithCharset(charset.Turkish)) 170 | assert.Nil(t, err) 171 | assert.Equal(t, []byte{0x07, 0x0b}, out) 172 | out, err = gsm7.Decode(out, gsm7.WithCharset(charset.Turkish)) 173 | assert.Nil(t, err) 174 | assert.Equal(t, []byte("ıĞ"), out) 175 | } 176 | 177 | func TestWithExtCharset(t *testing.T) { 178 | out, err := gsm7.Encode([]byte("ıĞ"), gsm7.WithExtCharset(charset.Turkish)) 179 | assert.Nil(t, err) 180 | assert.Equal(t, []byte{0x1b, 0x69, 0x1b, 0x47}, out) 181 | out, err = gsm7.Decode(out, gsm7.WithExtCharset(charset.Turkish)) 182 | assert.Nil(t, err) 183 | assert.Equal(t, []byte("ıĞ"), out) 184 | } 185 | 186 | func TestWithoutExtCharset(t *testing.T) { 187 | out, err := gsm7.Decode([]byte{0x1b, 0x69, 0x1b, 0x47}, gsm7.WithoutExtCharset) 188 | assert.Nil(t, err) 189 | assert.Equal(t, []byte("iG"), out) 190 | out, err = gsm7.Decode([]byte{0x1b, 0x69, 0x1b, 0x47}, gsm7.WithoutExtCharset, gsm7.Strict) 191 | assert.Equal(t, gsm7.ErrInvalidSeptet(0x69), err) 192 | assert.Nil(t, out) 193 | } 194 | -------------------------------------------------------------------------------- /encoding/pdumode/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // Package pdumode provides functions to encode and decode PDU mode frames 6 | // exchanged with a GSM modem in PDU mode. 7 | package pdumode 8 | -------------------------------------------------------------------------------- /encoding/pdumode/pdu.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package pdumode 6 | 7 | import ( 8 | "encoding/hex" 9 | ) 10 | 11 | // PDU represents the PDU exchanged with the GSM modem. 12 | type PDU struct { 13 | // SMCS Address 14 | SMSC SMSCAddress 15 | 16 | // TPDU in binary form 17 | TPDU []byte 18 | } 19 | 20 | // UnmarshalBinary decodes the binary form of the PDU provided by the modem. 21 | // 22 | // Returns the unmarshalled PDU, or an error if unmarshalling fails. 23 | func UnmarshalBinary(src []byte) (p *PDU, err error) { 24 | pdu := PDU{} 25 | err = pdu.UnmarshalBinary(src) 26 | if err != nil { 27 | return 28 | } 29 | p = &pdu 30 | return 31 | } 32 | 33 | // UnmarshalHexString decodes the hex string provided by the modem. 34 | func UnmarshalHexString(s string) (p *PDU, err error) { 35 | pdu := PDU{} 36 | err = pdu.UnmarshalHexString(s) 37 | if err != nil { 38 | return 39 | } 40 | p = &pdu 41 | return 42 | } 43 | 44 | // UnmarshalBinary decodes the binary form of the PDU provided by the modem. 45 | func (p *PDU) UnmarshalBinary(src []byte) error { 46 | n, err := p.SMSC.UnmarshalBinary(src) 47 | if err != nil { 48 | return err 49 | } 50 | p.TPDU = src[n:] 51 | return nil 52 | } 53 | 54 | // UnmarshalHexString decodes the hex string provided by the modem. 55 | func (p *PDU) UnmarshalHexString(s string) error { 56 | b, err := hex.DecodeString(s) 57 | if err != nil { 58 | return err 59 | } 60 | return p.UnmarshalBinary(b) 61 | } 62 | 63 | // MarshalBinary marshals the PDU into binary form. 64 | func (p *PDU) MarshalBinary() ([]byte, error) { 65 | dst, err := p.SMSC.MarshalBinary() 66 | if err != nil { 67 | return nil, err 68 | } 69 | dst = append(dst, p.TPDU...) 70 | return dst, nil 71 | } 72 | 73 | // MarshalHexString encodes the PDU into the hex string expected by the modem. 74 | func (p *PDU) MarshalHexString() (string, error) { 75 | b, err := p.MarshalBinary() 76 | if err != nil { 77 | return "", err 78 | } 79 | return hex.EncodeToString(b), nil 80 | } 81 | -------------------------------------------------------------------------------- /encoding/pdumode/pdu_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package pdumode_test 6 | 7 | import ( 8 | "encoding/hex" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "github.com/warthog618/sms/encoding/pdumode" 14 | "github.com/warthog618/sms/encoding/semioctet" 15 | "github.com/warthog618/sms/encoding/tpdu" 16 | ) 17 | 18 | type testPattern struct { 19 | name string 20 | pdu string 21 | smsc *pdumode.SMSCAddress 22 | tpdu []byte 23 | err error 24 | } 25 | 26 | func TestUnmarshalBinary(t *testing.T) { 27 | decodePatterns := []testPattern{ 28 | { 29 | "empty", 30 | "", 31 | nil, 32 | nil, 33 | tpdu.NewDecodeError("length", 0, tpdu.ErrUnderflow), 34 | }, 35 | { 36 | "valid", 37 | "0791361907002039010203040506070809", 38 | &pdumode.SMSCAddress{ 39 | tpdu.Address{Addr: "639170000293", TOA: 0x91}, 40 | }, 41 | []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}, 42 | nil, 43 | }, 44 | } 45 | for _, p := range decodePatterns { 46 | f := func(t *testing.T) { 47 | b, err := hex.DecodeString(p.pdu) 48 | if err != nil { 49 | t.Fatalf("error converting in: %v", err) 50 | } 51 | pdu, err := pdumode.UnmarshalBinary(b) 52 | assert.Equal(t, p.err, err) 53 | if err == nil { 54 | require.NotNil(t, pdu) 55 | assert.Equal(t, *p.smsc, pdu.SMSC) 56 | assert.Equal(t, p.tpdu, pdu.TPDU) 57 | } else { 58 | assert.Nil(t, pdu) 59 | } 60 | } 61 | t.Run(p.name, f) 62 | } 63 | } 64 | 65 | func TestUnmarshalHexString(t *testing.T) { 66 | decodePatterns := []testPattern{ 67 | { 68 | "nothex", 69 | "nothex", 70 | nil, 71 | nil, 72 | hex.InvalidByteError('n'), 73 | }, 74 | { 75 | "empty", 76 | "", 77 | nil, 78 | nil, 79 | tpdu.NewDecodeError("length", 0, tpdu.ErrUnderflow), 80 | }, 81 | { 82 | "valid", "0791361907002039010203040506070809", 83 | &pdumode.SMSCAddress{ 84 | tpdu.Address{Addr: "639170000293", TOA: 0x91}, 85 | }, 86 | []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}, 87 | nil, 88 | }, 89 | } 90 | for _, p := range decodePatterns { 91 | f := func(t *testing.T) { 92 | pdu, err := pdumode.UnmarshalHexString(p.pdu) 93 | assert.Equal(t, p.err, err) 94 | if err == nil { 95 | require.NotNil(t, pdu) 96 | assert.Equal(t, *p.smsc, pdu.SMSC) 97 | assert.Equal(t, p.tpdu, pdu.TPDU) 98 | } else { 99 | assert.Nil(t, pdu) 100 | } 101 | } 102 | t.Run(p.name, f) 103 | } 104 | } 105 | 106 | func TestMarshalBinary(t *testing.T) { 107 | patterns := []testPattern{ 108 | { 109 | "empty", 110 | "00", 111 | &pdumode.SMSCAddress{}, 112 | nil, 113 | nil, 114 | }, 115 | { 116 | "valid", "0791361907002039010203040506070809", 117 | &pdumode.SMSCAddress{ 118 | tpdu.Address{Addr: "639170000293", TOA: 0x91}, 119 | }, 120 | []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}, 121 | nil, 122 | }, 123 | { 124 | "invalid addr", "", 125 | &pdumode.SMSCAddress{ 126 | tpdu.Address{Addr: "banana"}, 127 | }, 128 | nil, 129 | tpdu.EncodeError("addr", semioctet.ErrInvalidDigit(0x6e)), 130 | }, 131 | } 132 | for _, p := range patterns { 133 | f := func(t *testing.T) { 134 | pdu := pdumode.PDU{SMSC: *p.smsc, TPDU: p.tpdu} 135 | b, err := pdu.MarshalBinary() 136 | s := hex.EncodeToString(b) 137 | assert.Equal(t, p.pdu, s) 138 | assert.Equal(t, p.err, err) 139 | } 140 | t.Run(p.name, f) 141 | } 142 | } 143 | 144 | func TestMarshalHexString(t *testing.T) { 145 | patterns := []testPattern{ 146 | { 147 | "empty", 148 | "00", 149 | &pdumode.SMSCAddress{}, 150 | nil, 151 | nil, 152 | }, 153 | { 154 | "valid", 155 | "0791361907002039010203040506070809", 156 | &pdumode.SMSCAddress{ 157 | tpdu.Address{Addr: "639170000293", TOA: 0x91}, 158 | }, 159 | []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}, 160 | nil, 161 | }, 162 | { 163 | "invalid addr", 164 | "", 165 | &pdumode.SMSCAddress{ 166 | tpdu.Address{Addr: "banana"}, 167 | }, 168 | nil, 169 | tpdu.EncodeError("addr", semioctet.ErrInvalidDigit(0x6e)), 170 | }, 171 | } 172 | for _, p := range patterns { 173 | f := func(t *testing.T) { 174 | pdu := pdumode.PDU{SMSC: *p.smsc, TPDU: p.tpdu} 175 | b, err := pdu.MarshalHexString() 176 | assert.Equal(t, p.pdu, b) 177 | assert.Equal(t, p.err, err) 178 | } 179 | t.Run(p.name, f) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /encoding/pdumode/smscaddress.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package pdumode 6 | 7 | import ( 8 | "github.com/warthog618/sms/encoding/semioctet" 9 | "github.com/warthog618/sms/encoding/tpdu" 10 | ) 11 | 12 | // SMSCAddress is the address of the SMSC. 13 | // 14 | // The SMCSAddress is similar to a TPDU Address, but the binary form is 15 | // marshalled differently, hence the subtype. 16 | // 17 | // The Type-of-number should typically be TonNational or TonInternational, but 18 | // that is not enforced. 19 | // 20 | // The NumberingPlan should typically be NpISDN, but that is not enforced 21 | // either. 22 | type SMSCAddress struct { 23 | tpdu.Address 24 | } 25 | 26 | // MarshalBinary marshals the SMSC Address into binary. 27 | func (a *SMSCAddress) MarshalBinary() (dst []byte, err error) { 28 | addr, err := semioctet.Encode([]byte(a.Addr)) 29 | if err != nil { 30 | return nil, tpdu.EncodeError("addr", err) 31 | } 32 | if len(addr) == 0 { 33 | return []byte{0}, nil 34 | } 35 | l := len(addr) + 1 // in octets and includes the toa 36 | dst = make([]byte, 2, l+1) 37 | dst[0] = byte(l) 38 | dst[1] = a.TOA 39 | dst = append(dst, addr...) 40 | return dst, nil 41 | } 42 | 43 | // UnmarshalBinary unmarshals an SMSC Address from a TPDU field. 44 | // 45 | // It returns the number of bytes read from the source, and any error detected 46 | // while decoding. 47 | func (a *SMSCAddress) UnmarshalBinary(src []byte) (int, error) { 48 | if len(src) < 1 { 49 | return 0, tpdu.NewDecodeError("length", 0, tpdu.ErrUnderflow) 50 | } 51 | l := int(src[0]) // len is octets including toa 52 | if l == 0 { 53 | return 1, nil 54 | } 55 | if len(src) < 2 { 56 | return 1, tpdu.NewDecodeError("toa", 1, tpdu.ErrUnderflow) 57 | } 58 | toa := src[1] 59 | ri := 2 60 | l-- // encoded length includes toa 61 | if len(src) < ri+l { 62 | return len(src), tpdu.NewDecodeError("addr", ri, tpdu.ErrUnderflow) 63 | } 64 | baddr, n, err := semioctet.Decode(make([]byte, l*2), src[ri:ri+l]) 65 | ri += n 66 | // should never trip - semioctet.Decode can only fail if dst length is odd. 67 | if err != nil { 68 | return ri, tpdu.NewDecodeError("addr", ri-n, err) 69 | } 70 | a.Addr = string(baddr) 71 | a.TOA = toa 72 | return ri, nil 73 | } 74 | -------------------------------------------------------------------------------- /encoding/pdumode/smscaddress_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package pdumode_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/warthog618/sms/encoding/pdumode" 12 | "github.com/warthog618/sms/encoding/semioctet" 13 | "github.com/warthog618/sms/encoding/tpdu" 14 | ) 15 | 16 | func TestSMSCAddressMarshalBinary(t *testing.T) { 17 | patterns := []struct { 18 | name string 19 | in pdumode.SMSCAddress 20 | out []byte 21 | err error 22 | }{ 23 | { 24 | "empty", 25 | pdumode.SMSCAddress{}, 26 | []byte{0}, 27 | nil, 28 | }, 29 | { 30 | "number", 31 | pdumode.SMSCAddress{ 32 | tpdu.Address{Addr: "61409865629", TOA: 0x91}, 33 | }, 34 | []byte{7, 0x91, 0x16, 0x04, 0x89, 0x56, 0x26, 0xf9}, 35 | nil, 36 | }, 37 | { 38 | "number alphabet", 39 | pdumode.SMSCAddress{ 40 | tpdu.Address{Addr: "0123456789*#abc", TOA: 0x91}, 41 | }, 42 | []byte{9, 0x91, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, 43 | nil, 44 | }, 45 | { 46 | "alpha", 47 | pdumode.SMSCAddress{ 48 | tpdu.Address{Addr: "messages", TOA: 0xd1}, 49 | }, 50 | nil, 51 | tpdu.EncodeError("addr", semioctet.ErrInvalidDigit(0x6d)), 52 | }, 53 | { 54 | "invalid number", 55 | pdumode.SMSCAddress{ 56 | tpdu.Address{Addr: "6140f98656", TOA: 0x91}, 57 | }, 58 | nil, 59 | tpdu.EncodeError("addr", semioctet.ErrInvalidDigit('f')), 60 | }, 61 | } 62 | for _, p := range patterns { 63 | f := func(t *testing.T) { 64 | b, err := p.in.MarshalBinary() 65 | assert.Equal(t, p.err, err) 66 | assert.Equal(t, p.out, b) 67 | } 68 | t.Run(p.name, f) 69 | } 70 | } 71 | 72 | func TestSMSCAddressUnmarshalBinary(t *testing.T) { 73 | patterns := []struct { 74 | name string 75 | in []byte 76 | out pdumode.SMSCAddress 77 | n int 78 | err error 79 | }{ 80 | { 81 | "nil", 82 | nil, 83 | pdumode.SMSCAddress{}, 84 | 0, 85 | tpdu.NewDecodeError("length", 0, tpdu.ErrUnderflow), 86 | }, 87 | { 88 | "only toa", 89 | []byte{1, 0}, 90 | pdumode.SMSCAddress{}, 91 | 2, 92 | nil, 93 | }, 94 | { 95 | "number", 96 | []byte{7, 0x91, 0x16, 0x04, 0x89, 0x56, 0x26, 0xf9}, 97 | pdumode.SMSCAddress{ 98 | tpdu.Address{Addr: "61409865629", TOA: 0x91}, 99 | }, 100 | 8, 101 | nil, 102 | }, 103 | { 104 | "number alphabet", 105 | []byte{9, 0x91, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, 106 | pdumode.SMSCAddress{ 107 | tpdu.Address{Addr: "0123456789*#abc", TOA: 0x91}, 108 | }, 109 | 10, 110 | nil, 111 | }, 112 | { 113 | "alpha", 114 | []byte{8, 0xd1, 0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0xE7}, 115 | pdumode.SMSCAddress{ 116 | tpdu.Address{Addr: "bc2a7c1c3797c", TOA: 0xd1}, 117 | }, 118 | 9, 119 | nil, 120 | }, 121 | { 122 | "zero length", 123 | []byte{0}, 124 | pdumode.SMSCAddress{}, 125 | 1, 126 | nil, 127 | }, 128 | { 129 | "short number", 130 | []byte{11, 0x91, 0x16, 0x04, 0x89, 0x56}, 131 | pdumode.SMSCAddress{}, 132 | 6, 133 | tpdu.NewDecodeError("addr", 2, tpdu.ErrUnderflow), 134 | }, 135 | { 136 | "short number pad", 137 | []byte{12, 0x91, 0x16, 0x04, 0x89, 0x56, 0x97, 0xf7}, 138 | pdumode.SMSCAddress{}, 139 | 8, 140 | tpdu.NewDecodeError("addr", 2, tpdu.ErrUnderflow), 141 | }, 142 | { 143 | "underflow alpha", 144 | []byte{5, 0xd1, 0xCF, 0xE5, 0x39}, 145 | pdumode.SMSCAddress{}, 146 | 5, 147 | tpdu.NewDecodeError("addr", 2, tpdu.ErrUnderflow), 148 | }, 149 | { 150 | "underflow toa", 151 | []byte{1}, 152 | pdumode.SMSCAddress{}, 153 | 1, 154 | tpdu.NewDecodeError("toa", 1, tpdu.ErrUnderflow), 155 | }, 156 | } 157 | for _, p := range patterns { 158 | f := func(t *testing.T) { 159 | a := pdumode.SMSCAddress{} 160 | n, err := a.UnmarshalBinary(p.in) 161 | assert.Equal(t, p.err, err) 162 | assert.Equal(t, p.n, n) 163 | assert.Equal(t, p.out, a) 164 | } 165 | t.Run(p.name, f) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /encoding/semioctet/semioctet.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // Package semioctet provides conversions to and from semioctet format. 6 | package semioctet 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | ) 12 | 13 | // Mapping from half-octet value to UTF-8 digit. 14 | // The trailing 'F' is never returned but is provided to detect fill. 15 | var decodeDigits = "0123456789*#abcF" 16 | 17 | // Decode converts a semi-octet encoded field into the corresponding array of 18 | // UTF-8 digits, as per 3GPP TS 23.040 Section 9.1.2.3 19 | // 20 | // Conversion is terminated by the length of the src or dst. 21 | // The length of the dst indicates the maximum number of digits to return. 22 | // The return values are the decoded field (a slice of dst), the number of 23 | // bytes read from src, and any error detected during the conversion. 24 | func Decode(dst, src []byte) ([]byte, int, error) { 25 | wi := 0 26 | ri := 0 27 | for wi < len(dst) && ri < len(src) { 28 | d := decodeDigits[src[ri]&0x0f] 29 | if d != 'F' { 30 | dst[wi] = d 31 | wi++ 32 | } 33 | d = decodeDigits[src[ri]>>4] 34 | ri++ 35 | if wi == len(dst) && d != 'F' { 36 | return nil, ri, ErrMissingFill 37 | } 38 | if d == 'F' { 39 | continue 40 | } 41 | dst[wi] = d 42 | wi++ 43 | } 44 | return dst[:wi], ri, nil 45 | } 46 | 47 | var encodeDigits = map[byte]int{ 48 | '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, 49 | '8': 8, '9': 9, '*': 10, '#': 11, 'a': 12, 'b': 13, 'c': 14, 50 | } 51 | 52 | // Encode converts an array of UTF-8 digits into semi-octet format, as per 3GPP 53 | // TS 23.040 Section 9.1.2.3 54 | // 55 | // The return values are the encoded field, and any error detected during the 56 | // conversion. 57 | func Encode(src []byte) ([]byte, error) { 58 | l := (len(src) + 1) / 2 59 | if l == 0 { 60 | return append(src[:0:0], src...), nil 61 | } 62 | b := make([]byte, l) 63 | hi := false 64 | p := 0 65 | wi := 0 66 | for _, d := range src { 67 | e, ok := encodeDigits[d] 68 | if !ok { 69 | return nil, ErrInvalidDigit(d) 70 | } 71 | if hi { 72 | p = p | int(e<<4) 73 | b[wi] = byte(p) 74 | wi++ 75 | hi = false 76 | } else { 77 | p = e 78 | hi = true 79 | } 80 | } 81 | if hi { 82 | p = p | 0xf0 83 | b[wi] = byte(p) 84 | } 85 | return b, nil 86 | } 87 | 88 | // ErrInvalidDigit indicates that the digit can not be encoded into semioctet 89 | // format. 90 | type ErrInvalidDigit byte 91 | 92 | func (e ErrInvalidDigit) Error() string { 93 | return fmt.Sprintf("semioctet: invalid digit: '%c' - 0x%x", byte(e), int(e)) 94 | } 95 | 96 | var ( 97 | // ErrMissingFill indicates the final src octet does not contain the 98 | // expected fill character 'F'. 99 | ErrMissingFill = errors.New("semioctet: last src octet missing expected fill") 100 | ) 101 | -------------------------------------------------------------------------------- /encoding/semioctet/semioctet_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package semioctet_test 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/warthog618/sms/encoding/semioctet" 13 | ) 14 | 15 | func TestDecode(t *testing.T) { 16 | patterns := []struct { 17 | name string 18 | inDst []byte 19 | inSrc []byte 20 | out []byte 21 | outReadCount int 22 | err error 23 | }{ 24 | { 25 | "nil", 26 | nil, 27 | nil, 28 | nil, 29 | 0, 30 | nil, 31 | }, 32 | { 33 | "nil dst", 34 | nil, 35 | []byte{0x10, 0x32, 0x54, 0x76}, 36 | nil, 37 | 0, 38 | nil, 39 | }, 40 | { 41 | "nil src", 42 | make([]byte, 3), 43 | nil, 44 | []byte{}, 45 | 0, 46 | nil, 47 | }, 48 | { 49 | "empty dst", 50 | []byte{}, 51 | []byte{0x10, 0x32, 0x54, 0x76}, 52 | []byte{}, 53 | 0, 54 | nil, 55 | }, 56 | { 57 | "empty src", 58 | make([]byte, 3), 59 | []byte{}, 60 | []byte{}, 61 | 0, 62 | nil, 63 | }, 64 | { 65 | "limit src", 66 | make([]byte, 8), 67 | []byte{0x10, 0x32, 0x54, 0x76}, 68 | []byte("01234567"), 69 | 4, 70 | nil, 71 | }, 72 | { 73 | "fill limit src", 74 | make([]byte, 8), 75 | []byte{0x10, 0x32, 0x54, 0xf6}, 76 | []byte("0123456"), 77 | 4, 78 | nil, 79 | }, 80 | { 81 | "fill limit even dst", 82 | make([]byte, 6), 83 | []byte{0x10, 0x32, 0x54, 0xf6, 0x98}, 84 | []byte("012345"), 85 | 3, 86 | nil, 87 | }, 88 | { 89 | "no fill limit even dst", 90 | make([]byte, 6), 91 | []byte{0x10, 0x32, 0x54, 0x76, 0x98}, 92 | []byte("012345"), 93 | 3, 94 | nil, 95 | }, 96 | { 97 | "fill limit odd dst", 98 | make([]byte, 5), 99 | []byte{0x10, 0x32, 0xf4, 0x76}, 100 | []byte("01234"), 101 | 3, 102 | nil, 103 | }, 104 | { 105 | "alphabet", 106 | make([]byte, 15), 107 | []byte{0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, 108 | []byte("0123456789*#abc"), 109 | 8, 110 | nil, 111 | }, 112 | { 113 | "no fill limit even dst", 114 | make([]byte, 6), 115 | []byte{0x10, 0x32, 0x54, 0x76, 0x98}, 116 | []byte("012345"), 117 | 3, 118 | nil, 119 | }, 120 | { 121 | "no fill limit odd dst", 122 | make([]byte, 5), 123 | []byte{0x10, 0x32, 0x54, 0x76}, 124 | nil, 125 | 3, 126 | semioctet.ErrMissingFill, 127 | }, 128 | { 129 | "skip inter fill", 130 | make([]byte, 10), 131 | []byte{0x10, 0x32, 0xF4, 0x76, 0xF8}, 132 | []byte("01234678"), 133 | 5, 134 | nil, 135 | }, 136 | } 137 | for _, p := range patterns { 138 | f := func(t *testing.T) { 139 | dst, count, err := semioctet.Decode(p.inDst, p.inSrc) 140 | assert.Equal(t, p.err, err) 141 | assert.Equal(t, p.outReadCount, count) 142 | assert.Equal(t, p.out, dst) 143 | } 144 | t.Run(p.name, f) 145 | } 146 | } 147 | 148 | func TestEncode(t *testing.T) { 149 | patterns := []struct { 150 | name string 151 | in []byte 152 | out []byte 153 | err error 154 | }{ 155 | { 156 | "nil", 157 | nil, 158 | nil, 159 | nil, 160 | }, 161 | { 162 | "empty src", 163 | []byte{}, 164 | []byte{}, 165 | nil, 166 | }, 167 | { 168 | "even src", 169 | []byte("01234567"), 170 | []byte{0x10, 0x32, 0x54, 0x76}, 171 | nil, 172 | }, 173 | { 174 | "odd src", 175 | []byte("0123456"), 176 | []byte{0x10, 0x32, 0x54, 0xf6}, 177 | nil, 178 | }, 179 | { 180 | "alphabet", 181 | []byte("0123456789*#abc"), 182 | []byte{0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, 183 | nil, 184 | }, 185 | { 186 | "invalid digit", 187 | []byte("012345D6789"), 188 | nil, 189 | semioctet.ErrInvalidDigit('D'), 190 | }, 191 | } 192 | for _, p := range patterns { 193 | f := func(t *testing.T) { 194 | dst, err := semioctet.Encode(p.in) 195 | assert.Equal(t, p.err, err) 196 | assert.Equal(t, p.out, dst) 197 | } 198 | t.Run(p.name, f) 199 | } 200 | } 201 | 202 | // TestErrInvalidDigit tests that the errors can be stringified. 203 | // It is fragile, as it compares the strings exactly, but its main purpose is 204 | // to confirm the Error function doesn't recurse, as that is bad. 205 | func TestErrInvalidOctet(t *testing.T) { 206 | patterns := []byte{0x00, 0xa0, 0x0a, 0x9a, 0xa9, 0xff} 207 | for _, p := range patterns { 208 | f := func(t *testing.T) { 209 | err := semioctet.ErrInvalidDigit(p) 210 | expected := fmt.Sprintf("semioctet: invalid digit: '%c' - 0x%x", byte(p), int(p)) 211 | s := err.Error() 212 | assert.Equal(t, expected, s) 213 | } 214 | t.Run(fmt.Sprintf("%x", p), f) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /encoding/tpdu/address.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package tpdu 6 | 7 | import ( 8 | "github.com/warthog618/sms/encoding/gsm7" 9 | "github.com/warthog618/sms/encoding/semioctet" 10 | ) 11 | 12 | // Address represents a phone number. 13 | type Address struct { 14 | TOA byte 15 | Addr string 16 | } 17 | 18 | // AddressOption returns a new Address with an option applied. 19 | type AddressOption func(Address) Address 20 | 21 | // NewAddress creates an Address and initialises the TOA. 22 | func NewAddress(options ...AddressOption) Address { 23 | a := Address{} 24 | for _, option := range options { 25 | a = option(a) 26 | } 27 | // TOA must have bit 7 set. 28 | a.TOA |= 0x80 29 | return a 30 | } 31 | 32 | // FromNumber creates an AddressOption thats sets the address to the 33 | // international number. 34 | // 35 | // The number may be optionally prefixed with '+'. 36 | func FromNumber(number string) AddressOption { 37 | return func(a Address) Address { 38 | a.SetNumber(number) 39 | return a 40 | } 41 | } 42 | 43 | // MarshalBinary marshals an Address into binary. 44 | // 45 | // It returns the marshalled address and any error detected 46 | // while marshalling. 47 | func (a *Address) MarshalBinary() (dst []byte, err error) { 48 | ton := a.TypeOfNumber() 49 | var addr []byte 50 | var l int // is digits and ignores the toa 51 | switch ton { 52 | case TonAlphanumeric: 53 | e := gsm7.NewEncoder().WithExtCharset(nil) // without escapes 54 | addr, err = e.Encode([]byte(a.Addr)) 55 | if err != nil { 56 | return nil, EncodeError("addr", err) 57 | } 58 | l = (len(addr)*7 + 3) / 4 59 | addr = gsm7.Pack7Bit(addr, 0) 60 | default: 61 | addr, err = semioctet.Encode([]byte(a.Addr)) 62 | if err != nil { 63 | return nil, EncodeError("addr", err) 64 | } 65 | l = len(a.Addr) 66 | } 67 | dst = make([]byte, 0, l+2) 68 | dst = append(dst, byte(l), a.TOA) 69 | dst = append(dst, addr...) 70 | return dst, nil 71 | } 72 | 73 | // UnmarshalBinary unmarshals an Address from a binary TPDU. 74 | // 75 | // It returns the number of bytes read from the source, and any error detected 76 | // while unmarshalling. 77 | func (a *Address) UnmarshalBinary(src []byte) (int, error) { 78 | if len(src) < 2 { 79 | return 0, NewDecodeError("addr", 0, ErrUnderflow) 80 | } 81 | l := int(src[0]) // len is semi-octets and ignores toa 82 | ol := (l + 1) / 2 // octet length 83 | toa := src[1] 84 | ton := TypeOfNumber((toa >> 4) & 0x07) 85 | ri := 2 86 | if len(src) < ri+ol { 87 | return len(src), NewDecodeError("addr", ri, ErrUnderflow) 88 | } 89 | switch ton { 90 | case TonAlphanumeric: 91 | u := gsm7.Unpack7Bit(src[ri:ri+ol], 0) 92 | if (len(u)*7+3)/4 > l { 93 | // drop septet of fill 94 | u = u[:len(u)-1] 95 | } 96 | d := gsm7.NewDecoder().WithExtCharset(nil).Strict() // without escapes 97 | baddr, err := d.Decode(u) 98 | if err != nil { 99 | return ri, NewDecodeError("addr", ri, err) 100 | } 101 | ri += ol 102 | a.Addr = string(baddr) 103 | default: 104 | baddr, n, err := semioctet.Decode(make([]byte, l), src[ri:ri+ol]) 105 | ri += n 106 | if err != nil { 107 | return ri, NewDecodeError("addr", ri-n, err) 108 | } 109 | if n != ol || len(baddr) < l { 110 | return ri, NewDecodeError("addr", ri-n, ErrUnderflow) 111 | } 112 | a.Addr = string(baddr) 113 | } 114 | a.TOA = toa 115 | return ri, nil 116 | } 117 | 118 | // Number returns the stringified number corresponding to the Address. 119 | func (a Address) Number() string { 120 | if a.TypeOfNumber() == TonInternational { 121 | return "+" + a.Addr 122 | } 123 | return a.Addr 124 | } 125 | 126 | // SetNumber sets the address to the international number. 127 | // 128 | // The number may be optionally prefixed with '+'. 129 | func (a *Address) SetNumber(number string) { 130 | if len(number) > 0 && number[0] == '+' { 131 | number = number[1:] 132 | } 133 | a.SetTypeOfNumber(TonInternational) 134 | a.SetNumberingPlan(NpISDN) 135 | a.Addr = number 136 | } 137 | 138 | // NumberingPlan extracts the NPI field from the TOA. 139 | func (a Address) NumberingPlan() NumberingPlan { 140 | return NumberingPlan(a.TOA & 0x0f) 141 | } 142 | 143 | // SetNumberingPlan sets the NPI field in the TOA. 144 | func (a *Address) SetNumberingPlan(np NumberingPlan) { 145 | a.TOA = (a.TOA &^ 0x0f) | byte(np&0x0f) 146 | } 147 | 148 | // SetTypeOfNumber sets the TON field in the TOA. 149 | func (a *Address) SetTypeOfNumber(ton TypeOfNumber) { 150 | a.TOA = (a.TOA &^ 0x70) | (byte(ton&0x7) << 4) 151 | } 152 | 153 | // TypeOfNumber extracts the TON field from the TOA. 154 | func (a Address) TypeOfNumber() TypeOfNumber { 155 | return TypeOfNumber((a.TOA >> 4) & 0x07) 156 | } 157 | 158 | // TypeOfNumber corresponds to bits 6,5,4 of the Address TOA field. 159 | // 160 | // i.e. 1xxxyyyy, 161 | // as defined in 3GPP TS 23.040 Section 9.1.2.5. 162 | type TypeOfNumber int 163 | 164 | const ( 165 | // TonUnknown indicates the type of the number is unknown. 166 | TonUnknown TypeOfNumber = iota 167 | 168 | // TonInternational indicates the number is international. 169 | TonInternational 170 | 171 | // TonNational indicates the number is national. 172 | TonNational 173 | 174 | // TonNetworkSpecific indicates the number is specific to the carrier network. 175 | TonNetworkSpecific 176 | 177 | // TonSubscriberNumber indicates the number is a subscriber number. 178 | TonSubscriberNumber 179 | 180 | // TonAlphanumeric indicates the number is in alphanumeric format. 181 | TonAlphanumeric 182 | 183 | // TonAbbreviated indicates the number is in abbreviated format. 184 | TonAbbreviated 185 | 186 | // TonExtension is reserved for future extension. 187 | TonExtension 188 | ) 189 | 190 | // NumberingPlan corresponds to bits 4,3,2,1 of the Address TOA field. 191 | // i.e. 1yyyxxxx 192 | // as defined in 3GPP TS 23.040 Section 9.1.2.5 193 | type NumberingPlan int 194 | 195 | const ( 196 | // NpUnknown indicates the numbering plan is unknown. 197 | NpUnknown NumberingPlan = iota 198 | 199 | // NpISDN indicates the number is in ISDN/E.164 format. 200 | NpISDN 201 | 202 | _ 203 | 204 | // NpData indicates a data numbering plan (X.121). 205 | NpData 206 | 207 | // NpTelex indicates a telex numbering plan. 208 | NpTelex 209 | 210 | // NpScSpecificA indicates a service center specific numbering plan. 211 | NpScSpecificA 212 | 213 | // NpScSpecificB indicates a service center specific numbering plan. 214 | NpScSpecificB 215 | 216 | _ 217 | 218 | // NpNational indicates a national numbering plan. 219 | NpNational 220 | 221 | // NpPrivate indicates a private numbering plan. 222 | NpPrivate 223 | 224 | // NpErmes indicates the ERMES (ETSI DE/PS 3 01-3) numbering plan. 225 | NpErmes 226 | 227 | // NpExtension is reserved for future extensions. 228 | NpExtension = 0x0f 229 | 230 | // all other values reserved. 231 | ) 232 | -------------------------------------------------------------------------------- /encoding/tpdu/address_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package tpdu_test 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/warthog618/sms/encoding/gsm7" 13 | "github.com/warthog618/sms/encoding/semioctet" 14 | "github.com/warthog618/sms/encoding/tpdu" 15 | ) 16 | 17 | func TestNewAddress(t *testing.T) { 18 | a := tpdu.NewAddress() 19 | assert.Equal(t, uint8(0x80), a.TOA) 20 | a = tpdu.NewAddress(tpdu.FromNumber("1234")) 21 | assert.Equal(t, uint8(0x91), a.TOA) 22 | assert.Equal(t, "1234", a.Addr) 23 | a = tpdu.NewAddress(tpdu.FromNumber("+4321")) 24 | assert.Equal(t, uint8(0x91), a.TOA) 25 | assert.Equal(t, "4321", a.Addr) 26 | } 27 | 28 | type addressMarshalPattern struct { 29 | name string 30 | in tpdu.Address 31 | out []byte 32 | err error 33 | } 34 | 35 | func TestAddressMarshalBinary(t *testing.T) { 36 | patterns := []addressMarshalPattern{ 37 | {"empty", 38 | tpdu.Address{}, 39 | []byte{0, 0}, 40 | nil, 41 | }, 42 | {"number", 43 | tpdu.Address{Addr: "61409865629", TOA: 0x91}, 44 | []byte{11, 0x91, 0x16, 0x04, 0x89, 0x56, 0x26, 0xf9}, 45 | nil, 46 | }, 47 | {"number alphabet", 48 | tpdu.Address{Addr: "0123456789*#abc", TOA: 0x91}, 49 | []byte{15, 0x91, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, 50 | nil, 51 | }, 52 | {"alpha", 53 | tpdu.Address{Addr: "messages", TOA: 0xd1}, 54 | []byte{14, 0xd1, 0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0xE7}, 55 | nil, 56 | }, 57 | {"alpha odd", 58 | tpdu.Address{Addr: "message", TOA: 0xd1}, 59 | []byte{13, 0xd1, 0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0x01}, 60 | nil, 61 | }, 62 | {"alpha Vodafone", 63 | tpdu.Address{Addr: "Vodafone", TOA: 0xd0}, 64 | []byte{14, 0xd0, 0xD6, 0x37, 0x39, 0x6C, 0x7E, 0xBB, 0xCB}, 65 | nil, 66 | }, 67 | {"invalid number", 68 | tpdu.Address{Addr: "6140f98656", TOA: 0x91}, 69 | nil, 70 | tpdu.EncodeError("addr", semioctet.ErrInvalidDigit('f')), 71 | }, 72 | // test characters only available in the extension table - which should 73 | // be unavailable. 74 | {"invalid alpha euro", 75 | tpdu.Address{Addr: "a euro €32", TOA: 0xd1}, 76 | nil, 77 | tpdu.EncodeError("addr", gsm7.ErrInvalidUTF8('€')), 78 | }, 79 | {"invalid alpha bar", 80 | tpdu.Address{Addr: "a bar | ", TOA: 0xd1}, 81 | nil, 82 | tpdu.EncodeError("addr", gsm7.ErrInvalidUTF8('|')), 83 | }, 84 | // test characters not available in the default character set at all. 85 | {"invalid alpha", 86 | tpdu.Address{Addr: "mes⌘sages", TOA: 0xd1}, 87 | nil, 88 | tpdu.EncodeError("addr", gsm7.ErrInvalidUTF8('⌘')), 89 | }, 90 | } 91 | for _, p := range patterns { 92 | f := func(t *testing.T) { 93 | b, err := p.in.MarshalBinary() 94 | assert.Equal(t, p.err, err) 95 | assert.Equal(t, p.out, b) 96 | } 97 | t.Run(p.name, f) 98 | } 99 | } 100 | 101 | type addressUnmarshalPattern struct { 102 | name string 103 | in []byte 104 | out tpdu.Address 105 | n int 106 | err error 107 | } 108 | 109 | func TestAddressUnmarshalBinary(t *testing.T) { 110 | patterns := []addressUnmarshalPattern{ 111 | {"empty", 112 | []byte{0, 0}, 113 | tpdu.Address{}, 114 | 2, 115 | nil, 116 | }, 117 | {"number", 118 | []byte{11, 0x91, 0x16, 0x04, 0x89, 0x56, 0x26, 0xf9}, 119 | tpdu.Address{Addr: "61409865629", TOA: 0x91}, 120 | 8, 121 | nil, 122 | }, 123 | {"number alphabet", 124 | []byte{15, 0x91, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, 125 | tpdu.Address{Addr: "0123456789*#abc", TOA: 0x91}, 126 | 10, 127 | nil}, 128 | {"alpha", 129 | []byte{14, 0xd1, 0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0xE7}, 130 | tpdu.Address{Addr: "messages", TOA: 0xd1}, 131 | 9, 132 | nil, 133 | }, 134 | {"alpha odd", 135 | []byte{13, 0xd1, 0xED, 0xF2, 0x7C, 0x1E, 0x3E, 0x97, 0x01}, 136 | tpdu.Address{Addr: "message", TOA: 0xd1}, 137 | 9, 138 | nil, 139 | }, 140 | {"alpha Vodafone", 141 | []byte{14, 0xd0, 0xD6, 0x37, 0x39, 0x6C, 0x7E, 0xBB, 0xCB}, 142 | tpdu.Address{Addr: "Vodafone", TOA: 0xd0}, 143 | 9, 144 | nil, 145 | }, 146 | {"overlong number", 147 | []byte{11, 0x91, 0x16, 0x04, 0x89, 0x56, 0x26, 0x09}, 148 | tpdu.Address{}, 149 | 8, 150 | tpdu.NewDecodeError("addr", 2, semioctet.ErrMissingFill), 151 | }, 152 | {"short binary", 153 | []byte{0}, tpdu.Address{}, 154 | 0, 155 | tpdu.NewDecodeError("addr", 0, tpdu.ErrUnderflow), 156 | }, 157 | {"short number", 158 | []byte{11, 0x91, 0x16, 0x04, 0x89, 0x56}, 159 | tpdu.Address{}, 160 | 6, 161 | tpdu.NewDecodeError("addr", 2, tpdu.ErrUnderflow), 162 | }, 163 | {"short number pad", 164 | []byte{12, 0x91, 0x16, 0x04, 0x89, 0x56, 0x97, 0xf7}, 165 | tpdu.Address{}, 166 | 8, 167 | tpdu.NewDecodeError("addr", 2, tpdu.ErrUnderflow), 168 | }, 169 | {"invalid alpha bar", 170 | []byte{10, 0xd1, 0xED, 0xF2, 0x7C, 0x03, 0x9c, 0x87, 0xCF, 0xE5, 0x39}, 171 | tpdu.Address{}, 172 | 2, 173 | tpdu.NewDecodeError("addr", 2, gsm7.ErrInvalidSeptet(0x40)), 174 | }, 175 | {"underflow alpha", 176 | []byte{10, 0xd1, 0xCF, 0xE5, 0x39}, 177 | tpdu.Address{}, 178 | 5, 179 | tpdu.NewDecodeError("addr", 2, tpdu.ErrUnderflow), 180 | }, 181 | } 182 | for _, p := range patterns { 183 | f := func(t *testing.T) { 184 | a := tpdu.Address{} 185 | n, err := a.UnmarshalBinary(p.in) 186 | assert.Equal(t, p.err, err) 187 | assert.Equal(t, p.n, n) 188 | assert.Equal(t, p.out, a) 189 | } 190 | t.Run(p.name, f) 191 | } 192 | } 193 | 194 | func TestAddressNumber(t *testing.T) { 195 | a := tpdu.Address{Addr: "61409865629", TOA: 0} 196 | assert.Equal(t, "61409865629", a.Number()) 197 | a.SetTypeOfNumber(tpdu.TonInternational) 198 | assert.Equal(t, "+61409865629", a.Number()) 199 | } 200 | 201 | func TestAddressNumberingPlan(t *testing.T) { 202 | patterns := []tpdu.NumberingPlan{0, 2, 0xf, 0x12} 203 | for _, p := range patterns { 204 | f := func(t *testing.T) { 205 | a := tpdu.NewAddress() 206 | a.SetNumberingPlan((p)) 207 | assert.Equal(t, p&0x0f, a.NumberingPlan()) 208 | } 209 | t.Run(fmt.Sprintf("%02x", p), f) 210 | } 211 | } 212 | 213 | func TestAddressSetNumber(t *testing.T) { 214 | a := tpdu.NewAddress() 215 | assert.Equal(t, uint8(0x80), a.TOA) 216 | a.SetNumber("1234") 217 | assert.Equal(t, uint8(0x91), a.TOA) 218 | assert.Equal(t, "1234", a.Addr) 219 | assert.Equal(t, "+1234", a.Number()) 220 | a.SetNumber("+4321") 221 | assert.Equal(t, uint8(0x91), a.TOA) 222 | assert.Equal(t, "4321", a.Addr) 223 | assert.Equal(t, "+4321", a.Number()) 224 | } 225 | 226 | func TestAddressTypeOfNumber(t *testing.T) { 227 | patterns := []tpdu.TypeOfNumber{0, 2, 3, 5} 228 | for _, p := range patterns { 229 | f := func(t *testing.T) { 230 | a := tpdu.NewAddress() 231 | a.SetTypeOfNumber((p)) 232 | ton := a.TypeOfNumber() 233 | assert.Equal(t, p&0x0f, ton) 234 | } 235 | t.Run(fmt.Sprintf("%02x", p), f) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /encoding/tpdu/dcs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package tpdu 6 | 7 | import "fmt" 8 | 9 | // DCS represents the SMS Data Coding Scheme field as defined in 3GPP TS 23.040 10 | // Section 4. 11 | type DCS byte 12 | 13 | // Alphabet defines the encoding of the SMS User Data, as defined in 3GPP TS 14 | // 23.038 Section 4. 15 | type Alphabet int 16 | 17 | const ( 18 | // Alpha7Bit indicates that the UD is encoded using GSM 7 bit encoding. 19 | // The character set used for the decoding is determined from the UDH. 20 | Alpha7Bit Alphabet = iota 21 | 22 | // Alpha8Bit indicates that the UD is encoded as raw 8bit data. 23 | Alpha8Bit 24 | 25 | // AlphaUCS2 indicates that the UD is encoded as UCS-2 (16bit) characters. 26 | AlphaUCS2 27 | 28 | // AlphaReserved indicates the alphabet is not defined. 29 | AlphaReserved 30 | ) 31 | 32 | // Alphabet returns the alphabet used to encode the User Data according to the DCS. 33 | // 34 | // The DCS is assumed to be defined as per 3GPP TS 23.038 Section 4. 35 | func (d DCS) Alphabet() (Alphabet, error) { 36 | alpha := Alpha7Bit 37 | switch { 38 | case d&0x80 == 0x00: // 0xxx 39 | alpha = Alphabet((d >> 2) & 0x3) 40 | if alpha == AlphaReserved { 41 | alpha = Alpha7Bit 42 | } 43 | case d&0xe0 == 0xc0: // 110x 44 | // is 7bit 45 | case d&0xf0 == 0xe0: // 1110 46 | alpha = AlphaUCS2 47 | case d&0xf0 == 0xf0: // 1111 48 | if d&0x04 == 0x04 { 49 | alpha = Alpha8Bit 50 | } // else 7bit 51 | default: // includes 10xx reserved coding groups 52 | return Alpha7Bit, ErrInvalid 53 | } 54 | return alpha, nil 55 | } 56 | 57 | // ApplyTPDUOption applies the DCS value to the TPDU DCS field. 58 | func (d DCS) ApplyTPDUOption(t *TPDU) error { 59 | t.SetDCS(byte(d)) 60 | return nil 61 | } 62 | 63 | func (d DCS) String() string { 64 | str := fmt.Sprintf("0x%02x", int(d)) 65 | alpha, err := d.Alphabet() 66 | if err != nil { 67 | return str 68 | } 69 | switch alpha { 70 | case Alpha7Bit: 71 | str += " 7bit" 72 | case Alpha8Bit: 73 | str += " 8bit" 74 | case AlphaUCS2: 75 | str += " UCS-2" 76 | } 77 | return str 78 | } 79 | 80 | // WithAlphabet sets the Alphabet bits of the DCS, given the state of the other 81 | // bits. 82 | // 83 | // An error is returned if the state is incompatible with setting the alphabet. 84 | func (d DCS) WithAlphabet(a Alphabet) (DCS, error) { 85 | switch { 86 | case d&0x80 == 0x00: // 0xxx 87 | return d&^0x0c | (DCS(a) << 2), nil 88 | case d&0xe0 == 0xc0 && a == Alpha7Bit: // 110x is 7Bit 89 | return d, nil 90 | case d&0xf0 == 0xe0 && a == AlphaUCS2: // 1110 is UCS2 91 | return d, nil 92 | case d&0xf0 == 0xf0 && a <= Alpha8Bit: 93 | return d&^0x0c | (DCS(a) << 2), nil 94 | default: // includes 110x, 1110, and 10xx reserved coding groups 95 | return d, ErrInvalid 96 | } 97 | } 98 | 99 | const ( 100 | // Dcs8BitData is a DCS indicating 8 bit data 101 | Dcs8BitData DCS = 0x04 102 | 103 | // DcsUCS2Data is a DCS indicating UCS2 data 104 | DcsUCS2Data DCS = 0x08 105 | ) 106 | 107 | // MessageClass indicates the class of the message as specified in 3GPP TS 108 | // 23.038 Section 4. 109 | type MessageClass int 110 | 111 | const ( 112 | // MClass0 is a flash message which is not to be stored in memory. 113 | MClass0 MessageClass = iota 114 | 115 | // MClass1 is an ME specific message. 116 | MClass1 117 | 118 | // MClass2 is a SIM/USIM specific message. 119 | MClass2 120 | 121 | // MClass3 is a TE specific message. 122 | MClass3 123 | 124 | // MClassUnknown indicates no message class is set. 125 | MClassUnknown 126 | ) 127 | 128 | // Class returns the MessageClass indicated by the DCS. 129 | // The DCS is assumed to be defined as per 3GPP TS 23.038 Section 4. 130 | func (d DCS) Class() (MessageClass, error) { 131 | switch { 132 | case d&0x90 == 0x10, d&0xf0 == 0xf0: // 0xx1 and 1111 133 | return MessageClass(d & 0x3), nil 134 | case d&0xe0 == 0xc0, d&0xf0 == 0xe0: // 110x and 1110 135 | return MClassUnknown, nil 136 | default: // includes 10xx reserved coding groups 137 | return MClassUnknown, ErrInvalid 138 | } 139 | } 140 | 141 | // WithClass sets the MessageClass bits of the DCS, given the state of the 142 | // other bits. 143 | // 144 | // An error is returned if the state is incompatible with setting the message 145 | // class. 146 | func (d DCS) WithClass(c MessageClass) (DCS, error) { 147 | switch { 148 | case d&0x80 == 0x00: // 0xxx 149 | return (d&^0x03 | 0x10 | DCS(c)), nil 150 | case d&0xf0 == 0xf0: // 1111 151 | return (d&^0x03 | DCS(c)), nil 152 | default: // includes 10xx reserved coding groups 153 | return d, ErrInvalid 154 | } 155 | } 156 | 157 | // Compressed indicates whether the text is compressed using the algorithm 158 | // defined in 3GPP TS 23.024, as determined from the DCS. 159 | // 160 | // The DCS is assumed to be defined as per 3GPP TS 23.038 Section 4. 161 | func (d DCS) Compressed() bool { 162 | // only true for 0x1xxxxx (binary) 163 | return (d&0xa0 == 0x20) 164 | } 165 | -------------------------------------------------------------------------------- /encoding/tpdu/dcs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Kent Gibson . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package tpdu_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | "github.com/warthog618/sms/encoding/tpdu" 15 | ) 16 | 17 | type dcsAlphabetPattern struct { 18 | in byte 19 | out tpdu.Alphabet 20 | err error 21 | } 22 | 23 | func TestDCSAlphabet(t *testing.T) { 24 | // might as well test them all.. 25 | patterns := []dcsAlphabetPattern{} 26 | for i := 0; i < 8; i++ { 27 | m := byte(i << 4) 28 | patterns = append(patterns, 29 | dcsAlphabetPattern{0x00 | m, tpdu.Alpha7Bit, nil}, 30 | dcsAlphabetPattern{0x04 | m, tpdu.Alpha8Bit, nil}, 31 | dcsAlphabetPattern{0x08 | m, tpdu.AlphaUCS2, nil}, 32 | dcsAlphabetPattern{0x0c | m, tpdu.Alpha7Bit, nil}, 33 | ) 34 | } 35 | for i := 0x80; i < 0xc0; i++ { 36 | patterns = append(patterns, 37 | dcsAlphabetPattern{byte(i), tpdu.Alpha7Bit, tpdu.ErrInvalid}, 38 | ) 39 | } 40 | for i := 0xc0; i < 0xe0; i++ { 41 | patterns = append(patterns, 42 | dcsAlphabetPattern{byte(i), tpdu.Alpha7Bit, nil}, 43 | ) 44 | } 45 | for i := 0xe0; i < 0xf0; i++ { 46 | patterns = append(patterns, 47 | dcsAlphabetPattern{byte(i), tpdu.AlphaUCS2, nil}, 48 | ) 49 | } 50 | for i := 0xf0; i <= 0xff; i++ { 51 | a := tpdu.Alpha7Bit 52 | if i&0x04 == 0x04 { 53 | a = tpdu.Alpha8Bit 54 | } 55 | patterns = append(patterns, 56 | dcsAlphabetPattern{byte(i), a, nil}, 57 | ) 58 | } 59 | for _, p := range patterns { 60 | f := func(t *testing.T) { 61 | d := tpdu.DCS(p.in) 62 | a, err := d.Alphabet() 63 | assert.Equal(t, p.err, err) 64 | assert.Equal(t, p.out, a) 65 | } 66 | t.Run(fmt.Sprintf("%08b", p.in), f) 67 | } 68 | } 69 | 70 | func TestApplyTPDUOption(t *testing.T) { 71 | s, err := tpdu.New(tpdu.DCS(0x34)) 72 | require.Nil(t, err) 73 | assert.Equal(t, tpdu.DCS(0x34), s.DCS) 74 | } 75 | 76 | type dcsWithAlphabetIn struct { 77 | in byte 78 | a tpdu.Alphabet 79 | } 80 | 81 | type dcsWithAlphabetOut struct { 82 | out tpdu.DCS 83 | err error 84 | } 85 | 86 | func TestDCSWithAlphabet(t *testing.T) { 87 | // might as well test them all.. 88 | patterns := make(map[dcsWithAlphabetIn]dcsWithAlphabetOut) // the good ones 89 | for i := 0x00; i < 0x80; i++ { 90 | for a := 0; a < 4; a++ { 91 | patterns[dcsWithAlphabetIn{byte(i), tpdu.Alphabet(a)}] = 92 | dcsWithAlphabetOut{tpdu.DCS(i&^0x0c | a<<2), nil} 93 | } 94 | } 95 | for i := 0xc0; i < 0xe0; i++ { 96 | patterns[dcsWithAlphabetIn{byte(i), tpdu.Alpha7Bit}] = 97 | dcsWithAlphabetOut{tpdu.DCS(i), nil} 98 | } 99 | for i := 0xe0; i < 0xf0; i++ { 100 | patterns[dcsWithAlphabetIn{byte(i), tpdu.AlphaUCS2}] = 101 | dcsWithAlphabetOut{tpdu.DCS(i), nil} 102 | } 103 | for i := 0xf0; i <= 0xff; i++ { 104 | for a := 0; a < 2; a++ { 105 | patterns[dcsWithAlphabetIn{byte(i), tpdu.Alphabet(a)}] = 106 | dcsWithAlphabetOut{tpdu.DCS(i&^0x0c | a<<2), nil} 107 | } 108 | } 109 | for i := 0x00; i <= 0xff; i++ { 110 | for a := 0; a < 4; a++ { 111 | p, ok := patterns[dcsWithAlphabetIn{byte(i), tpdu.Alphabet(a)}] 112 | if !ok { 113 | p = dcsWithAlphabetOut{tpdu.DCS(i), tpdu.ErrInvalid} // the bad ones 114 | } 115 | f := func(t *testing.T) { 116 | d := tpdu.DCS(i) 117 | dcs, err := d.WithAlphabet(tpdu.Alphabet(a)) 118 | assert.Equal(t, p.err, err) 119 | assert.Equal(t, p.out, dcs) 120 | } 121 | t.Run(fmt.Sprintf("%08b_%d", i, a), f) 122 | } 123 | } 124 | } 125 | 126 | type dcsClassPattern struct { 127 | in byte 128 | out tpdu.MessageClass 129 | err error 130 | } 131 | 132 | func TestDCSClass(t *testing.T) { 133 | // might as well test them all.. 134 | patterns := []dcsClassPattern{} 135 | for i := 0; i < 4; i++ { 136 | m := byte(i << 5) 137 | patterns = append(patterns, 138 | dcsClassPattern{0x00 | m, tpdu.MClassUnknown, tpdu.ErrInvalid}, 139 | dcsClassPattern{0x01 | m, tpdu.MClassUnknown, tpdu.ErrInvalid}, 140 | dcsClassPattern{0x02 | m, tpdu.MClassUnknown, tpdu.ErrInvalid}, 141 | dcsClassPattern{0x03 | m, tpdu.MClassUnknown, tpdu.ErrInvalid}, 142 | dcsClassPattern{0x10 | m, tpdu.MClass0, nil}, 143 | dcsClassPattern{0x11 | m, tpdu.MClass1, nil}, 144 | dcsClassPattern{0x12 | m, tpdu.MClass2, nil}, 145 | dcsClassPattern{0x13 | m, tpdu.MClass3, nil}, 146 | ) 147 | } 148 | for i := 0x80; i < 0xc0; i++ { 149 | patterns = append(patterns, 150 | dcsClassPattern{byte(i), tpdu.MClassUnknown, tpdu.ErrInvalid}, 151 | ) 152 | } 153 | for i := 0xc0; i < 0xe0; i++ { 154 | patterns = append(patterns, 155 | dcsClassPattern{byte(i), tpdu.MClassUnknown, nil}, 156 | ) 157 | } 158 | for i := 0xe0; i < 0xf0; i++ { 159 | patterns = append(patterns, 160 | dcsClassPattern{byte(i), tpdu.MClassUnknown, nil}, 161 | ) 162 | } 163 | for i := 0xf0; i <= 0xff; i++ { 164 | patterns = append(patterns, 165 | dcsClassPattern{byte(i), tpdu.MessageClass(i & 0x3), nil}, 166 | ) 167 | } 168 | for _, p := range patterns { 169 | f := func(t *testing.T) { 170 | d := tpdu.DCS(p.in) 171 | c, err := d.Class() 172 | assert.Equal(t, p.err, err) 173 | assert.Equal(t, p.out, c) 174 | } 175 | t.Run(fmt.Sprintf("%08b", p.in), f) 176 | } 177 | } 178 | 179 | type dcsWithClassIn struct { 180 | in byte 181 | c tpdu.MessageClass 182 | } 183 | 184 | type dcsWithClassOut struct { 185 | out tpdu.DCS 186 | err error 187 | } 188 | 189 | func TestDCSWithClass(t *testing.T) { 190 | // might as well test them all.. 191 | patterns := make(map[dcsWithClassIn]dcsWithClassOut) // the good ones 192 | for i := 0x00; i < 0x80; i++ { 193 | for a := 0; a < 4; a++ { 194 | patterns[dcsWithClassIn{byte(i), tpdu.MessageClass(a)}] = 195 | dcsWithClassOut{tpdu.DCS(i&^0x03 | 0x10 | a), nil} 196 | } 197 | } 198 | for i := 0xf0; i <= 0xff; i++ { 199 | for a := 0; a < 4; a++ { 200 | patterns[dcsWithClassIn{byte(i), tpdu.MessageClass(a)}] = 201 | dcsWithClassOut{tpdu.DCS(i&^0x03 | a), nil} 202 | } 203 | } 204 | for i := 0x00; i <= 0xff; i++ { 205 | for a := 0; a < 4; a++ { 206 | p, ok := patterns[dcsWithClassIn{byte(i), tpdu.MessageClass(a)}] 207 | if !ok { 208 | p = dcsWithClassOut{tpdu.DCS(i), tpdu.ErrInvalid} // the bad ones 209 | } 210 | f := func(t *testing.T) { 211 | d := tpdu.DCS(i) 212 | dcs, err := d.WithClass(tpdu.MessageClass(a)) 213 | assert.Equal(t, p.err, err) 214 | assert.Equal(t, p.out, dcs) 215 | } 216 | t.Run(fmt.Sprintf("%08b_%d", i, a), f) 217 | } 218 | } 219 | } 220 | 221 | func TestDCSCompressed(t *testing.T) { 222 | patterns := []struct { 223 | in int 224 | out bool 225 | }{ 226 | {0x00, false}, 227 | {0x10, false}, 228 | {0x20, true}, 229 | {0x30, true}, 230 | {0x40, false}, 231 | {0x50, false}, 232 | {0x60, true}, 233 | {0x70, true}, 234 | {0x80, false}, 235 | {0x90, false}, 236 | {0xa0, false}, 237 | {0xb0, false}, 238 | {0xc0, false}, 239 | {0xd0, false}, 240 | {0xe0, false}, 241 | {0xf0, false}, 242 | } 243 | for _, p := range patterns { 244 | f := func(t *testing.T) { 245 | d := tpdu.DCS(p.in) 246 | c := d.Compressed() 247 | assert.Equal(t, p.out, c) 248 | } 249 | t.Run(fmt.Sprintf("%02x", p.in), f) 250 | } 251 | } 252 | 253 | func TestDCSString(t *testing.T) { 254 | patterns := []struct { 255 | in int 256 | out string 257 | }{ 258 | {0x00, "0x00 7bit"}, 259 | {0xf4, "0xf4 8bit"}, 260 | {0xe0, "0xe0 UCS-2"}, 261 | {0x80, "0x80"}, 262 | } 263 | for _, p := range patterns { 264 | f := func(t *testing.T) { 265 | out := tpdu.DCS(p.in).String() 266 | assert.Equal(t, p.out, out) 267 | } 268 | t.Run(fmt.Sprintf("%02x", p.in), f) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /encoding/tpdu/error.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Kent Gibson . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package tpdu 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "io" 12 | ) 13 | 14 | // DecodeError contains the details of an error detected whilew decoding a TPDU. 15 | type DecodeError struct { 16 | Field string 17 | Offset int 18 | Err error 19 | } 20 | 21 | // NewDecodeError creates a decodeError which identifies the field being 22 | // decoded, and the offset into the byte array where the field starts. 23 | // 24 | // If the provided error is a nested decodeError then the offset is updated to 25 | // provide the offset from the beginning of the enclosing field, and the field 26 | // names are combined in outer.inner format. 27 | func NewDecodeError(f string, o int, e error) DecodeError { 28 | if s, ok := e.(DecodeError); ok { 29 | s.Field = fmt.Sprintf("%s.%s", f, s.Field) 30 | s.Offset = s.Offset + o 31 | return s 32 | } 33 | if e == io.EOF { 34 | e = ErrUnderflow 35 | } 36 | return DecodeError{f, o, e} 37 | } 38 | 39 | type encodeError struct { 40 | Field string 41 | Err error 42 | } 43 | 44 | // EncodeError creates an encodeError which identifies the field being encoded. 45 | // 46 | // If the provided error is a nested encodeError then the error is returned as 47 | // is rather than wrapping it. 48 | func EncodeError(f string, e error) error { 49 | if s, ok := e.(encodeError); ok { 50 | s.Field = fmt.Sprintf("%s.%s", f, s.Field) 51 | return s 52 | } 53 | return encodeError{f, e} 54 | } 55 | 56 | func (e encodeError) Error() string { 57 | return fmt.Sprintf("tpdu: error encoding %s: %v", e.Field, e.Err) 58 | } 59 | 60 | func (e DecodeError) Error() string { 61 | return fmt.Sprintf("tpdu: error decoding %s at octet %d: %v", e.Field, e.Offset, e.Err) 62 | } 63 | 64 | // ErrUnsupportedSmsType indicates the type of TPDU being decoded is not 65 | // unsupported by the decoder. 66 | type ErrUnsupportedSmsType byte 67 | 68 | func (e ErrUnsupportedSmsType) Error() string { 69 | return fmt.Sprintf("unsupported SMS type: 0x%x", uint(e)) 70 | } 71 | 72 | var ( 73 | // ErrInvalid indicates the value of a field provided to an encoder is not valid. 74 | ErrInvalid = errors.New("invalid") 75 | 76 | // ErrOddUCS2Length indicates the length of a binary array containing UCS2 77 | // characters has an uneven length, and so has split a UCS2 character. 78 | ErrOddUCS2Length = errors.New("odd UCS2 length") 79 | 80 | // ErrOverlength indicates the binary provided contains more bytes than 81 | // expected by the TPDU decoder. 82 | ErrOverlength = errors.New("overlength") 83 | 84 | // ErrMissing indicates a field requiored to marshal an object is missing. 85 | ErrMissing = errors.New("missing") 86 | 87 | // ErrNonZero indicates a field which is expected to be zeroed, but contains 88 | // non-zero data. 89 | ErrNonZero = errors.New("non-zero fill") 90 | 91 | // ErrUnderflow indicates the binary provided does not contain 92 | // sufficient bytes to correctly decode the TPDU. 93 | ErrUnderflow = errors.New("underflow") 94 | ) 95 | -------------------------------------------------------------------------------- /encoding/tpdu/error_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package tpdu_test 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "testing" 11 | 12 | "github.com/warthog618/sms/encoding/tpdu" 13 | ) 14 | 15 | type decodeTestPattern struct { 16 | Field string 17 | Offset int 18 | Err error 19 | } 20 | 21 | type DecodeError interface { 22 | Field() string 23 | Offset() int 24 | } 25 | 26 | type EncodeError interface { 27 | Field() string 28 | } 29 | 30 | // TestDecodeError tests that the errors can be stringified. 31 | // It is fragile, as it compares the strings exactly, but its main purpose is 32 | // to confirm the Error function doesn't recurse, as that is bad. 33 | func TestDecodeError(t *testing.T) { 34 | patterns := []decodeTestPattern{ 35 | {"nil", 0, nil}, 36 | {"err", 2, errors.New("an error")}, 37 | } 38 | for _, p := range patterns { 39 | f := func(t *testing.T) { 40 | err := tpdu.NewDecodeError(p.Field, p.Offset, p.Err) 41 | expected := fmt.Sprintf("tpdu: error decoding %s at octet %d: %v", p.Field, p.Offset, p.Err) 42 | s := err.Error() 43 | if s != expected { 44 | t.Errorf("failed to stringify, expected '%s', got '%s'", expected, s) 45 | } 46 | } 47 | t.Run(p.Field, f) 48 | } 49 | // nested 50 | f := func(t *testing.T) { 51 | err := tpdu.NewDecodeError("nested", 40, tpdu.NewDecodeError("inner", 2, nil)) 52 | expected := fmt.Sprintf("tpdu: error decoding nested.inner at octet 42: %v", nil) 53 | s := err.Error() 54 | if s != expected { 55 | t.Errorf("failed to stringify, expected '%s', got '%s'", expected, s) 56 | } 57 | } 58 | t.Run("nested", f) 59 | } 60 | 61 | // TestEncodeError tests that the errors can be stringified. 62 | // It is fragile, as it compares the strings exactly, but its main purpose is 63 | // to confirm the Error function doesn't recurse, as that is bad. 64 | func TestEncodeError(t *testing.T) { 65 | patterns := []decodeTestPattern{ 66 | {"nil", 0, nil}, 67 | {"err", 2, errors.New("an error")}, 68 | } 69 | for _, p := range patterns { 70 | f := func(t *testing.T) { 71 | err := tpdu.EncodeError(p.Field, p.Err) 72 | expected := fmt.Sprintf("tpdu: error encoding %s: %v", p.Field, p.Err) 73 | s := err.Error() 74 | if s != expected { 75 | t.Errorf("failed to stringify, expected '%s', got '%s'", expected, s) 76 | } 77 | } 78 | t.Run(p.Field, f) 79 | } 80 | // nested 81 | f := func(t *testing.T) { 82 | err := tpdu.EncodeError("nested", tpdu.EncodeError("inner", nil)) 83 | expected := fmt.Sprintf("tpdu: error encoding nested.inner: %v", nil) 84 | s := err.Error() 85 | if s != expected { 86 | t.Errorf("failed to stringify, expected '%s', got '%s'", expected, s) 87 | } 88 | } 89 | t.Run("nested", f) 90 | } 91 | 92 | // TestErrUnsupportedMTI tests that the errors can be stringified. 93 | // It is fragile, as it compares the strings exactly, but its main purpose is 94 | // to confirm the Error function doesn't recurse, as that is bad. 95 | func TestErrUnsupportedSmsType(t *testing.T) { 96 | patterns := []byte{0x00, 0xa0, 0x0a, 0x9a, 0xa9, 0xff} 97 | for _, p := range patterns { 98 | f := func(t *testing.T) { 99 | err := tpdu.ErrUnsupportedSmsType(p) 100 | expected := fmt.Sprintf("unsupported SMS type: 0x%x", uint(err)) 101 | s := err.Error() 102 | if s != expected { 103 | t.Errorf("failed to stringify %02x, expected '%s', got '%s'", p, expected, s) 104 | } 105 | } 106 | t.Run(fmt.Sprintf("%x", p), f) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /encoding/tpdu/firstoctet.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package tpdu 6 | 7 | // FirstOctet is the first byte of a SMS-TPDU. 8 | type FirstOctet byte 9 | 10 | // LP returns true if the TP-LP flag is set. 11 | func (f FirstOctet) LP() bool { 12 | return f&FoLP != 0 13 | } 14 | 15 | // MMS returns true if the TP-MMS flag is set. 16 | func (f FirstOctet) MMS() bool { 17 | return f&FoMMS != 0 18 | } 19 | 20 | // MTI returns the message type field. 21 | func (f FirstOctet) MTI() MessageType { 22 | return MessageType(f & FoMTIMask) 23 | } 24 | 25 | // RD returns true if the TP-RD flag is set. 26 | func (f FirstOctet) RD() bool { 27 | return f&FoRD != 0 28 | } 29 | 30 | // RP returns true if the TP-RP flag is set. 31 | func (f FirstOctet) RP() bool { 32 | return f&FoRP != 0 33 | } 34 | 35 | // SRI returns true if the TP-SRI flag is set. 36 | func (f FirstOctet) SRI() bool { 37 | return f&FoSRI != 0 38 | } 39 | 40 | // SRR returns true if the TP-SRR flag is set. 41 | func (f FirstOctet) SRR() bool { 42 | return f&FoSRR != 0 43 | } 44 | 45 | // SRQ returns true if the TP-SRQ flag is set. 46 | func (f FirstOctet) SRQ() bool { 47 | return f&FoSRQ != 0 48 | } 49 | 50 | // VPF returns the TP-VPF field. 51 | func (f FirstOctet) VPF() ValidityPeriodFormat { 52 | return ValidityPeriodFormat((f & FoVPFMask) >> FoVPFShift) 53 | } 54 | 55 | // WithMTI returns a FirstOctet with the TP-MTI field set. 56 | func (f FirstOctet) WithMTI(mti MessageType) FirstOctet { 57 | f &^= FoMTIMask 58 | f |= FirstOctet(mti << FoMTIShift) 59 | return f 60 | } 61 | 62 | // WithVPF returns a FirstOctet with the TP-VPF field set. 63 | func (f FirstOctet) WithVPF(vpf ValidityPeriodFormat) FirstOctet { 64 | f &^= FoVPFMask 65 | f |= FirstOctet(vpf << FoVPFShift) 66 | return f 67 | } 68 | 69 | // UDHI returns true if the TP-UDHI flag is set. 70 | func (f FirstOctet) UDHI() bool { 71 | return f&FoUDHI != 0 72 | } 73 | 74 | const ( 75 | // FirstOctet bit fields 76 | 77 | // FoMTIMask masks the bit for the TP-MTI field 78 | FoMTIMask = 0x3 79 | 80 | // FoMTIShift defines the shift required to move the MTI field to/from bit 0 81 | FoMTIShift = 0 82 | 83 | // FoMMS defines the TP-MMS More Messages to Send bit 84 | // 85 | // Only applies to SMS-DELIVER and SMS-STATUS-REPORT 86 | FoMMS = 0x4 87 | 88 | // FoRD defines the TP-RD Reject Duplicates bit 89 | // 90 | // Only applies to SMS-SUBMIT 91 | FoRD = 0x4 92 | 93 | // FoLP defines the TP-LP Loop Prevention bit 94 | // 95 | // Only applies to SMS-DELIVER and SMS-STATUS-REPORT 96 | FoLP = 0x8 97 | 98 | // FoVPFMask masks the bit for the TP-VPF field 99 | // 100 | // Only applies to SMS-SUBMIT 101 | FoVPFMask = 0x18 102 | 103 | // FoVPFShift defines the shift required to move the VPF field to/from bit 0 104 | FoVPFShift = 3 105 | 106 | // FoSRI defines the TP-SRI bit 107 | // 108 | // Only applies to SMS-DELIVER 109 | FoSRI = 0x20 110 | 111 | // FoSRR defines the TP-SRR bit 112 | // 113 | // Only applies to the SMS-SUBMIT and SMS-COMMAND 114 | FoSRR = 0x20 115 | 116 | // FoSRQ defines the TP-SRQ bit 117 | // 118 | // Only applies to the SMS-STATUS-REPORT 119 | FoSRQ = 0x20 120 | 121 | // FoUDHI defines the TP-UDHI bit 122 | FoUDHI = 0x40 123 | 124 | // FoRP defines the TP-RP bit 125 | // 126 | // Only applies to the SMS-SUBMIT and SMS-DELIVER 127 | FoRP = 0x80 128 | ) 129 | -------------------------------------------------------------------------------- /encoding/tpdu/firstoctet_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package tpdu_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/warthog618/sms/encoding/tpdu" 12 | ) 13 | 14 | func TestFirstOctetLP(t *testing.T) { 15 | assert.False(t, tpdu.FirstOctet(0).LP()) 16 | assert.True(t, tpdu.FirstOctet(tpdu.FoLP).LP()) 17 | } 18 | 19 | func TestFirstOctetMMS(t *testing.T) { 20 | assert.False(t, tpdu.FirstOctet(0).MMS()) 21 | assert.True(t, tpdu.FirstOctet(tpdu.FoMMS).MMS()) 22 | } 23 | 24 | func TestFirstMTI(t *testing.T) { 25 | patterns := []struct { 26 | inout tpdu.MessageType 27 | }{ 28 | {tpdu.MtCommand}, 29 | {tpdu.MtDeliver}, 30 | {tpdu.MtSubmit}, 31 | {tpdu.MtReserved}, 32 | } 33 | for _, p := range patterns { 34 | assert.Equal(t, p.inout, tpdu.FirstOctet(p.inout).MTI()) 35 | } 36 | } 37 | func TestFirstOctetRD(t *testing.T) { 38 | assert.False(t, tpdu.FirstOctet(0).RD()) 39 | assert.True(t, tpdu.FirstOctet(tpdu.FoRD).RD()) 40 | } 41 | 42 | func TestFirstOctetRP(t *testing.T) { 43 | assert.False(t, tpdu.FirstOctet(0).RP()) 44 | assert.True(t, tpdu.FirstOctet(tpdu.FoRP).RP()) 45 | } 46 | 47 | func TestFirstOctetSRI(t *testing.T) { 48 | assert.False(t, tpdu.FirstOctet(0).SRI()) 49 | assert.True(t, tpdu.FirstOctet(tpdu.FoSRI).SRI()) 50 | } 51 | 52 | func TestFirstOctetSRR(t *testing.T) { 53 | assert.False(t, tpdu.FirstOctet(0).SRR()) 54 | assert.True(t, tpdu.FirstOctet(tpdu.FoSRR).SRR()) 55 | } 56 | 57 | func TestFirstOctetSRQ(t *testing.T) { 58 | assert.False(t, tpdu.FirstOctet(0).SRQ()) 59 | assert.True(t, tpdu.FirstOctet(tpdu.FoSRQ).SRQ()) 60 | } 61 | 62 | func TestFirstOctetUDHI(t *testing.T) { 63 | assert.False(t, tpdu.FirstOctet(0).UDHI()) 64 | assert.True(t, tpdu.FirstOctet(tpdu.FoUDHI).UDHI()) 65 | } 66 | 67 | func TestFirstOctetVPF(t *testing.T) { 68 | patterns := []struct { 69 | in tpdu.FirstOctet 70 | out tpdu.ValidityPeriodFormat 71 | }{ 72 | {0, tpdu.VpfNotPresent}, 73 | {0x10, tpdu.VpfRelative}, 74 | {0x18, tpdu.VpfAbsolute}, 75 | {0x08, tpdu.VpfEnhanced}, 76 | } 77 | for _, p := range patterns { 78 | fo := tpdu.FirstOctet(p.in) 79 | assert.Equal(t, p.out, fo.VPF()) 80 | } 81 | } 82 | 83 | func TestFirstWithMTI(t *testing.T) { 84 | patterns := []struct { 85 | inout tpdu.MessageType 86 | }{ 87 | {tpdu.MtCommand}, 88 | {tpdu.MtDeliver}, 89 | {tpdu.MtSubmit}, 90 | {tpdu.MtReserved}, 91 | } 92 | for _, p := range patterns { 93 | fo := tpdu.FirstOctet(0).WithMTI(p.inout) 94 | assert.Equal(t, p.inout, fo.MTI()) 95 | } 96 | } 97 | 98 | func TestFirstOctetWithVPF(t *testing.T) { 99 | patterns := []struct { 100 | inout tpdu.ValidityPeriodFormat 101 | }{ 102 | {tpdu.VpfNotPresent}, 103 | {tpdu.VpfRelative}, 104 | {tpdu.VpfAbsolute}, 105 | {tpdu.VpfEnhanced}, 106 | } 107 | for _, p := range patterns { 108 | assert.Equal(t, p.inout, tpdu.FirstOctet(0).WithVPF(p.inout).VPF()) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /encoding/tpdu/options.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package tpdu 6 | 7 | // Option applies a construction option to a TPDU. 8 | type Option interface { 9 | ApplyTPDUOption(*TPDU) error 10 | } 11 | 12 | // DAOption specifies the DA for the TPDU. 13 | type DAOption struct { 14 | addr Address 15 | } 16 | 17 | // ApplyTPDUOption applies the DA to the TPDU. 18 | func (o DAOption) ApplyTPDUOption(t *TPDU) error { 19 | t.DA = o.addr 20 | return nil 21 | } 22 | 23 | // WithDA creates a DAOption to apply to a TPDU. 24 | func WithDA(addr Address) DAOption { 25 | return DAOption{addr} 26 | } 27 | 28 | // OAOption specifies the OA for the TPDU. 29 | type OAOption struct { 30 | addr Address 31 | } 32 | 33 | // ApplyTPDUOption applies the OA to the TPDU. 34 | func (o OAOption) ApplyTPDUOption(t *TPDU) error { 35 | t.OA = o.addr 36 | return nil 37 | } 38 | 39 | // WithOA creates a OAOption to apply to a TPDU. 40 | func WithOA(addr Address) OAOption { 41 | return OAOption{addr} 42 | } 43 | 44 | // UDHOption specifies the UDH for the TPDU. 45 | type UDHOption struct { 46 | udh UserDataHeader 47 | } 48 | 49 | // ApplyTPDUOption applies the UDH to the TPDU. 50 | func (o UDHOption) ApplyTPDUOption(t *TPDU) error { 51 | t.UDH = o.udh 52 | return nil 53 | } 54 | 55 | // WithUDH creates a UDHOption to apply to a TPDU. 56 | func WithUDH(udh UserDataHeader) UDHOption { 57 | return UDHOption{udh} 58 | } 59 | -------------------------------------------------------------------------------- /encoding/tpdu/options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Kent Gibson . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package tpdu_test 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "github.com/warthog618/sms/encoding/tpdu" 14 | ) 15 | 16 | func TestWithDA(t *testing.T) { 17 | addr := tpdu.NewAddress(tpdu.FromNumber("12345")) 18 | s, err := tpdu.New(tpdu.WithDA(addr)) 19 | require.Nil(t, err) 20 | assert.Equal(t, addr, s.DA) 21 | } 22 | 23 | func TestWithOA(t *testing.T) { 24 | addr := tpdu.NewAddress(tpdu.FromNumber("12345")) 25 | s, err := tpdu.New(tpdu.WithOA(addr)) 26 | require.Nil(t, err) 27 | assert.Equal(t, addr, s.OA) 28 | } 29 | 30 | func TestWithUDH(t *testing.T) { 31 | udh := tpdu.UserDataHeader{ 32 | tpdu.InformationElement{ID: 0, Data: []byte{3, 2, 1}}, 33 | } 34 | s, err := tpdu.New(tpdu.WithUDH(udh)) 35 | require.Nil(t, err) 36 | assert.Equal(t, udh, s.UDH) 37 | } 38 | 39 | func TestWithMTI(t *testing.T) { 40 | s, err := tpdu.New(tpdu.MtSubmit) 41 | require.Nil(t, err) 42 | assert.Equal(t, tpdu.MtSubmit, s.MTI()) 43 | } 44 | 45 | func TestWithDirection(t *testing.T) { 46 | s, err := tpdu.New(tpdu.MO) 47 | require.Nil(t, err) 48 | assert.Equal(t, tpdu.MO, s.Direction) 49 | } 50 | -------------------------------------------------------------------------------- /encoding/tpdu/parameterindicator.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package tpdu 6 | 7 | import "strings" 8 | 9 | // PI is the parameter indicator bitfield. 10 | type PI byte 11 | 12 | // PID returns true if a PID field is present in the TPDU. 13 | func (p PI) PID() bool { 14 | return p&PiPID != 0 15 | } 16 | 17 | // DCS returns true if a DCS field is present in the TPDU. 18 | func (p PI) DCS() bool { 19 | return p&PiDCS != 0 20 | } 21 | 22 | // UDL returns true if a UDL, and hence a UD, field is present in the TPDU. 23 | func (p PI) UDL() bool { 24 | return p&PiUDL != 0 25 | } 26 | 27 | func (p PI) String() string { 28 | if p == 0 { 29 | return "0" 30 | } 31 | elems := []string{} 32 | if p.PID() { 33 | elems = append(elems, "PID") 34 | } 35 | if p.DCS() { 36 | elems = append(elems, "DCS") 37 | } 38 | if p.UDL() { 39 | elems = append(elems, "UDL") 40 | } 41 | return strings.Join(elems, "|") 42 | } 43 | 44 | const ( 45 | // PI bit fields 46 | 47 | // PiPID indicates a TP-PID field is present in the TPDU 48 | PiPID = 1 << iota 49 | 50 | // PiDCS indicates a TP-DCS field is present in the TPDU 51 | PiDCS 52 | 53 | // PiUDL indicates a TP-UDL field is present in the TPDU 54 | PiUDL 55 | ) 56 | -------------------------------------------------------------------------------- /encoding/tpdu/parameterindicator_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package tpdu_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/warthog618/sms/encoding/tpdu" 12 | ) 13 | 14 | func TestParameterIndicatorPID(t *testing.T) { 15 | assert.False(t, tpdu.PI(0).PID()) 16 | assert.True(t, tpdu.PI(tpdu.PiPID).PID()) 17 | } 18 | 19 | func TestParameterIndicatorDCS(t *testing.T) { 20 | assert.False(t, tpdu.PI(0).DCS()) 21 | assert.True(t, tpdu.PI(tpdu.PiDCS).DCS()) 22 | } 23 | 24 | func TestParameterIndicatorUDL(t *testing.T) { 25 | assert.False(t, tpdu.PI(0).UDL()) 26 | assert.True(t, tpdu.PI(tpdu.PiUDL).UDL()) 27 | } 28 | 29 | func TestParameterIndicatorString(t *testing.T) { 30 | patterns := []struct { 31 | in tpdu.PI 32 | out string 33 | }{ 34 | {0, "0"}, 35 | {tpdu.PiPID, "PID"}, 36 | {tpdu.PiDCS, "DCS"}, 37 | {tpdu.PiUDL, "UDL"}, 38 | {tpdu.PiPID | tpdu.PiDCS, "PID|DCS"}, 39 | {tpdu.PiPID | tpdu.PiUDL, "PID|UDL"}, 40 | {tpdu.PiDCS | tpdu.PiUDL, "DCS|UDL"}, 41 | {tpdu.PiPID | tpdu.PiDCS | tpdu.PiUDL, "PID|DCS|UDL"}, 42 | } 43 | for _, p := range patterns { 44 | assert.Equal(t, p.out, p.in.String()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /encoding/tpdu/timestamp.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package tpdu 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/warthog618/sms/encoding/bcd" 11 | ) 12 | 13 | // Timestamp represents a SCTS timestamp, as defined in 3GPP TS 23.040 Section 14 | // 9.2.3.11. 15 | type Timestamp struct { 16 | time.Time 17 | } 18 | 19 | func (t Timestamp) String() string { 20 | return t.Format("2006-01-02 15:04:05 -0700") 21 | } 22 | 23 | // MarshalBinary encodes the SCTS timestamp into binary. 24 | func (t *Timestamp) MarshalBinary() (dst []byte, err error) { 25 | dst = make([]byte, 7) 26 | y := t.Year() % 100 27 | f := []int{y, int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second()} 28 | for i, v := range f { 29 | dst[i], err = bcd.Encode(v) 30 | // this should never trip, assuming the time methods return values in 31 | // the expected ranges... 32 | if err != nil { 33 | return nil, err 34 | } 35 | } 36 | _, tz := t.Zone() 37 | dst[6], err = bcd.EncodeSigned(tz / (15 * 60)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return dst, nil 42 | } 43 | 44 | // UnmarshalBinary decodes the SCTS timestamp from binary. 45 | func (t *Timestamp) UnmarshalBinary(src []byte) error { 46 | if len(src) < 7 { 47 | return ErrUnderflow 48 | } 49 | i := make([]int, 6) 50 | var err error 51 | for idx := 0; idx < 6; idx++ { 52 | i[idx], err = bcd.Decode(src[idx]) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | tz, err := bcd.DecodeSigned(src[6]) 58 | if err != nil { 59 | return err 60 | } 61 | loc := time.UTC 62 | if tz != 0 { 63 | tzoffset := tz * 15 * 60 // seconds east of UTC 64 | loc = time.FixedZone("SCTS", tzoffset) 65 | } 66 | year := i[0] 67 | if year < 70 { 68 | year += 2000 69 | } else { 70 | year += 1900 71 | } 72 | mon := time.Month(i[1]) 73 | t.Time = time.Date(year, mon, i[2], i[3], i[4], i[5], 0, loc) 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /encoding/tpdu/timestamp_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package tpdu_test 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | "github.com/warthog618/sms/encoding/bcd" 15 | "github.com/warthog618/sms/encoding/tpdu" 16 | ) 17 | 18 | type marshalTimestampPattern struct { 19 | name string 20 | in tpdu.Timestamp 21 | out []byte 22 | err error 23 | } 24 | 25 | func TestMarhalBinary(t *testing.T) { 26 | patterns := []marshalTimestampPattern{ 27 | { 28 | "19700101", 29 | tpdu.Timestamp{ 30 | Time: time.Date(1970, time.January, 1, 1, 2, 3, 0, time.UTC), 31 | }, 32 | []byte{0x07, 0x10, 0x10, 0x10, 0x20, 0x30, 0x00}, 33 | nil, 34 | }, 35 | { 36 | "19991231", 37 | tpdu.Timestamp{ 38 | Time: time.Date(1999, time.December, 31, 23, 59, 59, 0, time.FixedZone("SCTS", -15*60)), 39 | }, 40 | []byte{0x99, 0x21, 0x13, 0x32, 0x95, 0x95, 0x18}, 41 | nil, 42 | }, 43 | { 44 | "20001231", 45 | tpdu.Timestamp{Time: time.Date(2000, time.December, 31, 23, 59, 59, 0, time.FixedZone("SCTS", 15*60))}, 46 | []byte{0x00, 0x21, 0x13, 0x32, 0x95, 0x95, 0x10}, 47 | nil, 48 | }, 49 | { 50 | "20170831", 51 | tpdu.Timestamp{ 52 | Time: time.Date(2017, time.August, 31, 11, 21, 54, 0, time.FixedZone("any", 8*3600)), 53 | }, 54 | []byte{0x71, 0x80, 0x13, 0x11, 0x12, 0x45, 0x23}, 55 | nil, 56 | }, 57 | { 58 | "20700101", 59 | tpdu.Timestamp{ 60 | Time: time.Date(2070, time.January, 1, 1, 2, 3, 0, time.UTC), 61 | }, 62 | []byte{0x07, 0x10, 0x10, 0x10, 0x20, 0x30, 0x00}, 63 | nil, 64 | }, 65 | { 66 | "21001231", 67 | tpdu.Timestamp{ 68 | Time: time.Date(2100, time.December, 31, 23, 59, 59, 0, time.FixedZone("SCTS", 15*60)), 69 | }, 70 | []byte{0x00, 0x21, 0x13, 0x32, 0x95, 0x95, 0x10}, 71 | nil, 72 | }, 73 | { 74 | "20701231", 75 | tpdu.Timestamp{ 76 | Time: time.Date(2070, time.December, 31, 23, 59, 59, 0, time.FixedZone("SCTS", 24*3600)), 77 | }, 78 | nil, 79 | bcd.ErrInvalidInteger(96), 80 | }, 81 | // how to trigger invalid integer in date (other than tz)?? 82 | } 83 | for _, p := range patterns { 84 | f := func(t *testing.T) { 85 | b, err := p.in.MarshalBinary() 86 | require.Equal(t, p.err, err) 87 | assert.Equal(t, p.out, b) 88 | } 89 | t.Run(p.name, f) 90 | } 91 | } 92 | 93 | func TestUnmarhalBinary(t *testing.T) { 94 | patterns := []struct { 95 | name string 96 | in []byte 97 | out tpdu.Timestamp 98 | err error 99 | }{ 100 | { 101 | "19700101", 102 | []byte{0x07, 0x10, 0x10, 0x10, 0x20, 0x30, 0x00}, 103 | tpdu.Timestamp{ 104 | Time: time.Date(1970, time.January, 1, 1, 2, 3, 0, time.UTC), 105 | }, 106 | nil, 107 | }, 108 | { 109 | "19991231", 110 | []byte{0x99, 0x21, 0x13, 0x32, 0x95, 0x95, 0x18}, 111 | tpdu.Timestamp{ 112 | Time: time.Date(1999, time.December, 31, 23, 59, 59, 0, time.FixedZone("SCTS", -15*60)), 113 | }, 114 | nil, 115 | }, 116 | { 117 | "20001231", 118 | []byte{0x00, 0x21, 0x13, 0x32, 0x95, 0x95, 0x10}, 119 | tpdu.Timestamp{ 120 | Time: time.Date(2000, time.December, 31, 23, 59, 59, 0, time.FixedZone("SCTS", 15*60)), 121 | }, 122 | nil, 123 | }, 124 | { 125 | "20170831", 126 | []byte{0x71, 0x80, 0x13, 0x11, 0x12, 0x45, 0x23}, 127 | tpdu.Timestamp{ 128 | Time: time.Date(2017, time.August, 31, 11, 21, 54, 0, time.FixedZone("SCTS", 8*3600)), 129 | }, 130 | nil, 131 | }, 132 | { 133 | "short", 134 | []byte{0x71, 0x80, 0x13, 0x11, 0x12, 0x45}, 135 | tpdu.Timestamp{}, 136 | tpdu.ErrUnderflow, 137 | }, 138 | { 139 | "invalid digit", 140 | []byte{0xa1, 0x80, 0x13, 0x11, 0x12, 0x45, 0x00}, 141 | tpdu.Timestamp{}, 142 | bcd.ErrInvalidOctet(0xa1), 143 | }, 144 | { 145 | "invalid signed digit", 146 | []byte{0x71, 0x80, 0x13, 0x11, 0x12, 0x45, 0xa0}, 147 | tpdu.Timestamp{}, 148 | bcd.ErrInvalidOctet(0xa0), 149 | }, 150 | } 151 | for _, p := range patterns { 152 | f := func(t *testing.T) { 153 | s := tpdu.Timestamp{} 154 | err := s.UnmarshalBinary(p.in) 155 | if err != p.err { 156 | t.Fatalf("error unmarshalling %v: %v", p.in, err) 157 | } 158 | if !s.Equal(p.out.Time) { 159 | t.Fatalf("failed to unmarshal %v: expected %v, got %v", p.in, p.out, s) 160 | } 161 | szn, szo := s.Zone() 162 | ozn, ozo := p.out.Zone() 163 | assert.Equal(t, ozn, szn) 164 | assert.Equal(t, ozo, szo) 165 | } 166 | t.Run(p.name, f) 167 | } 168 | } 169 | 170 | func TestTimestampString(t *testing.T) { 171 | patterns := []struct { 172 | in time.Time 173 | out string 174 | }{ 175 | { 176 | time.Date(2000, 11, 2, 3, 4, 5, 65, time.FixedZone("ABC", 22800)), 177 | "2000-11-02 03:04:05 +0620", 178 | }, 179 | { 180 | time.Date(2000, 11, 2, 3, 4, 5, 0, time.FixedZone("ABC", 22800)), 181 | "2000-11-02 03:04:05 +0620", 182 | }, 183 | { 184 | time.Date(2000, 11, 2, 3, 4, 5, 0, time.FixedZone("TEST", 0)), 185 | "2000-11-02 03:04:05 +0000", 186 | }, 187 | } 188 | for _, p := range patterns { 189 | f := func(t *testing.T) { 190 | out := tpdu.Timestamp{p.in}.String() 191 | assert.Equal(t, p.out, out) 192 | } 193 | t.Run(fmt.Sprintf("%02x", p.in), f) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /encoding/tpdu/validityperiod.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package tpdu 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/warthog618/sms/encoding/bcd" 11 | ) 12 | 13 | // ValidityPeriod represents the validity period as defined in 3GPP TS 34.040 14 | // Section 9.2.3.12. 15 | type ValidityPeriod struct { 16 | Format ValidityPeriodFormat 17 | Time Timestamp // for VpfAbsolute 18 | Duration time.Duration // for VpfRelative and VpfEnhanced 19 | EFI byte // enhanced functionality indicator - first octet of enhanced format 20 | } 21 | 22 | // EnhancedFormat extracts the format field from the EFI. 23 | func EnhancedFormat(efi byte) EnhancedValidityPeriodFormat { 24 | return EnhancedValidityPeriodFormat(efi & 0x07) 25 | } 26 | 27 | // SetAbsolute sets the validity period to an absolute time. 28 | func (v *ValidityPeriod) SetAbsolute(t Timestamp) { 29 | v.Format = VpfAbsolute 30 | v.Duration = 0 31 | v.Time = t 32 | v.EFI = 0 33 | } 34 | 35 | // SetRelative sets the validity period to a relative time. 36 | func (v *ValidityPeriod) SetRelative(d time.Duration) { 37 | v.Format = VpfRelative 38 | v.Duration = d 39 | v.Time = Timestamp{} 40 | v.EFI = 0 41 | } 42 | 43 | // SetEnhanced sets the validity period to an enhnaced format as determined 44 | // from the functionality identifier (efi). 45 | func (v *ValidityPeriod) SetEnhanced(d time.Duration, efi byte) { 46 | v.Format = VpfEnhanced 47 | v.Duration = d 48 | v.Time = Timestamp{} 49 | v.EFI = efi 50 | } 51 | 52 | // MarshalBinary marshals a ValidityPeriod. 53 | func (v *ValidityPeriod) MarshalBinary() ([]byte, error) { 54 | switch v.Format { 55 | case VpfAbsolute: 56 | return v.Time.MarshalBinary() 57 | case VpfEnhanced: 58 | evpf := EnhancedFormat(v.EFI) 59 | if evpf > EvpfRelativeHHMMSS { 60 | return nil, EncodeError("fi", ErrInvalid) 61 | } 62 | dst := make([]byte, 7) 63 | dst[0] = v.EFI 64 | switch evpf { 65 | case EvpfRelative: 66 | dst[1] = durationToRelative(v.Duration) 67 | case EvpfRelativeSeconds: 68 | secs := v.Duration / time.Second 69 | if secs > 255 { 70 | secs = 255 71 | } 72 | dst[1] = byte(secs) 73 | case EvpfRelativeHHMMSS: 74 | f := []int{int(v.Duration.Hours()) % 100, int(v.Duration.Minutes()) % 60, int(v.Duration.Seconds()) % 60} 75 | for i, tf := range f { 76 | t, err := bcd.Encode(tf) 77 | // this should never trip, as the encoded values should always be valid, but just in case... 78 | if err != nil { 79 | return nil, EncodeError("enhanced", err) 80 | } 81 | dst[i+1] = t 82 | } 83 | } 84 | return dst, nil 85 | case VpfRelative: 86 | t := durationToRelative(v.Duration) 87 | return []byte{t}, nil 88 | case VpfNotPresent: 89 | return nil, nil 90 | } 91 | return nil, EncodeError("vpf", ErrInvalid) 92 | } 93 | 94 | // UnmarshalBinary unmarshals a ValidityPeriod stored in the given format. 95 | // 96 | // Returns the number of bytes read from the src, and any error detected during 97 | // the unmarshalling. 98 | func (v *ValidityPeriod) UnmarshalBinary(src []byte, vpf ValidityPeriodFormat) (int, error) { 99 | v.Format = VpfNotPresent 100 | switch vpf { 101 | case VpfAbsolute: 102 | t := Timestamp{} 103 | err := t.UnmarshalBinary(src) 104 | if err == nil { 105 | v.Time = t 106 | v.Format = vpf 107 | } 108 | return 7, err 109 | case VpfEnhanced: 110 | used, err := v.unmarshalVPEnhanced(src) 111 | if err == nil { 112 | v.Format = vpf 113 | } 114 | return used, err 115 | case VpfRelative: 116 | if len(src) < 1 { 117 | return 0, ErrUnderflow 118 | } 119 | v.Duration = relativeToDuration(src[0]) 120 | v.Format = vpf 121 | return 1, nil 122 | case VpfNotPresent: 123 | return 0, nil 124 | } 125 | return 0, NewDecodeError("vpf", 0, ErrInvalid) 126 | } 127 | 128 | func (v *ValidityPeriod) unmarshalVPEnhanced(src []byte) (int, error) { 129 | if len(src) < 7 { 130 | return 0, ErrUnderflow 131 | } 132 | efi := src[0] 133 | evpf := EnhancedValidityPeriodFormat(efi & 0x7) 134 | used := 0 135 | d := time.Duration(0) 136 | switch evpf { 137 | case EvpfNotPresent: 138 | case EvpfRelative: 139 | d = relativeToDuration(src[1]) 140 | used = 1 141 | case EvpfRelativeSeconds: 142 | d = time.Second * time.Duration(src[1]) 143 | used = 1 144 | case EvpfRelativeHHMMSS: 145 | i := make([]int, 3) 146 | var err error 147 | for idx := 0; idx < 3; idx++ { 148 | i[idx], err = bcd.Decode(src[idx+1]) 149 | if err != nil { 150 | return 4, NewDecodeError("enhanced", 1, err) 151 | } 152 | } 153 | d = time.Duration(i[0])*time.Hour + time.Duration(i[1])*time.Minute + time.Duration(i[2])*time.Second 154 | used = 3 155 | default: 156 | return 7, NewDecodeError("enhanced", 0, ErrInvalid) 157 | } 158 | for i := used + 1; i < 7; i++ { 159 | if src[i] != 0 { 160 | return used + 1, NewDecodeError("enhanced", i, ErrNonZero) 161 | } 162 | } 163 | v.EFI = efi 164 | v.Duration = d 165 | return 7, nil 166 | } 167 | 168 | // ValidityPeriodFormat identifies the format of the ValidityPeriod when encoded to binary. 169 | type ValidityPeriodFormat byte 170 | 171 | const ( 172 | // VpfNotPresent indicates no VP is present. 173 | VpfNotPresent ValidityPeriodFormat = iota 174 | 175 | // VpfEnhanced indicates the VP is stored in enhanced format as per 3GPP TS 176 | // 23.038 Section 9.2.3.12.3. 177 | VpfEnhanced 178 | 179 | // VpfRelative indicates the VP is stored in relative format as per 3GPP TS 180 | // 23.038 Section 9.2.3.12.1. 181 | VpfRelative 182 | 183 | // VpfAbsolute indicates the VP is stored in absolute format as per 3GPP TS 184 | // 23.038 Section 9.2.3.12.2. The absolute format is the same format as the 185 | // SCTS. 186 | VpfAbsolute 187 | ) 188 | 189 | // EnhancedValidityPeriodFormat identifies the subformat of the ValidityPeriod 190 | // when encoded to binary in enhanced format, as per 3GPP TS 23.038 Section 191 | // 9.2.3.12.3 192 | type EnhancedValidityPeriodFormat byte 193 | 194 | const ( 195 | // EvpfNotPresent indicates no VP is present. 196 | EvpfNotPresent EnhancedValidityPeriodFormat = iota 197 | 198 | // EvpfRelative indicates the VP is stored in relative format as per 3GPP 199 | // TS 23.038 Section 9.2.3.12.1. 200 | EvpfRelative 201 | 202 | // EvpfRelativeSeconds indicates the VP is stored in relative format as an 203 | // integer number of seconds, from 0 to 255. 204 | EvpfRelativeSeconds 205 | 206 | // EvpfRelativeHHMMSS indicates the VP is stored in relative format as a 207 | // period of hours, minutes and seconds in semioctet format as per SCTS 208 | // time. 209 | EvpfRelativeHHMMSS 210 | 211 | // All other values currently reserved. 212 | ) 213 | 214 | func durationToRelative(d time.Duration) byte { 215 | switch { 216 | case d < time.Hour*12: 217 | t := byte(d / (time.Minute * 5)) 218 | if t > 1 { 219 | t-- 220 | } 221 | return t 222 | case d < time.Hour*24: 223 | return 119 + byte(d/(time.Minute*30)) 224 | case d < time.Hour*24*30: 225 | return 166 + byte(d/(time.Hour*24)) 226 | case d < time.Hour*24*7*63: 227 | return 192 + byte(d/(time.Hour*24*7)) 228 | default: 229 | return 255 230 | } 231 | } 232 | 233 | func relativeToDuration(t byte) time.Duration { 234 | switch { 235 | case t < 144: 236 | return time.Minute * 5 * time.Duration(t+1) 237 | case t < 168: 238 | return time.Minute * 30 * time.Duration(t-119) 239 | case t < 197: 240 | return time.Hour * 24 * time.Duration(t-166) 241 | default: 242 | return time.Hour * 24 * 7 * time.Duration(t-192) 243 | } 244 | } 245 | 246 | func (vpf ValidityPeriodFormat) String() string { 247 | switch vpf { 248 | default: 249 | return "Unknown" 250 | case VpfNotPresent: 251 | return "Not Present" 252 | case VpfAbsolute: 253 | return "Absolute" 254 | case VpfRelative: 255 | return "Relative" 256 | case VpfEnhanced: 257 | return "Enhanced" 258 | } 259 | } 260 | 261 | func (evpf EnhancedValidityPeriodFormat) String() string { 262 | switch evpf { 263 | default: 264 | return "Unknown" 265 | case EvpfNotPresent: 266 | return "Not Present" 267 | case EvpfRelative: 268 | return "Relative" 269 | case EvpfRelativeSeconds: 270 | return "Relative Seconds" 271 | case EvpfRelativeHHMMSS: 272 | return "Relative HHMMSS" 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /encoding/ucs2/ucs2.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | // Package ucs2 provides conversions between UCS-2 and UTF-8. 6 | package ucs2 7 | 8 | import ( 9 | "encoding/binary" 10 | "errors" 11 | "fmt" 12 | "unicode/utf16" 13 | ) 14 | 15 | // Decode converts an array of UCS2 characters into an array of runes. 16 | // 17 | // As the UCS2 characters are packed into a byte array, the length of the byte 18 | // array provided must be even. 19 | func Decode(src []byte) ([]rune, error) { 20 | if len(src) == 0 { 21 | return nil, nil 22 | } 23 | if len(src)&0x01 == 0x01 { 24 | return nil, ErrInvalidLength 25 | } 26 | l := len(src) / 2 27 | dst := make([]rune, 0, l) 28 | for ri := 0; ri < len(src)-1; ri = ri + 2 { 29 | r := rune(binary.BigEndian.Uint16(src[ri:])) 30 | if utf16.IsSurrogate(r) { 31 | if ri >= len(src)-3 { 32 | return dst, ErrDanglingSurrogate(src[ri:]) 33 | } 34 | ri += 2 35 | r2 := rune(binary.BigEndian.Uint16(src[ri:])) 36 | r = utf16.DecodeRune(r, r2) 37 | } 38 | dst = append(dst, r) 39 | } 40 | return dst, nil 41 | } 42 | 43 | // Encode converts an array of UCS2 runes into an array of bytes, where pairs 44 | // of bytes (in Big Endian) represent a UCS2 character. 45 | func Encode(src []rune) []byte { 46 | if len(src) == 0 { 47 | return nil 48 | } 49 | u := utf16.Encode(src) 50 | dst := make([]byte, len(u)*2) 51 | wi := 0 52 | for _, r := range u { 53 | binary.BigEndian.PutUint16(dst[wi:], uint16(r)) 54 | wi += 2 55 | } 56 | return dst 57 | } 58 | 59 | // ErrDanglingSurrogate indicates only half of a surrogate pair is provided at 60 | // the end of the byte array being decoded. 61 | type ErrDanglingSurrogate []byte 62 | 63 | func (e ErrDanglingSurrogate) Error() string { 64 | return fmt.Sprintf("ucs2: dangling surrogate: %#v", []byte(e)) 65 | } 66 | 67 | var ( 68 | // ErrInvalidLength indicates the binary provided has an invalid (odd) length. 69 | ErrInvalidLength = errors.New("ucs2: length must be even") 70 | ) 71 | -------------------------------------------------------------------------------- /encoding/ucs2/ucs2_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2018 Kent Gibson . 4 | 5 | package ucs2_test 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/warthog618/sms/encoding/ucs2" 13 | ) 14 | 15 | type decodePattern struct { 16 | name string 17 | in []byte 18 | out []rune 19 | err error 20 | } 21 | 22 | func TestDecode(t *testing.T) { 23 | patterns := []decodePattern{ 24 | { 25 | "nil", 26 | nil, 27 | nil, 28 | nil, 29 | }, 30 | { 31 | "empty", 32 | []byte(""), 33 | nil, 34 | nil, 35 | }, 36 | { 37 | "odd", 38 | []byte{1, 2, 3, 4, 5}, 39 | nil, 40 | ucs2.ErrInvalidLength, 41 | }, 42 | { 43 | "howdy", 44 | []byte{ 45 | 0x4F, 0x60, 0x59, 0x7D, 0xFF, 0x01, 0x00, 0x48, 0x00, 0x6F, 46 | 0x00, 0x77, 0x00, 0x64, 0x00, 0x79, 47 | }, 48 | []rune("你好!Howdy"), 49 | nil, 50 | }, 51 | { 52 | "grin", 53 | []byte{0xd8, 0x3d, 0xde, 0x01}, 54 | []rune("😁"), 55 | nil, 56 | }, 57 | { 58 | "dangling surrogate", 59 | []byte{ 60 | 0x00, 0x48, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x64, 0x00, 0x79, 61 | 0xd8, 0x3d, 62 | }, 63 | []rune("Howdy"), 64 | ucs2.ErrDanglingSurrogate([]byte{0xD8, 0x3D}), 65 | }, 66 | } 67 | for _, p := range patterns { 68 | f := func(t *testing.T) { 69 | dst, err := ucs2.Decode(p.in) 70 | assert.Equal(t, p.err, err) 71 | assert.Equal(t, p.out, dst) 72 | } 73 | t.Run(p.name, f) 74 | } 75 | } 76 | 77 | type encodePattern struct { 78 | name string 79 | in []rune 80 | out []byte 81 | } 82 | 83 | func TestEncode(t *testing.T) { 84 | patterns := []encodePattern{ 85 | { 86 | "nil", 87 | nil, 88 | nil, 89 | }, 90 | { 91 | "empty", 92 | []rune(""), 93 | nil, 94 | }, 95 | { 96 | "howdy", 97 | []rune("你好!Howdy"), 98 | []byte{ 99 | 0x4F, 0x60, 0x59, 0x7D, 0xFF, 0x01, 0x00, 0x48, 0x00, 0x6F, 100 | 0x00, 0x77, 0x00, 0x64, 0x00, 0x79, 101 | }, 102 | }, 103 | { 104 | "grin", 105 | []rune("😁"), 106 | []byte{0xd8, 0x3d, 0xde, 0x01}, 107 | }, 108 | } 109 | for _, p := range patterns { 110 | f := func(t *testing.T) { 111 | dst := ucs2.Encode([]rune(p.in)) 112 | assert.Equal(t, p.out, dst) 113 | } 114 | t.Run(p.name, f) 115 | } 116 | } 117 | 118 | func TestErrDanglingSurrogate(t *testing.T) { 119 | patterns := [][]byte{ 120 | {0xd8, 0x00}, 121 | {0xd8, 0xa0}, 122 | {0xd8, 0x0a}, 123 | {0xd8, 0x9a}, 124 | {0xd8, 0xa9}, 125 | {0xd8, 0xff}, 126 | } 127 | for _, p := range patterns { 128 | f := func(t *testing.T) { 129 | err := ucs2.ErrDanglingSurrogate(p) 130 | expected := fmt.Sprintf("ucs2: dangling surrogate: %#v", p) 131 | s := err.Error() 132 | assert.Equal(t, expected, s) 133 | } 134 | t.Run(fmt.Sprintf("%x", p), f) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package sms 6 | 7 | import ( 8 | "errors" 9 | ) 10 | 11 | var ( 12 | // ErrClosed indicates that the collector has been closed and is no longer 13 | // accepting PDUs. 14 | ErrClosed = errors.New("closed") 15 | // ErrDcsConflict indicates the required encoding for user data conflicts with the 16 | // encoding specified in the template TPDU DCS. 17 | ErrDcsConflict = errors.New("DCS conflict") 18 | // ErrDuplicateSegment indicates a segment has arrived for a reassembly 19 | // that already has that segment. 20 | // The segments are duplicates in terms of their concatentation information. 21 | // They may differ in other fields, particularly UD, but those fields 22 | // cannot be used to determine which of the two may better fit the 23 | // reassembly, so the first is kept and the second discarded. 24 | ErrDuplicateSegment = errors.New("duplicate segment") 25 | // ErrReassemblyInconsistency indicates a segment has arrived for a 26 | // reassembly that has a seqno greater than the number of segments in the 27 | // reassembly. 28 | ErrReassemblyInconsistency = errors.New("reassembly inconsistency") 29 | ) 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/warthog618/sms 2 | 3 | require ( 4 | github.com/davecgh/go-spew v1.1.1 // indirect 5 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 6 | github.com/stretchr/testify v1.4.0 7 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 8 | gopkg.in/yaml.v2 v2.2.8 // indirect 9 | ) 10 | 11 | go 1.13 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 9 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 14 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 18 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 20 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 21 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 22 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 23 | -------------------------------------------------------------------------------- /internal/readme/readme.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/warthog618/sms" 9 | "github.com/warthog618/sms/encoding/gsm7/charset" 10 | "github.com/warthog618/sms/encoding/tpdu" 11 | ) 12 | 13 | func main() { 14 | 15 | } 16 | 17 | func encode() { 18 | tpdus, _ := sms.Encode([]byte("hello world")) 19 | for _, p := range tpdus { 20 | b, _ := p.MarshalBinary() 21 | sendPDU(b) // send binary TPDU... 22 | } 23 | } 24 | 25 | func encoder() { 26 | msgChan := make(chan []byte) 27 | e := sms.NewEncoder() 28 | for { 29 | msg := <-msgChan 30 | tpdus, _ := e.Encode(msg) 31 | for _, p := range tpdus { 32 | b, _ := p.MarshalBinary() 33 | sendPDU(b) // send binary TPDU... 34 | } 35 | } 36 | } 37 | 38 | func unmarshal() tpdu.TPDU { 39 | bintpdu := []byte{} 40 | 41 | pdu, _ := sms.Unmarshal(bintpdu) 42 | 43 | return *pdu 44 | } 45 | 46 | func decodeOne() []byte { 47 | pdu := &tpdu.TPDU{} 48 | 49 | msg, _ := sms.Decode([]*tpdu.TPDU{pdu}) 50 | 51 | return msg 52 | } 53 | 54 | func decodeMany() []byte { 55 | tpdus := []*tpdu.TPDU{} 56 | 57 | msg, _ := sms.Decode(tpdus) 58 | 59 | return msg 60 | } 61 | 62 | func collect() { 63 | pduChan := make(chan []byte) 64 | c := sms.NewCollector() 65 | for { 66 | bintpdu := <-pduChan 67 | pdu, _ := sms.Unmarshal(bintpdu) 68 | tpdus, _ := c.Collect(*pdu) 69 | if len(tpdus) > 0 { 70 | msg, _ := sms.Decode(tpdus) 71 | // handle msg... 72 | handleMsg(msg) 73 | } 74 | } 75 | 76 | } 77 | 78 | func to() []tpdu.TPDU { 79 | tpdus, _ := sms.Encode([]byte("hello"), sms.To("12345")) 80 | 81 | return tpdus 82 | } 83 | 84 | func urdu() []tpdu.TPDU { 85 | 86 | tpdus, _ := sms.Encode([]byte("hello ٻ"), sms.WithCharset(charset.Urdu)) 87 | 88 | return tpdus 89 | } 90 | 91 | func deliver() []tpdu.TPDU { 92 | 93 | tpdus, _ := sms.Encode([]byte("hello"), sms.AsDeliver, sms.From("12345")) 94 | 95 | return tpdus 96 | } 97 | 98 | func unmarshalOpt() tpdu.TPDU { 99 | bintpdu := []byte("") 100 | 101 | tpdu, _ := sms.Unmarshal(bintpdu, sms.AsMO) 102 | 103 | return *tpdu 104 | } 105 | 106 | func handleMsg([]byte) { 107 | 108 | } 109 | 110 | func sendPDU([]byte) { 111 | } 112 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | package sms 6 | 7 | import "github.com/warthog618/sms/encoding/tpdu" 8 | 9 | // EncoderOption is an optional mutator for the Encoder. 10 | type EncoderOption interface { 11 | ApplyEncoderOption(*Encoder) 12 | } 13 | 14 | // DecodeOption defines options for Decode. 15 | type DecodeOption interface { 16 | ApplyDecodeOption(*DecodeConfig) 17 | } 18 | 19 | // UnmarshalOption defines options for Unmarhsal. 20 | type UnmarshalOption interface { 21 | ApplyUnmarshalOption(*UnmarshalConfig) 22 | } 23 | 24 | // WithTemplate specifies the TPDU to be used as the template for encoding. 25 | func WithTemplate(t tpdu.TPDU) EncoderOption { 26 | return tpduTemplate{t} 27 | } 28 | 29 | type tpduTemplate struct { 30 | t tpdu.TPDU 31 | } 32 | 33 | func (o tpduTemplate) ApplyEncoderOption(e *Encoder) { 34 | e.pdu = o.t 35 | } 36 | 37 | type templateOption struct { 38 | tpdu.Option 39 | } 40 | 41 | func (o templateOption) ApplyEncoderOption(e *Encoder) { 42 | o.ApplyTPDUOption(&e.pdu) 43 | } 44 | 45 | // WithTemplateOption wraps a TPDU option in a TemplateOption so it can be 46 | // applied to an Encoder template PDU. 47 | func WithTemplateOption(option tpdu.Option) EncoderOption { 48 | return templateOption{option} 49 | } 50 | 51 | var ( 52 | // AsSubmit indicates that generated PDUs will be of type SmsSubmit. 53 | AsSubmit = templateOption{tpdu.SmsSubmit} 54 | 55 | // AsDeliver indicates that generated PDUs will be of type SmsDeliver. 56 | AsDeliver = templateOption{tpdu.SmsDeliver} 57 | 58 | // As8Bit indicates that generated PDUs encode user data as 8bit. 59 | As8Bit = templateOption{tpdu.Dcs8BitData} 60 | 61 | // AsUCS2 indicates that generated PDUs encode user data as UCS2. 62 | AsUCS2 = templateOption{tpdu.DcsUCS2Data} 63 | 64 | // AsMO indicates that the TPDU originated from the mobile station. 65 | AsMO = directionOption{tpdu.MO} 66 | 67 | // AsMT indicates that the TPDU as destined for the mobile station. 68 | AsMT = directionOption{tpdu.MT} 69 | 70 | // WithAllCharsets specifies that all character sets are available for 71 | // encoding or decoding. 72 | // 73 | // This is the default policy for decoding. 74 | WithAllCharsets = AllCharsetsOption{} 75 | 76 | // WithDefaultCharset specifies that only the default character set is 77 | // available for encoding or decoding. 78 | // 79 | // This is the default policy for encoding. 80 | WithDefaultCharset = CharsetOption{} 81 | ) 82 | 83 | // To specifies the DA for a SMS-SUBMIT TPDU. 84 | func To(number string) EncoderOption { 85 | addr := tpdu.NewAddress(tpdu.FromNumber(number)) 86 | return templateOption{tpdu.WithDA(addr)} 87 | } 88 | 89 | // From specifies the OA for a SMS-DELIVER TPDU. 90 | func From(number string) EncoderOption { 91 | addr := tpdu.NewAddress(tpdu.FromNumber(number)) 92 | return templateOption{tpdu.WithOA(addr)} 93 | } 94 | 95 | // AllCharsetsOption specifies that all charactersets are available for encoding. 96 | type AllCharsetsOption struct{} 97 | 98 | // ApplyEncoderOption applies the AllCharsetsOption to an Encoder. 99 | func (o AllCharsetsOption) ApplyEncoderOption(e *Encoder) { 100 | e.eopts = append(e.eopts, tpdu.WithAllCharsets) 101 | } 102 | 103 | // WithCharset creates an CharsetOption. 104 | func WithCharset(nli ...int) CharsetOption { 105 | return CharsetOption{nli} 106 | } 107 | 108 | // CharsetOption defines the character sets available for encoding or decoding. 109 | type CharsetOption struct { 110 | nli []int 111 | } 112 | 113 | // ApplyEncoderOption applies the CharsetOption to an Encoder. 114 | func (o CharsetOption) ApplyEncoderOption(e *Encoder) { 115 | e.eopts = append(e.eopts, tpdu.WithCharset(o.nli...)) 116 | } 117 | 118 | // ApplyDecodeOption applies the CharsetOption to decoding. 119 | func (o CharsetOption) ApplyDecodeOption(cc *DecodeConfig) { 120 | cc.dopts = append(cc.dopts, tpdu.WithCharset(o.nli...)) 121 | } 122 | 123 | // WithLockingCharset creates an LockingCharsetOption. 124 | func WithLockingCharset(nli ...int) LockingCharsetOption { 125 | return LockingCharsetOption{nli} 126 | } 127 | 128 | // LockingCharsetOption defines the locking character sets available for 129 | // encoding or decoding. 130 | type LockingCharsetOption struct { 131 | nli []int 132 | } 133 | 134 | // ApplyEncoderOption applies the LockingCharsetOption to an Encoder. 135 | func (o LockingCharsetOption) ApplyEncoderOption(e *Encoder) { 136 | e.eopts = append(e.eopts, tpdu.WithLockingCharset(o.nli...)) 137 | } 138 | 139 | // ApplyDecodeOption applies the LockingCharsetOption to decoding. 140 | func (o LockingCharsetOption) ApplyDecodeOption(cc *DecodeConfig) { 141 | cc.dopts = append(cc.dopts, tpdu.WithLockingCharset(o.nli...)) 142 | } 143 | 144 | // WithShiftCharset creates an ShiftCharsetOption. 145 | func WithShiftCharset(nli ...int) ShiftCharsetOption { 146 | return ShiftCharsetOption{nli} 147 | } 148 | 149 | // ShiftCharsetOption defines the shift character sets available for encoding 150 | // or decoding. 151 | type ShiftCharsetOption struct { 152 | nli []int 153 | } 154 | 155 | // ApplyEncoderOption applies the ShiftCharsetOption to an Encoder. 156 | func (o ShiftCharsetOption) ApplyEncoderOption(e *Encoder) { 157 | e.eopts = append(e.eopts, tpdu.WithShiftCharset(o.nli...)) 158 | } 159 | 160 | // ApplyDecodeOption applies the ShiftCharsetOption to decoding. 161 | func (o ShiftCharsetOption) ApplyDecodeOption(cc *DecodeConfig) { 162 | cc.dopts = append(cc.dopts, tpdu.WithShiftCharset(o.nli...)) 163 | } 164 | 165 | type directionOption struct { 166 | d tpdu.Direction 167 | } 168 | 169 | func (o directionOption) ApplyUnmarshalOption(d *UnmarshalConfig) { 170 | d.dirn = o.d 171 | } 172 | -------------------------------------------------------------------------------- /sms.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright © 2020 Kent Gibson . 4 | 5 | // Package sms provides encoders and decoders for SMS PDUs. 6 | package sms 7 | 8 | import ( 9 | "github.com/warthog618/sms/encoding/tpdu" 10 | "github.com/warthog618/sms/encoding/ucs2" 11 | ) 12 | 13 | // DecodeConfig contains configuration option for Decode. 14 | type DecodeConfig struct { 15 | dopts []tpdu.UDDecodeOption 16 | } 17 | 18 | // Decode returns the UTF-8 message contained in a set of TPDUs. 19 | // 20 | // For concatenated messages the segments assumed to be the component TPDUs, in 21 | // correct order. This is the case for segments returned by the Collector. It 22 | // can be tested using IsCompleteMessage. 23 | func Decode(segments []*tpdu.TPDU, options ...DecodeOption) ([]byte, error) { 24 | cfg := DecodeConfig{} 25 | for _, option := range options { 26 | option.ApplyDecodeOption(&cfg) 27 | } 28 | if len(cfg.dopts) == 0 { 29 | cfg.dopts = []tpdu.UDDecodeOption{tpdu.WithAllCharsets} 30 | } 31 | bl := 0 32 | ts := make([][]byte, len(segments)) 33 | var danglingSurrogate ucs2.ErrDanglingSurrogate 34 | for i, s := range segments { 35 | a, _ := s.Alphabet() 36 | ud := s.UD 37 | if danglingSurrogate != nil { 38 | ud = append([]byte(danglingSurrogate), ud...) 39 | danglingSurrogate = nil 40 | } 41 | d, err := tpdu.DecodeUserData(ud, s.UDH, a, cfg.dopts...) 42 | if err != nil { 43 | switch e := err.(type) { 44 | case ucs2.ErrDanglingSurrogate: 45 | danglingSurrogate = e 46 | default: 47 | return nil, err 48 | } 49 | } 50 | ts[i] = d 51 | bl += len(d) 52 | } 53 | if danglingSurrogate != nil { 54 | return nil, danglingSurrogate 55 | } 56 | m := make([]byte, 0, bl) 57 | for _, t := range ts { 58 | m = append(m, t...) 59 | } 60 | return m, nil 61 | } 62 | 63 | // IsCompleteMessage confirms that the TPDUs contain all the sgements required 64 | // to reassemble a complete message and are in the correct order. 65 | func IsCompleteMessage(segments []*tpdu.TPDU) bool { 66 | if len(segments) == 0 { 67 | return false 68 | } 69 | baseSegs, _, baseConcatRef, ok := segments[0].ConcatInfo() 70 | if !ok { 71 | return len(segments) == 1 72 | } 73 | if baseSegs != len(segments) { 74 | return false 75 | } 76 | for i, s := range segments { 77 | segs, seqno, concatRef, ok := s.ConcatInfo() 78 | if !ok { 79 | return false 80 | } 81 | if segs != baseSegs { 82 | return false 83 | } 84 | if concatRef != baseConcatRef { 85 | return false 86 | } 87 | if seqno != i+1 { 88 | return false 89 | } 90 | } 91 | return true 92 | } 93 | 94 | // UnmarshalConfig contains configuration options for Unmarshal. 95 | type UnmarshalConfig struct { 96 | dirn tpdu.Direction 97 | } 98 | 99 | // Unmarshal converts a binary SMS TPDU into the corresponding TPDU object. 100 | func Unmarshal(src []byte, options ...UnmarshalOption) (*tpdu.TPDU, error) { 101 | cfg := UnmarshalConfig{} 102 | for _, option := range options { 103 | option.ApplyUnmarshalOption(&cfg) 104 | } 105 | t := tpdu.TPDU{Direction: cfg.dirn} 106 | err := t.UnmarshalBinary(src) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return &t, nil 111 | } 112 | --------------------------------------------------------------------------------