pdu_tools 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|