gsm_encoder 0.1.7 → 0.1.8
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 +4 -4
- data/README.md +36 -0
- data/lib/gsm_encoder.rb +124 -41
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 328cd3d4611243a39035910afc138fda9bdcc23620edc2c6fa83eb4bf9bfa4dc
|
|
4
|
+
data.tar.gz: 450843ca2309b3070ed92835b5e81620c7bf38467c8e435017e5692c3e8cce7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98bd3e1a50f2e05b957cff8c041666e4647d8e9c0ef28ad795ef610fa5423e9978d9015b318a9b116e6dd4b65deea03b0ed8298c7c04eba50452ec59ce43aece
|
|
7
|
+
data.tar.gz: e2b7a8daacf2c746946fc772b0eb22aa344de19e811322c637c24d810cb50c455db399b2b1dc10c55371c533d2f46726700aac148701d55faa0d08eb76421eea
|
data/README.md
CHANGED
|
@@ -29,6 +29,35 @@ __NOTE: ruby >= 1.9.2 is required__
|
|
|
29
29
|
# replaces unsupported chars with a provided value ' '
|
|
30
30
|
GSMEncoder.encode('`', ' ') # => ' '
|
|
31
31
|
|
|
32
|
+
## Extension tables
|
|
33
|
+
|
|
34
|
+
Characters like `{ } [ ] ~ \ | € ^` are sent as an escape plus a code from an
|
|
35
|
+
extension table. Two tables are built in:
|
|
36
|
+
|
|
37
|
+
- `:spanish` (the default) - the Spanish national language single shift table.
|
|
38
|
+
It adds `ç Á Í Ó Ú á í ó ú` on top of the standard table.
|
|
39
|
+
- `:standard` - the GSM 03.38 default extension table.
|
|
40
|
+
|
|
41
|
+
The Spanish characters are only valid GSM 7-bit when the message header selects
|
|
42
|
+
the Spanish shift table. Most SMS providers (Twilio, Telnyx, Plivo, ...) don't,
|
|
43
|
+
so a message containing them is sent as UCS-2 instead. That cuts a segment from
|
|
44
|
+
160 characters to 70. Use `:standard` if you rely on `can_encode?` or
|
|
45
|
+
`encode` to keep messages in GSM 7-bit:
|
|
46
|
+
|
|
47
|
+
# for every call
|
|
48
|
+
GSMEncoder.extension_table = :standard
|
|
49
|
+
|
|
50
|
+
# or for one call
|
|
51
|
+
GSMEncoder.can_encode?('á', extension_table: :standard) # => false
|
|
52
|
+
GSMEncoder.encode('á', extension_table: :standard) # => '?'
|
|
53
|
+
GSMEncoder.decode(bytes, extension_table: :standard)
|
|
54
|
+
|
|
55
|
+
You can also register your own table, an Array indexed by GSM code with `nil`
|
|
56
|
+
for unused codes:
|
|
57
|
+
|
|
58
|
+
GSMEncoder.register_extension_table(:mine, table)
|
|
59
|
+
GSMEncoder.extension_table = :mine
|
|
60
|
+
|
|
32
61
|
## Updates
|
|
33
62
|
|
|
34
63
|
### 0.1.1
|
|
@@ -55,3 +84,10 @@ Even faster. About 4 times. Many thanks to @dlarrabee
|
|
|
55
84
|
### 0.1.7
|
|
56
85
|
|
|
57
86
|
Even faster! Many thanks to @ThomasSevestre
|
|
87
|
+
|
|
88
|
+
### 0.1.8
|
|
89
|
+
|
|
90
|
+
- Configurable extension table (`:standard` or `:spanish`), defaults to `:spanish`
|
|
91
|
+
- Adds form feed to the extension tables
|
|
92
|
+
- `decode` returns the replacement for unknown codes instead of raising `TypeError`
|
|
93
|
+
|
data/lib/gsm_encoder.rb
CHANGED
|
@@ -4,11 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
#
|
|
6
6
|
# This class encodes and decodes Ruby Strings to and from the SMS default
|
|
7
|
-
# alphabet. It also supports
|
|
8
|
-
#
|
|
7
|
+
# alphabet. It also supports an extension table. The default alphabet and
|
|
8
|
+
# its extension tables are defined in GSM 03.38 (3GPP TS 23.038).
|
|
9
|
+
#
|
|
10
|
+
# Which extension table is used can be configured:
|
|
11
|
+
#
|
|
12
|
+
# GSMEncoder.extension_table = :standard # GSM 03.38 default extension table only
|
|
13
|
+
# GSMEncoder.extension_table = :spanish # Spanish single shift table (the default)
|
|
14
|
+
#
|
|
15
|
+
# or per call:
|
|
16
|
+
#
|
|
17
|
+
# GSMEncoder.encode("¿Qué?", extension_table: :standard)
|
|
9
18
|
module GSMEncoder
|
|
10
19
|
# GSM to UTF8 tables
|
|
11
20
|
nl = 10.chr
|
|
21
|
+
ff = 12.chr
|
|
12
22
|
cr = 13.chr
|
|
13
23
|
bs = 92.chr
|
|
14
24
|
|
|
@@ -33,8 +43,24 @@ module GSMEncoder
|
|
|
33
43
|
'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à',
|
|
34
44
|
].freeze
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
# GSM 03.38 default extension table. This is the only extension table a
|
|
47
|
+
# handset (or SMS provider) uses unless the message explicitly selects a
|
|
48
|
+
# national language shift table in its header.
|
|
49
|
+
STANDARD_EXT_TABLE = [
|
|
50
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, ff, nil, nil, nil, nil, nil,
|
|
51
|
+
nil, nil, nil, nil, '^', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
52
|
+
nil, nil, nil, nil, nil, nil, nil, nil, '{', '}', nil, nil, nil, nil, nil, bs,
|
|
53
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, '[', '~', ']', nil,
|
|
54
|
+
'|', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
55
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
56
|
+
nil, nil, nil, nil, nil, '€', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
57
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
# Spanish national language single shift table (3GPP TS 23.038 A.2.2).
|
|
61
|
+
# Only decoded correctly when the message header selects the Spanish shift.
|
|
62
|
+
SPANISH_EXT_TABLE = [
|
|
63
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, 'ç', ff, nil, nil, nil, nil, nil,
|
|
38
64
|
nil, nil, nil, nil, '^', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
39
65
|
nil, nil, nil, nil, nil, nil, nil, nil, '{', '}', nil, nil, nil, nil, nil, bs,
|
|
40
66
|
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, '[', '~', ']', nil,
|
|
@@ -44,73 +70,130 @@ module GSMEncoder
|
|
|
44
70
|
nil, nil, nil, nil, nil, 'ú', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
45
71
|
].freeze
|
|
46
72
|
|
|
47
|
-
#
|
|
73
|
+
# kept for backwards compatibility, this was the only extension table before
|
|
74
|
+
GSM_EXT_TABLE = SPANISH_EXT_TABLE
|
|
75
|
+
|
|
48
76
|
UTF8_ARRAY_SIZE = 128
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
|
|
78
|
+
# UTF8 to GSM lookups for one extension table
|
|
79
|
+
Charset = Struct.new(:ext_table, :utf8_array, :utf8_hash, :regexp)
|
|
80
|
+
|
|
81
|
+
def self.build_charset(ext_table)
|
|
82
|
+
unless ext_table.is_a?(Array) && ext_table.size <= UTF8_ARRAY_SIZE
|
|
83
|
+
raise ArgumentError, "extension table must be an Array of at most #{UTF8_ARRAY_SIZE} entries"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
utf8_array = []
|
|
87
|
+
utf8_hash = {}
|
|
88
|
+
[
|
|
89
|
+
[ GSM_TABLE, nil ],
|
|
90
|
+
[ ext_table, GSM_ESCAPE ]
|
|
91
|
+
].each do |table, prefix|
|
|
92
|
+
table.each.with_index do |char, index|
|
|
93
|
+
next if char.nil?
|
|
94
|
+
# build GSM value
|
|
95
|
+
if prefix
|
|
96
|
+
value = +''.encode('binary')
|
|
97
|
+
value << prefix
|
|
98
|
+
value << index
|
|
99
|
+
else
|
|
100
|
+
value = index
|
|
101
|
+
end
|
|
102
|
+
# store GSM value
|
|
103
|
+
key = char.codepoints.first
|
|
104
|
+
if key < UTF8_ARRAY_SIZE
|
|
105
|
+
utf8_array[key] = value
|
|
106
|
+
else
|
|
107
|
+
utf8_hash[key] = value
|
|
108
|
+
end
|
|
71
109
|
end
|
|
72
110
|
end
|
|
111
|
+
utf8_array.each(&:freeze)
|
|
112
|
+
utf8_hash.each_value(&:freeze)
|
|
113
|
+
|
|
114
|
+
regexp = /\A[#{Regexp.escape((GSM_TABLE + ext_table).compact.join)}]*\Z/
|
|
115
|
+
|
|
116
|
+
Charset.new(ext_table.dup.freeze, utf8_array.freeze, utf8_hash.freeze, regexp).freeze
|
|
73
117
|
end
|
|
74
|
-
UTF8_ARRAY.freeze
|
|
75
|
-
UTF8_ARRAY.each(&:freeze)
|
|
76
|
-
UTF8_HASH.freeze
|
|
77
|
-
UTF8_HASH.each_value(&:freeze)
|
|
78
118
|
|
|
79
|
-
|
|
119
|
+
@charsets = {
|
|
120
|
+
standard: build_charset(STANDARD_EXT_TABLE),
|
|
121
|
+
spanish: build_charset(SPANISH_EXT_TABLE)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# kept for backwards compatibility
|
|
125
|
+
UTF8_ARRAY = @charsets[:spanish].utf8_array
|
|
126
|
+
UTF8_HASH = @charsets[:spanish].utf8_hash
|
|
127
|
+
REGEXP = @charsets[:spanish].regexp
|
|
128
|
+
|
|
129
|
+
@extension_table = :spanish
|
|
130
|
+
|
|
131
|
+
class << self
|
|
132
|
+
# The extension table used when a call doesn't pass one (:spanish by default)
|
|
133
|
+
attr_reader :extension_table
|
|
134
|
+
|
|
135
|
+
def extension_table=(name)
|
|
136
|
+
charset(name) # validate
|
|
137
|
+
@extension_table = name
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Register a custom extension table (an Array indexed by GSM code, nil for unused codes)
|
|
141
|
+
#
|
|
142
|
+
# GSMEncoder.register_extension_table(:mine, table)
|
|
143
|
+
# GSMEncoder.extension_table = :mine
|
|
144
|
+
def register_extension_table(name, table)
|
|
145
|
+
@charsets = @charsets.merge(name.to_sym => build_charset(table)).freeze
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def extension_tables
|
|
149
|
+
@charsets.keys
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def charset(name = nil)
|
|
153
|
+
name ||= @extension_table
|
|
154
|
+
@charsets.fetch(name.to_sym) do
|
|
155
|
+
raise ArgumentError, "unknown extension table #{name.inspect}, expected one of #{@charsets.keys.inspect}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
80
159
|
|
|
81
160
|
# Verifies that the given string can be encoded in GSM 03.38
|
|
82
|
-
def can_encode?(str)
|
|
83
|
-
!str || !!(
|
|
161
|
+
def can_encode?(str, extension_table: nil)
|
|
162
|
+
!str || !!(GSMEncoder.charset(extension_table).regexp =~ str)
|
|
84
163
|
end
|
|
85
164
|
|
|
86
165
|
# Encode given UTF-8 string to GSM 03.38
|
|
87
|
-
def encode(string, replacement = "?")
|
|
166
|
+
def encode(string, replacement = "?", extension_table: nil)
|
|
88
167
|
return nil if string.nil?
|
|
89
|
-
|
|
168
|
+
charset = GSMEncoder.charset(extension_table)
|
|
169
|
+
utf8_array = charset.utf8_array
|
|
170
|
+
utf8_hash = charset.utf8_hash
|
|
171
|
+
replacement = replacement == "?" ? 63 : encode(replacement, extension_table: extension_table)
|
|
90
172
|
buffer = String.new(encoding: "binary")
|
|
91
173
|
string.each_codepoint do |codepoint|
|
|
92
174
|
if codepoint < UTF8_ARRAY_SIZE
|
|
93
|
-
buffer << (
|
|
175
|
+
buffer << (utf8_array[codepoint] || replacement)
|
|
94
176
|
else
|
|
95
|
-
buffer << (
|
|
177
|
+
buffer << (utf8_hash[codepoint] || replacement)
|
|
96
178
|
end
|
|
97
179
|
end
|
|
98
180
|
buffer
|
|
99
181
|
end
|
|
100
182
|
|
|
101
183
|
# Encode given GSM 03.38 string to UTF-8
|
|
102
|
-
def decode(string, replacement = "?")
|
|
184
|
+
def decode(string, replacement = "?", extension_table: nil)
|
|
103
185
|
return nil if string.nil?
|
|
186
|
+
ext_table = GSMEncoder.charset(extension_table).ext_table
|
|
104
187
|
buffer = +""
|
|
105
188
|
escaped = false
|
|
106
189
|
string.each_byte do |c|
|
|
107
190
|
if c == GSM_ESCAPE
|
|
108
191
|
escaped = true
|
|
109
192
|
elsif escaped
|
|
110
|
-
buffer <<
|
|
193
|
+
buffer << (ext_table[c] || replacement)
|
|
111
194
|
escaped = false
|
|
112
195
|
else
|
|
113
|
-
buffer << GSM_TABLE[c] || replacement
|
|
196
|
+
buffer << (GSM_TABLE[c] || replacement)
|
|
114
197
|
end
|
|
115
198
|
end
|
|
116
199
|
buffer
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gsm_encoder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yury Korolev
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2026-11-12 00:00:00.000000000 Z
|
|
13
13
|
dependencies: []
|
|
14
14
|
description: GSMEncoder encodes and decodes Ruby Strings to and from the SMS default
|
|
15
15
|
alphabet. It also supports the default extension table. The default alphabet and
|