da_funk 0.4.4

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.yardopts +12 -0
  4. data/Gemfile +7 -0
  5. data/Gemfile.lock +21 -0
  6. data/README.md +108 -0
  7. data/README_GUIDE.md +52 -0
  8. data/RELEASE_NOTES.md +23 -0
  9. data/Rakefile +69 -0
  10. data/da_funk.gemspec +31 -0
  11. data/ext/da_funk/Makefile +5 -0
  12. data/ext/da_funk/extconf.rb +8 -0
  13. data/guides/sample_input_output.rb +14 -0
  14. data/guides/sample_message_iso8583.rb +14 -0
  15. data/guides/sample_network_gprs.rb +26 -0
  16. data/guides/sample_read_magnect_card.rb +8 -0
  17. data/guides/sample_socket.rb +29 -0
  18. data/guides/sample_transaction.rb +23 -0
  19. data/guides/sample_transaction_download_application.rb +9 -0
  20. data/guides/sample_transaction_download_file.rb +6 -0
  21. data/guides/sample_transaction_download_parameter_file.rb +18 -0
  22. data/guides/sample_transaction_iso8583.rb +222 -0
  23. data/imgs/daft-punk-da-funk.jpg +0 -0
  24. data/lib/da_funk/rake_task.rb +129 -0
  25. data/lib/da_funk/test.rb +87 -0
  26. data/lib/da_funk.rb +39 -0
  27. data/lib/device/application.rb +41 -0
  28. data/lib/device/audio.rb +17 -0
  29. data/lib/device/crypto.rb +52 -0
  30. data/lib/device/display.rb +47 -0
  31. data/lib/device/helper.rb +161 -0
  32. data/lib/device/io.rb +86 -0
  33. data/lib/device/magnetic.rb +79 -0
  34. data/lib/device/network.rb +192 -0
  35. data/lib/device/notification.rb +116 -0
  36. data/lib/device/notification_callback.rb +47 -0
  37. data/lib/device/notification_event.rb +29 -0
  38. data/lib/device/params_dat.rb +119 -0
  39. data/lib/device/printer.rb +35 -0
  40. data/lib/device/runtime.rb +22 -0
  41. data/lib/device/setting.rb +63 -0
  42. data/lib/device/support.rb +28 -0
  43. data/lib/device/system.rb +61 -0
  44. data/lib/device/transaction/download.rb +268 -0
  45. data/lib/device/transaction/emv.rb +45 -0
  46. data/lib/device/transaction/iso.rb +75 -0
  47. data/lib/device/version.rb +11 -0
  48. data/lib/device/walk.rb +8 -0
  49. data/lib/device.rb +28 -0
  50. data/lib/ext/kernel.rb +9 -0
  51. data/lib/file_db.rb +47 -0
  52. data/lib/iso8583/bitmap.rb +114 -0
  53. data/lib/iso8583/codec.rb +197 -0
  54. data/lib/iso8583/exception.rb +4 -0
  55. data/lib/iso8583/field.rb +90 -0
  56. data/lib/iso8583/fields.rb +171 -0
  57. data/lib/iso8583/message.rb +455 -0
  58. data/lib/iso8583/util.rb +91 -0
  59. data/lib/iso8583/version.rb +6 -0
  60. data/lib/serfx/commands.rb +191 -0
  61. data/lib/serfx/connection.rb +160 -0
  62. data/lib/serfx/exceptions.rb +5 -0
  63. data/lib/serfx/response.rb +28 -0
  64. data/lib/serfx.rb +27 -0
  65. data/lib/version.rb +5 -0
  66. data/lib/zip.rb +29 -0
  67. data/test/integration/getc_test.rb +6 -0
  68. data/test/integration/mrb_eval_test.rb +36 -0
  69. data/test/integration/notification_test.rb +20 -0
  70. data/test/integration/params_dat_test.rb +11 -0
  71. data/test/test_helper.rb +18 -0
  72. data/test/unit/device/display_test.rb +43 -0
  73. data/test/unit/device/helper_test.rb +61 -0
  74. data/test/unit/device/notification_callback_test.rb +6 -0
  75. data/test/unit/device/notification_event_test.rb +73 -0
  76. data/test/unit/device/notification_test.rb +21 -0
  77. data/utils/command_line_platform.rb +67 -0
  78. data/utils/test_run.rb +3 -0
  79. metadata +177 -0
