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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -0
  3. data/lib/gsm_encoder.rb +124 -41
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be392aec01587b907e1e0454bec610cc068359a88d614cb31850d5208503cf9d
4
- data.tar.gz: 13b2b3a007cae53be426a8da7413676e020554cccf62ef838b87e4266f71d368
3
+ metadata.gz: 328cd3d4611243a39035910afc138fda9bdcc23620edc2c6fa83eb4bf9bfa4dc
4
+ data.tar.gz: 450843ca2309b3070ed92835b5e81620c7bf38467c8e435017e5692c3e8cce7c
5
5
  SHA512:
6
- metadata.gz: fc9ace5dc2f040ad1fc4ed994dc43c74c3db7b513cdf9d6a1cf3491eb794635dc64c88e6c5ad2c3fe73cc1b9d4d65b9202d388cfc9ee32f46b0bf4c6eac2301b
7
- data.tar.gz: 4edfb38d92c90f16963bea9525e47f082af4a320badd64b79b1e382f56abc8c291a3cd3fa0c8187fcfdff9cc8cb5ce1ad5724928c214c7e1769f5cca12244b10
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 the default extension table. The default alphabet
8
- # and it's extension table is defined in GSM 03.38.
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
- GSM_EXT_TABLE = [
37
- nil, nil, nil, nil, nil, nil, nil, nil, nil, 'ç', nil, nil, nil, nil, nil, nil,
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
- # build UTF8 to GSM tables
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
- UTF8_ARRAY = []
50
- UTF8_HASH = {}
51
- [
52
- [ GSM_TABLE, nil ],
53
- [ GSM_EXT_TABLE, GSM_ESCAPE ]
54
- ].each do |table, prefix|
55
- table.each.with_index do |char, index|
56
- next if char.nil?
57
- # build GSM value
58
- if prefix
59
- value = +''.encode('binary')
60
- value << prefix
61
- value << index
62
- else
63
- value = index
64
- end
65
- # store GSM value
66
- key = char.codepoints.first
67
- if key < UTF8_ARRAY_SIZE
68
- UTF8_ARRAY[key] = value
69
- else
70
- UTF8_HASH[key] = value
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
- REGEXP = /\A[#{Regexp.escape((GSM_TABLE + GSM_EXT_TABLE).compact.join)}]*\Z/
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 || !!(REGEXP =~ 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
- replacement = replacement == "?" ? 63 : encode(replacement)
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 << (UTF8_ARRAY[codepoint] || replacement)
175
+ buffer << (utf8_array[codepoint] || replacement)
94
176
  else
95
- buffer << (UTF8_HASH[codepoint] || replacement)
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 << GSM_EXT_TABLE[c] || replacement
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.7
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: 2024-09-13 00:00:00.000000000 Z
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