gsm_encoder 0.1.6 → 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 +5 -5
- data/README.md +40 -0
- data/lib/gsm_encoder.rb +161 -65
- metadata +3 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
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
|
|
@@ -51,3 +80,14 @@ Code cleanup and speedup
|
|
|
51
80
|
### 0.1.5
|
|
52
81
|
|
|
53
82
|
Even faster. About 4 times. Many thanks to @dlarrabee
|
|
83
|
+
|
|
84
|
+
### 0.1.7
|
|
85
|
+
|
|
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
|
@@ -1,24 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
# Stealing from Twitter's Java implementation
|
|
3
3
|
# https://github.com/twitter/cloudhopper-commons-charset/blob/master/src/main/java/com/cloudhopper/commons/charset/GSMCharset.java
|
|
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
|
|
20
|
+
nl = 10.chr
|
|
21
|
+
ff = 12.chr
|
|
22
|
+
cr = 13.chr
|
|
23
|
+
bs = 92.chr
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
NL = 10.chr
|
|
14
|
-
CR = 13.chr
|
|
15
|
-
BS = 92.chr
|
|
25
|
+
GSM_ESCAPE = 0x1b
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
GSM_TABLE = [
|
|
18
28
|
'@', '£', '$', '¥', 'è', 'é', 'ù', 'ì',
|
|
19
|
-
'ò', 'Ç',
|
|
29
|
+
'ò', 'Ç', nl, 'Ø', 'ø', cr , 'Å', 'å',
|
|
20
30
|
'Δ', '_', 'Φ', 'Γ', 'Λ', 'Ω', 'Π', 'Ψ',
|
|
21
|
-
'Σ', 'Θ', 'Ξ',
|
|
31
|
+
'Σ', 'Θ', 'Ξ', nil, 'Æ', 'æ', 'ß', 'É', # 0x1b is the escape
|
|
22
32
|
" ", '!', '"', '#', '¤', '%', '&', "'",
|
|
23
33
|
'(', ')', '*', '+', ',', '-', '.', '/',
|
|
24
34
|
'0', '1', '2', '3', '4', '5', '6', '7',
|
|
@@ -31,73 +41,159 @@ module GSMEncoder
|
|
|
31
41
|
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
|
32
42
|
'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
|
|
33
43
|
'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à',
|
|
34
|
-
].
|
|
35
|
-
|
|
36
|
-
# Extended character table. Characters in this table are accessed by the
|
|
37
|
-
# 'escape' character in the base table. It is important that none of the
|
|
38
|
-
# 'inactive' characters ever be matchable with a valid base-table
|
|
39
|
-
# character as this breaks the encoding loop.
|
|
40
|
-
EXT_CHAR_TABLE = [
|
|
41
|
-
0, 0, 0, 0, 0, 0, 0, 0, 0, 'ç', 0, 0, 0, 0, 0, 0,
|
|
42
|
-
0, 0, 0, 0, '^', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
43
|
-
0, 0, 0, 0, 0, 0, 0, 0, '{', '}', 0, 0, 0, 0, 0, BS,
|
|
44
|
-
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '[', '~', ']', 0,
|
|
45
|
-
'|', 'Á', 0, 0, 0, 0, 0, 0, 0, 'Í', 0, 0, 0, 0, 0, 'Ó',
|
|
46
|
-
0, 0, 0, 0, 0, 'Ú', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
47
|
-
0, 'á', 0, 0, 0, '€', 0, 0, 0, 'í', 0, 0, 0, 0, 0, 'ó',
|
|
48
|
-
0, 0, 0, 0, 0, 'ú', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
REGEX = /\A[ -_a-~#{Regexp.escape(CHAR_TABLE + EXT_CHAR_TABLE.select {|c| c != 0}.join)}]*\Z/
|
|
52
|
-
|
|
53
|
-
# Verifies that this charset can represent every character in the Ruby
|
|
54
|
-
# String.
|
|
55
|
-
# @param str The String to verfiy
|
|
56
|
-
# @return True if the charset can represent every character in the Ruby
|
|
57
|
-
# String, otherwise false.
|
|
58
|
-
def can_encode?(str)
|
|
59
|
-
!str || !!(REGEX =~ str)
|
|
60
|
-
end
|
|
44
|
+
].freeze
|
|
61
45
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
64
59
|
|
|
65
|
-
|
|
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,
|
|
64
|
+
nil, nil, nil, nil, '^', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
65
|
+
nil, nil, nil, nil, nil, nil, nil, nil, '{', '}', nil, nil, nil, nil, nil, bs,
|
|
66
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, '[', '~', ']', nil,
|
|
67
|
+
'|', 'Á', nil, nil, nil, nil, nil, nil, nil, 'Í', nil, nil, nil, nil, nil, 'Ó',
|
|
68
|
+
nil, nil, nil, nil, nil, 'Ú', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
69
|
+
nil, 'á', nil, nil, nil, '€', nil, nil, nil, 'í', nil, nil, nil, nil, nil, 'ó',
|
|
70
|
+
nil, nil, nil, nil, nil, 'ú', nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
71
|
+
].freeze
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
# kept for backwards compatibility, this was the only extension table before
|
|
74
|
+
GSM_EXT_TABLE = SPANISH_EXT_TABLE
|
|
75
|
+
|
|
76
|
+
UTF8_ARRAY_SIZE = 128
|
|
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
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
99
|
else
|
|
77
|
-
|
|
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
|
|
78
108
|
end
|
|
79
109
|
end
|
|
80
|
-
rescue
|
|
81
|
-
# TODO: ?
|
|
82
110
|
end
|
|
83
|
-
|
|
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
|
|
117
|
+
end
|
|
118
|
+
|
|
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
|
|
84
158
|
end
|
|
85
159
|
|
|
86
|
-
|
|
87
|
-
|
|
160
|
+
# Verifies that the given string can be encoded in GSM 03.38
|
|
161
|
+
def can_encode?(str, extension_table: nil)
|
|
162
|
+
!str || !!(GSMEncoder.charset(extension_table).regexp =~ str)
|
|
163
|
+
end
|
|
88
164
|
|
|
89
|
-
|
|
165
|
+
# Encode given UTF-8 string to GSM 03.38
|
|
166
|
+
def encode(string, replacement = "?", extension_table: nil)
|
|
167
|
+
return nil if string.nil?
|
|
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)
|
|
172
|
+
buffer = String.new(encoding: "binary")
|
|
173
|
+
string.each_codepoint do |codepoint|
|
|
174
|
+
if codepoint < UTF8_ARRAY_SIZE
|
|
175
|
+
buffer << (utf8_array[codepoint] || replacement)
|
|
176
|
+
else
|
|
177
|
+
buffer << (utf8_hash[codepoint] || replacement)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
buffer
|
|
181
|
+
end
|
|
90
182
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
183
|
+
# Encode given GSM 03.38 string to UTF-8
|
|
184
|
+
def decode(string, replacement = "?", extension_table: nil)
|
|
185
|
+
return nil if string.nil?
|
|
186
|
+
ext_table = GSMEncoder.charset(extension_table).ext_table
|
|
187
|
+
buffer = +""
|
|
188
|
+
escaped = false
|
|
189
|
+
string.each_byte do |c|
|
|
190
|
+
if c == GSM_ESCAPE
|
|
191
|
+
escaped = true
|
|
192
|
+
elsif escaped
|
|
193
|
+
buffer << (ext_table[c] || replacement)
|
|
194
|
+
escaped = false
|
|
97
195
|
else
|
|
98
|
-
buffer << (
|
|
99
|
-
# go back to the default table
|
|
100
|
-
table = CHAR_TABLE
|
|
196
|
+
buffer << (GSM_TABLE[c] || replacement)
|
|
101
197
|
end
|
|
102
198
|
end
|
|
103
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
|
|
@@ -41,8 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
41
41
|
- !ruby/object:Gem::Version
|
|
42
42
|
version: 1.3.6
|
|
43
43
|
requirements: []
|
|
44
|
-
|
|
45
|
-
rubygems_version: 2.2.0
|
|
44
|
+
rubygems_version: 3.0.3.1
|
|
46
45
|
signing_key:
|
|
47
46
|
specification_version: 4
|
|
48
47
|
summary: ruby GSM 03.38 encoder/decoder
|