@@ -0,0 +1,75 @@
1
+
2
+ class Device
3
+ class Transaction
4
+ # Object responsible to create ISO8583 message
5
+ #
6
+ # == Sample:
7
+ #
8
+ # message = Device::Iso.new
9
+ # message.mti = 1110
10
+ # message[2] = 474747474747
11
+ # message["Processing Code"] = "123456"
12
+ #
13
+ # pan = message["Primary Account Number (PAN)"]
14
+ # message.pan = 47474747474747
15
+ #
16
+ # puts message.pan
17
+ # puts message.to_b
18
+ # puts message.to_s
19
+ # mes2 = Device::Iso.parse input
20
+ #
21
+ class Iso < ISO8583::Message
22
+ include ISO8583
23
+ mti_format N, :length => 4
24
+ mti 1100, "Authorization Request Acquirer Gateway"
25
+
26
+ mti 1110, "Authorization Request Response Issuer Gateway"
27
+ mti 1420, "Reversal Advice Acquirer Gateway"
28
+ mti 1421, "Reversal Advice Repeat Acquirer Gateway"
29
+ mti 1430, "Reversal Advice Response Issuer Gateway"
30
+ mti 1804, "Network Management Request Acquirer Gateway or Issuer Gateway"
31
+ mti 1814, "Network Management Request Response Issuer Gateway or Acquirer Gateway"
32
+
33
+ bmp 2, "Primary Account Number (PAN)", LLVAR_N, :max => 19
34
+ bmp 3, "Processing Code", N, :length => 6
35
+ bmp 4, "Amount (Transaction)", N, :length => 12
36
+ bmp 6, "Amount, Cardholder Billing" , N, :length => 12
37
+ bmp 7, "Date and Time, Transmission" , MMDDhhmmss
38
+ bmp 10, "Conversion Rate, Cardholder Billing", N, :length => 8
39
+ bmp 11, "System Trace Audit Number (STAN)", N, :length => 6
40
+ bmp 12, "Date and Time, Local Transaction", YYMMDDhhmmss
41
+ bmp 14, "Date, Expiration", YYMM
42
+ bmp 22, "POS Data Code", AN, :length => 12
43
+ bmp 23, "Card Sequence Number", N, :length => 3
44
+ bmp 24, "Function Code", N, :length => 3
45
+ bmp 25, "Message Reason Code", N, :length => 4
46
+ bmp 26, "Card Acceptor Business Code", N, :length => 4
47
+ bmp 30, "Amounts, Original", N, :length => 24
48
+ bmp 32, "Acquiring Institution Identification Code", LLVAR_N, :max => 11
49
+ bmp 35, "Track 2 Data", LLVAR_Z, :max => 37
50
+ bmp 37, "Retrieval Reference Number", ANP, :length => 12
51
+ bmp 38, "Approval Code", ANP, :length => 6
52
+ bmp 39, "Action Code", N, :length => 3
53
+ bmp 41, "Card Acceptor Terminal Identification", ANS, :length => 8
54
+ bmp 42, "Card Acceptor Identification Code", ANS, :length => 15
55
+ bmp 43, "Card Acceptor Name/Location", LLVAR_ANS, :max => 56
56
+ bmp 49, "Currency Code, Transaction", N, :length => 3
57
+ bmp 51, "Currency Code, Cardholder Billing", N, :length => 3
58
+ bmp 52, "Personal Identification Number (PIN) Data", B, :length => 8
59
+ bmp 53, "Security Related Control Information", LLVAR_B, :max => 48
60
+ bmp 54, "Amounts, Additional", LLLVAR_ANS,:max => 40
61
+
62
+ bmp 55, "Integrated Circuit Card (ICC) System Related Data", LLLVAR_B, :max => 255
63
+ bmp 56, "Original Data Elements", LLVAR_N, :max => 35
64
+ bmp 58, "Authorizing Agent Institution Identification Code", LLVAR_N, :max => 11
65
+ bmp 59, "Additional Data - Private", LLLVAR_ANS, :max => 67
66
+ bmp 64, "Message Authentication Code (MAC) Field", B, :length => 8
67
+
68
+ bmp_alias 2, :pan
69
+ bmp_alias 3, :proc_code
70
+ bmp_alias 4, :amount
71
+ bmp_alias 12, :exp_date
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,11 @@
1
+
2
+ class Device
3
+ def self.api_version
4
+ Device::API_VERSION
5
+ end
6
+
7
+ def self.version
8
+ adapter.version
9
+ end
10
+ end
11
+
@@ -0,0 +1,8 @@
1
+
2
+ class Device
3
+ class Walk
4
+ def self.execute(file)
5
+ load file
6
+ end
7
+ end
8
+ end
data/lib/device.rb ADDED
@@ -0,0 +1,28 @@
1
+ class Device
2
+ class << self
3
+ attr_accessor :adapter
4
+ end
5
+
6
+ # Flat syntax/behaviour API between versions, to any application be able to execute on whole versions.
7
+ #
8
+ # @return [Class] the class object flatted
9
+ def self.flat_api
10
+ klass_version = Device.version.gsub(".", "")
11
+ # Sample
12
+ #if Device.version == "0.4.3"
13
+ #const_get("VersionFlat#{klass_version}").flat Device
14
+ #end
15
+ end
16
+
17
+ def self.call(*args)
18
+ end
19
+
20
+ def self.app_loop(&block)
21
+ Notification.setup
22
+ loop do
23
+ Notification.check
24
+ block.call(self)
25
+ end
26
+ end
27
+ end
28
+
data/lib/ext/kernel.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Kernel
2
+ def get_format(*args)
3
+ Device::IO.get_format(*args)
4
+ end
5
+
6
+ def print_line(buf, row = 0, column = 0)
7
+ Device::Display.print_line(buf, row, column)
8
+ end
9
+ end
data/lib/file_db.rb ADDED
@@ -0,0 +1,47 @@
1
+
2
+ class FileDb
3
+ attr_accessor :path, :hash
4
+
5
+ def initialize(path, default_value = {})
6
+ @hash = default_value.dup
7
+ @path = path
8
+ self.open
9
+ end
10
+
11
+ def open
12
+ if File.exist?(@path)
13
+ file = File.open(@path)
14
+ self.parse(file.read)
15
+ end
16
+ ensure
17
+ file.close if file
18
+ end
19
+
20
+ def parse(text)
21
+ text.split("\n").compact.each do |line|
22
+ key_value = line.split("=")
23
+ if key_value[1] && (@hash[key_value[0]].nil? || @hash[key_value[0]].empty?)
24
+ @hash[key_value[0]] = key_value[1]
25
+ end
26
+ end
27
+ end
28
+
29
+ def save
30
+ file_new = File.open(@path, "w+")
31
+ @hash.each do |line_key, line_value|
32
+ file_new.puts("#{line_key}=#{line_value}")
33
+ end
34
+ file_new.close
35
+ end
36
+
37
+ def []=(key, value)
38
+ ret = @hash[key] = value
39
+ save
40
+ ret
41
+ end
42
+
43
+ def [](key)
44
+ @hash[key]
45
+ end
46
+ end
47
+
@@ -0,0 +1,114 @@
1
+
2
+ module ISO8583
3
+
4
+ # This class constructs an object for handling bitmaps
5
+ # with which ISO8583 messages typically begin.
6
+ # Bitmaps are either 8 or 16 bytes long, an extended length
7
+ # bitmap is indicated by the first bit being set.
8
+ # In all likelyhood, you won't be using this class much, it's used
9
+ # transparently by the Message class.
10
+ class Bitmap
11
+
12
+ # create a new Bitmap object. In case an iso message
13
+ # is passed in, that messages bitmap will be parsed. If
14
+ # not, this initializes and empty bitmap.
15
+ def initialize(message = nil)
16
+ @bmp = Array.new(128, false)
17
+ if !message
18
+
19
+ else
20
+ initialize_from_message message
21
+ end
22
+ end
23
+
24
+ # yield once with the number of each set field.
25
+ def each #:yields: each bit set in the bitmap.
26
+ @bmp.each_with_index {|set, i| yield i+1 if set}
27
+ end
28
+
29
+ # Returns whether the bit is set or not.
30
+ def [](i)
31
+ @bmp[i-1]
32
+ end
33
+
34
+ # Set the bit to the indicated value. Only `true` sets the
35
+ # bit, any other value unsets it.
36
+ def []=(i, value)
37
+ if i > 128
38
+ raise ISO8583Exception.new("Bits > 128 are not permitted.")
39
+ elsif i < 2
40
+ raise ISO8583Exception.new("Bits < 2 are not permitted (continutation bit is set automatically)")
41
+ end
42
+ @bmp[i-1] = (value == true)
43
+ end
44
+
45
+ # Sets bit #i
46
+ def set(i)
47
+ self[i] = true
48
+ end
49
+
50
+ # Unsets bit #i
51
+ def unset(i)
52
+ self[i] = false
53
+ end
54
+
55
+ # Generate the bytes representing this bitmap.
56
+ def to_bytes
57
+ arr = [self.to_s]
58
+ # tricky and ugly, setting bit[1] only when generating to_s...
59
+ count = self[1] ? 128 : 64
60
+ arr.pack("B#{count}")
61
+ end
62
+ alias_method :to_b, :to_bytes
63
+
64
+ # Generate a String representation of this bitmap in the form:
65
+ # 01001100110000011010110110010100100110011000001101011011001010
66
+ def to_s
67
+ #check whether any `high` bits are set
68
+ @bmp[0] = false
69
+ 65.upto(128) {|i|
70
+ if self[i]
71
+ # if so, set continuation bit
72
+ @bmp[0] = true
73
+ break
74
+ end
75
+ }
76
+ str = ""
77
+ 1.upto(self[1] ? 128 : 64) {|i|
78
+ str << (self[i] ? "1" : "0")
79
+ }
80
+ str
81
+ end
82
+
83
+
84
+ private
85
+
86
+ def initialize_from_message(message)
87
+ bmp = message.to_i(16).to_s(2)
88
+ if bmp[0,1] == "1"
89
+ bmp = message.to_i(16).to_s(2)
90
+ end
91
+
92
+ 0.upto(bmp.length-1) do |i|
93
+ @bmp[i] = (bmp[i,1] == "1")
94
+ end
95
+ end
96
+
97
+ class << self
98
+ # Parse the bytes in string and return the Bitmap and bytes remaining in `str`
99
+ # after the bitmap is taken away.
100
+ def parse(str)
101
+ bmp = Bitmap.new(str)
102
+ rest = bmp[1] ? str[32, str.length] : str[16, str.length]
103
+ [ bmp, rest ]
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+
110
+ if __FILE__==$0
111
+ mp = ISO8583::Bitmap.new
112
+ 20.step(128,7) {|i| mp.set(i)}
113
+ print mp.to_bytes
114
+ end
@@ -0,0 +1,197 @@
1
+ #require 'date'
2
+
3
+ module ISO8583
4
+
5
+ # Codec provides functionality to encode and decode values, codecs are
6
+ # used internally by Field instances in order to do character conversions
7
+ # and checking for proper values.
8
+ # Although they are used internally, you will probably need to write
9
+ # your own Codec sooner or later. The codecs used by Field instances are
10
+ # typically instances of Codec, it may or may not be usefull to subclass
11
+ # Codec.
12
+ #
13
+ # Say, for example, a text field needs to be encoded in EBCDIC in the
14
+ # message, this is how a corresponding codec would be constructed:
15
+ #
16
+ # EBCDIC_Codec = Codec.new
17
+ # EBCDIC_Codec.encoder = lambda {|ascii_str|
18
+ # raise ISO8583Exception.new("String (#{ascii_str})not valid!") unless =~ /someregexp/
19
+ # ascii2ebcdic ascii_str # implementing ascii_str is left as an excercise
20
+ # }
21
+ # EBCDIC_Codec.decode = lambda {|ebcdic_str|
22
+ # # you may or may not want to raise exceptions at this point ....
23
+ # # strip removes any padding...
24
+ # ebcdic2ascii(ebcdic_str).strip
25
+ # }
26
+ #
27
+ # This instance of Codec would then be used be the corresponding Field
28
+ # encoder/decoder, which may look similar to this:
29
+ #
30
+ # EBCDIC = Field.new
31
+ # EBCDIC.codec = EBCDIC_Codec
32
+ # EBCDIC.padding = PADDING_LEFT_JUSTIFIED_SPACES
33
+ #
34
+ # Notice there is a bit of inconsistancy: the padding is added by the
35
+ # field, but removed by the codec. I would like to find a better
36
+ # solution to this...
37
+ #
38
+ # See also: Field, link:files/lib/fields_rb.html
39
+ #
40
+ # The following codecs are already implemented:
41
+ # [+ASCII_Number+] encodes either a Number or String representation of
42
+ # a number to the ASCII represenation of the number,
43
+ # decodes ASCII numerals to a number
44
+ # [+A_Codec+] passes through ASCII string checking they conform to [A-Za-z]
45
+ # during encoding, no validity check during decoding.
46
+ # [+AN_Codec+] passes through ASCII string checking they conform to [A-Za-z0-9]
47
+ # during encoding, no validity check during decoding.
48
+ # [+ANP_Codec+] passes through ASCII string checking they conform to [A-Za-z0-9 ]
49
+ # during encoding, no validity check during decoding.
50
+ # [+ANS_Codec+] passes through ASCII string checking they conform to [\x20-\x7E]
51
+ # during encoding, no validity check during decoding.
52
+ # [+Null_Codec+] passes anything along untouched.
53
+ # [<tt>Track2</tt>] rudimentary check that string conforms to Track2
54
+ # [+MMDDhhmmssCodec+] encodes Time, Datetime or String to the described date format, checking
55
+ # that it is a valid date. Decodes to a DateTime instance, decoding and
56
+ # encoding perform validity checks!
57
+ # [+YYMMDDhhmmssCodec+] encodes Time, Datetime or String to the described date format, checking
58
+ # that it is a valid date. Decodes to a DateTime instance, decoding and
59
+ # encoding perform validity checks!
60
+ # [+YYMMCodec+] encodes Time, Datetime or String to the described date format (exp date),
61
+ # checking that it is a valid date. Decodes to a DateTime instance, decoding
62
+ # and encoding perform validity checks!
63
+ #
64
+ class Codec
65
+ attr_accessor :encoder
66
+ attr_accessor :decoder
67
+
68
+ def decode(raw)
69
+ decoder.call(raw)
70
+ end
71
+
72
+ # length is either a fixnum or a lenth encoder.
73
+ def encode(value)
74
+ encoder.call(value)
75
+ end
76
+ end
77
+
78
+ # ASCII_Number
79
+ ASCII_Number = Codec.new
80
+ ASCII_Number.encoder= lambda{|num|
81
+ enc = num.to_s
82
+ raise ISO8583Exception.new("Invalid value: #{enc} must be numeric!") unless enc =~ /^[0-9]*$/
83
+ enc
84
+ }
85
+
86
+ ASCII_Number.decoder = lambda{|raw|
87
+ raw.to_i
88
+ }
89
+
90
+ PASS_THROUGH_DECODER = lambda{|str|
91
+ str.strip # remove padding
92
+ }
93
+
94
+ # Takes a number or str representation of a number and BCD encodes it, e.g.
95
+ # "1234" => "\x12\x34"
96
+ # 3456 => "\x34\x56"
97
+ #
98
+ # right justified with null ... (correct to do this? almost certainly not...)
99
+ Packed_Number = Codec.new
100
+ Packed_Number.encoder = lambda { |val|
101
+ val = val.to_s
102
+ val = val.length % 2 == 0 ? val : "0"+val
103
+ raise ISO8583Exception.new("Invalid value: #{val} must be numeric!") unless val =~ /^[0-9]*$/
104
+ [val].pack("H*")
105
+ }
106
+ Packed_Number.decoder = lambda{|encoded|
107
+ d = encoded.unpack("H*")[0].to_i
108
+ }
109
+
110
+ A_Codec = Codec.new
111
+ A_Codec.encoder = lambda{|str|
112
+ raise ISO8583Exception.new("Invalid value: #{str} must be [A-Za-z]") unless str =~ /^[A-Za-z]*$/
113
+ str
114
+ }
115
+ A_Codec.decoder = PASS_THROUGH_DECODER
116
+
117
+ AN_Codec = Codec.new
118
+ AN_Codec.encoder = lambda{|str|
119
+ raise ISO8583Exception.new("Invalid value: #{str} must be [A-Za-y0-9]") unless str =~ /^[A-Za-z0-9]*$/
120
+ str
121
+ }
122
+ AN_Codec.decoder = PASS_THROUGH_DECODER
123
+
124
+ ANP_Codec = Codec.new
125
+ ANP_Codec.encoder = lambda{|str|
126
+ raise ISO8583Exception.new("Invalid value: #{str} must be [A-Za-y0-9 ]") unless str =~ /^[A-Za-z0-9 ]*$/
127
+ str
128
+ }
129
+ ANP_Codec.decoder = PASS_THROUGH_DECODER
130
+
131
+ ANS_Codec = Codec.new
132
+ ANS_Codec.encoder = lambda{|str|
133
+ raise ISO8583Exception.new("Invalid value: #{str} must be [\x20-\x7E]") unless str =~ /^[\x20-\x7E]*$/
134
+ str
135
+ }
136
+ ANS_Codec.decoder = PASS_THROUGH_DECODER
137
+
138
+ Null_Codec = Codec.new
139
+ Null_Codec.encoder = lambda {|str|
140
+ str
141
+ }
142
+ Null_Codec.decoder = lambda {|str|
143
+ str.gsub(/\000*$/, '')
144
+ }
145
+
146
+ Track2 = Codec.new
147
+ Track2.encoder = lambda{|track2|
148
+ #SS | PAN | FS | Expiration Date | Service Code | Discretionary Data | ES | LRC
149
+ # SS = ;
150
+ # PAN = up to 19 digits (at least 9?)
151
+ # FS = '='
152
+ # Exp Date = YYMM
153
+ # SC: 3 digits or =
154
+ # ES = ?
155
+ # lrc : 1byte
156
+ raise ISO8583Exception.new("Invalid Track2 data: #{track2}") unless track2 =~ /^;*(\d{9,19})=(.*)\?.$/
157
+ track2
158
+ }
159
+ Track2.decoder = PASS_THROUGH_DECODER
160
+
161
+ def self._date_codec(fmt)
162
+ c = Codec.new
163
+ c.encoder = lambda {|date|
164
+ enc = case date
165
+ when DateTime, Date, Time
166
+ date.strftime(fmt)
167
+ when String
168
+ begin
169
+ dt = DateTime.strptime(date, fmt)
170
+ dt.strftime(fmt)
171
+ rescue
172
+ raise ISO8583Exception.new("Invalid format encoding: #{date}, must be #{fmt}.")
173
+ end
174
+ else
175
+ raise ISO8583Exception.new("Don't know how to encode: #{date.class} to a time.")
176
+ end
177
+ return enc
178
+ }
179
+ c.decoder = lambda {|str|
180
+ begin
181
+ DateTime.strptime(str, fmt)
182
+ rescue
183
+ raise ISO8583Exception.new("Invalid format decoding: #{str}, must be #{fmt}.")
184
+ end
185
+ }
186
+
187
+ c
188
+ end
189
+
190
+ MMDDhhmmssCodec = _date_codec("%m%d%H%M%S")
191
+ HhmmssCodec = _date_codec("%H%M%S")
192
+ YYMMDDhhmmssCodec = _date_codec("%y%m%d%H%M%S")
193
+ YYMMCodec = _date_codec("%y%m")
194
+ MMDDCodec = _date_codec("%m%d")
195
+ YYMMDDCodec = _date_codec("%y%m%d")
196
+
197
+ end
@@ -0,0 +1,4 @@
1
+ module ISO8583
2
+ class ISO8583Exception < Exception; end
3
+ class ISO8583ParseException < ISO8583Exception; end
4
+ end
@@ -0,0 +1,90 @@
1
+ module ISO8583
2
+
3
+ class Field
4
+ # may either be some other Field in which the length is encoded or a Fixnum for
5
+ # fixed length fields. Length should always be the length of the *encoded* value.
6
+ # A 6 digit BCD field will require a length 3, as will a 5 digit BCD field.
7
+ # The subclass BCDField handles this to keep things consistant.
8
+ attr_accessor :length
9
+ attr_accessor :codec
10
+ attr_accessor :padding
11
+ attr_accessor :max
12
+
13
+ attr_writer :name
14
+ attr_accessor :bmp
15
+
16
+ def name
17
+ "BMP #{bmp}: #{@name}"
18
+ end
19
+
20
+ def parse(raw)
21
+ len, raw = case length
22
+ when Fixnum
23
+ [length, raw]
24
+ when Field
25
+ length.parse(raw)
26
+ else
27
+ raise ISO8583Exception.new("Cannot determine the length of '#{name}' field")
28
+ end
29
+
30
+ raw_value = raw[0,len]
31
+
32
+ # make sure we have enough data ...
33
+ if raw_value.length != len
34
+ mes = "Field has incorrect length! field: #{raw_value} len/expected: #{raw_value.length}/#{len}"
35
+ raise ISO8583ParseException.new(mes)
36
+ end
37
+
38
+ rest = raw[len, raw.length]
39
+ begin
40
+ real_value = codec.decode(raw_value)
41
+ rescue
42
+ raise ISO8583ParseException.new($!.message+" (#{name})")
43
+ end
44
+
45
+ [ real_value, rest ]
46
+ end
47
+
48
+
49
+ # Encoding needs to consider length representation, the actual encoding (such as charset or BCD)
50
+ # and padding.
51
+ # The order may be important! This impl calls codec.encode and then pads, in case you need the other
52
+ # special treatment, you may need to override this method alltogether.
53
+ # In other cases, the padding has to be implemented by the codec, such as BCD with an odd number of nibbles.
54
+ def encode(value)
55
+ encoded_value = codec.encode(value)
56
+
57
+ if padding
58
+ if padding.arity == 1
59
+ encoded_value = padding.call(encoded_value)
60
+ elsif padding.arity == 2
61
+ encoded_value = padding.call(encoded_value, length)
62
+ end
63
+ end
64
+
65
+ len_str = case length
66
+ when Fixnum
67
+ raise ISO8583Exception.new("Too long: #{value} (#{name})! length=#{length}") if encoded_value.length > length
68
+ raise ISO8583Exception.new("Too short: #{value} (#{name})! length=#{length}") if encoded_value.length < length
69
+ ""
70
+ when Field
71
+ raise ISO8583Exception.new("Max lenth exceeded: #{value}, max: #{max}") if max && encoded_value.length > max
72
+ length.encode(encoded_value.length)
73
+ else
74
+ raise ISO8583Exception.new("Invalid length (#{length}) for '#{name}' field")
75
+ end
76
+
77
+ len_str + encoded_value
78
+ end
79
+ end
80
+
81
+ class BCDField < Field
82
+ # This corrects the length for BCD fields, as their encoded length is half (+ parity) of the
83
+ # content length. E.g. 123 (length = 3) encodes to "\x01\x23" (length 2)
84
+ def length
85
+ _length = super
86
+ (_length % 2) != 0 ? (_length / 2) + 1 : _length / 2
87
+ end
88
+ end
89
+
90
+ end