pdu_tools 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/pdu_tools.rb +20 -0
- data/lib/pdu_tools/decoder.rb +197 -0
- data/lib/pdu_tools/encoder.rb +161 -0
- data/lib/pdu_tools/helpers.rb +111 -0
- data/lib/pdu_tools/message_part.rb +21 -0
- data/lib/pdu_tools/pdu.rb +19 -0
- data/spec/decoder_spec.rb +35 -0
- data/spec/encoder_spec.rb +42 -0
- data/spec/spec_helper.rb +1 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d1b59db9f41bc1d37660c6af01c3d7fda3cc2800
|
4
|
+
data.tar.gz: 64d571e2f9da156852825a43a967171fc107eae7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b8ec02cc0b4c1fb1eedc474877812cdce0906d61233f1ed5bae8bd47b9943e82f1bd7c9071b4158f33bfe8d12c97a54c101db40926003e102ff5e6dde2b687b
|
7
|
+
data.tar.gz: c337b899ae90c7435a1ff38696861ecdf99fda21db98e8aa7e0c90fbd1133c55dad0777fabb47beef86951c5de0ea854efd6380f3daa8f0080af532532337bd4
|
data/lib/pdu_tools.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'phone'
|
2
|
+
require 'active_support/all'
|
3
|
+
|
4
|
+
require_relative './pdu_tools/helpers'
|
5
|
+
require_relative './pdu_tools/pdu'
|
6
|
+
require_relative './pdu_tools/message_part'
|
7
|
+
require_relative './pdu_tools/decoder'
|
8
|
+
require_relative './pdu_tools/encoder'
|
9
|
+
|
10
|
+
module PDUTools
|
11
|
+
ALPHABETS = {
|
12
|
+
a7bit: '00',
|
13
|
+
a8bit: '01',
|
14
|
+
a16bit: '10'
|
15
|
+
}
|
16
|
+
|
17
|
+
MAX_MESSAGE_LENGTH = 39015
|
18
|
+
MAX_GSM_MESSAGE_7BIT_PART_LENGTH = 152
|
19
|
+
MAX_GSM_MESSAGE_16BIT_PART_LENGTH = 66
|
20
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
|
2
|
+
module PDUTools
|
3
|
+
class Decoder
|
4
|
+
include Helpers
|
5
|
+
# http://read.pudn.com/downloads150/sourcecode/embed/646395/Short%20Message%20in%20PDU%20Encoding.pdf
|
6
|
+
def initialize pdu_hex, direction=:sc_to_ms
|
7
|
+
@pdu_hex = pdu_hex
|
8
|
+
@direction = direction
|
9
|
+
end
|
10
|
+
|
11
|
+
def decode
|
12
|
+
@sca_length = take(2, :integer) * 2 # Service center address length
|
13
|
+
if @sca_length > 0
|
14
|
+
@sca_type = parse_address_type take(2) # Service center address type
|
15
|
+
@sca = parse_address take(@sca_length - 2), @sca_type # Service center address
|
16
|
+
end
|
17
|
+
@pdu_type = parse_pdu_type take(2, :binary) # PDU type octet
|
18
|
+
@message_reference = take(2) if @pdu_type[:mti] == :sms_submit
|
19
|
+
@address_length = take(2, :integer)
|
20
|
+
@address_type = parse_address_type take(2)
|
21
|
+
@address = parse_address take(@address_length), @address_type
|
22
|
+
@pid = take(2)
|
23
|
+
@data_coding_scheme = parse_data_coding_scheme take(2, :binary)
|
24
|
+
@sc_timestamp = parse_7byte_timestamp take(14) if @pdu_type[:mti] == :sms_deliver
|
25
|
+
case @pdu_type[:vpf]
|
26
|
+
when :absolute
|
27
|
+
@validity_period = parse_7byte_timestamp take(14)
|
28
|
+
when :relative
|
29
|
+
@validity_period = parse_validity_period take(2, :integer)
|
30
|
+
end
|
31
|
+
@user_data_length = take(2, :integer)
|
32
|
+
parse_user_data @user_data_length
|
33
|
+
|
34
|
+
MessagePart.new @address, @message, @sc_timestamp, @validity_period, @user_data_header
|
35
|
+
end
|
36
|
+
|
37
|
+
def inspect2
|
38
|
+
r = "<PDUTools::Decoder"
|
39
|
+
r << "PDU: #{@pdu_hex}\n"
|
40
|
+
r << "SCA LENGTH: #{@sca_length}\n"
|
41
|
+
r << "SCA TYPE: #{@sca_type}\n"
|
42
|
+
r << "SCA: #{@sca}\n"
|
43
|
+
r << "PDU TYPE: #{@pdu_type}\n"
|
44
|
+
r << "MESSAGE REFERENCE: #{@message_reference}\n" if @message_reference
|
45
|
+
r << ">"
|
46
|
+
r
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def take n, format=:string, data=@pdu_hex
|
52
|
+
part = data.slice!(0,n)
|
53
|
+
case format
|
54
|
+
when :string
|
55
|
+
return part
|
56
|
+
when :integer
|
57
|
+
return part.to_i(16)
|
58
|
+
when :binary
|
59
|
+
bytes = n/2
|
60
|
+
return "%0#{bytes*8}b" % part.to_i(16)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_address_type type
|
65
|
+
case type
|
66
|
+
when "91"
|
67
|
+
:international
|
68
|
+
when "81"
|
69
|
+
:national
|
70
|
+
else
|
71
|
+
raise StandardError, "unknown address type"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_address address, type
|
76
|
+
address = swapped2normal address
|
77
|
+
if type == :international
|
78
|
+
address.prepend "+"
|
79
|
+
end
|
80
|
+
address
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_pdu_type pdu_type
|
84
|
+
rp = pdu_type.slice!(0,1) == "1"
|
85
|
+
udhi = pdu_type.slice!(0,1) == "1"
|
86
|
+
srr_or_sri = pdu_type.slice!(0,1) == "1"
|
87
|
+
vpf = (case pdu_type.slice!(0,2)
|
88
|
+
when "00", "01"
|
89
|
+
:none
|
90
|
+
when "10"
|
91
|
+
:relative
|
92
|
+
when "11"
|
93
|
+
:absolute
|
94
|
+
end)
|
95
|
+
rd_or_mms = pdu_type.slice!(0,1)
|
96
|
+
mti = pdu_type.slice!(0,2)
|
97
|
+
|
98
|
+
type = { rp: rp, udhi: udhi, vpf: vpf }
|
99
|
+
case @direction
|
100
|
+
when :sc_to_ms
|
101
|
+
type[:mti] = case mti
|
102
|
+
when "00"
|
103
|
+
:sms_deliver
|
104
|
+
when "01"
|
105
|
+
:sms_submit_report
|
106
|
+
when "10"
|
107
|
+
:sms_status_report
|
108
|
+
when "11"
|
109
|
+
:reserved
|
110
|
+
end
|
111
|
+
type[:sri] = srr_or_sri
|
112
|
+
type[:mms] = rd_or_mms == "0"
|
113
|
+
when :ms_to_sc
|
114
|
+
type[:mti] = case mti
|
115
|
+
when "00"
|
116
|
+
:sms_deliver_report
|
117
|
+
when "01"
|
118
|
+
:sms_submit
|
119
|
+
when "10"
|
120
|
+
:sms_command
|
121
|
+
when "11"
|
122
|
+
:reserved
|
123
|
+
end
|
124
|
+
type[:srr] = srr_or_sri
|
125
|
+
type[:rd] = rd_or_mms == "0"
|
126
|
+
end
|
127
|
+
type
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_data_coding_scheme scheme
|
131
|
+
{
|
132
|
+
coding_group: scheme.slice!(0,2),
|
133
|
+
compresion: scheme.slice!(0,1) == "1",
|
134
|
+
klass_meaning: scheme.slice!(0,1) == "1",
|
135
|
+
alphabet: ALPHABETS.key(scheme.slice!(0,2)),
|
136
|
+
klass: scheme.slice!(0,2)
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_7byte_timestamp timestamp
|
141
|
+
year, month, day, hour, minute, second, zone = swapped2normal(timestamp).split('').in_groups_of(2).collect(&:join)
|
142
|
+
d = "#{year}-#{month}-#{day} #{hour}:#{minute}:#{second} +%02d:00" % (zone.to_i / 4)
|
143
|
+
Time.parse(d)
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_validity_period period
|
147
|
+
case period
|
148
|
+
when 0..143
|
149
|
+
((period + 1) * 5).minutes
|
150
|
+
when 144..167
|
151
|
+
12.hours + ((period - 143) * 30).minutes
|
152
|
+
when 168..196
|
153
|
+
(period - 166).days
|
154
|
+
when 197..255
|
155
|
+
(period - 192).weeks
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_user_data data_length
|
160
|
+
if @pdu_type[:udhi]
|
161
|
+
@udh_length = take(2, :integer) * 2
|
162
|
+
udh = take(@udh_length)
|
163
|
+
@user_data_header = parse_user_data_header udh
|
164
|
+
end
|
165
|
+
case @data_coding_scheme[:alphabet]
|
166
|
+
when :a7bit
|
167
|
+
@message = decode7bit @pdu_hex, data_length
|
168
|
+
when :a8bit
|
169
|
+
@message = decode8bit @pdu_hex, data_length
|
170
|
+
when :a16bit
|
171
|
+
@message = decode16bit @pdu_hex, data_length
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_user_data_header header
|
176
|
+
iei = take 2, :string, header
|
177
|
+
header_length = take 2, :integer, header
|
178
|
+
case iei
|
179
|
+
when "00"
|
180
|
+
reference = take 2, :integer, header
|
181
|
+
when "08"
|
182
|
+
reference = take 4, :integer, header
|
183
|
+
else
|
184
|
+
binding.pry
|
185
|
+
raise StandardError, "unsupported Information Element Identifier in User Data Header: #{iei}"
|
186
|
+
end
|
187
|
+
parts = take 2, :integer, header
|
188
|
+
part_number = take 2, :integer, header
|
189
|
+
{
|
190
|
+
reference: reference,
|
191
|
+
parts: parts,
|
192
|
+
part_number: part_number
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module PDUTools
|
2
|
+
class Encoder
|
3
|
+
include Helpers
|
4
|
+
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
klass: nil,
|
7
|
+
require_receipt: false,
|
8
|
+
expiry_seconds: nil
|
9
|
+
}
|
10
|
+
|
11
|
+
MessagePart = Struct.new(:data, :length)
|
12
|
+
|
13
|
+
# PDU structure - http://read.pudn.com/downloads150/sourcecode/embed/646395/Short%20Message%20in%20PDU%20Encoding.pdf
|
14
|
+
# X Bytes - SMSC - Service Center Address
|
15
|
+
# 1 Byte - Flags / PDU Type
|
16
|
+
# - 1 bit Reply Path parameter indicator
|
17
|
+
# - 1 bit User Data Header Indicator
|
18
|
+
# - 1 bit Status Request Report
|
19
|
+
# - 2 bits Validity Period Format
|
20
|
+
# - 1 bit Reject Duplicates
|
21
|
+
# - 2 bits Message Type Indicator
|
22
|
+
# 2 Bytes - Message Reference
|
23
|
+
# X Bytes - Address length and address
|
24
|
+
# 1 Byte - Protocol identificator (PID)
|
25
|
+
# 1 Byte - Data Coding Scheme
|
26
|
+
# X Bytes - Validity Period
|
27
|
+
# 1 Byte - User Data Length
|
28
|
+
# X Bytes - User Data
|
29
|
+
|
30
|
+
def initialize options
|
31
|
+
raise ArgumentError, :recipient unless options[:recipient]
|
32
|
+
raise ArgumentError, :message unless options[:message]
|
33
|
+
@options = DEFAULT_OPTIONS.merge options
|
34
|
+
|
35
|
+
@smsc = '00' # Phone Specified
|
36
|
+
@message_parts, @alphabet = prepare_message options[:message]
|
37
|
+
@pdu_type = pdu_type @concatenated_message_reference, options[:require_receipt], options[:expiry_seconds]
|
38
|
+
@message_reference = '00' # Phone Specified
|
39
|
+
@address = prepare_recipient options[:recipient]
|
40
|
+
@protocol_identifier = '00' # SMS
|
41
|
+
@data_coding_scheme = data_coding_scheme options[:klass], @alphabet
|
42
|
+
@validity_period = validity_period options[:expiry_seconds]
|
43
|
+
end
|
44
|
+
|
45
|
+
def encode
|
46
|
+
head = ""
|
47
|
+
head << @smsc
|
48
|
+
head << @pdu_type
|
49
|
+
head << @message_reference
|
50
|
+
head << @address
|
51
|
+
head << @protocol_identifier
|
52
|
+
head << @data_coding_scheme
|
53
|
+
head << @validity_period
|
54
|
+
pdus = []
|
55
|
+
@message_parts.each do |part|
|
56
|
+
pdus << PDU.new(head + part.length + part.data)
|
57
|
+
end
|
58
|
+
pdus
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def prepare_message message
|
63
|
+
if message.ascii_only?
|
64
|
+
# parts = message.scan(/.{1,#{MAX_GSM_MESSAGE_7BIT_PART_LENGTH}}/)
|
65
|
+
parts = message.split('').in_groups_of(MAX_GSM_MESSAGE_7BIT_PART_LENGTH).collect(&:join)
|
66
|
+
message_parts = []
|
67
|
+
parts.each_with_index do |part, i|
|
68
|
+
part_gsm0338 = utf8_to_gsm0338 part
|
69
|
+
part_7bit = encode7bit(part_gsm0338)
|
70
|
+
udh = user_data_header parts.size, i+1
|
71
|
+
udh_length = (udh.present? ? (udh.length / 2) + 1 : 0)
|
72
|
+
part_length = "%02X" % (part_gsm0338.length + udh_length)
|
73
|
+
message_parts << MessagePart.new((udh + part_7bit), part_length)
|
74
|
+
end
|
75
|
+
[message_parts, :a7bit]
|
76
|
+
else
|
77
|
+
parts = message.split('').in_groups_of(MAX_GSM_MESSAGE_16BIT_PART_LENGTH).collect(&:join)
|
78
|
+
message_parts = []
|
79
|
+
parts.each_with_index do |part, i|
|
80
|
+
part_8bit = encode8bit(part)
|
81
|
+
udh = user_data_header parts.size, i+1
|
82
|
+
part_length = "%02X" % ((udh + part_8bit).length / 2)
|
83
|
+
message_parts << MessagePart.new((udh + part_8bit), part_length)
|
84
|
+
end
|
85
|
+
[message_parts, :a16bit]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# http://en.wikipedia.org/wiki/Concatenated_SMS#Sending_a_concatenated_SMS_using_a_User_Data_Header
|
90
|
+
def user_data_header parts_count, part_number
|
91
|
+
return '' if parts_count == 1
|
92
|
+
@concatenated_message_reference ||= rand((2**16)-1)
|
93
|
+
udh = '06' # Length of User Data Header
|
94
|
+
udh << '08' # Concatenated short messages, 16-bit reference number
|
95
|
+
udh << '04' # Length of the header, excluding the first two fields
|
96
|
+
udh << "%04X" % @concatenated_message_reference
|
97
|
+
udh << "%02X" % parts_count
|
98
|
+
udh << "%02X" % part_number
|
99
|
+
udh
|
100
|
+
end
|
101
|
+
|
102
|
+
def prepare_recipient recipient
|
103
|
+
Phoner::Phone.default_country_code ||= "421"
|
104
|
+
|
105
|
+
address_type = "91" # International
|
106
|
+
address = Phoner::Phone.parse(recipient).format("%c%a%n")
|
107
|
+
address_length = "%02X" % address.length
|
108
|
+
address_encoded = normal2swapped address
|
109
|
+
address_length + address_type + address_encoded
|
110
|
+
end
|
111
|
+
|
112
|
+
def data_coding_scheme klass, alphabet
|
113
|
+
if klass
|
114
|
+
klass_meaning = '1'
|
115
|
+
klass = ("%02b" % klass)[-2,2]
|
116
|
+
else
|
117
|
+
klass_meaning = '0'
|
118
|
+
klass = '00'
|
119
|
+
end
|
120
|
+
|
121
|
+
scheme_bin = ""
|
122
|
+
scheme_bin << '00' # 2 bits - coding_group
|
123
|
+
scheme_bin << '0' # 1 bit - compression
|
124
|
+
scheme_bin << klass_meaning # 1 bit - klass meaning flag
|
125
|
+
scheme_bin << ALPHABETS[alphabet] # 2 bits - alphabet
|
126
|
+
scheme_bin << klass # 2 bits - klass
|
127
|
+
|
128
|
+
data_coding_scheme_dec = scheme_bin.to_i(2)
|
129
|
+
dec2hexbyte data_coding_scheme_dec
|
130
|
+
end
|
131
|
+
|
132
|
+
def pdu_type uhdi, srr, vpf
|
133
|
+
reply_path = '0'
|
134
|
+
uhdi_flag = (uhdi ? '1' : '0') # User Data Header indicator
|
135
|
+
srr_flag = (srr ? '1' : '0') # Status Request Report
|
136
|
+
vpf_flag = (vpf ? '10' : '00') # Validity Period Format
|
137
|
+
rj = '0' # Reject Duplicates
|
138
|
+
mti = '01' # Message Type Indicator (SMS-SUBMIT)
|
139
|
+
first_octet_dec = (reply_path + uhdi_flag + srr_flag + vpf_flag + rj + mti).to_i(2)
|
140
|
+
dec2hexbyte first_octet_dec
|
141
|
+
end
|
142
|
+
|
143
|
+
def validity_period expiry_seconds
|
144
|
+
return '' unless expiry_seconds
|
145
|
+
raise ArgumentError, "Expiry must be at least 300 seconds (5 minutes)" if expiry_seconds < 5.minutes
|
146
|
+
validity_period_dec = case expiry_seconds
|
147
|
+
when 5.minutes..12.hours
|
148
|
+
(expiry_seconds / 5.minutes) - 1
|
149
|
+
when 12.hours..24.hours
|
150
|
+
((expiry_seconds - 12.hours) / 5.minutes) + 143
|
151
|
+
when 24.hours..30.days
|
152
|
+
(expiry_seconds / 24.hours) + 166
|
153
|
+
when 30.days..63.weeks
|
154
|
+
(expiry_seconds / 1.week) + 192
|
155
|
+
else
|
156
|
+
raise ArgumentError, "Expiry must be 38102400 seconds (63 weeks) or less"
|
157
|
+
end
|
158
|
+
dec2hexbyte validity_period_dec.ceil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module PDUTools
|
2
|
+
module Helpers
|
3
|
+
GSM_03_38_ESCAPES = {
|
4
|
+
"@" => "\x00",
|
5
|
+
"$" => "\x02",
|
6
|
+
"_" => "\x11",
|
7
|
+
"^" => "\x1B\x14",
|
8
|
+
"{" => "\x1B\x28",
|
9
|
+
"}" => "\x1B\x29",
|
10
|
+
"\\" => "\x1B\x2F",
|
11
|
+
"[" => "\x1B\x3C",
|
12
|
+
"~" => "\x1B\x3D",
|
13
|
+
"]" => "\x1B\x3E",
|
14
|
+
"|" => "\x1B\x40"
|
15
|
+
# "\x80" => "\x1B\x65"
|
16
|
+
}
|
17
|
+
|
18
|
+
def utf8_to_gsm0338 string
|
19
|
+
GSM_03_38_ESCAPES.each do |find, replace|
|
20
|
+
string.gsub! find, replace
|
21
|
+
end
|
22
|
+
string
|
23
|
+
end
|
24
|
+
|
25
|
+
def dec2hexbyte dec
|
26
|
+
"%02X" % dec
|
27
|
+
end
|
28
|
+
|
29
|
+
def encode7bit string, padding=0
|
30
|
+
current_byte = 0
|
31
|
+
offset = padding
|
32
|
+
packed = []
|
33
|
+
string.chars.to_a.each_with_index do |char, i|
|
34
|
+
# cap off any excess bytes
|
35
|
+
septet = char.ord & 0x7F
|
36
|
+
# append the septet and then cap off excess bytes
|
37
|
+
current_byte |= (septet << offset) & 0xFF
|
38
|
+
offset += 7
|
39
|
+
if offset > 7
|
40
|
+
# the current byte is full, add it to the encoded data.
|
41
|
+
packed << current_byte
|
42
|
+
# shift left and append the left shifted septet to the current byte
|
43
|
+
septet = septet >> (7 - (offset - 8 ))
|
44
|
+
current_byte = septet
|
45
|
+
# update offset
|
46
|
+
offset -= 8
|
47
|
+
end
|
48
|
+
end
|
49
|
+
packed << current_byte if offset > 0 # append the last byte
|
50
|
+
packed.collect{|c| "%02X" % c }.join
|
51
|
+
end
|
52
|
+
|
53
|
+
def encode8bit string
|
54
|
+
string.chars.to_a.collect do |char|
|
55
|
+
"%04X" % char.ord
|
56
|
+
end.join
|
57
|
+
end
|
58
|
+
|
59
|
+
def normal2swapped string
|
60
|
+
string << "F" if string.length.odd?
|
61
|
+
string.scan(/../).collect(&:reverse).join
|
62
|
+
end
|
63
|
+
|
64
|
+
def swapped2normal string
|
65
|
+
string.scan(/../).collect(&:reverse).join.gsub(/F$/,'')
|
66
|
+
end
|
67
|
+
|
68
|
+
# def decode7bit data, length
|
69
|
+
# septets = data.to_i(16).to_s(2).split('').in_groups_of(7).collect(&:join)[0,length]
|
70
|
+
# septets.collect do |s|
|
71
|
+
# s.to_i(2).chr
|
72
|
+
# end.join
|
73
|
+
# end
|
74
|
+
|
75
|
+
def decode7bit textdata, length
|
76
|
+
bytes = []
|
77
|
+
textdata.split('').each_slice(2) do |s|
|
78
|
+
bytes << "%08b" % s.join.to_i(16)
|
79
|
+
end
|
80
|
+
bit = (bytes.size % 7)
|
81
|
+
bytes.reverse!
|
82
|
+
bytes.each_with_index do |byte, index|
|
83
|
+
if bit == 0 or index == 0
|
84
|
+
bytes.insert(index, "")
|
85
|
+
bit = 7 if bit == 0
|
86
|
+
next
|
87
|
+
else
|
88
|
+
bytes[index-1] = "#{bytes[index-1]}#{(bytes[index]||"")[0,bit]}"
|
89
|
+
bytes[index] = bytes[index][bit..-1] if bytes[index]
|
90
|
+
bit -= 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
bytes = bytes.reverse.collect{|b| "0#{b}".to_i(2) }.collect{|b| b.zero? ? nil : b }.compact
|
94
|
+
bytes.collect{|b| b.chr }.join
|
95
|
+
end
|
96
|
+
|
97
|
+
def decode8bit data, length
|
98
|
+
octets = data.split('').in_groups_of(2).collect(&:join)[0, length]
|
99
|
+
octets.collect do |o|
|
100
|
+
o.to_i(16).chr
|
101
|
+
end.join
|
102
|
+
end
|
103
|
+
|
104
|
+
def decode16bit data, length
|
105
|
+
dobule_octets = data.split('').in_groups_of(4).collect(&:join)[0, length/2]
|
106
|
+
dobule_octets.collect do |o|
|
107
|
+
[o.to_i(16)].pack("U")
|
108
|
+
end.join
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module PDUTools
|
2
|
+
class MessagePart
|
3
|
+
attr_reader :address, :body, :timestamp, :validity_period, :user_data_header
|
4
|
+
def initialize address, body, timestamp, validity_period, user_data_header
|
5
|
+
@address = address
|
6
|
+
@body = body
|
7
|
+
@timestamp = timestamp
|
8
|
+
@validity_period = validity_period
|
9
|
+
@user_data_header = user_data_header
|
10
|
+
end
|
11
|
+
|
12
|
+
def complete?
|
13
|
+
return true unless @user_data_header
|
14
|
+
if @user_data_header[:parts] > 1
|
15
|
+
false
|
16
|
+
else
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module PDUTools
|
2
|
+
class PDU
|
3
|
+
attr_reader :pdu_hex
|
4
|
+
def initialize pdu_hex
|
5
|
+
@pdu_hex = pdu_hex
|
6
|
+
end
|
7
|
+
|
8
|
+
def checksum
|
9
|
+
@checksum ||= begin
|
10
|
+
sum = @pdu_hex.scan(/../).collect{|c| c.to_i(16)}.sum
|
11
|
+
"%02X" % (sum & 0xFF)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def length
|
16
|
+
@length ||= (@pdu_hex.length / 2) - 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
|
5
|
+
describe PDUTools::Decoder do
|
6
|
+
let(:decoder) { PDUTools::Decoder.new pdu, :ms_to_sc }
|
7
|
+
|
8
|
+
context "7 bit data" do
|
9
|
+
let(:pdu) { "0001000C9124910001100000001654747A0E4ACF416110BD3CA783DAE5F93C7C2E03" }
|
10
|
+
it "should decode" do
|
11
|
+
message_part = decoder.decode
|
12
|
+
expect(message_part.body).to eq "This is a test message"
|
13
|
+
expect(message_part.address).to eq "+421900100100"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "16 bit data" do
|
18
|
+
let(:pdu) { "0001000C9124910001100000083E0054006800690073002000690073002000640069006100630072006900740069006300730020013E0161010D0165017E00FD00E100ED00E400FA00F40148" }
|
19
|
+
it "should decode" do
|
20
|
+
message_part = decoder.decode
|
21
|
+
expect(message_part.body).to eq "This is diacritics ľščťžýáíäúôň"
|
22
|
+
expect(message_part.address).to eq "+421900100100"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "user data header" do
|
27
|
+
let(:pdu) { "0041000C912491000110000000A0060804C643020154747A0E4ACF416110BD3CA783DAE5F93C7C2E53D1E939283D078541F4F29C0E6A97E7F3F0B94C45A7E7A0F41C1406D1CB733AA85D9ECFC3E732159D9E83D2735018442FCFE9A076793E0F9FCB54747A0E4ACF416110BD3CA783DAE5F93C7C2E53D1E939283D078541F4F29C0E6A97E7F3F0B94C45A7E7A0F41C1406D1CB733AA85D9ECFC3" }
|
28
|
+
it "should decode" do
|
29
|
+
message_part = decoder.decode
|
30
|
+
expect(message_part.user_data_header).to be_present
|
31
|
+
expect(message_part.user_data_header[:parts]).to eq 2
|
32
|
+
expect(message_part.user_data_header[:part_number]).to eq 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe PDUTools::Encoder do
|
5
|
+
let(:recipient) { "+421 900 100 100" }
|
6
|
+
let(:encoder) { PDUTools::Encoder.new recipient: recipient, message: message }
|
7
|
+
context "short" do
|
8
|
+
context "7bit text" do
|
9
|
+
let(:message) { "This is a test message" }
|
10
|
+
it "should encode pdu" do
|
11
|
+
pdus = encoder.encode
|
12
|
+
expect(pdus.size).to eq(1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "16bit text" do
|
17
|
+
let(:message) { "This is diacritics ľščťžýáíäúôň" }
|
18
|
+
it "should encode pdu" do
|
19
|
+
pdus = encoder.encode
|
20
|
+
expect(pdus.size).to eq(1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "lonh" do
|
26
|
+
context "7bit text" do
|
27
|
+
let(:message) { "This is a test message" * 10 }
|
28
|
+
it "should encode pdu" do
|
29
|
+
pdus = encoder.encode
|
30
|
+
expect(pdus.size).to eq(2)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "16bit text" do
|
35
|
+
let(:message) { "This is diacritics ľščťžýáíäúôň" * 3 }
|
36
|
+
it "should encode pdu" do
|
37
|
+
pdus = encoder.encode
|
38
|
+
expect(pdus.size).to eq(2)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'pdu_tools'
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pdu_tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Filip Zachar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2014-10-06 00:00:00 Z
|
13
|
+
dependencies: []
|
14
|
+
|
15
|
+
description: Tools for encoding and decoding GSM SMS PDUs.
|
16
|
+
email: tulak45@gmail.com
|
17
|
+
executables: []
|
18
|
+
|
19
|
+
extensions: []
|
20
|
+
|
21
|
+
extra_rdoc_files: []
|
22
|
+
|
23
|
+
files:
|
24
|
+
- lib/pdu_tools/decoder.rb
|
25
|
+
- lib/pdu_tools/encoder.rb
|
26
|
+
- lib/pdu_tools/helpers.rb
|
27
|
+
- lib/pdu_tools/message_part.rb
|
28
|
+
- lib/pdu_tools/pdu.rb
|
29
|
+
- lib/pdu_tools.rb
|
30
|
+
- spec/decoder_spec.rb
|
31
|
+
- spec/encoder_spec.rb
|
32
|
+
- spec/spec_helper.rb
|
33
|
+
homepage: http://rubygems.org/gems/pdu_tools
|
34
|
+
licenses:
|
35
|
+
- MIT
|
36
|
+
metadata: {}
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- &id001
|
46
|
+
- ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- *id001
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 2.0.4
|
56
|
+
signing_key:
|
57
|
+
specification_version: 4
|
58
|
+
summary: Tools for encoding and decoding GSM SMS PDUs.
|
59
|
+
test_files: []
|
60
|
+
|