diff --git a/README.md b/README.md index 8bc96f2..209b505 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ smsSubmitGsm7 := Message{ Type: MessageTypes.Submit, Address: "+79261234567", ServiceCenterAddress: "+79262000331", - VP: ValidityPeriod(time.Hour * 24 * 4), + VP: RelativeValidityPeriod(time.Hour * 24 * 4), VPFormat: ValidityPeriodFormats.Relative, } n, octets, err := smsSubmitGsm7.PDU() diff --git a/at.go b/at.go index cdcb773..ac4a8b7 100644 --- a/at.go +++ b/at.go @@ -438,7 +438,7 @@ func (d *Device) SendSMS(text string, address sms.PhoneNumber) (err error) { Encoding: sms.Encodings.Gsm7Bit, Address: address, VPFormat: sms.ValidityPeriodFormats.Relative, - VP: sms.ValidityPeriod(24 * time.Hour * 4), + VP: sms.RelativeValidityPeriod(24 * time.Hour * 4), } if !pdu.Is7BitEncodable(text) { diff --git a/sms/sms.go b/sms/sms.go index cf21d4e..458e025 100644 --- a/sms/sms.go +++ b/sms/sms.go @@ -15,7 +15,9 @@ var ( ErrUnknownEncoding = errors.New("sms: unsupported encoding") ErrUnknownMessageType = errors.New("sms: unsupported message type") ErrIncorrectSize = errors.New("sms: decoded incorrect size of field") - ErrNonRelative = errors.New("sms: non-relative validity period support is not implemented yet") + ErrLongEnhancedVpNotSupported = errors.New("sms: extended functionality indicator for enhanced validity period is not supported") + ErrUnknownEnhancedVpReservedBits = errors.New("sms: unknown reserved bits for enhanced validity period were set") + ErrUnknownVpf = errors.New("sms: unknown validity period format") ErrIncorrectUserDataHeaderLength = errors.New("sms: incorrect user data header length ") ErrUnsupportedTypeOfNumber = errors.New("sms: unsupported type-of-number") ) @@ -26,7 +28,9 @@ var ( type Message struct { Type MessageType Encoding Encoding - VP ValidityPeriod + VP RelativeValidityPeriod + AbsoluteVP AbsoluteValidityPeriod + EnhancedVP EnhancedValidityPeriod VPFormat ValidityPeriodFormat ServiceCenterTime Timestamp DischargeTime Timestamp @@ -151,10 +155,17 @@ func (s *Message) encodeSubmit(buf *bytes.Buffer) (n int, err error) { sms.DataCodingScheme = byte(s.Encoding) switch s.VPFormat { + case ValidityPeriodFormats.FieldNotPresent: + sms.ValidityPeriod = make([]byte, 0) case ValidityPeriodFormats.Relative: - sms.ValidityPeriod = byte(s.VP.Octet()) - case ValidityPeriodFormats.Absolute, ValidityPeriodFormats.Enhanced: - return 0, ErrNonRelative + sms.ValidityPeriod = []byte{s.VP.Octet()} + case ValidityPeriodFormats.Absolute: + sms.ValidityPeriod = s.AbsoluteVP.PDU() + case ValidityPeriodFormats.Enhanced: + sms.ValidityPeriod, err = s.EnhancedVP.PDU() + if err != nil { + return 0, err + } } sms.UserData, sms.UserDataLength, err = s.encodedUserData() @@ -272,11 +283,20 @@ func (s *Message) decodeSubmit(data []byte) (n int, err error) { } s.RejectDuplicates = sms.RejectDuplicates - switch ValidityPeriodFormat(sms.ValidityPeriodFormat) { - case ValidityPeriodFormats.Absolute, ValidityPeriodFormats.Enhanced: - return n, ErrNonRelative + s.VPFormat = ValidityPeriodFormat(sms.ValidityPeriodFormat) + switch s.VPFormat { + case ValidityPeriodFormats.FieldNotPresent: + case ValidityPeriodFormats.Absolute: + s.AbsoluteVP.ReadFrom(sms.ValidityPeriod) + case ValidityPeriodFormats.Relative: + s.VP.ReadFrom(sms.ValidityPeriod[0]) + case ValidityPeriodFormats.Enhanced: + err = s.EnhancedVP.ReadFrom(sms.ValidityPeriod) + if err != nil { + return n, err + } default: - s.VPFormat = ValidityPeriodFormat(sms.ValidityPeriodFormat) + return n, ErrUnknownVpf } s.MessageReference = sms.MessageReference @@ -286,9 +306,6 @@ func (s *Message) decodeSubmit(data []byte) (n int, err error) { s.Address.ReadFrom(sms.DestinationAddress[1:]) s.Encoding = Encoding(sms.DataCodingScheme) - if s.VPFormat != ValidityPeriodFormats.FieldNotPresent { - s.VP.ReadFrom(sms.ValidityPeriod) - } err = s.decodeUserData(sms.UserData, sms.UserDataLength) return n, err } diff --git a/sms/sms_submit.go b/sms/sms_submit.go index aabdfec..2ad6af0 100644 --- a/sms/sms_submit.go +++ b/sms/sms_submit.go @@ -18,7 +18,7 @@ type smsSubmit struct { DestinationAddress []byte ProtocolIdentifier byte DataCodingScheme byte - ValidityPeriod byte + ValidityPeriod []byte UserDataLength byte UserData []byte } @@ -44,9 +44,7 @@ func (s *smsSubmit) Bytes() []byte { buf.Write(s.DestinationAddress) buf.WriteByte(s.ProtocolIdentifier) buf.WriteByte(s.DataCodingScheme) - if ValidityPeriodFormat(s.ValidityPeriodFormat) != ValidityPeriodFormats.FieldNotPresent { - buf.WriteByte(s.ValidityPeriod) - } + buf.Write(s.ValidityPeriod) buf.WriteByte(s.UserDataLength) buf.Write(s.UserData) return buf.Bytes() @@ -105,13 +103,30 @@ func (s *smsSubmit) FromBytes(octets []byte) (n int, err error) { //nolint:funle if err != nil { return } - if ValidityPeriodFormat(s.ValidityPeriodFormat) != ValidityPeriodFormats.FieldNotPresent { - s.ValidityPeriod, err = buf.ReadByte() - n++ + + switch ValidityPeriodFormat(s.ValidityPeriodFormat) { + case ValidityPeriodFormats.FieldNotPresent: + s.ValidityPeriod = make([]byte, 0) + case ValidityPeriodFormats.Relative: + s.ValidityPeriod = make([]byte, 1) + off, err = io.ReadFull(buf, s.ValidityPeriod) + n += off if err != nil { return } + case ValidityPeriodFormats.Absolute: + fallthrough + case ValidityPeriodFormats.Enhanced: + s.ValidityPeriod = make([]byte, 7) + off, err = io.ReadFull(buf, s.ValidityPeriod) + n += off + if err != nil { + return + } + default: + return } + s.UserDataLength, err = buf.ReadByte() n++ if err != nil { diff --git a/sms/sms_test.go b/sms/sms_test.go index 9430a29..d8ff12c 100644 --- a/sms/sms_test.go +++ b/sms/sms_test.go @@ -24,6 +24,12 @@ var ( pduSubmitGsm7 = "07919762020033F111000B919762995696F00000AA066379180E8200" pduSubmitGsm7_EnhancedTpVp = "05915155010009010891515511110000420300000000001e547" + "47a0e9a36a72074780e9a81e6e5f1db4d9e83e86f103b6d2f03" + pduSubmitGsm7_EnhancedTpVp2 = "05915155020009000891515522220000010000000000001f54" + + "747a0e9a36a7a0f41c640fb3d36490f92d07c940edb4bb4e2fcf1b" + pduSubmitGsm7_AbsoluteTpVp = "059151550100190008915155111100001010103295953246cfb" + + "a1ce42cc3c3ecf2bc0c32cbd36537790eba87dd74101d9d9e83a6cd29485c36bfe565900c068" + + "bb560b1162c0692cd74b59cae960355a9c3554d47ab01" + pduDeliverGsm7_2 = "0791551010010201040D91551699296568F80011719022124215293DD4B71C5E26BF" + "41D3E6145476D3E5E573BD0C82BF40B59A2D96CBE564351BCE8603A164319D8CA6ABD540E432482673C172AED82DE502" @@ -61,7 +67,7 @@ var ( Type: MessageTypes.Submit, Address: "+79269965690", ServiceCenterAddress: "+79168999100", - VP: ValidityPeriod(time.Hour * 24 * 4), + VP: RelativeValidityPeriod(time.Hour * 24 * 4), VPFormat: ValidityPeriodFormats.Relative, } smsSubmitGsm7 = Message{ @@ -70,15 +76,46 @@ var ( Type: MessageTypes.Submit, Address: "+79269965690", ServiceCenterAddress: "+79262000331", - VP: ValidityPeriod(time.Hour * 24 * 4), + VP: RelativeValidityPeriod(time.Hour * 24 * 4), VPFormat: ValidityPeriodFormats.Relative, } + smsSubmitGsm7_AbsoluteTpVp = Message{ + Text: "Our Nepalese friends want this SMS before 2001-01-01 23:59:59 UTC+5:45", + Encoding: Encodings.Gsm7Bit, + Type: MessageTypes.Submit, + Address: "+15551111", + ServiceCenterAddress: "+15551000", + AbsoluteVP: AbsoluteValidityPeriod(parseTimestamp("2001-01-01T23:59:59+05:45")), + VPFormat: ValidityPeriodFormats.Absolute, + } smsSubmitGsm7_EnhancedTpVp = Message{ Text: "This SMS has 3 seconds to live", Encoding: Encodings.Gsm7Bit, Type: MessageTypes.Submit, Address: "+15551111", ServiceCenterAddress: "+15551000", + VPFormat: ValidityPeriodFormats.Enhanced, + EnhancedVP: EnhancedValidityPeriod{ + ExtensionBit: false, + SingleShotSm: true, + EnhancedFormat: EnhancedValidityPeriodFormats.RelativeInteger, + RelativeIntegerVP: 3, + }, + MessageReference: 1, + } + smsSubmitGsm7_EnhancedTpVp2 = Message{ + Text: "This SMS is valid for 2 minutes", + Encoding: Encodings.Gsm7Bit, + Type: MessageTypes.Submit, + Address: "+15552222", + ServiceCenterAddress: "+15552000", + VPFormat: ValidityPeriodFormats.Enhanced, + EnhancedVP: EnhancedValidityPeriod{ + ExtensionBit: false, + SingleShotSm: false, + EnhancedFormat: EnhancedValidityPeriodFormats.Relative, + RelativeVP: 0, + }, } smsReport = Message{ Type: MessageTypes.StatusReport, @@ -101,6 +138,14 @@ func parseTimestamp(timetamp string) Timestamp { return Timestamp(date) } +func asBytes(str string) []byte { + bytes, err := util.Bytes(str) + if err != nil { + panic(err) + } + return bytes +} + func TestSmsDeliverReadFromUCS2(t *testing.T) { t.Parallel() @@ -185,14 +230,40 @@ func TestSmsSubmitReadFromGsm7(t *testing.T) { assert.Equal(t, smsSubmitGsm7, msg) } +func TestSmsSubmitReadFromGsm7_AbsoluteTpVp(t *testing.T) { + t.Parallel() + + var msg Message + data, err := util.Bytes(pduSubmitGsm7_AbsoluteTpVp) + require.NoError(t, err) + n, err := msg.ReadFrom(data) + require.NoError(t, err) + assert.Equal(t, n, len(data)) + assert.Equal(t, smsSubmitGsm7_AbsoluteTpVp, msg) +} + func TestSmsSubmitReadFromGsm7_EnhancedTpVp(t *testing.T) { t.Parallel() var msg Message data, err := util.Bytes(pduSubmitGsm7_EnhancedTpVp) require.NoError(t, err) - _, err = msg.ReadFrom(data) - assert.Equal(t, err, ErrNonRelative) + n, err := msg.ReadFrom(data) + require.NoError(t, err) + assert.Equal(t, n, len(data)) + assert.Equal(t, smsSubmitGsm7_EnhancedTpVp, msg) +} + +func TestSmsSubmitReadFromGsm7_EnhancedTpVp2(t *testing.T) { + t.Parallel() + + var msg Message + data, err := util.Bytes(pduSubmitGsm7_EnhancedTpVp2) + require.NoError(t, err) + n, err := msg.ReadFrom(data) + require.NoError(t, err) + assert.Equal(t, n, len(data)) + assert.Equal(t, smsSubmitGsm7_EnhancedTpVp2, msg) } func TestSmsSubmitPduUCS2(t *testing.T) { @@ -217,6 +288,36 @@ func TestSmsSubmitPduGsm7(t *testing.T) { assert.Equal(t, data, octets) } +func TestSmsSubmitPduGsm7_AbsoluteTpVp(t *testing.T) { + t.Parallel() + + n, octets, err := smsSubmitGsm7_AbsoluteTpVp.PDU() + require.NoError(t, err) + data := asBytes(pduSubmitGsm7_AbsoluteTpVp) + assert.Equal(t, len(data) - 6, n) + assert.Equal(t, data, octets) +} + +func TestSmsSubmitPduGsm7_EnhancedTpVp(t *testing.T) { + t.Parallel() + + n, octets, err := smsSubmitGsm7_EnhancedTpVp.PDU() + require.NoError(t, err) + data := asBytes(pduSubmitGsm7_EnhancedTpVp) + assert.Equal(t, len(data) - 6, n) + assert.Equal(t, data, octets) +} + +func TestSmsSubmitPduGsm7_EnhancedTpVp2(t *testing.T) { + t.Parallel() + + n, octets, err := smsSubmitGsm7_EnhancedTpVp2.PDU() + require.NoError(t, err) + data := asBytes(pduSubmitGsm7_EnhancedTpVp2) + assert.Equal(t, len(data) - 6, n) + assert.Equal(t, data, octets) +} + func TestSmsStatusReport(t *testing.T) { t.Parallel() diff --git a/sms/validity_period.go b/sms/validity_period.go index d4d3f86..e95acd7 100644 --- a/sms/validity_period.go +++ b/sms/validity_period.go @@ -1,6 +1,9 @@ package sms -import "time" +import ( + "fmt" + "time" +) // ValidityPeriodFormat represents the format of message's validity period. type ValidityPeriodFormat byte @@ -16,11 +19,40 @@ var ValidityPeriodFormats = struct { 0x00, 0x02, 0x01, 0x03, } -// ValidityPeriod represents the validity period of message. -type ValidityPeriod time.Duration +type EnhancedValidityPeriodFormat byte + +var EnhancedValidityPeriodFormats = struct { + NotPresent EnhancedValidityPeriodFormat + Relative EnhancedValidityPeriodFormat + RelativeInteger EnhancedValidityPeriodFormat + RelativeSemiOctet EnhancedValidityPeriodFormat +}{ + 0x00, 0x01, 0x02, 0x03, +} + +// Enhanced "0b010" validity period format (3GPP TS 23.040 9.2.3.12.3) +type RelativeIntegerValidityPeriod byte + +// Enhanced validity period (3GPP TS 23.040 9.2.3.12.3) +type EnhancedValidityPeriod struct { + ExtensionBit bool + SingleShotSm bool + EnhancedFormat EnhancedValidityPeriodFormat + RelativeVP RelativeValidityPeriod + RelativeIntegerVP RelativeIntegerValidityPeriod +} + +// Absolute validity period (3GPP TS 23.040 9.2.3.12.2) +type AbsoluteValidityPeriod = Timestamp + +// Relative validity period (3GPP TS 23.040 9.2.3.12.1) +type RelativeValidityPeriod time.Duration + +// Type alias for backwards compatibility +type ValidityPeriod = RelativeValidityPeriod // Octet return a one-byte representation of the validity period. -func (v ValidityPeriod) Octet() byte { +func (v RelativeValidityPeriod) Octet() byte { switch d := time.Duration(v); { case d/time.Minute < 5: return 0x00 @@ -41,15 +73,68 @@ func (v ValidityPeriod) Octet() byte { } // ReadFrom reads the validity period form the given byte. -func (v *ValidityPeriod) ReadFrom(oct byte) { +func (v *RelativeValidityPeriod) ReadFrom(oct byte) { switch n := time.Duration(oct); { case n >= 0 && n <= 143: - *v = ValidityPeriod(5 * time.Minute * n) + *v = RelativeValidityPeriod(5 * time.Minute * n) case n >= 144 && n <= 167: - *v = ValidityPeriod(12*time.Hour + 30*time.Minute*(n-143)) + *v = RelativeValidityPeriod(12*time.Hour + 30*time.Minute*(n-143)) case n >= 168 && n <= 196: - *v = ValidityPeriod(24 * time.Hour * (n - 166)) + *v = RelativeValidityPeriod(24 * time.Hour * (n - 166)) case n >= 197 && n <= 255: - *v = ValidityPeriod(7 * 24 * time.Hour * (n - 192)) + *v = RelativeValidityPeriod(7 * 24 * time.Hour * (n - 192)) + } +} + +func (v *EnhancedValidityPeriod) PDU() ([]byte, error) { + if v.ExtensionBit { + return nil, ErrLongEnhancedVpNotSupported + } + + pdu := make([]byte, 7) + pdu[0] = 0b0000_0000 + if v.SingleShotSm { + pdu[0] |= 0b0100_0000 + } + + pdu[0] |= byte(v.EnhancedFormat) & 0b0000_0111 + switch v.EnhancedFormat { + case EnhancedValidityPeriodFormats.NotPresent: + case EnhancedValidityPeriodFormats.Relative: + pdu[1] = v.RelativeVP.Octet() + case EnhancedValidityPeriodFormats.RelativeInteger: + pdu[1] = byte(v.RelativeIntegerVP) + default: + return nil, fmt.Errorf("%w: Enhanced Type(0x%x)", ErrUnknownVpf, v.EnhancedFormat) + } + return pdu, nil +} + +func (v *EnhancedValidityPeriod) ReadFrom(octets []byte) error { + if len(octets) != 7 { + return ErrIncorrectSize + } + + v.ExtensionBit = (octets[0] & 0b1000_0000) != 0 + v.SingleShotSm = (octets[0] & 0b0100_0000) != 0 + v.EnhancedFormat = EnhancedValidityPeriodFormat(octets[0] & 0b0111) + + reservedBits := (octets[0] & 0b0011_1000) != 0 + if reservedBits { + return ErrUnknownEnhancedVpReservedBits + } + if v.ExtensionBit { + return ErrLongEnhancedVpNotSupported + } + + switch v.EnhancedFormat { + case EnhancedValidityPeriodFormats.NotPresent: + case EnhancedValidityPeriodFormats.Relative: + v.RelativeVP.ReadFrom(octets[1]) + case EnhancedValidityPeriodFormats.RelativeInteger: + v.RelativeIntegerVP = RelativeIntegerValidityPeriod(octets[1]) + default: + return fmt.Errorf("%w: Enhanced Type(0x%x)", ErrUnknownVpf, v.EnhancedFormat) } + return nil }