Files
2026-08-09 05:33:21 +08:00

945 lines
23 KiB
Go

package device
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"time"
"unicode/utf16"
)
var gsm7DefaultAlphabet = [128]rune{
'@', '£', '$', '¥', 'è', 'é', 'ù', 'ì',
'ò', 'Ç', '\n', 'Ø', 'ø', '\r', 'Å', 'å',
'Δ', '_', 'Φ', 'Γ', 'Λ', 'Ω', 'Π', 'Ψ',
'Σ', 'Θ', 'Ξ', '\x1b', 'Æ', 'æ', 'ß', 'É',
' ', '!', '"', '#', '¤', '%', '&', '\'',
'(', ')', '*', '+', ',', '-', '.', '/',
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', ':', ';', '<', '=', '>', '?',
'¡', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
'X', 'Y', 'Z', 'Ä', 'Ö', 'Ñ', 'Ü', '§',
'¿', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à',
}
var gsm7ExtensionAlphabet = map[byte]rune{
0x0a: '\f',
0x14: '^',
0x28: '{',
0x29: '}',
0x2f: '\\',
0x3c: '[',
0x3d: '~',
0x3e: ']',
0x40: '|',
0x65: '€',
}
var gsm7Encoder = func() map[rune][]byte {
result := make(map[rune][]byte, len(gsm7DefaultAlphabet)+len(gsm7ExtensionAlphabet))
for code, character := range gsm7DefaultAlphabet {
if code == 0x1b {
continue
}
result[character] = []byte{byte(code)}
}
for code, character := range gsm7ExtensionAlphabet {
result[character] = []byte{0x1b, code}
}
return result
}()
type preparedSMS struct {
to string
encoding SMSEncoding
payload []byte
prompt string
setup []string
tpduLength int
part int
total int
concatReference *int
}
// PrepareSMSSubmitTPDUs applies the same recipient validation, alphabet
// selection and concatenation rules as SendSMS, but returns raw SMS-SUBMIT
// TPDUs suitable for a non-AT transport such as 3GPP SMS over IMS.
func PrepareSMSSubmitTPDUs(recipient, text string) ([]SMSSubmitTPDU, error) {
parts, err := prepareSMSParts(recipient, text)
if err != nil {
return nil, err
}
result := make([]SMSSubmitTPDU, 0, len(parts))
for _, part := range parts {
pduHex := string(part.payload)
encoding := part.encoding
if part.encoding == SMSEncodingGSM7Text {
septets, ok := encodeGSM7(text)
if !ok {
return nil, errors.New("SMS text could not be encoded as GSM-7")
}
pduHex, _, err = encodeSubmitPDU(part.to, septets, nil)
if err != nil {
return nil, err
}
encoding = SMSEncodingGSM7PDU
}
pdu, err := hex.DecodeString(pduHex)
if err != nil || len(pdu) < 2 {
return nil, errors.New("encoded SMS PDU is invalid")
}
smscBytes := int(pdu[0])
if 1+smscBytes >= len(pdu) {
return nil, errors.New("encoded SMS PDU has no TPDU")
}
result = append(result, SMSSubmitTPDU{
To: part.to,
Encoding: encoding,
TPDU: append([]byte(nil), pdu[1+smscBytes:]...),
Part: part.part,
Total: part.total,
ConcatReference: part.concatReference,
})
}
return result, nil
}
// DecodeSMSDeliverTPDU decodes a TPDU carried by RP-DATA. The existing modem
// decoder expects the AT PDU-mode SMSC prefix, so an empty SMSC is prepended.
func DecodeSMSDeliverTPDU(tpdu []byte) (SMSMessage, error) {
if len(tpdu) == 0 {
return SMSMessage{}, errors.New("SMS TPDU is empty")
}
pdu := make([]byte, 1, len(tpdu)+1)
pdu = append(pdu, tpdu...)
return decodeSMSPDU(hex.EncodeToString(pdu))
}
func prepareSMS(recipient, text string) (preparedSMS, error) {
parts, err := prepareSMSPartsWithReference(recipient, text, 0)
if err != nil {
return preparedSMS{}, err
}
if len(parts) != 1 {
return preparedSMS{}, fmt.Errorf(
"%w: message requires %d parts",
ErrSMSTooLong,
len(parts),
)
}
return parts[0], nil
}
func prepareSMSParts(recipient, text string) ([]preparedSMS, error) {
parts, err := prepareSMSPartsWithReference(recipient, text, 0)
if err != nil || len(parts) <= 1 {
return parts, err
}
var reference [1]byte
if _, err := rand.Read(reference[:]); err != nil {
return nil, fmt.Errorf("generate SMS concatenation reference: %w", err)
}
return prepareSMSPartsWithReference(recipient, text, reference[0])
}
func prepareSMSPartsWithReference(
recipient string,
text string,
reference byte,
) ([]preparedSMS, error) {
recipient, err := normalizeSMSRecipient(recipient)
if err != nil {
return nil, err
}
if text == "" {
return nil, ErrSMSEmpty
}
if strings.IndexByte(text, 0) >= 0 {
return nil, errors.New("SMS text contains NUL")
}
septets, gsm7 := encodeGSM7(text)
if gsm7 && len(septets) <= 160 && canSendDirectGSM7(text) {
return []preparedSMS{{
to: recipient,
encoding: SMSEncodingGSM7Text,
payload: []byte(text),
prompt: fmt.Sprintf(`AT+CMGS="%s"`, recipient),
setup: []string{
"AT+CMGF=1",
`AT+CSCS="GSM"`,
// TP-SRR requests a network delivery status report.
"AT+CSMP=49,167,0,0",
},
part: 1,
total: 1,
}}, nil
}
if gsm7 && len(septets) <= 160 {
pdu, tpduLength, err := encodeSubmitPDU(recipient, septets, nil)
if err != nil {
return nil, err
}
return []preparedSMS{{
to: recipient,
encoding: SMSEncodingGSM7PDU,
payload: []byte(pdu),
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
setup: []string{"AT+CMGF=0"},
tpduLength: tpduLength,
part: 1,
total: 1,
}}, nil
}
if gsm7 {
segments := splitGSM7(septets, 153)
if len(segments) > 255 {
return nil, fmt.Errorf(
"%w: GSM-7 requires %d concatenated parts; maximum is 255",
ErrSMSTooLong,
len(segments),
)
}
referenceValue := int(reference)
result := make([]preparedSMS, 0, len(segments))
for index, segment := range segments {
header := concatUDH(reference, len(segments), index+1)
pdu, tpduLength, encodeErr := encodeSubmitPDUWithHeader(
recipient,
segment,
nil,
header,
)
if encodeErr != nil {
return nil, encodeErr
}
result = append(result, preparedSMS{
to: recipient,
encoding: SMSEncodingGSM7PDU,
payload: []byte(pdu),
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
setup: []string{"AT+CMGF=0"},
tpduLength: tpduLength,
part: index + 1,
total: len(segments),
concatReference: &referenceValue,
})
}
return result, nil
}
units := utf16.Encode([]rune(text))
if len(units) <= 70 {
ucs2 := encodeUCS2Units(units)
pdu, tpduLength, err := encodeSubmitPDU(recipient, nil, ucs2)
if err != nil {
return nil, err
}
return []preparedSMS{{
to: recipient,
encoding: SMSEncodingUCS2PDU,
payload: []byte(pdu),
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
setup: []string{"AT+CMGF=0"},
tpduLength: tpduLength,
part: 1,
total: 1,
}}, nil
}
segments := splitUCS2(units, 67)
if len(segments) > 255 {
return nil, fmt.Errorf(
"%w: UCS2 requires %d concatenated parts; maximum is 255",
ErrSMSTooLong,
len(segments),
)
}
referenceValue := int(reference)
result := make([]preparedSMS, 0, len(segments))
for index, segment := range segments {
header := concatUDH(reference, len(segments), index+1)
pdu, tpduLength, encodeErr := encodeSubmitPDUWithHeader(
recipient,
nil,
encodeUCS2Units(segment),
header,
)
if encodeErr != nil {
return nil, encodeErr
}
result = append(result, preparedSMS{
to: recipient,
encoding: SMSEncodingUCS2PDU,
payload: []byte(pdu),
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
setup: []string{"AT+CMGF=0"},
tpduLength: tpduLength,
part: index + 1,
total: len(segments),
concatReference: &referenceValue,
})
}
return result, nil
}
func splitGSM7(septets []byte, maximum int) [][]byte {
var result [][]byte
for len(septets) > 0 {
count := maximum
if count > len(septets) {
count = len(septets)
}
if count < len(septets) && septets[count-1] == 0x1b {
count--
}
result = append(result, append([]byte(nil), septets[:count]...))
septets = septets[count:]
}
return result
}
func splitUCS2(units []uint16, maximum int) [][]uint16 {
var result [][]uint16
for len(units) > 0 {
count := maximum
if count > len(units) {
count = len(units)
}
if count < len(units) &&
utf16.IsSurrogate(rune(units[count-1])) &&
units[count-1] >= 0xd800 && units[count-1] <= 0xdbff &&
units[count] >= 0xdc00 && units[count] <= 0xdfff {
count--
}
result = append(result, append([]uint16(nil), units[:count]...))
units = units[count:]
}
return result
}
func encodeUCS2Units(units []uint16) []byte {
result := make([]byte, 0, len(units)*2)
for _, unit := range units {
result = append(result, byte(unit>>8), byte(unit))
}
return result
}
func concatUDH(reference byte, total, sequence int) []byte {
return []byte{0x05, 0x00, 0x03, reference, byte(total), byte(sequence)}
}
func normalizeSMSRecipient(value string) (string, error) {
var result strings.Builder
for _, character := range strings.TrimSpace(value) {
switch {
case character >= '0' && character <= '9':
result.WriteRune(character)
case character == '+' && result.Len() == 0:
result.WriteRune(character)
case character == ' ' || character == '-' ||
character == '(' || character == ')':
default:
return "", ErrSMSInvalidRecipient
}
}
normalized := result.String()
digits := strings.TrimPrefix(normalized, "+")
if len(digits) < 1 || len(digits) > 20 {
return "", ErrSMSInvalidRecipient
}
if strings.IndexFunc(digits, func(character rune) bool {
return character < '0' || character > '9'
}) >= 0 {
return "", ErrSMSInvalidRecipient
}
return normalized, nil
}
func encodeGSM7(text string) ([]byte, bool) {
result := make([]byte, 0, len(text))
for _, character := range text {
encoded, ok := gsm7Encoder[character]
if !ok {
return nil, false
}
result = append(result, encoded...)
}
return result, true
}
func canSendDirectGSM7(text string) bool {
for _, character := range text {
encoded, ok := gsm7Encoder[character]
if !ok || len(encoded) != 1 || encoded[0] == 0x1b ||
character > 0x7f || byte(character) != encoded[0] {
return false
}
}
return true
}
func encodeSubmitPDU(
recipient string,
gsm7Septets []byte,
ucs2 []byte,
) (string, int, error) {
return encodeSubmitPDUWithHeader(recipient, gsm7Septets, ucs2, nil)
}
func encodeSubmitPDUWithHeader(
recipient string,
gsm7Septets []byte,
ucs2 []byte,
userDataHeader []byte,
) (string, int, error) {
digits := strings.TrimPrefix(recipient, "+")
address, err := encodeSemiOctets(digits)
if err != nil {
return "", 0, err
}
toa := byte(0x81)
if strings.HasPrefix(recipient, "+") {
toa = 0x91
}
// SMS-SUBMIT with TP-SRR set so both AT PDU mode and SMS over IMS can
// receive an SMS-STATUS-REPORT for this submission.
firstOctet := byte(0x21)
if len(userDataHeader) > 0 {
if int(userDataHeader[0])+1 != len(userDataHeader) {
return "", 0, errors.New("invalid SMS concatenation header")
}
firstOctet |= 0x40
}
pdu := []byte{
0x00, // Use the SMSC configured on the SIM.
firstOctet,
0x00, // TP-MR allocated by the modem/network.
byte(len(digits)),
toa,
}
pdu = append(pdu, address...)
pdu = append(pdu, 0x00) // TP-PID
switch {
case gsm7Septets != nil:
headerSeptets := 0
startBit := 0
if len(userDataHeader) > 0 {
headerSeptets = (len(userDataHeader)*8 + 6) / 7
startBit = headerSeptets * 7
}
userDataLength := headerSeptets + len(gsm7Septets)
if userDataLength > 160 {
return "", 0, ErrSMSTooLong
}
packed := packSeptets(gsm7Septets, startBit)
copy(packed, userDataHeader)
pdu = append(pdu, 0x00, byte(userDataLength))
pdu = append(pdu, packed...)
case ucs2 != nil:
userDataLength := len(userDataHeader) + len(ucs2)
if userDataLength > 140 {
return "", 0, ErrSMSTooLong
}
pdu = append(pdu, 0x08, byte(userDataLength))
pdu = append(pdu, userDataHeader...)
pdu = append(pdu, ucs2...)
default:
return "", 0, errors.New("SMS PDU has no user data")
}
return strings.ToUpper(hex.EncodeToString(pdu)), len(pdu) - 1, nil
}
func encodeSemiOctets(digits string) ([]byte, error) {
if digits == "" {
return nil, ErrSMSInvalidRecipient
}
result := make([]byte, (len(digits)+1)/2)
for index := 0; index < len(digits); index += 2 {
low := digits[index]
if low < '0' || low > '9' {
return nil, ErrSMSInvalidRecipient
}
high := byte('F')
if index+1 < len(digits) {
high = digits[index+1]
if high < '0' || high > '9' {
return nil, ErrSMSInvalidRecipient
}
}
highNibble := byte(0x0f)
if high != 'F' {
highNibble = high - '0'
}
result[index/2] = (highNibble << 4) | (low - '0')
}
return result, nil
}
func packSeptets(septets []byte, startBit int) []byte {
if len(septets) == 0 {
return nil
}
bitLength := startBit + len(septets)*7
result := make([]byte, (bitLength+7)/8)
for index, septet := range septets {
bit := startBit + index*7
byteIndex := bit / 8
shift := uint(bit % 8)
result[byteIndex] |= (septet & 0x7f) << shift
if shift > 1 && byteIndex+1 < len(result) {
result[byteIndex+1] |= (septet & 0x7f) >> (8 - shift)
}
}
return result
}
func unpackSeptets(data []byte, count, startBit int) ([]byte, error) {
if count < 0 || startBit < 0 || startBit+count*7 > len(data)*8 {
return nil, errors.New("GSM-7 user data is truncated")
}
result := make([]byte, count)
for index := 0; index < count; index++ {
bit := startBit + index*7
byteIndex := bit / 8
shift := uint(bit % 8)
value := data[byteIndex] >> shift
if shift > 1 && byteIndex+1 < len(data) {
value |= data[byteIndex+1] << (8 - shift)
}
result[index] = value & 0x7f
}
return result, nil
}
func decodeGSM7(septets []byte) (string, error) {
var result strings.Builder
for index := 0; index < len(septets); index++ {
code := septets[index]
if code != 0x1b {
if int(code) >= len(gsm7DefaultAlphabet) {
return result.String(), errors.New("invalid GSM-7 code")
}
result.WriteRune(gsm7DefaultAlphabet[code])
continue
}
index++
if index >= len(septets) {
return result.String(), errors.New("trailing GSM-7 escape")
}
character, ok := gsm7ExtensionAlphabet[septets[index]]
if !ok {
return result.String(), fmt.Errorf(
"unknown GSM-7 extension 0x%02X",
septets[index],
)
}
result.WriteRune(character)
}
return result.String(), nil
}
type pduCursor struct {
data []byte
index int
}
func (cursor *pduCursor) byte() (byte, error) {
if cursor.index >= len(cursor.data) {
return 0, errors.New("SMS PDU is truncated")
}
value := cursor.data[cursor.index]
cursor.index++
return value, nil
}
func (cursor *pduCursor) bytes(count int) ([]byte, error) {
if count < 0 || cursor.index+count > len(cursor.data) {
return nil, errors.New("SMS PDU is truncated")
}
value := cursor.data[cursor.index : cursor.index+count]
cursor.index += count
return value, nil
}
func decodeSMSPDU(raw string) (SMSMessage, error) {
raw = strings.ToUpper(strings.TrimSpace(raw))
message := SMSMessage{
Direction: SMSDirectionUnknown,
Encoding: SMSEncodingUnknown,
StorageStatus: SMSStatusUnknown,
RawPDU: raw,
}
decoded, err := hex.DecodeString(raw)
if err != nil || len(decoded) < 2 {
if err == nil {
err = errors.New("SMS PDU is too short")
}
return message, err
}
cursor := &pduCursor{data: decoded}
smscLength, _ := cursor.byte()
if int(smscLength) > len(decoded)-1 {
return message, errors.New("SMS PDU SMSC length exceeds payload")
}
if smscLength > 0 {
smsc, _ := cursor.bytes(int(smscLength))
if len(smsc) > 1 {
message.ServiceCenter = decodeNumericAddress(
smsc[1:],
(len(smsc)-1)*2,
smsc[0],
)
}
}
firstOctet, err := cursor.byte()
if err != nil {
return message, err
}
switch firstOctet & 0x03 {
case 0:
message.Direction = SMSDirectionReceived
err = decodeDeliverPDU(cursor, firstOctet, &message)
case 1:
message.Direction = SMSDirectionSubmitted
err = decodeSubmitPDU(cursor, firstOctet, &message)
case 2:
message.Direction = SMSDirectionStatusReport
err = decodeStatusReportPDU(cursor, &message)
default:
err = errors.New("unsupported SMS PDU message type")
}
return message, err
}
func decodeDeliverPDU(
cursor *pduCursor,
firstOctet byte,
message *SMSMessage,
) error {
address, err := readTPAddress(cursor)
if err != nil {
return err
}
message.From = address
pid, err := cursor.byte()
if err != nil {
return err
}
dcs, err := cursor.byte()
if err != nil {
return err
}
message.ProtocolID = int(pid)
message.DataCodingScheme = int(dcs)
timestamp, err := cursor.bytes(7)
if err != nil {
return err
}
if parsed, timestampErr := decodeSMSTimestamp(timestamp); timestampErr == nil {
message.ServiceCenterTimestamp = parsed
}
udl, err := cursor.byte()
if err != nil {
return err
}
return decodeUserData(cursor.data[cursor.index:], firstOctet, dcs, int(udl), message)
}
func decodeSubmitPDU(
cursor *pduCursor,
firstOctet byte,
message *SMSMessage,
) error {
reference, err := cursor.byte()
if err != nil {
return err
}
message.MessageReference = intPointer(int(reference))
address, err := readTPAddress(cursor)
if err != nil {
return err
}
message.To = address
pid, err := cursor.byte()
if err != nil {
return err
}
dcs, err := cursor.byte()
if err != nil {
return err
}
message.ProtocolID = int(pid)
message.DataCodingScheme = int(dcs)
switch (firstOctet >> 3) & 0x03 {
case 2:
if _, err := cursor.byte(); err != nil {
return err
}
case 1, 3:
if _, err := cursor.bytes(7); err != nil {
return err
}
}
udl, err := cursor.byte()
if err != nil {
return err
}
return decodeUserData(cursor.data[cursor.index:], firstOctet, dcs, int(udl), message)
}
func decodeStatusReportPDU(
cursor *pduCursor,
message *SMSMessage,
) error {
reference, err := cursor.byte()
if err != nil {
return err
}
message.MessageReference = intPointer(int(reference))
address, err := readTPAddress(cursor)
if err != nil {
return err
}
message.To = address
scts, err := cursor.bytes(7)
if err != nil {
return err
}
discharge, err := cursor.bytes(7)
if err != nil {
return err
}
status, err := cursor.byte()
if err != nil {
return err
}
message.StatusCode = intPointer(int(status))
message.DeliveryStatus = smsDeliveryStatus(status)
if parsed, parseErr := decodeSMSTimestamp(scts); parseErr == nil {
message.ServiceCenterTimestamp = parsed
}
if parsed, parseErr := decodeSMSTimestamp(discharge); parseErr == nil {
message.DischargeTimestamp = parsed
}
return nil
}
func smsDeliveryStatus(status byte) string {
switch {
case status <= 0x1f:
return "delivered"
case status <= 0x3f:
return "temporary_error"
case status <= 0x5f:
return "permanent_error"
case status <= 0x7f:
return "temporary_error_no_retry"
default:
return "reserved"
}
}
func readTPAddress(cursor *pduCursor) (string, error) {
length, err := cursor.byte()
if err != nil {
return "", err
}
toa, err := cursor.byte()
if err != nil {
return "", err
}
byteCount := (int(length) + 1) / 2
value, err := cursor.bytes(byteCount)
if err != nil {
return "", err
}
if toa&0x70 == 0x50 {
septetCount := int(length) * 4 / 7
septets, unpackErr := unpackSeptets(value, septetCount, 0)
if unpackErr != nil {
return "", unpackErr
}
return decodeGSM7(septets)
}
return decodeNumericAddress(value, int(length), toa), nil
}
func decodeNumericAddress(value []byte, digits int, toa byte) string {
var result strings.Builder
if toa&0x70 == 0x10 {
result.WriteByte('+')
}
written := 0
for _, octet := range value {
for _, digit := range []byte{octet & 0x0f, octet >> 4} {
if written >= digits {
return result.String()
}
if digit <= 9 {
result.WriteByte('0' + digit)
written++
}
}
}
return result.String()
}
func decodeUserData(
data []byte,
firstOctet byte,
dcs byte,
udl int,
message *SMSMessage,
) error {
alphabet := dcs & 0x0c
expectedBytes := udl
if alphabet == 0 {
expectedBytes = (udl*7 + 7) / 8
}
if expectedBytes > len(data) {
return errors.New("SMS user data is truncated")
}
data = data[:expectedBytes]
message.RawUserData = strings.ToUpper(hex.EncodeToString(data))
headerBytes := 0
if firstOctet&0x40 != 0 {
if len(data) == 0 {
return errors.New("SMS has UDHI but no user data header")
}
headerBytes = int(data[0]) + 1
if headerBytes > len(data) {
return errors.New("SMS user data header is truncated")
}
message.Concat = parseConcatHeader(data[1:headerBytes])
}
switch alphabet {
case 0:
message.Encoding = SMSEncodingGSM7PDU
headerSeptets := 0
if headerBytes > 0 {
headerSeptets = (headerBytes*8 + 6) / 7
}
textSeptets := udl - headerSeptets
septets, err := unpackSeptets(data, textSeptets, headerSeptets*7)
if err != nil {
return err
}
text, err := decodeGSM7(septets)
message.Text = text
return err
case 8:
message.Encoding = SMSEncodingUCS2PDU
payload := data[headerBytes:]
if len(payload)%2 != 0 {
return errors.New("UCS2 SMS has an odd byte count")
}
units := make([]uint16, 0, len(payload)/2)
for index := 0; index < len(payload); index += 2 {
units = append(units, uint16(payload[index])<<8|uint16(payload[index+1]))
}
message.Text = string(utf16.Decode(units))
return nil
default:
message.Encoding = SMSEncoding8BitPDU
return nil
}
}
func parseConcatHeader(header []byte) *SMSConcatInfo {
for index := 0; index+1 < len(header); {
identifier := header[index]
length := int(header[index+1])
index += 2
if index+length > len(header) {
return nil
}
value := header[index : index+length]
switch {
case identifier == 0x00 && length == 3:
return &SMSConcatInfo{
Reference: int(value[0]),
Total: int(value[1]),
Sequence: int(value[2]),
}
case identifier == 0x08 && length == 4:
return &SMSConcatInfo{
Reference: int(value[0])<<8 | int(value[1]),
Total: int(value[2]),
Sequence: int(value[3]),
}
}
index += length
}
return nil
}
func decodeSMSTimestamp(value []byte) (*time.Time, error) {
if len(value) != 7 {
return nil, errors.New("SMS timestamp must be seven octets")
}
component := func(octet byte) (int, error) {
low, high := int(octet&0x0f), int(octet>>4)
if low > 9 || high > 9 {
return 0, errors.New("invalid timestamp semi-octet")
}
return low*10 + high, nil
}
parts := make([]int, 6)
for index := range parts {
parsed, err := component(value[index])
if err != nil {
return nil, err
}
parts[index] = parsed
}
zoneByte := value[6]
negative := zoneByte&0x08 != 0
zoneByte &^= 0x08
quarters, err := component(zoneByte)
if err != nil || quarters > 56 {
return nil, errors.New("invalid SMS timestamp timezone")
}
offset := quarters * 15 * 60
if negative {
offset = -offset
}
year := 2000 + parts[0]
if parts[0] >= 90 {
year = 1900 + parts[0]
}
if parts[1] < 1 || parts[1] > 12 ||
parts[2] < 1 || parts[2] > 31 ||
parts[3] > 23 || parts[4] > 59 || parts[5] > 60 {
return nil, errors.New("invalid SMS timestamp component")
}
zone := time.FixedZone("SMS", offset)
result := time.Date(
year,
time.Month(parts[1]),
parts[2],
parts[3],
parts[4],
parts[5],
0,
zone,
)
return &result, nil
}
func parseDecimal(value string) (int, bool) {
number, err := strconv.Atoi(strings.Trim(strings.TrimSpace(value), `"`))
return number, err == nil
}