konto_check_ruby 1.0.0
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 +7 -0
- data/CHANGELOG.md +26 -0
- data/COPYRIGHT +22 -0
- data/LICENSE +504 -0
- data/README.md +295 -0
- data/bin/konto_check_ruby +202 -0
- data/data/README.md +17 -0
- data/data/blz.lut2f +0 -0
- data/data/iban_regeln.txt +1260 -0
- data/lib/konto_check_ruby/bank_data.rb +421 -0
- data/lib/konto_check_ruby/blz_file.rb +705 -0
- data/lib/konto_check_ruby/check_methods/part1.rb +1634 -0
- data/lib/konto_check_ruby/check_methods/part2.rb +1604 -0
- data/lib/konto_check_ruby/check_methods/part3.rb +1905 -0
- data/lib/konto_check_ruby/check_methods/part4.rb +1738 -0
- data/lib/konto_check_ruby/check_methods/part5.rb +1407 -0
- data/lib/konto_check_ruby/check_methods.rb +147 -0
- data/lib/konto_check_ruby/collation.rb +52 -0
- data/lib/konto_check_ruby/engine.rb +1006 -0
- data/lib/konto_check_ruby/iban_rules.rb +1060 -0
- data/lib/konto_check_ruby/konto_check.rb +1578 -0
- data/lib/konto_check_ruby/lut_file.rb +457 -0
- data/lib/konto_check_ruby/raw.rb +614 -0
- data/lib/konto_check_ruby/retvals.rb +826 -0
- data/lib/konto_check_ruby/search.rb +440 -0
- data/lib/konto_check_ruby/update.rb +176 -0
- data/lib/konto_check_ruby/version.rb +18 -0
- data/lib/konto_check_ruby.rb +42 -0
- metadata +112 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is part of konto_check_ruby, a Ruby port of the C library
|
|
4
|
+
# konto_check, Copyright (C) 2002-2023 Michael Plugge <konto_check@yahoo.com>.
|
|
5
|
+
# Ruby port Copyright (C) 2026 tickettoaster GmbH <https://tickettoaster.de>.
|
|
6
|
+
#
|
|
7
|
+
# konto_check_ruby is free software; you can redistribute it and/or modify it
|
|
8
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 2.1 of the License, or (at
|
|
10
|
+
# your option) any later version. It is distributed WITHOUT ANY WARRANTY; see
|
|
11
|
+
# the file LICENSE for details.
|
|
12
|
+
|
|
13
|
+
require "zlib"
|
|
14
|
+
require_relative "retvals"
|
|
15
|
+
|
|
16
|
+
module KontoCheckRuby
|
|
17
|
+
# Reader and writer for the LUT2 file format of konto_check
|
|
18
|
+
# ("BLZ Lookup Table/Format 2.0").
|
|
19
|
+
#
|
|
20
|
+
# File layout:
|
|
21
|
+
#
|
|
22
|
+
# text prolog (several lines, the first one is the signature), ending
|
|
23
|
+
# with a line "DATA"
|
|
24
|
+
# 2 bytes number of directory slots (little endian)
|
|
25
|
+
# slots*12 directory: per slot (type, file offset, compressed length),
|
|
26
|
+
# each an unsigned 32 bit little endian integer; type 0 = empty
|
|
27
|
+
# blocks per block a 16 byte header (type, compressed length,
|
|
28
|
+
# uncompressed length, adler32 of the uncompressed data)
|
|
29
|
+
# followed by the (zlib compressed) block data
|
|
30
|
+
#
|
|
31
|
+
# All methods of this module return [return_code, value]; return_code is
|
|
32
|
+
# one of the integer constants of KontoCheckRuby (OK or an error code).
|
|
33
|
+
module LutFile
|
|
34
|
+
SIGNATURE_V2 = "BLZ Lookup Table/Format 2."
|
|
35
|
+
SIGNATURE_V1 = "BLZ Lookup Table/Format 1."
|
|
36
|
+
MAX_SLOTS = 500
|
|
37
|
+
SLOT_CNT_MIN = 60
|
|
38
|
+
DEFAULT_SLOTS = 60
|
|
39
|
+
SET_OFFSET = 100
|
|
40
|
+
|
|
41
|
+
COMPRESSION_NAMES = {
|
|
42
|
+
COMPRESSION_NONE => "keine",
|
|
43
|
+
COMPRESSION_ZLIB => "zlib",
|
|
44
|
+
COMPRESSION_BZIP2 => "bzip2",
|
|
45
|
+
COMPRESSION_LZO => "lzo",
|
|
46
|
+
COMPRESSION_LZMA => "lzma"
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Names of the block types as used in the info block and in lut_blocks()
|
|
50
|
+
BLOCK_NAME1 = Hash.new(" (unbekannt)").merge(
|
|
51
|
+
1 => "BLZ", 2 => "FILIALEN", 3 => "NAME", 4 => "PLZ", 5 => "ORT", 6 => "NAME_KURZ",
|
|
52
|
+
7 => "PAN", 8 => "BIC", 9 => "PZ", 10 => "NR", 11 => "AENDERUNG", 12 => "LOESCHUNG",
|
|
53
|
+
13 => "NACHFOLGE_BLZ", 14 => "NAME_NAME_KURZ", 15 => "INFO", 16 => "BIC_SORT",
|
|
54
|
+
17 => "NAME_SORT", 18 => "NAME_KURZ_SORT", 19 => "ORT_SORT", 20 => "PLZ_SORT",
|
|
55
|
+
21 => "PZ_SORT", 22 => "OWN_IBAN", 23 => "VOLLTEXT_TXT", 24 => "VOLLTEXT_IDX",
|
|
56
|
+
25 => "IBAN_REGEL", 26 => "IBAN_REGEL_SORT", 27 => "BIC_H_SORT", 28 => "SCL_INFO",
|
|
57
|
+
29 => "SCL_BIC", 30 => "SCL_NAME", 31 => "SCL_FLAGS", 32 => "SCL_FLAGS_ORIG"
|
|
58
|
+
).freeze
|
|
59
|
+
|
|
60
|
+
BLOCK_NAME2 = Hash.new(" (unbekannt)").merge(
|
|
61
|
+
0 => "leer",
|
|
62
|
+
1 => "BLZ", 2 => "Anzahl Fil.", 3 => "Name", 4 => "Plz", 5 => "Ort", 6 => "Name (kurz)",
|
|
63
|
+
7 => "PAN", 8 => "BIC", 9 => "Pruefziffer", 10 => "Lfd. Nr.", 11 => "Aenderung",
|
|
64
|
+
12 => "Loeschung", 13 => "NachfolgeBLZ", 14 => "Name, Kurzn.", 15 => "Infoblock",
|
|
65
|
+
16 => "BIC idx", 17 => "Name idx", 18 => "Kurzname idx", 19 => "Ort idx", 20 => "PLZ idx",
|
|
66
|
+
21 => "PZ idx", 22 => "IBAN Blacklist", 23 => "Volltext txt", 24 => "Volltext idx",
|
|
67
|
+
25 => "IBAN Regel", 26 => "IBAN Regel idx", 27 => "BIC Hauptst.idx", 28 => "SCL Infoblock",
|
|
68
|
+
29 => "SCL BIC", 30 => "SCL Banknamen", 31 => "SCL Flags", 32 => "SCL Flags orig"
|
|
69
|
+
).freeze
|
|
70
|
+
|
|
71
|
+
# Block types for which an index block is generated (lut_block_idx in C)
|
|
72
|
+
BLOCKS_WITH_INDEX = [3, 4, 5, 6, 8, 9, 14].freeze
|
|
73
|
+
|
|
74
|
+
# Human readable name of a block type (mode 1: short, 2: with set number,
|
|
75
|
+
# 3: the C macro name)
|
|
76
|
+
def self.block_name(typ, mode = 1)
|
|
77
|
+
base = typ > SET_OFFSET && typ < 400 ? typ - SET_OFFSET : typ
|
|
78
|
+
case mode
|
|
79
|
+
when 2
|
|
80
|
+
return "(Userblock)" if typ >= 400
|
|
81
|
+
return BLOCK_NAME2[0] if typ == 0
|
|
82
|
+
"#{typ > SET_OFFSET ? 2 : 1}. #{BLOCK_NAME2[base]}"
|
|
83
|
+
when 3
|
|
84
|
+
n = LUT_BLOCK_TYPES[typ]
|
|
85
|
+
n ? "LUT2_#{typ > SET_OFFSET ? '2_' : ''}#{n}" : ""
|
|
86
|
+
else
|
|
87
|
+
typ > SET_OFFSET ? "#{BLOCK_NAME1[base]} (2)" : BLOCK_NAME1[base]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Adler-32 variant used by konto_check (adler32a in the C source). It
|
|
92
|
+
# differs from the standard zlib adler32 in that the bytes are treated as
|
|
93
|
+
# *signed* chars. It is kept for compatibility with existing LUT files.
|
|
94
|
+
def self.adler32a(data, adler = 1)
|
|
95
|
+
s1 = adler & 0xffff
|
|
96
|
+
s2 = (adler >> 16) & 0xffff
|
|
97
|
+
bytes = data.unpack("c*")
|
|
98
|
+
i = 0
|
|
99
|
+
n = bytes.length
|
|
100
|
+
while i < n
|
|
101
|
+
k = [n - i, 5552].min
|
|
102
|
+
j = 0
|
|
103
|
+
while j < k
|
|
104
|
+
s1 = (s1 + bytes[i + j]) & 0xffffffff
|
|
105
|
+
s2 = (s2 + s1) & 0xffffffff
|
|
106
|
+
j += 1
|
|
107
|
+
end
|
|
108
|
+
i += k
|
|
109
|
+
s1 %= 65521
|
|
110
|
+
s2 %= 65521
|
|
111
|
+
end
|
|
112
|
+
(s2 << 16) | s1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Parsed header/directory of a LUT file
|
|
116
|
+
class Directory
|
|
117
|
+
attr_reader :prolog_lines, :compression, :slots, :entries, :data_pos
|
|
118
|
+
|
|
119
|
+
# entries: Array of [typ, offset, compressed_len]
|
|
120
|
+
def initialize(prolog_lines, compression, slots, entries, data_pos)
|
|
121
|
+
@prolog_lines = prolog_lines
|
|
122
|
+
@compression = compression
|
|
123
|
+
@slots = slots
|
|
124
|
+
@entries = entries
|
|
125
|
+
@data_pos = data_pos
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Slot index (0 based) of the *last* block of the given type, or nil
|
|
129
|
+
def slot_of(typ)
|
|
130
|
+
idx = nil
|
|
131
|
+
@entries.each_with_index { |(t, _o, _l), i| idx = i if t == typ }
|
|
132
|
+
idx
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def free_slot
|
|
136
|
+
@entries.index { |(t, _o, _l)| t == 0 }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def types
|
|
140
|
+
@entries.map(&:first)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Reads the prolog and the directory. Returns [code, Directory].
|
|
145
|
+
def self.read_directory(io)
|
|
146
|
+
io.rewind
|
|
147
|
+
first = io.gets
|
|
148
|
+
return [INVALID_LUT_FILE, nil] unless first
|
|
149
|
+
line = first.chomp
|
|
150
|
+
return [LUT1_FILE_USED, nil] if line[0, SIGNATURE_V1.length] == SIGNATURE_V1
|
|
151
|
+
return [INVALID_LUT_FILE, nil] unless line[0, SIGNATURE_V2.length] == SIGNATURE_V2
|
|
152
|
+
prolog = [line]
|
|
153
|
+
compression = 0
|
|
154
|
+
loop do
|
|
155
|
+
l = io.gets
|
|
156
|
+
return [INVALID_LUT_FILE, nil] if l.nil?
|
|
157
|
+
break if l == "DATA\n"
|
|
158
|
+
prolog << l.chomp
|
|
159
|
+
case l
|
|
160
|
+
when "Kompression: keine\n" then compression = COMPRESSION_NONE
|
|
161
|
+
when "Kompression: gzip\n" then compression = COMPRESSION_ZLIB
|
|
162
|
+
when "Kompression: bzip2\n" then compression = COMPRESSION_BZIP2
|
|
163
|
+
when "Kompression: lzo\n" then compression = COMPRESSION_LZO
|
|
164
|
+
when "Kompression: lzma\n" then compression = COMPRESSION_LZMA
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
compression = COMPRESSION_ZLIB if compression == 0
|
|
168
|
+
hdr = io.read(2)
|
|
169
|
+
return [LUT2_FILE_CORRUPTED, nil] if hdr.nil? || hdr.bytesize < 2
|
|
170
|
+
slots = hdr.unpack1("v")
|
|
171
|
+
dir_pos = io.pos
|
|
172
|
+
dir = io.read(slots * 12)
|
|
173
|
+
return [LUT2_FILE_CORRUPTED, nil] if dir.nil? || dir.bytesize != slots * 12
|
|
174
|
+
entries = dir.unpack("V*").each_slice(3).to_a
|
|
175
|
+
[OK, Directory.new(prolog, compression, slots, entries, dir_pos)]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Reads one block. Either typ (the last block of that type wins) or
|
|
179
|
+
# slot (1 based) must be given. Returns [code, data(binary String)].
|
|
180
|
+
def self.read_block_io(io, dir, typ: nil, slot: nil)
|
|
181
|
+
if slot && slot > 0 && slot <= dir.slots
|
|
182
|
+
typ, read_pos, compressed_len1 = dir.entries[slot - 1]
|
|
183
|
+
else
|
|
184
|
+
read_pos = 0
|
|
185
|
+
compressed_len1 = 0
|
|
186
|
+
dir.entries.each do |t, o, l|
|
|
187
|
+
next unless t == typ
|
|
188
|
+
read_pos = o
|
|
189
|
+
compressed_len1 = l
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
return [LUT2_BLOCK_NOT_IN_FILE, nil] if read_pos.nil? || read_pos == 0
|
|
193
|
+
io.seek(read_pos)
|
|
194
|
+
header = io.read(16)
|
|
195
|
+
return [FILE_READ_ERROR, nil] if header.nil? || header.bytesize < 16
|
|
196
|
+
typ2, compressed_len, len, adler = header.unpack("V4")
|
|
197
|
+
return [LUT2_FILE_CORRUPTED, nil] if typ2 != typ || compressed_len != compressed_len1
|
|
198
|
+
raw = io.read(compressed_len)
|
|
199
|
+
return [FILE_READ_ERROR, nil] if raw.nil? || raw.bytesize < compressed_len
|
|
200
|
+
data = case dir.compression
|
|
201
|
+
when COMPRESSION_NONE then raw
|
|
202
|
+
when COMPRESSION_ZLIB
|
|
203
|
+
begin
|
|
204
|
+
Zlib::Inflate.inflate(raw)
|
|
205
|
+
rescue Zlib::DataError
|
|
206
|
+
return [LUT2_Z_DATA_ERROR, nil]
|
|
207
|
+
rescue Zlib::BufError
|
|
208
|
+
return [LUT2_Z_BUF_ERROR, nil]
|
|
209
|
+
rescue Zlib::MemError
|
|
210
|
+
return [LUT2_Z_MEM_ERROR, nil]
|
|
211
|
+
end
|
|
212
|
+
else
|
|
213
|
+
return [KTO_CHECK_UNSUPPORTED_COMPRESSION, nil]
|
|
214
|
+
end
|
|
215
|
+
return [LUT2_Z_DATA_ERROR, nil] if data.bytesize != len
|
|
216
|
+
return [LUT_CRC_ERROR, nil] if adler32a(data) != adler
|
|
217
|
+
[OK, data]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.read_block(filename, typ)
|
|
221
|
+
return [FILE_READ_ERROR, nil] unless File.file?(filename)
|
|
222
|
+
File.open(filename, "rb") do |io|
|
|
223
|
+
code, dir = read_directory(io)
|
|
224
|
+
return [code, nil] unless code == OK
|
|
225
|
+
read_block_io(io, dir, typ: typ)
|
|
226
|
+
end
|
|
227
|
+
rescue SystemCallError
|
|
228
|
+
[FILE_READ_ERROR, nil]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.read_slot(filename, slot)
|
|
232
|
+
return [FILE_READ_ERROR, nil] unless File.file?(filename)
|
|
233
|
+
File.open(filename, "rb") do |io|
|
|
234
|
+
code, dir = read_directory(io)
|
|
235
|
+
return [code, nil] unless code == OK
|
|
236
|
+
read_block_io(io, dir, slot: slot)
|
|
237
|
+
end
|
|
238
|
+
rescue SystemCallError
|
|
239
|
+
[FILE_READ_ERROR, nil]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Reads the directory of a file. Returns [code, Directory].
|
|
243
|
+
def self.directory(filename)
|
|
244
|
+
return [FILE_READ_ERROR, nil] unless File.file?(filename)
|
|
245
|
+
File.open(filename, "rb") { |io| read_directory(io) }
|
|
246
|
+
rescue SystemCallError
|
|
247
|
+
[FILE_READ_ERROR, nil]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Creates a new (empty) LUT file with the given prolog and number of
|
|
251
|
+
# directory slots. Returns a return code.
|
|
252
|
+
def self.create(filename, prolog, slots = DEFAULT_SLOTS)
|
|
253
|
+
return TOO_MANY_SLOTS if slots > MAX_SLOTS
|
|
254
|
+
File.open(filename, "wb") do |io|
|
|
255
|
+
io.write(prolog)
|
|
256
|
+
io.write("\nDATA\n")
|
|
257
|
+
io.write([slots].pack("v"))
|
|
258
|
+
io.write("\0" * (slots * 12))
|
|
259
|
+
end
|
|
260
|
+
OK
|
|
261
|
+
rescue SystemCallError
|
|
262
|
+
FILE_WRITE_ERROR
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Appends a block to an existing LUT file (opened read/write). A slot
|
|
266
|
+
# with the same type is reused (the old block stays in the file but is
|
|
267
|
+
# no longer referenced), otherwise the first free slot is taken.
|
|
268
|
+
def self.write_block_io(io, typ, data)
|
|
269
|
+
code, dir = read_directory(io)
|
|
270
|
+
return code unless code == OK
|
|
271
|
+
slot = dir.slot_of(typ) || dir.free_slot
|
|
272
|
+
return LUT2_NO_SLOT_FREE if slot.nil?
|
|
273
|
+
data = data.b
|
|
274
|
+
compressed = case dir.compression
|
|
275
|
+
when COMPRESSION_NONE then data
|
|
276
|
+
when COMPRESSION_ZLIB then Zlib::Deflate.deflate(data, Zlib::BEST_COMPRESSION)
|
|
277
|
+
else return KTO_CHECK_UNSUPPORTED_COMPRESSION
|
|
278
|
+
end
|
|
279
|
+
io.seek(0, IO::SEEK_END)
|
|
280
|
+
write_pos = io.pos
|
|
281
|
+
io.write([typ, compressed.bytesize, data.bytesize, adler32a(data)].pack("V4"))
|
|
282
|
+
io.write(compressed)
|
|
283
|
+
io.seek(dir.data_pos + slot * 12)
|
|
284
|
+
io.write([typ, write_pos, compressed.bytesize].pack("V3"))
|
|
285
|
+
io.flush
|
|
286
|
+
OK
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def self.write_block(filename, typ, data)
|
|
290
|
+
return FILE_WRITE_ERROR unless File.file?(filename)
|
|
291
|
+
File.open(filename, "r+b") { |io| write_block_io(io, typ, data) }
|
|
292
|
+
rescue SystemCallError
|
|
293
|
+
FILE_WRITE_ERROR
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Textual dump of the directory (lut_dir_dump_str in C). Returns [code, String].
|
|
297
|
+
def self.dump(filename)
|
|
298
|
+
return [FILE_READ_ERROR, nil] unless File.file?(filename)
|
|
299
|
+
File.open(filename, "rb") do |io|
|
|
300
|
+
code, dir = read_directory(io)
|
|
301
|
+
return [code, nil] unless code == OK
|
|
302
|
+
out = +" Slot retval Typ Inhalt Laenge kompr. Verh. Adler32 Test\n"
|
|
303
|
+
len1 = len2 = 0
|
|
304
|
+
dir.entries.each_with_index do |(typ, offset, clen), i|
|
|
305
|
+
if typ == 0
|
|
306
|
+
out << format("%2d/%2d %3d %8d %-20s %8d %8d%7s 0x%08x %s\n", i + 1, dir.slots, 1, 0, " (ungenutzt)", 0, 0, "-", 0, "OK")
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
io.seek(offset)
|
|
310
|
+
header = io.read(16)
|
|
311
|
+
typ2, clen2, len, adler = header ? header.unpack("V4") : [0, 0, 0, 0]
|
|
312
|
+
retval = OK
|
|
313
|
+
retval = LUT2_FILE_CORRUPTED if typ2 != typ || clen2 != clen
|
|
314
|
+
if retval == OK
|
|
315
|
+
rc, data = read_block_io(io, dir, slot: i + 1)
|
|
316
|
+
retval = rc
|
|
317
|
+
retval = LUT_CRC_ERROR if rc == OK && adler32a(data) != adler
|
|
318
|
+
end
|
|
319
|
+
out << format("%2d/%2d %3d %8d %-20s %8d %8d%7.1f%% 0x%08x %s\n",
|
|
320
|
+
i + 1, dir.slots, retval, typ, block_name(typ, 2), len, clen,
|
|
321
|
+
len > 0 ? clen.fdiv(len) * 100 : 0.0, adler, retval == OK ? "OK" : "FEHLER")
|
|
322
|
+
len1 += len
|
|
323
|
+
len2 += clen
|
|
324
|
+
end
|
|
325
|
+
out << format("\nGesamtgroesse unkomprimiert: %d, Gesamtgroesse komprimiert: %d\nKompressionsrate: %1.2f%% (Kompression: %s)\nSlotdir (kurz): ",
|
|
326
|
+
len1, len2, len1 > 0 ? 100.0 * len2 / len1 : 0.0, COMPRESSION_NAMES[dir.compression] || "???")
|
|
327
|
+
out << dir.entries.map(&:first).reject(&:zero?).map(&:to_s).join(" ") << " \n"
|
|
328
|
+
[OK, out]
|
|
329
|
+
end
|
|
330
|
+
rescue SystemCallError
|
|
331
|
+
[FILE_READ_ERROR, nil]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Extracts the validity dates (JJJJMMTT integers) from an info block.
|
|
335
|
+
# Returns [from, to] (0 if missing).
|
|
336
|
+
def self.parse_validity(info)
|
|
337
|
+
return [0, 0] unless info
|
|
338
|
+
first = info.lines.first.to_s
|
|
339
|
+
m = first.match(/(\d+)\D*-?\D*(\d+)?/)
|
|
340
|
+
return [0, 0] unless m
|
|
341
|
+
[m[1].to_i, m[2].to_i]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Validity status of an info block for the given date (Integer JJJJMMTT)
|
|
345
|
+
def self.validity(info, current)
|
|
346
|
+
v1, v2 = parse_validity(info)
|
|
347
|
+
return LUT2_NO_VALID_DATE if v1 == 0 || v2 == 0
|
|
348
|
+
return LUT2_VALID if current >= v1 && current <= v2
|
|
349
|
+
return LUT2_NOT_YET_VALID if current < v1
|
|
350
|
+
LUT2_NO_LONGER_VALID
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Extracts the file id ("Datei-ID ...") from an info block or nil.
|
|
354
|
+
def self.file_id(info)
|
|
355
|
+
return nil unless info
|
|
356
|
+
lines = info.lines.map(&:chomp)
|
|
357
|
+
i = lines.index { |l| l.start_with?("Datei-ID (zuf") }
|
|
358
|
+
i && lines[i + 1]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Reads both info blocks of a LUT file. Returns
|
|
362
|
+
# [code, info1, info2, valid1, valid2] (lut_info in C for a file name).
|
|
363
|
+
def self.info(filename, current_date = today_int)
|
|
364
|
+
return [FILE_READ_ERROR, nil, nil, 0, 0] unless File.file?(filename)
|
|
365
|
+
File.open(filename, "rb") do |io|
|
|
366
|
+
code, dir = read_directory(io)
|
|
367
|
+
return [code, nil, nil, code, code] unless code == OK
|
|
368
|
+
c1, i1 = read_block_io(io, dir, typ: LUT2_INFO)
|
|
369
|
+
unless c1 == OK
|
|
370
|
+
# no info block: return the prolog like get_lut_info2()
|
|
371
|
+
prolog = dir.prolog_lines.join("\n") + "\n"
|
|
372
|
+
return [c1, prolog, nil, c1, 0]
|
|
373
|
+
end
|
|
374
|
+
i1 = i1.force_encoding("ISO-8859-1").encode("UTF-8")
|
|
375
|
+
v1 = validity(i1, current_date)
|
|
376
|
+
c2, i2 = read_block_io(io, dir, typ: LUT2_2_INFO)
|
|
377
|
+
if c2 == OK
|
|
378
|
+
i2 = i2.force_encoding("ISO-8859-1").encode("UTF-8")
|
|
379
|
+
v2 = validity(i2, current_date)
|
|
380
|
+
if v2 == LUT2_NO_LONGER_VALID && v1 == LUT2_NO_LONGER_VALID
|
|
381
|
+
# both sets expired: mark the younger one as "better"
|
|
382
|
+
e1 = parse_validity(i1)[1]
|
|
383
|
+
e2 = parse_validity(i2)[1]
|
|
384
|
+
if e2 > e1
|
|
385
|
+
v2 = LUT2_NO_LONGER_VALID_BETTER
|
|
386
|
+
else
|
|
387
|
+
v1 = LUT2_NO_LONGER_VALID_BETTER
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
else
|
|
391
|
+
i2 = nil
|
|
392
|
+
v2 = c2
|
|
393
|
+
end
|
|
394
|
+
[OK, i1, i2, v1, v2]
|
|
395
|
+
end
|
|
396
|
+
rescue SystemCallError
|
|
397
|
+
[FILE_READ_ERROR, nil, nil, 0, 0]
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def self.today_int(time = Time.now)
|
|
401
|
+
time.year * 10000 + time.month * 100 + time.day
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Prolog, system info line and user info line of the file
|
|
405
|
+
# (get_lut_info2 in C). Returns [code, version, prolog, info, user_info].
|
|
406
|
+
def self.prolog_info(filename)
|
|
407
|
+
code, dir = directory(filename)
|
|
408
|
+
if code == LUT1_FILE_USED
|
|
409
|
+
return [OK, 1, "", "", ""]
|
|
410
|
+
end
|
|
411
|
+
return [code, 0, nil, nil, nil] unless code == OK
|
|
412
|
+
lines = dir.prolog_lines
|
|
413
|
+
prolog = lines.join("\n") + "\n"
|
|
414
|
+
info = lines[1] || ""
|
|
415
|
+
user_info = ""
|
|
416
|
+
if info.end_with?("\\")
|
|
417
|
+
info = info.chomp("\\")
|
|
418
|
+
user_info = lines[2] || ""
|
|
419
|
+
end
|
|
420
|
+
[OK, 3, prolog, info, user_info]
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
module KontoCheckRuby
|
|
426
|
+
module LutFile
|
|
427
|
+
# Writes the IBAN blacklist block (LUT2_OWN_IBAN) from a text file with
|
|
428
|
+
# lines "<BLZ>=0" into an existing LUT file (lut_keine_iban_berechnung in
|
|
429
|
+
# C). A line "2718281=0" marks the list as coming from the IBAN service
|
|
430
|
+
# portal; only then the block is evaluated by the IBAN functions.
|
|
431
|
+
def self.write_iban_blacklist(blacklist_file, lutfile, set = 0)
|
|
432
|
+
return FILE_READ_ERROR unless File.file?(blacklist_file)
|
|
433
|
+
blzs = []
|
|
434
|
+
File.foreach(blacklist_file) do |line|
|
|
435
|
+
next unless line =~ /\A(\d{7,8})=0/
|
|
436
|
+
blzs << Regexp.last_match(1).to_i
|
|
437
|
+
end
|
|
438
|
+
blzs.sort!
|
|
439
|
+
data = [blzs.size].pack("V") + blzs.pack("V*")
|
|
440
|
+
offset = set == 2 ? SET_OFFSET : 0
|
|
441
|
+
File.open(lutfile, "r+b") do |io|
|
|
442
|
+
code = write_block_io(io, LUT2_OWN_IBAN + offset, data)
|
|
443
|
+
return code unless code == OK
|
|
444
|
+
c, dir = read_directory(io)
|
|
445
|
+
return c unless c == OK
|
|
446
|
+
c, info = read_block_io(io, dir, typ: LUT2_INFO + offset)
|
|
447
|
+
if c == OK && info =~ /^Enthaltene Felder:.*$/ && !info.include?(", OWN_IBAN")
|
|
448
|
+
info = info.sub(/^(Enthaltene Felder:[^\n]*)/) { "#{Regexp.last_match(1)}, OWN_IBAN" }
|
|
449
|
+
write_block_io(io, LUT2_INFO + offset, info)
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
OK
|
|
453
|
+
rescue SystemCallError
|
|
454
|
+
FILE_WRITE_ERROR
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|