cordawyn-iso8583 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +2 -0
- data/CHANGELOG +5 -0
- data/LICENSE +21 -0
- data/README +27 -0
- data/Rakefile +127 -0
- data/TODO +10 -0
- data/lib/berlin.rb +82 -0
- data/lib/bitmap.rb +117 -0
- data/lib/codec.rb +189 -0
- data/lib/exception.rb +4 -0
- data/lib/field.rb +90 -0
- data/lib/fields.rb +148 -0
- data/lib/iso8583.rb +12 -0
- data/lib/message.rb +417 -0
- data/lib/util.rb +94 -0
- data/test/BitmapTests.rb +80 -0
- data/test/message_test.rb +163 -0
- data/test/test_codec.rb +97 -0
- data/test/test_fields.rb +188 -0
- data/test/test_util.rb +32 -0
- metadata +91 -0
data/AUTHORS
ADDED
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2009 Tim Becker
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
= ISO 8583 Financial Messaging for Ruby
|
3
|
+
|
4
|
+
This package currently contains code for coding an decoding ISO 8583
|
5
|
+
Financial Message.
|
6
|
+
|
7
|
+
== Using
|
8
|
+
|
9
|
+
The best place to get started using the library is the documentation of the
|
10
|
+
Message class. Once a stable state is reached, this should be the only
|
11
|
+
class you need to use ...
|
12
|
+
|
13
|
+
== Installing
|
14
|
+
|
15
|
+
You can install the +iso8583+ package by executing:
|
16
|
+
|
17
|
+
gem install iso8583
|
18
|
+
|
19
|
+
== Source
|
20
|
+
|
21
|
+
The source is most readily available on github[http://github.com/cordawyn/8583].
|
22
|
+
|
23
|
+
== Mailing List
|
24
|
+
|
25
|
+
In case you discover bugs, spelling errors, offer suggestions for
|
26
|
+
improvements or would like to help out with the project, you can contact
|
27
|
+
me directly (tim@kuriositaet.de).
|
data/Rakefile
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require "rake/rdoctask"
|
2
|
+
require "rake/gempackagetask"
|
3
|
+
require "rake/testtask"
|
4
|
+
require "rake/clean"
|
5
|
+
require "rubygems"
|
6
|
+
|
7
|
+
# Some definitions that you'll need to edit in case you reuse this
|
8
|
+
# Rakefile for your own project.
|
9
|
+
|
10
|
+
SHORTNAME = "iso8583" # this should be the rubyforge project name
|
11
|
+
DESC = "Ruby implementation of ISO 8583 financial messaging"
|
12
|
+
PKG_VERSION = "0.1.2"
|
13
|
+
LONG_DESC = <<END_DESC
|
14
|
+
Ruby implementation of ISO 8583 financial messaging
|
15
|
+
END_DESC
|
16
|
+
RUBYFORGE_USER = "cordawyn"
|
17
|
+
|
18
|
+
# Specifies the default task to execute. This is often the "test" task
|
19
|
+
# and we'll change things around as soon as we have some tests.
|
20
|
+
|
21
|
+
task :default => [:rdoc]
|
22
|
+
|
23
|
+
# The directory to generate +rdoc+ in.
|
24
|
+
RDOC_DIR = "doc/html"
|
25
|
+
|
26
|
+
# This global variable contains files that will be erased by the `clean` task.
|
27
|
+
# The `clean` task itself is automatically generated by requiring `rake/clean`.
|
28
|
+
|
29
|
+
CLEAN << RDOC_DIR << "pkg"
|
30
|
+
|
31
|
+
|
32
|
+
# This is the task that generates the +rdoc+ documentation from the
|
33
|
+
# source files. Instantiating Rake::RDocTask automatically generates a
|
34
|
+
# task called `rdoc`.
|
35
|
+
|
36
|
+
Rake::RDocTask.new do |rd|
|
37
|
+
# Options for documenation generation are specified inside of
|
38
|
+
# this block. For example the following line specifies that the
|
39
|
+
# content of the README file should be the main page of the
|
40
|
+
# documenation.
|
41
|
+
rd.main = "README"
|
42
|
+
|
43
|
+
# The following line specifies all the files to extract
|
44
|
+
# documenation from.
|
45
|
+
rd.rdoc_files.include("README", "AUTHORS", "LICENSE", "TODO",
|
46
|
+
"CHANGELOG", "bin/**/*", "lib/**/*.rb",
|
47
|
+
"examples/**/*rb", "doc/*.rdoc")
|
48
|
+
# This one specifies the output directory ...
|
49
|
+
rd.rdoc_dir = "doc/html"
|
50
|
+
|
51
|
+
# Or the HTML title of the generated documentation set.
|
52
|
+
rd.title = "#{SHORTNAME}: #{DESC}"
|
53
|
+
|
54
|
+
# These are options specifiying how source code inlined in the
|
55
|
+
# documentation should be formatted.
|
56
|
+
|
57
|
+
rd.options = ["--line-numbers", "--inline-source"]
|
58
|
+
|
59
|
+
# Check:
|
60
|
+
# `rdoc --help` for more rdoc options
|
61
|
+
# the {rdoc documenation home}[http://www.ruby-doc.org/stdlib/libdoc/rdoc/rdoc/index.html]
|
62
|
+
# or the documentation for the +Rake::RDocTask+ task[http://rake.rubyforge.org/classes/Rake/RDocTask.html]
|
63
|
+
end
|
64
|
+
|
65
|
+
# The GemPackageTask facilitates getting all your files collected
|
66
|
+
# together into gem archives. You can also use it to generate tarball
|
67
|
+
# and zip archives.
|
68
|
+
|
69
|
+
# First you'll need to assemble a gemspec
|
70
|
+
|
71
|
+
PKG_FILES = FileList["lib/**/*.rb", "bin/**/*", "examples/**/*", "[A-Z]*", "test/**/*"].to_a
|
72
|
+
|
73
|
+
spec = Gem::Specification.new do |s|
|
74
|
+
s.platform = Gem::Platform::RUBY
|
75
|
+
s.summary = "#{SHORTNAME}: #{DESC}"
|
76
|
+
s.name = "#{RUBYFORGE_USER}-#{SHORTNAME}"
|
77
|
+
s.rubyforge_project = SHORTNAME
|
78
|
+
s.version = PKG_VERSION
|
79
|
+
s.files = PKG_FILES
|
80
|
+
s.requirements << "none"
|
81
|
+
s.require_path = "lib"
|
82
|
+
s.description = LONG_DESC
|
83
|
+
s.has_rdoc = true
|
84
|
+
s.authors = ["Slava Kravchenko", "Tim Becker"]
|
85
|
+
s.email = ["cordawyn@gmail.com", "tim.becker@kuriositaet.de"]
|
86
|
+
s.homepage = "http://github.com/cordawyn/8583/"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Adding a new GemPackageTask adds a task named `package`, which generates
|
90
|
+
# packages as gems, tarball and zip archives.
|
91
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
92
|
+
pkg.need_zip = true
|
93
|
+
pkg.need_tar_gz = true
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# This task is used to demonstrate how to upload files to Rubyforge.
|
98
|
+
# Calling `upload_page` creates a current version of the +rdoc+
|
99
|
+
# documentation and uploads it to the Rubyforge homepage of the project,
|
100
|
+
# assuming it's hosted there and naming conventions haven't changed.
|
101
|
+
#
|
102
|
+
# This task uses `sh` to call the `scp` binary, which is plattform
|
103
|
+
# dependant and may not be installed on your computer if you're using
|
104
|
+
# Windows. I'm currently not aware of any pure ruby way to do scp
|
105
|
+
# transfers.
|
106
|
+
|
107
|
+
RubyForgeProject = SHORTNAME
|
108
|
+
|
109
|
+
desc "Upload the web pages to the web."
|
110
|
+
task :upload_pages => ["rdoc"] do
|
111
|
+
if RubyForgeProject then
|
112
|
+
path = "/var/www/gforge-projects/#{RubyForgeProject}"
|
113
|
+
sh "scp -r doc/html/* #{RUBYFORGE_USER}@rubyforge.org:#{path}"
|
114
|
+
sh "scp doc/images/*.png #{RUBYFORGE_USER}@rubyforge.org:#{path}/images"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# This task will run the unit tests provided in files called
|
119
|
+
# `test/test*.rb`. The task itself can be run with a call to `rake test`
|
120
|
+
|
121
|
+
Rake::TestTask.new do |t|
|
122
|
+
t.libs << "test"
|
123
|
+
t.libs << "lib"
|
124
|
+
t.ruby_opts = ["-rubygems"]
|
125
|
+
t.test_files = FileList["test/*.rb"]
|
126
|
+
t.verbose = true
|
127
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
* mti_required : or something of the sort to indicate which fields are
|
3
|
+
required/mirrored/conditional for each message type
|
4
|
+
|
5
|
+
* decode_check : currently the codecs are very liberal about what to accept
|
6
|
+
or parse. E.g. if an invalid date is received, it doesn't complain. Consider
|
7
|
+
collecting the errors and presenting them through in interface so
|
8
|
+
it may be decided after parsing the message whether to reject it or no
|
9
|
+
|
10
|
+
* general cleanup/license/docs/readme
|
data/lib/berlin.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
|
2
|
+
# MIT License, for details, see the LICENSE file accompaning
|
3
|
+
# this distribution
|
4
|
+
|
5
|
+
require 'lib/iso8583'
|
6
|
+
|
7
|
+
|
8
|
+
# Example of a protocol specification based on:
|
9
|
+
# http://www.berlin-group.org/documents/BG_Authorisation_3.0.pdf
|
10
|
+
# The Berlin Groups Authorisation Interface specification.
|
11
|
+
# No gurantees are made that this is an accurate implemenation.
|
12
|
+
# It currently serves as an example only.
|
13
|
+
|
14
|
+
module ISO8583
|
15
|
+
|
16
|
+
class BerlinMessage < Message
|
17
|
+
mti_format N, :length => 4
|
18
|
+
mti 1100, "Authorization Request Acquirer Gateway"
|
19
|
+
mti 1110, "Authorization Request Response Issuer Gateway"
|
20
|
+
mti 1420, "Reversal Advice Acquirer Gateway"
|
21
|
+
mti 1421, "Reversal Advice Repeat Acquirer Gateway"
|
22
|
+
mti 1430, "Reversal Advice Response Issuer Gateway"
|
23
|
+
mti 1804, "Network Management Request Acquirer Gateway or Issuer Gateway"
|
24
|
+
mti 1814, "Network Management Request Response Issuer Gateway or Acquirer Gateway"
|
25
|
+
|
26
|
+
bmp 2, "Primary Account Number (PAN)", LLVAR_N, :max => 19
|
27
|
+
bmp 3, "Processing Code", N, :length => 6
|
28
|
+
bmp 4, "Amount (Transaction)", N, :length => 12
|
29
|
+
bmp 6, "Amount, Cardholder Billing" , N, :length => 12
|
30
|
+
bmp 7, "Date and Time, Transmission" , MMDDhhmmss
|
31
|
+
bmp 10, "Conversion Rate, Cardholder Billing", N, :length => 8
|
32
|
+
bmp 11, "System Trace Audit Number (STAN)", N, :length => 6
|
33
|
+
bmp 12, "Date and Time, Local Transaction", YYMMDDhhmmss
|
34
|
+
bmp 14, "Date, Expiration", YYMM
|
35
|
+
bmp 22, "POS Data Code", AN, :length => 12
|
36
|
+
bmp 23, "Card Sequence Number", N, :length => 3
|
37
|
+
bmp 24, "Function Code", N, :length => 3
|
38
|
+
bmp 25, "Message Reason Code", N, :length => 4
|
39
|
+
bmp 26, "Card Acceptor Business Code", N, :length => 4
|
40
|
+
bmp 30, "Amounts, Original", N, :length => 24
|
41
|
+
bmp 32, "Acquiring Institution Identification Code", LLVAR_N, :max => 11
|
42
|
+
bmp 35, "Track 2 Data", LLVAR_Z, :max => 37
|
43
|
+
bmp 37, "Retrieval Reference Number", ANP, :length => 12
|
44
|
+
bmp 38, "Approval Code", ANP, :length => 6
|
45
|
+
bmp 39, "Action Code", N, :length => 3
|
46
|
+
bmp 41, "Card Acceptor Terminal Identification", ANS, :length => 8
|
47
|
+
bmp 42, "Card Acceptor Identification Code", ANS, :length => 15
|
48
|
+
bmp 43, "Card Acceptor Name/Location", LLVAR_ANS, :max => 56
|
49
|
+
bmp 49, "Currency Code, Transaction", N, :length => 3
|
50
|
+
bmp 51, "Currency Code, Cardholder Billing", N, :length => 3
|
51
|
+
bmp 52, "Personal Identification Number (PIN) Data", B, :length => 8
|
52
|
+
bmp 53, "Security Related Control Information", LLVAR_B, :max => 48
|
53
|
+
bmp 54, "Amounts, Additional", LLLVAR_ANS,:max => 40
|
54
|
+
|
55
|
+
bmp 55, "Integrated Circuit Card (ICC) System Related Data", LLLVAR_B, :max => 255
|
56
|
+
bmp 56, "Original Data Elements", LLVAR_N, :max => 35
|
57
|
+
bmp 58, "Authorizing Agent Institution Identification Code", LLVAR_N, :max => 11
|
58
|
+
bmp 59, "Additional Data - Private", LLLVAR_ANS, :max => 67
|
59
|
+
bmp 64, "Message Authentication Code (MAC) Field", B, :length => 8
|
60
|
+
|
61
|
+
bmp_alias 2, :pan
|
62
|
+
bmp_alias 3, :proc_code
|
63
|
+
bmp_alias 4, :amount
|
64
|
+
bmp_alias 12, :exp_date
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
if __FILE__==$0
|
70
|
+
mes = ISO8583::BerlinMessage.new
|
71
|
+
mes.mti = 1110
|
72
|
+
mes[2] = 474747474747
|
73
|
+
mes["Processing Code"] = "123456"
|
74
|
+
|
75
|
+
pan = mes["Primary Account Number (PAN)"]
|
76
|
+
#mes.pan = 47474747474747
|
77
|
+
|
78
|
+
#puts mes.pan
|
79
|
+
puts mes.to_b
|
80
|
+
puts mes.to_s
|
81
|
+
#mes2 = BerlinMessage.parse input
|
82
|
+
end
|
data/lib/bitmap.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
|
2
|
+
# MIT License, for details, see the LICENSE file accompaning
|
3
|
+
# this distribution
|
4
|
+
|
5
|
+
module ISO8583
|
6
|
+
|
7
|
+
# This class constructs an object for handling bitmaps
|
8
|
+
# with which ISO8583 messages typically begin.
|
9
|
+
# Bitmaps are either 8 or 16 bytes long, an extended length
|
10
|
+
# bitmap is indicated by the first bit being set.
|
11
|
+
# In all likelyhood, you won't be using this class much, it's used
|
12
|
+
# transparently by the Message class.
|
13
|
+
class Bitmap
|
14
|
+
|
15
|
+
# create a new Bitmap object. In case an iso message
|
16
|
+
# is passed in, that messages bitmap will be parsed. If
|
17
|
+
# not, this initializes and empty bitmap.
|
18
|
+
def initialize(message = nil)
|
19
|
+
@bmp = Array.new(128, false)
|
20
|
+
if !message
|
21
|
+
|
22
|
+
else
|
23
|
+
initialize_from_message message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# yield once with the number of each set field.
|
28
|
+
def each #:yields: each bit set in the bitmap.
|
29
|
+
@bmp.each_with_index {|set, i| yield i+1 if set}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns whether the bit is set or not.
|
33
|
+
def [](i)
|
34
|
+
@bmp[i-1]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set the bit to the indicated value. Only `true` sets the
|
38
|
+
# bit, any other value unsets it.
|
39
|
+
def []=(i, value)
|
40
|
+
if i > 128
|
41
|
+
raise ISO8583Exception.new("Bits > 128 are not permitted.")
|
42
|
+
elsif i < 2
|
43
|
+
raise ISO8583Exception.new("Bits < 2 are not permitted (continutation bit is set automatically)")
|
44
|
+
end
|
45
|
+
@bmp[i-1] = (value == true)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets bit #i
|
49
|
+
def set(i)
|
50
|
+
self[i] = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Unsets bit #i
|
54
|
+
def unset(i)
|
55
|
+
self[i] = false
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generate the bytes representing this bitmap.
|
59
|
+
def to_bytes
|
60
|
+
arr = [self.to_s]
|
61
|
+
# tricky and ugly, setting bit[1] only when generating to_s...
|
62
|
+
count = self[1] ? 128 : 64
|
63
|
+
arr.pack("B#{count}")
|
64
|
+
end
|
65
|
+
alias_method :to_b, :to_bytes
|
66
|
+
|
67
|
+
# Generate a String representation of this bitmap in the form:
|
68
|
+
# 01001100110000011010110110010100100110011000001101011011001010
|
69
|
+
def to_s
|
70
|
+
#check whether any `high` bits are set
|
71
|
+
@bmp[0] = false
|
72
|
+
65.upto(128) {|i|
|
73
|
+
if self[i]
|
74
|
+
# if so, set continuation bit
|
75
|
+
@bmp[0] = true
|
76
|
+
break
|
77
|
+
end
|
78
|
+
}
|
79
|
+
str = ""
|
80
|
+
1.upto(self[1] ? 128 : 64) {|i|
|
81
|
+
str << (self[i] ? "1" : "0")
|
82
|
+
}
|
83
|
+
str
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def initialize_from_message(message)
|
90
|
+
bmp = message.unpack("B64")[0]
|
91
|
+
if bmp[0,1] == "1"
|
92
|
+
bmp = message.unpack("B128")[0]
|
93
|
+
end
|
94
|
+
|
95
|
+
0.upto(bmp.length-1) do |i|
|
96
|
+
@bmp[i] = (bmp[i,1] == "1")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class << self
|
101
|
+
# Parse the bytes in string and return the Bitmap and bytes remaining in `str`
|
102
|
+
# after the bitmap is taken away.
|
103
|
+
def parse(str)
|
104
|
+
bmp = Bitmap.new(str)
|
105
|
+
rest = bmp[1] ? str[16, str.length] : str[8, str.length]
|
106
|
+
[ bmp, rest ]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
if __FILE__==$0
|
114
|
+
mp = ISO8583::Bitmap.new
|
115
|
+
20.step(128,7) {|i| mp.set(i)}
|
116
|
+
print mp.to_bytes
|
117
|
+
end
|
data/lib/codec.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
# Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
|
2
|
+
# MIT License, for details, see the LICENSE file accompaning
|
3
|
+
# this distribution
|
4
|
+
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
module ISO8583
|
8
|
+
|
9
|
+
# Codec provides functionality to encode and decode values, codecs are
|
10
|
+
# used internally by Field instances in order to do character conversions
|
11
|
+
# and checking for proper values.
|
12
|
+
# Although they are used internally, you will probably need to write
|
13
|
+
# your own Codec sooner or later. The codecs used by Field instances are
|
14
|
+
# typically instances of Codec, it may or may not be usefull to subclass
|
15
|
+
# Codec.
|
16
|
+
#
|
17
|
+
# Say, for example, a text field needs to be encoded in EBCDIC in the
|
18
|
+
# message, this is how a corresponding codec would be constructed:
|
19
|
+
#
|
20
|
+
# EBCDIC_Codec = Codec.new
|
21
|
+
# EBCDIC_Codec.encoder = lambda {|ascii_str|
|
22
|
+
# raise ISO8583Exception.new("String (#{ascii_str})not valid!") unless =~ /someregexp/
|
23
|
+
# ascii2ebcdic ascii_str # implementing ascii_str is left as an excercise
|
24
|
+
# }
|
25
|
+
# EBCDIC_Codec.decode = lambda {|ebcdic_str|
|
26
|
+
# # you may or may not want to raise exceptions at this point ....
|
27
|
+
# # strip removes any padding...
|
28
|
+
# ebcdic2ascii(ebcdic_str).strip
|
29
|
+
# }
|
30
|
+
#
|
31
|
+
# This instance of Codec would then be used be the corresponding Field
|
32
|
+
# encoder/decoder, which may look similar to this:
|
33
|
+
#
|
34
|
+
# EBCDIC = Field.new
|
35
|
+
# EBCDIC.codec = EBCDIC_Codec
|
36
|
+
# EBCDIC.padding = PADDING_LEFT_JUSTIFIED_SPACES
|
37
|
+
#
|
38
|
+
# Notice there is a bit of inconsistancy: the padding is added by the
|
39
|
+
# field, but removed by the codec. I would like to find a better
|
40
|
+
# solution to this...
|
41
|
+
#
|
42
|
+
# See also: Field, link:files/lib/fields_rb.html
|
43
|
+
#
|
44
|
+
# The following codecs are already implemented:
|
45
|
+
# [+ASCII_Number+] encodes either a Number or String representation of
|
46
|
+
# a number to the ASCII represenation of the number,
|
47
|
+
# decodes ASCII numerals to a number
|
48
|
+
# [+AN_Codec+] passes through ASCII string checking they conform to [A-Za-z0-9]
|
49
|
+
# during encoding, no validity check during decoding.
|
50
|
+
# [+ANP_Codec+] passes through ASCII string checking they conform to [A-Za-z0-9 ]
|
51
|
+
# during encoding, no validity check during decoding.
|
52
|
+
# [+ANS_Codec+] passes through ASCII string checking they conform to [\x20-\x7E]
|
53
|
+
# during encoding, no validity check during decoding.
|
54
|
+
# [+Null_Codec+] passes anything along untouched.
|
55
|
+
# [<tt>Track2</tt>] rudimentary check that string conforms to Track2
|
56
|
+
# [+MMDDhhmmssCodec+] encodes Time, Datetime or String to the described date format, checking
|
57
|
+
# that it is a valid date. Decodes to a DateTime instance, decoding and
|
58
|
+
# encoding perform validity checks!
|
59
|
+
# [+YYMMDDhhmmssCodec+] encodes Time, Datetime or String to the described date format, checking
|
60
|
+
# that it is a valid date. Decodes to a DateTime instance, decoding and
|
61
|
+
# encoding perform validity checks!
|
62
|
+
# [+YYMMCodec+] encodes Time, Datetime or String to the described date format (exp date),
|
63
|
+
# checking that it is a valid date. Decodes to a DateTime instance, decoding
|
64
|
+
# and encoding perform validity checks!
|
65
|
+
#
|
66
|
+
class Codec
|
67
|
+
attr_accessor :encoder
|
68
|
+
attr_accessor :decoder
|
69
|
+
|
70
|
+
def decode(raw)
|
71
|
+
decoder.call(raw)
|
72
|
+
end
|
73
|
+
|
74
|
+
# length is either a fixnum or a lenth encoder.
|
75
|
+
def encode(value)
|
76
|
+
encoder.call(value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# ASCII_Number
|
81
|
+
ASCII_Number = Codec.new
|
82
|
+
ASCII_Number.encoder= lambda{|num|
|
83
|
+
enc = num.to_s
|
84
|
+
raise ISO8583Exception.new("Invalid value: #{enc} must be numeric!") unless enc =~ /^[0-9]*$/
|
85
|
+
enc
|
86
|
+
}
|
87
|
+
|
88
|
+
ASCII_Number.decoder = lambda{|raw|
|
89
|
+
raw.to_i
|
90
|
+
}
|
91
|
+
|
92
|
+
PASS_THROUGH_DECODER = lambda{|str|
|
93
|
+
str.strip # remove padding
|
94
|
+
}
|
95
|
+
|
96
|
+
# Takes a number or str representation of a number and BCD encodes it, e.g.
|
97
|
+
# "1234" => "\x12\x34"
|
98
|
+
# 3456 => "\x34\x56"
|
99
|
+
#
|
100
|
+
# right justified with null ... (correct to do this? almost certainly not...)
|
101
|
+
Packed_Number = Codec.new
|
102
|
+
Packed_Number.encoder = lambda { |val|
|
103
|
+
val = val.to_s
|
104
|
+
val = val.length % 2 == 0 ? val : "0"+val
|
105
|
+
raise ISO8583Exception.new("Invalid value: #{val} must be numeric!") unless val =~ /^[0-9]*$/
|
106
|
+
[val].pack("H*")
|
107
|
+
}
|
108
|
+
Packed_Number.decoder = lambda{|encoded|
|
109
|
+
d = encoded.unpack("H*")[0].to_i
|
110
|
+
}
|
111
|
+
|
112
|
+
AN_Codec = Codec.new
|
113
|
+
AN_Codec.encoder = lambda{|str|
|
114
|
+
raise ISO8583Exception.new("Invalid value: #{str} must be [A-Za-y0-9]") unless str =~ /^[A-Za-z0-9]*$/
|
115
|
+
str
|
116
|
+
}
|
117
|
+
AN_Codec.decoder = PASS_THROUGH_DECODER
|
118
|
+
|
119
|
+
ANP_Codec = Codec.new
|
120
|
+
ANP_Codec.encoder = lambda{|str|
|
121
|
+
raise ISO8583Exception.new("Invalid value: #{str} must be [A-Za-y0-9 ]") unless str =~ /^[A-Za-z0-9 ]*$/
|
122
|
+
str
|
123
|
+
}
|
124
|
+
ANP_Codec.decoder = PASS_THROUGH_DECODER
|
125
|
+
|
126
|
+
ANS_Codec = Codec.new
|
127
|
+
ANS_Codec.encoder = lambda{|str|
|
128
|
+
raise ISO8583Exception.new("Invalid value: #{str} must be [\x20-\x7E]") unless str =~ /^[\x20-\x7E]*$/
|
129
|
+
str
|
130
|
+
}
|
131
|
+
ANS_Codec.decoder = PASS_THROUGH_DECODER
|
132
|
+
|
133
|
+
Null_Codec = Codec.new
|
134
|
+
Null_Codec.encoder = lambda {|str|
|
135
|
+
str
|
136
|
+
}
|
137
|
+
Null_Codec.decoder = lambda {|str|
|
138
|
+
str.gsub(/\000*$/, '')
|
139
|
+
}
|
140
|
+
|
141
|
+
Track2 = Codec.new
|
142
|
+
Track2.encoder = lambda{|track2|
|
143
|
+
#SS | PAN | FS | Expiration Date | Service Code | Discretionary Data | ES | LRC
|
144
|
+
# SS = ;
|
145
|
+
# PAN = up to 19 digits (at least 9?)
|
146
|
+
# FS = '='
|
147
|
+
# Exp Date = YYMM
|
148
|
+
# SC: 3 digits or =
|
149
|
+
# ES = ?
|
150
|
+
# lrc : 1byte
|
151
|
+
raise ISO8583Exception.new("Invalid Track2 data: #{track2}") unless track2 =~ /^;*(\d{9,19})=(.*)\?.$/
|
152
|
+
track2
|
153
|
+
}
|
154
|
+
Track2.decoder = PASS_THROUGH_DECODER
|
155
|
+
|
156
|
+
def self._date_codec(fmt)
|
157
|
+
c = Codec.new
|
158
|
+
c.encoder = lambda {|date|
|
159
|
+
enc = case date
|
160
|
+
when Date, Time
|
161
|
+
date.strftime(fmt)
|
162
|
+
when String
|
163
|
+
begin
|
164
|
+
dt = DateTime.strptime(date, fmt)
|
165
|
+
dt.strftime(fmt)
|
166
|
+
rescue
|
167
|
+
raise ISO8583Exception.new("Invalid format encoding: #{date}, must be #{fmt}.")
|
168
|
+
end
|
169
|
+
else
|
170
|
+
raise ISO8583Exception.new("Don't know how to encode: #{date.class} to a time.")
|
171
|
+
end
|
172
|
+
return enc
|
173
|
+
}
|
174
|
+
c.decoder = lambda {|str|
|
175
|
+
begin
|
176
|
+
DateTime.strptime(str, fmt)
|
177
|
+
rescue
|
178
|
+
raise ISO8583Exception.new("Invalid format decoding: #{str}, must be #{fmt}.")
|
179
|
+
end
|
180
|
+
}
|
181
|
+
|
182
|
+
c
|
183
|
+
end
|
184
|
+
|
185
|
+
MMDDhhmmssCodec = _date_codec("%m%d%H%M%S")
|
186
|
+
YYMMDDhhmmssCodec = _date_codec("%y%m%d%H%M%S")
|
187
|
+
YYMMCodec = _date_codec("%y%m")
|
188
|
+
|
189
|
+
end
|