1--
2-- This file is part of Cardpeek, the smart card reader utility.
3--
4-- Copyright 2009-2013 by Alain Pannetrat <L1L1@gmx.com>
5--
6-- Cardpeek is free software: you can redistribute it and/or modify
7-- it under the terms of the GNU General Public License as published by
8-- the Free Software Foundation, either version 3 of the License, or
9-- (at your option) any later version.
10--
11-- Cardpeek is distributed in the hope that it will be useful,
12-- but WITHOUT ANY WARRANTY; without even the implied warranty of
13-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14-- GNU General Public License for more details.
15--
16-- You should have received a copy of the GNU General Public License
17-- along with Cardpeek.  If not, see <http://www.gnu.org/licenses/>.
18--
19--********************************************************************--
20--
21-- This file is based on c376n2.lua, which was authored and contributed
22-- to cardpeek by Anthony Berkow (C) 2013
23--
24-- Some minor alterations of the original file were performed for
25-- integration with calypso.lua
26--
27-- Jan 2015:
28-- This file c376n3.lua was created to take into account some specific
29-- isses discovered with a new type of RavKav card with network id = 3
30-- and which does not return a FCI.
31-- This induced a change in RavKav_getRecordInfo()
32--
33--********************************************************************--
34
35require('lib.apdu')
36require('lib.tlv')
37require('lib.en1545')
38require('lib.country_codes')
39require('etc.ravkav-strings')
40
41--Classes
42ISO_CLS_STD     = 0x00  --Structure and coding of command and response according to ISO/IEC 7816 5.4.1 (plus optional secure messaging and logical channel)
43ISO_CLS_PRO9    = 0x90  --As for ISO_CLS_STD but the coding and meaning of command and response are proprietary
44--Class modifiers (SM)
45ISO_CLS_SM_PRO  = 0x04  --Proprietary secure messaging format
46
47--Application ID
48AID_RavKav = "#315449432e494341"    --"1TIC.ICA"
49
50--LIDs
51CALYPSO_LID_ENVIRONMENT = "2001"    --SFI=0x07, linear, 1 record
52CALYPSO_LID_EVENTS_LOG  = "2010"    --SFI=0x08, cyclic, 3 records
53CALYPSO_LID_CONTRACTS   = "2020"    --SFI=0x09, linear, 4 records
54CALYPSO_LID_COUNTERS    = "2069"    --SFI=0x19, counters, 9 counters
55
56LID_LIST = {
57    {"General Information", CALYPSO_LID_ENVIRONMENT,    "environment"},
58    {"Counters",            CALYPSO_LID_COUNTERS,       "counters"},
59    {"Latest Travel",       CALYPSO_LID_EVENTS_LOG,     "event"},
60    {"Contracts",           CALYPSO_LID_CONTRACTS,      "contract"}
61}
62
63function ravkav_parse_serial_number(node,data)
64	nodes.set_attribute(node,"val", data)
65	nodes.set_attribute(node,"alt", string.format("%010d",bytes.tonumber(data)))
66end
67
68function ravkav_parse_ats(node,data)
69    if data and 5 <= #data then
70        local byteOffset = 1
71        byteOffset = RavKav_parseBits(data, byteOffset,             1, node, "File type",    en1545_NUMBER)
72        byteOffset = RavKav_parseBits(data, byteOffset,             1, node, "EF type",      en1545_NUMBER)
73        byteOffset, recordLen = RavKav_parseBits(data,byteOffset,   1, node, "Record size",  en1545_NUMBER)
74        byteOffset, numRecords = RavKav_parseBits(data,byteOffset,  1, node, "Record count", en1545_NUMBER)
75        byteOffset = RavKav_parseBits(data, byteOffset,             4, node, "Access",       en1545_UNDEFINED)
76    end
77end
78
79--Tags
80RAVKAV_IDO = {
81    ['A5/BF0C'] = {"Secure messaging data"},
82    ['BF0C/C7'] = {"Serial number", ravkav_parse_serial_number},
83    ['85']      = {"Record Info", ravkav_parse_ats},
84}
85
86ValidUntilOption = {
87    None = 0,
88    Date = 1,
89    EndOfService = 2,
90    Cancelled = 3
91}
92
93function bytes.is_all(bs, byte)
94	if 0 == #bs then return false end
95	local i
96	for i = 0, #bs - 1 do
97		if bs[i] ~= byte then return false end
98	end
99	return true
100end
101
102function RavKav_addTextNode(ctx, a_label, text)
103    local REF = nodes.append(ctx, { classname="item", label=a_label })
104    nodes.set_attribute(REF,"alt", text)
105end
106
107function RavKav_selectApplication(root)
108    card.CLA = ISO_CLS_STD
109	local sw, resp = card.select(AID_RavKav, card.SELECT_RETURN_FIRST + card.SELECT_RETURN_FCI, 0)
110    if 0x9000 ~= sw then
111		log.print(log.ERROR, "Failed to select Calypso application.")
112        return nil
113    end
114
115    local APP_REF = nodes.append(root, { classname="application", label="RavKav Card", size=#resp })
116    nodes.set_attribute(APP_REF,"val", resp)
117    return APP_REF
118end
119
120function RavKav_readCalypsoEf(lid, ctx)
121    card.CLA = ISO_CLS_STD
122	local sw, resp = card.select("."..lid)
123    if 0x9000 ~= sw then
124		log.print(log.ERROR, "Failed to select EF "..lid)
125    else
126        tlv_parse(ctx, resp)
127    end
128end
129
130function RavKav_parseEf(ctx)
131    local numRecords, recordLen
132    local RECINFO_REF, ri_data = RavKav_getField(ctx, "Record Info")
133    if ri_data and 5 <= #ri_data then
134        --recordLen = ri_data[3]
135        --numRecords = ri_data[4]
136        local byteOffset = 1
137        byteOffset = RavKav_parseBits(ri_data, byteOffset,             1, RECINFO_REF, "File type",    en1545_NUMBER)
138        byteOffset = RavKav_parseBits(ri_data, byteOffset,             1, RECINFO_REF, "EF type",      en1545_NUMBER)
139        byteOffset, recordLen = RavKav_parseBits(ri_data,byteOffset,   1, RECINFO_REF, "Record size",  en1545_NUMBER)
140        byteOffset, numRecords = RavKav_parseBits(ri_data,byteOffset,  1, RECINFO_REF, "Record count", en1545_NUMBER)
141        byteOffset = RavKav_parseBits(ri_data, byteOffset,             4, RECINFO_REF, "Access",       en1545_UNDEFINED)
142        log.print(log.DBG, string.format("numRecords: %d, recordLen: %d", numRecords, recordLen))
143    end
144    return numRecords, recordLen
145end
146
147function RavKav_getRecordInfo(ctx)
148    local numRecords = 0
149    local recordLen = 0
150    local RECINFO_REF = nodes.find_first(ctx, {label="Record Info"})
151    if RECINFO_REF then
152        numRecords = RavKav_getFieldAsNumber(RECINFO_REF, "Record count")
153        recordLen = RavKav_getFieldAsNumber(RECINFO_REF, "Record size")
154    else
155        local iter
156        for iter in nodes.find(ctx,{label="record"}) do
157            numRecords = numRecords + 1
158        end
159        recordLen = 29
160    end
161    return numRecords, recordLen
162end
163
164function RavKav_readRecord(nRec, recordLen)
165	card.CLA = ISO_CLS_PRO9 + ISO_CLS_SM_PRO
166	local sw, resp = card.read_record(0, nRec, recordLen)
167    if 0x9000 ~= sw then
168		log.print(log.ERROR, string.format("Failed to read record %d", nRec))
169        return nil
170    end
171    return resp
172end
173
174-- Note: this function can work with either bits or bytes depending on the data format
175function RavKav_parseBits(data, bitOffset, nBits, REC, a_label, func, a_id)
176	local valData = bytes.sub(data, bitOffset, bitOffset + nBits - 1)
177	local value = func(valData)
178	local intVal = bytes.tonumber(valData)
179	local NEW_REF = nil
180	if REC then
181	    NEW_REF = nodes.append(REC, {classname="item", label=a_label, id=a_id, size=nBits })
182        nodes.set_attribute(NEW_REF,"val", bytes.convert(valData,8))
183        nodes.set_attribute(NEW_REF,"alt", value)
184    end
185	return bitOffset + nBits, value, NEW_REF, intVal
186end
187
188function RavKav_rkDaysToSeconds(dateDaysPart)
189    bytes.pad_left(dateDaysPart, 32, 0)
190    return EPOCH + bytes.tonumber(dateDaysPart) * 24 * 3600
191end
192
193-- The bits in dateDaysPart have been inverted.
194function RavKav_rkInvertedDaysToSeconds(dateDaysPart)
195    bytes.pad_left(dateDaysPart, 32, 0)
196    return EPOCH + bit.XOR(bytes.tonumber(dateDaysPart), 0x3fff) * 24 * 3600
197end
198
199-- Return the date (in seconds) corresponding to the last day of the month for the given date (in seconds).
200-- If addMonths > 0 then the month is incremented by addMonths months.
201function RavKav_calculateMonthEnd(dateSeconds, addMonths)
202    local dt = os.date("*t", dateSeconds)
203
204    if 0 < addMonths then
205        dt.month = dt.month + addMonths
206        local addYears = math.floor(dt.month / 12)
207        dt.year = dt.year + addYears
208        dt.month = dt.month - addYears * 12
209    end
210
211    dt.day = 28
212    local tmp_dt = dt
213    repeat
214        local date_s = os.time(tmp_dt) + 24 * 3600 --add 1 day
215        tmp_dt = os.date("*t",date_s)
216        if 1 ~= tmp_dt.day then --did not wrap round to a new month
217            dt.day = dt.day + 1
218        end
219    until 1 == tmp_dt.day
220
221    return os.time(dt)
222end
223
224function RavKav_INVERTED_DATE(source)
225    return os.date("%x", RavKav_rkInvertedDaysToSeconds(source))
226end
227
228function RavKav_PERCENT(source)
229    return string.format("%u %%", bytes.tonumber(source))
230end
231
232-- source is in BCD!
233function RavKav_COUNTRY(source)
234    local countryId = bytes.tonumber(source)
235    countryId = tonumber(string.format("%X", countryId))
236    return iso_country_code_name(countryId)
237end
238
239function RavKav_ISSUER(source)
240    local issuerId = bytes.tonumber(source)
241    local issuer = RAVKAV_ISSUERS[issuerId]
242    if issuer then return issuer end
243    return tostring(issuerId)
244end
245
246function RavKav_PROFILE(source)
247    local profileId = bytes.tonumber(source)
248    local profile = RAVKAV_PROFILES[profileId]
249    if profile then return profile end
250    return tostring(profileId)
251end
252
253function RavKav_ROUTE(source)
254    local routeSystemId = bytes.tonumber(source)
255    local route = RAVKAV_ROUTES[routeSystemId]
256    if route then return route end
257    return tostring(routeSystemId)
258end
259
260function RavKav_LOCATION(source)
261    local locationId = bytes.tonumber(source)
262    local location = RAVKAV_LOCATIONS[locationId]
263    if location then return location end
264    return tostring(locationId)
265end
266
267function RavKav_VALIDITY_TYPE(source)
268    local validityType = bytes.tonumber(source)
269    local validity = RAVKAV_VALIDITY_TYPES[validityType]
270    if validity then return validity end
271    return tostring(validityType)
272end
273
274function RavKav_CONTRACT_TYPE(source)
275    local contractType = bytes.tonumber(source)
276    local contract = RAVKAV_CONTRACT_TYPES[contractType]
277    if contract then return contract end
278    return tostring(contractType)
279end
280
281-- Returns CSV list of which bits are set
282function RavKav_ZONES(source)
283    local zonesPart = bytes.sub(source, 0, 11) --12 bits
284    local zones = bytes.tonumber(zonesPart)
285    local haveZones = false
286    local zoneList = ""
287    local i
288    for i = 1, 12 do
289        if 1 == bit.AND(zones, 1) then
290            if haveZones then zoneList = zoneList.."," end
291            zoneList = zoneList..tostring(i)
292            haveZones = true
293        end
294        zones = bit.SHR(zones, 1)
295    end
296    return zoneList
297end
298
299function RavKav_readRecords(EF_REF, numRecords, recordLen, rec_desc)
300    local nRec
301    for nRec = 1, numRecords do
302        local record = RavKav_readRecord(nRec, recordLen)
303        if record then
304            local REC_REF = nodes.append(EF_REF, {classname="record", label=rec_desc, id=nRec, size=#record})
305            nodes.set_attribute(REC_REF,"val", record)
306        end
307    end
308end
309
310function RavKav_readFile(APP_REF, desc, lid, rec_desc)
311	log.print(log.DBG, "Reading "..desc.."...")
312    local EF_REF = nodes.append(APP_REF, {classname="file", label=desc})
313    RavKav_readCalypsoEf(lid, EF_REF)
314    local numRecords, recordLen = RavKav_parseEf(EF_REF)
315    if numRecords then
316        RavKav_readRecords(EF_REF, numRecords, recordLen, rec_desc)
317    end
318end
319
320function RavKav_getField(SRC_REF, a_label, a_id)
321    local data, text
322    local REF = nodes.find_first(SRC_REF, {label=a_label, id=a_id})
323    if REF then
324        data = nodes.get_attribute(REF,"val")
325        text = nodes.get_attribute(REF,"alt")
326    end
327    return REF, data, text
328end
329
330function RavKav_getFieldAsNumber(SRC_REF, label, id)
331    local REF, data, text = RavKav_getField(SRC_REF, label, id)
332    if data then return bytes.tonumber(data) end
333    return 0
334end
335
336function RavKav_getFieldsAsNumberAry(SRC_REF, a_label, a_id)
337    local node
338    local ary = {}
339
340    for node in nodes.find(SRC_REF, { label=a_label, id=a_id }) do
341        local data = nodes.get_attribute(node,"val")
342        if data then table.insert(ary, bytes.tonumber(data)) end
343    end
344    return ary
345end
346
347-- if ary is supplied then only create new node if the field is a pointer to a valid entry in ary
348function RavKav_copyField(SRC_REF, DEST_REF, a_label, a_id, ary)
349    local SRC_REF2, data, text = RavKav_getField(SRC_REF, a_label, a_id)
350    if nil == data then return nil, nil end
351
352    if ary then
353        local dataId = en1545_NUMBER(data)
354        if nil == ary[dataId] then return nil, nil end
355    end
356
357    NEW_REF = nodes.append(DEST_REF, {classname="item", label=a_label, id=a_id, size=#data })
358    nodes.set_attribute(NEW_REF,"val", data)
359    nodes.set_attribute(NEW_REF,"alt", text)
360    return SRC_REF2, NEW_REF
361end
362
363function RavKav_parseEnvironment(ENV_REF, nRec)
364    local ENV_REC_REF = nodes.find_first(ENV_REF, {label="record", id=nRec})
365    if nil == ENV_REC_REF then return false end
366
367    local record = nodes.get_attribute(ENV_REC_REF,"val")
368    if nil == record then return false end
369
370    local data = bytes.convert(record,1)
371
372    local bitOffset = 0
373    bitOffset = RavKav_parseBits(data, bitOffset,  3, ENV_REC_REF, "Version number",        en1545_NUMBER)
374    bitOffset = RavKav_parseBits(data, bitOffset, 12, ENV_REC_REF, "Country",               RavKav_COUNTRY)
375    bitOffset = RavKav_parseBits(data, bitOffset,  8, ENV_REC_REF, "Issuer",                RavKav_ISSUER)
376    bitOffset = RavKav_parseBits(data, bitOffset, 26, ENV_REC_REF, "Application number",    en1545_NUMBER)
377    bitOffset = RavKav_parseBits(data, bitOffset, 14, ENV_REC_REF, "Date of issue",         en1545_DATE)
378    bitOffset = RavKav_parseBits(data, bitOffset, 14, ENV_REC_REF, "End date",              en1545_DATE)
379    bitOffset = RavKav_parseBits(data, bitOffset,  3, ENV_REC_REF, "Pay method",            en1545_NUMBER)
380    bitOffset = RavKav_parseBits(data, bitOffset, 32, ENV_REC_REF, "Date of birth",         en1545_BCD_DATE)
381    bitOffset = RavKav_parseBits(data, bitOffset, 14, ENV_REC_REF, "Company (not set)",     en1545_NUMBER)
382    bitOffset = RavKav_parseBits(data, bitOffset, 30, ENV_REC_REF, "Company ID (not set)",  en1545_NUMBER)
383    bitOffset = RavKav_parseBits(data, bitOffset, 30, ENV_REC_REF, "ID number",             en1545_NUMBER)
384
385    local PROFILES_REF = nodes.append(ENV_REC_REF, {classname="item", label="Profiles"})
386    local profile, ref
387    bitOffset, profile, ref = RavKav_parseBits(data, bitOffset,  6, PROFILES_REF,   "Profile",      RavKav_PROFILE, 1)
388    bitOffset               = RavKav_parseBits(data, bitOffset, 14, ref,            "Valid until",  en1545_DATE)
389    bitOffset, profile, ref = RavKav_parseBits(data, bitOffset,  6, PROFILES_REF,   "Profile",      RavKav_PROFILE, 2)
390                              RavKav_parseBits(data, bitOffset, 14, ref,            "Valid until",  en1545_DATE)
391    return true
392end
393
394function RavKav_parseGeneralInfo(APP_REF)
395	log.print(log.DBG, "Parsing general info (environment)...")
396    local ENV_REF = nodes.find_first(APP_REF, {label="Environment"})
397    if ENV_REF then
398        local numRecords = 2 -- RavKav_getRecordInfo(ENV_REF)
399        local nRec
400        for nRec = 1, numRecords do
401            -- only the first record contains valid data
402            if RavKav_parseEnvironment(ENV_REF, nRec) then break end
403        end
404    end
405end
406
407function RavKav_parseCounter(CNTRS_REF, nRec)
408    local CNTR_REC_REF = nodes.find_first(CNTRS_REF, {label="record", id=nRec})
409    if CNTR_REC_REF then
410        local record = nodes.get_attribute(CNTR_REC_REF,"val")
411        if record then
412            local data = bytes.convert(record,1)
413            local bitOffset = 0
414            local maxCounters = math.floor(#data / 24)
415            local nRec, nCnt
416            local cntrAry = {}
417            for nRec = 1, maxCounters do
418                bitOffset, nCnt = RavKav_parseBits(data, bitOffset, 24, CNTR_REC_REF, "Counter", en1545_NUMBER, nRec)
419                cntrAry[nRec] = nCnt
420            end
421            return cntrAry
422        end
423    end
424    return nil
425end
426
427function RavKav_parseCounters(APP_REF)
428	log.print(log.DBG, "Parsing counters...")
429    local CNTRS_REF = nodes.find_first(APP_REF, {id=CALYPSO_LID_COUNTERS})
430    if CNTRS_REF then
431        local numRecords = RavKav_getRecordInfo(CNTRS_REF)
432        if 0 < numRecords then
433            -- there should only be one counter record
434            return RavKav_parseCounter(CNTRS_REF, 1)
435        end
436    end
437    return nil
438end
439
440function RavKav_parseEvent(EVENTS_REF, nRec)
441    local EVENT_REC_REF = nodes.find_first(EVENTS_REF, {label="record", id=nRec})
442    if nil == EVENT_REC_REF then return end
443
444    local record = nodes.get_attribute(EVENT_REC_REF,"val")
445    if nil == record then return end
446
447    local data = bytes.convert(record,1)
448
449	if bytes.is_all(data, 0) then return end
450
451    local bitOffset = 0
452
453    local text, ref, issuerId
454
455    bitOffset                       = RavKav_parseBits(data, bitOffset, 3, EVENT_REC_REF, "Version number", en1545_NUMBER)
456    bitOffset, text, ref, issuerId  = RavKav_parseBits(data, bitOffset, 8, EVENT_REC_REF, "Issuer",         RavKav_ISSUER)
457
458    if issuerId == 0 then return end
459
460    local contractId, eventType
461
462    bitOffset, contractId   = RavKav_parseBits(data, bitOffset,  4, EVENT_REC_REF, "Contract ID",               en1545_NUMBER)
463    bitOffset               = RavKav_parseBits(data, bitOffset,  4, EVENT_REC_REF, "Area ID",                   en1545_NUMBER)      --1 == urban, 2 == intercity
464    bitOffset, eventType    = RavKav_parseBits(data, bitOffset,  4, EVENT_REC_REF, "Event type",                en1545_NUMBER)
465    bitOffset               = RavKav_parseBits(data, bitOffset, 30, EVENT_REC_REF, "Event time",                en1545_DATE_TIME)
466    bitOffset               = RavKav_parseBits(data, bitOffset,  1, EVENT_REC_REF, "Journey interchanges flag", en1545_NUMBER)      --includes switching/continuing beyond
467    bitOffset               = RavKav_parseBits(data, bitOffset, 30, EVENT_REC_REF, "First event time",          en1545_DATE_TIME)   --identical to 'Event time'
468
469    local PRIORITIES_REF = nodes.append(EVENT_REC_REF, {classname="item", label="Best contract priorities"})
470    local nContract
471    for nContract = 1, 8 do
472        bitOffset = RavKav_parseBits(data, bitOffset, 4, PRIORITIES_REF, "Contract", RavKav_PERCENT, nContract)
473    end
474
475    local locationBitmap
476    local ln_ref = nil
477
478	bitOffset, text, ref, locationBitmap = RavKav_parseBits(data, bitOffset, 7, EVENT_REC_REF, "Location bitmap", en1545_UNDEFINED)    --defined per issuer
479
480    if 0 < bit.AND(locationBitmap, 1) then
481        if 2 == issuerId then   --Israel Rail
482            bitOffset = RavKav_parseBits(data, bitOffset, 16, EVENT_REC_REF, "Location", RavKav_LOCATION)   --station
483        else
484            bitOffset = RavKav_parseBits(data, bitOffset, 16, EVENT_REC_REF, "Location ID", en1545_NUMBER)  --place
485        end
486    end
487
488    if 0 < bit.AND(locationBitmap, 2) then
489        local lineNumber
490        bitOffset, lineNumber, ln_ref = RavKav_parseBits(data, bitOffset, 16, EVENT_REC_REF, "Line number", en1545_NUMBER)  --number of line on route
491    end
492
493    if 0 < bit.AND(locationBitmap, 4) then
494        bitOffset = RavKav_parseBits(data, bitOffset, 8, EVENT_REC_REF, "Stop en route", en1545_NUMBER)
495    end
496
497    if 0 < bit.AND(locationBitmap, 8) then  --12 unknown bits
498        bitOffset = RavKav_parseBits(data, bitOffset, 12, EVENT_REC_REF, "Location[3]", en1545_UNDEFINED)
499    end
500
501    if 0 < bit.AND(locationBitmap, 0x10) then
502        bitOffset = RavKav_parseBits(data, bitOffset, 14, EVENT_REC_REF, "Vehicle", en1545_UNDEFINED)
503    end
504
505    if 0 < bit.AND(locationBitmap, 0x20) then   --how many bits??
506	    log.print(log.DBG, string.format("Event[%d]: Location[5]: unknown value", nRec))
507    end
508
509    if 0 < bit.AND(locationBitmap, 0x40) then   --8 unknown bits
510        if 0 < bit.AND(locationBitmap, 0x20) then   -- we are lost due to previous unknown field
511	        log.print(log.DBG, string.format("Event[%d]: Location[6]: unknown value", nRec))
512        else
513            bitOffset = RavKav_parseBits(data, bitOffset, 8, EVENT_REC_REF, "Location[6]", en1545_UNDEFINED)
514        end
515    end
516
517    if 0 == bit.AND(locationBitmap, 0x20) then
518        local eventExtension
519        bitOffset, text, ref, eventExtension = RavKav_parseBits(data, bitOffset, 3, EVENT_REC_REF, "Event extension bitmap", en1545_UNDEFINED)
520
521        if 0 < bit.AND(eventExtension, 1) then
522            bitOffset = RavKav_parseBits(data, bitOffset, 10, EVENT_REC_REF, "Route system",    RavKav_ROUTE)
523            bitOffset = RavKav_parseBits(data, bitOffset,  8, EVENT_REC_REF, "Fare code",       en1545_NUMBER)
524            local debitAmount, dr_ref
525            bitOffset, debitAmount, dr_ref = RavKav_parseBits(data, bitOffset, 16, EVENT_REC_REF, "Debit amount", en1545_NUMBER)
526            if 0 < debitAmount and 6 ~= eventType and 0 < contractId and 9 > contractId then    --not a transit trip
527                if 21 > debitAmount then
528                    dr_ref:set_attribute("alt",string.format("%u trip(s)", debitAmount))
529                else
530                    dr_ref:set_attribute("alt",string.format("NIS %0.2f", debitAmount / 100.0))
531                end
532            end
533        end
534
535        if 0 < bit.AND(eventExtension, 2) then
536            bitOffset = RavKav_parseBits(data, bitOffset, 32, EVENT_REC_REF, "Event extension[2]", en1545_UNDEFINED)
537        end
538
539        if 0 < bit.AND(eventExtension, 4) then
540            bitOffset = RavKav_parseBits(data, bitOffset, 32, EVENT_REC_REF, "Event extension[3]", en1545_UNDEFINED)
541        end
542    end
543
544    -- fix 'Line number' field
545    local lnBitsize = 0
546    if 3 == issuerId then       --Egged
547        bitOffset = 155
548        lnBitsize = 10
549        if 0 < bit.AND(locationBitmap, 8) then bitOffset = bitOffset + 12 end
550    elseif 15 == issuerId then  --Metropolis
551        bitOffset = 123
552        lnBitsize = 32
553    elseif 2 ~= issuerId and 14 ~= issuerId and 16 ~= issuerId then --not Israel Rail, Nativ Express nor Superbus
554        bitOffset = 145
555        lnBitsize = 10
556    end
557    if 0 < lnBitsize then
558        if ln_ref then ln_ref:remove() end
559        RavKav_parseBits(data, bitOffset, lnBitsize, EVENT_REC_REF, "Line number", en1545_NUMBER)
560    end
561end
562
563function RavKav_parseEventsLog(APP_REF)
564	log.print(log.DBG, "Parsing events...")
565    local EVENTS_REF = nodes.find_first(APP_REF, {label="Event logs"} )
566    if EVENTS_REF then
567        local numRecords = RavKav_getRecordInfo(EVENTS_REF)
568        local nRec
569        for nRec = 1, numRecords do
570            RavKav_parseEvent(EVENTS_REF, nRec)
571        end
572    end
573end
574
575function RavKav_parseContract(CONTRACTS_REF, nRec, counter)
576    local CONTRACT_REC_REF = nodes.find_first(CONTRACTS_REF, {label="record", id=nRec})
577    if nil == CONTRACT_REC_REF then return end
578
579    local record = nodes.get_attribute(CONTRACT_REC_REF,"val")
580    if nil == record then return end
581
582    local data = bytes.convert(record,1)
583	if bytes.is_all(data, 0) then
584        nodes.set_attribute(CONTRACT_REC_REF,"alt","empty")
585        return
586    end
587
588    local bitOffset = 0
589
590    local text, ref, vfd_ref, issuerId
591
592    bitOffset                   = RavKav_parseBits(data, bitOffset,  3, CONTRACT_REC_REF, "Version number", en1545_NUMBER)
593    bitOffset, text, vfd_ref    = RavKav_parseBits(data, bitOffset, 14, CONTRACT_REC_REF, "Valid from",     RavKav_INVERTED_DATE)   --validity start date
594
595    local validFromSeconds = RavKav_rkInvertedDaysToSeconds(nodes.get_attribute(vfd_ref,"val"))
596
597    bitOffset, text, ref, issuerId = RavKav_parseBits(data, bitOffset, 8, CONTRACT_REC_REF, "Issuer", RavKav_ISSUER)
598    if issuerId == 0 then return end
599
600    local ticketType, validityBitmap
601    local contractValid = true
602
603    bitOffset               = RavKav_parseBits(data, bitOffset, 2, CONTRACT_REC_REF, "Tariff transport access", en1545_NUMBER)
604    bitOffset               = RavKav_parseBits(data, bitOffset, 3, CONTRACT_REC_REF, "Tariff counter use",      en1545_NUMBER)  --0 == not used, 2 == number of tokens, 3 == monetary amount
605    bitOffset, ticketType   = RavKav_parseBits(data, bitOffset, 6, CONTRACT_REC_REF, "Ticket type",             en1545_NUMBER)
606
607    if 1 == ticketType or 6 == ticketType or 7 == ticketType then   --Single or multiple, Aggregate value or Single or multiple
608        contractValid = 0 < counter
609        local balance
610        if 6 == ticketType then --Aggregate value
611    	    balance = string.format("NIS %0.2f", counter / 100.0)   --balanceRemaining
612        else
613            balance = string.format("%d trip(s)", counter)          --tripsRemaining
614        end
615	    local NEW_REF = nodes.append(CONTRACT_REC_REF, {classname="item", label="Balance", size=24})
616        nodes.set_attribute(NEW_REF,"val", bytes.new(8, string.format("%06X", counter)))
617        nodes.set_attribute(NEW_REF,"alt", balance)
618    end
619
620    bitOffset                               = RavKav_parseBits(data, bitOffset, 14, CONTRACT_REC_REF, "Date of purchase",           en1545_DATE)    --sale date
621    bitOffset                               = RavKav_parseBits(data, bitOffset, 12, CONTRACT_REC_REF, "Sale device",                en1545_NUMBER)  --serial number of device that made the sale
622    bitOffset                               = RavKav_parseBits(data, bitOffset, 10, CONTRACT_REC_REF, "Sale number",                en1545_NUMBER)  --runnning sale number on the day of the sale
623    bitOffset                               = RavKav_parseBits(data, bitOffset,  1, CONTRACT_REC_REF, "Journey interchanges flag",  en1545_NUMBER)  --includes switching/continuing beyond
624	bitOffset, text, ref, validityBitmap    = RavKav_parseBits(data, bitOffset,  9, CONTRACT_REC_REF, "Validity bitmap",            en1545_UNDEFINED)
625
626    if 0 < bit.AND(validityBitmap, 1) then
627        bitOffset = RavKav_parseBits(data, bitOffset, 5, CONTRACT_REC_REF, "Validity[1]", en1545_UNDEFINED)
628    end
629
630    local restrictionCode = 0
631    if 0 < bit.AND(validityBitmap, 2) then
632        bitOffset, restrictionCode = RavKav_parseBits(data, bitOffset, 5, CONTRACT_REC_REF, "Restriction code", en1545_NUMBER)
633    end
634
635    if 0 < bit.AND(validityBitmap, 4) then
636        local iDuration, rd_ref
637        bitOffset, iDuration, rd_ref = RavKav_parseBits(data, bitOffset, 6, CONTRACT_REC_REF, "Restriction duration", en1545_NUMBER)
638        if 16 == restrictionCode then
639            iDuration = iDuration * 5
640        else
641            iDuration = iDuration * 30
642        end
643        rd_ref:set_attribute("alt",string.format("%d minute(s)", iDuration))
644    end
645
646    if 0 < bit.AND(validityBitmap,8) then   --validity end date
647        local vtd_ref
648        bitOffset, text, vtd_ref = RavKav_parseBits(data, bitOffset, 14, CONTRACT_REC_REF, "Valid until", en1545_DATE)
649        if contractValid then
650            contractValid = RavKav_rkDaysToSeconds(vtd_ref:val()) > os.time()
651        end
652    end
653
654    local validUntilOption = ValidUntilOption.None
655    if 0 < bit.AND(validityBitmap, 0x10) then
656        local validMonths
657        bitOffset, validMonths = RavKav_parseBits(data, bitOffset, 8, CONTRACT_REC_REF, "Valid months", en1545_NUMBER)
658        if 0 < validMonths then
659            if 1 == validMonths then
660                validUntilOption = ValidUntilOption.Date
661                local validUntilSeconds = RavKav_calculateMonthEnd(validFromSeconds, validMonths - 1)   --month end date
662
663        	    local NEW_REF = nodes.append(CONTRACT_REC_REF, {classname="item", label="Valid to", size=14}) --month end date
664                nodes.set_attribute(NEW_REF,"val", bytes.new(8, string.format("%04X", validUntilSeconds)))
665                nodes.set_attribute(NEW_REF,"alt", os.date("%x", validUntilSeconds))
666
667                if contractValid then
668                    contractValid = validUntilSeconds > os.time()
669                end
670            elseif 129 == validMonths then
671                validUntilOption = ValidUntilOption.EndOfService
672	            log.print(log.DBG, string.format("Contract[%d]: Valid until the end of the service", nRec))
673
674                local NEW_REF = nodes.append(CONTRACT_REC_REF, {classname="item", label="Valid to", size=2})
675                nodes.set_attribute(NEW_REF,"val", bytes.new(8, string.format("%01X", validUntilOption)))
676                nodes.set_attribute(NEW_REF,"alt", "The end of the service")
677
678                if contractValid then
679                    contractValid = validFromSeconds > os.time()
680                end
681            else
682                log.print(log.DBG, string.format("Contract[%d]: Valid months unknown value: %d", nRec, validMonths))
683            end
684        else
685            validUntilOption = ValidUntilOption.Cancelled
686            log.print(log.DBG, string.format("Contract[%d]: Validity cancelled", nRec))
687
688            local NEW_REF = nodes.append(CONTRACT_REC_REF, {classname="item", label="Valid to", size=2})
689            nodes.set_attribute(NEW_REF,"val", bytes.new(8, string.format("%01X", validUntilOption)))
690            nodes.set_attribute(NEW_REF,"alt", "Validity cancelled")
691        end
692    end
693
694    if 0 < bit.AND(validityBitmap, 0x20) then
695        bitOffset = RavKav_parseBits(data, bitOffset, 32, CONTRACT_REC_REF, "Validity[5]", en1545_UNDEFINED)
696    end
697
698    if 0 < bit.AND(validityBitmap, 0x40) then
699        bitOffset = RavKav_parseBits(data, bitOffset, 6, CONTRACT_REC_REF, "Validity[6]", en1545_UNDEFINED)
700    end
701
702    if 0 < bit.AND(validityBitmap, 0x80) then
703        bitOffset = RavKav_parseBits(data, bitOffset, 32, CONTRACT_REC_REF, "Validity[7]", en1545_UNDEFINED)
704    end
705
706    if 0 < bit.AND(validityBitmap, 0x100) then
707        bitOffset = RavKav_parseBits(data, bitOffset, 32, CONTRACT_REC_REF, "Validity[8]", en1545_UNDEFINED)
708    end
709
710    -- read validity locations
711    local VALID_LOCS_REF = nodes.append(CONTRACT_REC_REF, {classname="item", label="Validity locations"})
712    local nLoc
713    for nLoc = 1, 7 do  --limit to 7 just in case the end marker is missing?
714        local LOC_REF = nodes.append(VALID_LOCS_REF, {classname="item", label="Validity", id=nLoc})
715
716        local validity, ref, validityType
717        bitOffset, validity, ref, validityType = RavKav_parseBits(data, bitOffset, 4, LOC_REF, "Validity type", RavKav_VALIDITY_TYPE)
718
719        if 15 == validityType then  --end of validity locations
720            log.print(log.DBG, string.format("Contract %d: Validity location[%d]: validityType: 15 (end of validity locations)", nRec, nLoc))
721            nodes.remove(LOC_REF)
722            break
723        end
724
725        if 0 == validityType then
726            bitOffset = RavKav_parseBits(data, bitOffset, 10, LOC_REF, "Route ID",      RavKav_ROUTE, validityType)
727            bitOffset = RavKav_parseBits(data, bitOffset, 12, LOC_REF, "Spatial zones", RavKav_ZONES, validityType)
728        elseif 1 == validityType then
729            bitOffset = RavKav_parseBits(data, bitOffset, 10, LOC_REF, "Route ID",      RavKav_ROUTE, validityType)
730            bitOffset = RavKav_parseBits(data, bitOffset,  8, LOC_REF, "Tariff code",   en1545_NUMBER, validityType)
731        elseif 3 == validityType then
732            bitOffset = RavKav_parseBits(data, bitOffset, 32, LOC_REF, "Validity",      en1545_UNDEFINED, validityType)
733        elseif 7 == validityType then
734            bitOffset = RavKav_parseBits(data, bitOffset, 10, LOC_REF, "Route ID",      RavKav_ROUTE, validityType)
735            bitOffset = RavKav_parseBits(data, bitOffset,  8, LOC_REF, "Tariff code",   en1545_NUMBER, validityType)
736        elseif 8 == validityType then   --unknown validity type except for Israel Rail?
737            bitOffset = RavKav_parseBits(data, bitOffset, 10, LOC_REF, "Route ID",      RavKav_ROUTE, validityType)
738            bitOffset = RavKav_parseBits(data, bitOffset,  8, LOC_REF, "Tariff code",   en1545_NUMBER, validityType)    --?
739            bitOffset = RavKav_parseBits(data, bitOffset, 14, LOC_REF, "Unknown",       en1545_UNDEFINED, validityType)
740        elseif 9 == validityType then
741            bitOffset = RavKav_parseBits(data, bitOffset,  3, LOC_REF, "Extended ticket type (ETT)", en1545_NUMBER, validityType)
742            bitOffset = RavKav_parseBits(data, bitOffset, 11, LOC_REF, "Contract type", RavKav_CONTRACT_TYPE, validityType)
743        elseif 11 == validityType then
744            bitOffset = RavKav_parseBits(data, bitOffset, 21, LOC_REF, "Validity",      en1545_UNDEFINED, validityType)
745        elseif 14 == validityType then
746            bitOffset = RavKav_parseBits(data, bitOffset, 10, LOC_REF, "Route ID",      en1545_NUMBER, validityType)
747            bitOffset = RavKav_parseBits(data, bitOffset, 12, LOC_REF, "Spatial zones", RavKav_ZONES, validityType)
748        else
749            log.print(log.DBG, string.format("Contract %d: Validity location[%d]: unrecognised validityType: %d", nRec, nLoc, validityType))
750            break   --since we don't know the next bit position
751        end
752    end
753
754    RavKav_parseBits(data, 224, 8,CONTRACT_REC_REF, "Contract authenticator", en1545_UNDEFINED) --checksum?
755
756    local cv_ref = nodes.append(CONTRACT_REC_REF, {classname="item", label="Contract valid", size=1})
757    if contractValid then
758        nodes.set_attribute(cv_ref,"val", bytes.new(1, 1))
759        nodes.set_attribute(cv_ref,"alt", "True")
760    else
761        nodes.set_attribute(cv_ref,"val", bytes.new(1, 0))
762        nodes.set_attribute(cv_ref,"alt", "False")
763    end
764end
765
766function RavKav_parseContracts(APP_REF, cntrAry)
767	log.print(log.DBG, "Parsing contracts...")
768    local CONTRACTS_REF = nodes.find_first(APP_REF, {label="Contracts"})
769    if CONTRACTS_REF then
770        local numRecords = RavKav_getRecordInfo(CONTRACTS_REF)
771        local nRec
772        for nRec = 1, numRecords do
773            RavKav_parseContract(CONTRACTS_REF, nRec, cntrAry[nRec])
774        end
775    end
776end
777
778function RavKav_SummariseProfile(PROFS_REF, id, GI_REF)
779    local SRC_REF, NEW_REF = RavKav_copyField(PROFS_REF, GI_REF, "Profile", id, RAVKAV_PROFILES)
780    if NEW_REF then
781        if 0 < RavKav_getFieldAsNumber(SRC_REF, "Valid until") then
782            RavKav_copyField(SRC_REF, NEW_REF, "Valid until")
783        end
784    end
785end
786
787function RavKav_summariseGeneralInfo(APP_REF, SUM_REF)
788	log.print(log.DBG, "Summarising general info...")
789    local ENV_REF = APP_REF:find_first({label="Environment"}):find_first({label="record"})
790    if ENV_REF then
791        local GI_REF = nodes.append(SUM_REF, {classname="file", label="General Information"})
792
793        RavKav_copyField(APP_REF, GI_REF, "Serial number")
794        RavKav_copyField(ENV_REF, GI_REF, "Issuer", nil, RAVKAV_ISSUERS)
795
796        local idNumber = RavKav_getFieldAsNumber(ENV_REF, "ID number")
797        if 0 == idNumber then
798            local REF = nodes.append(GI_REF, {classname="item", label="Identity number", size=0})
799            nodes.set_attribute(REF,"alt", "Anonymous Rav-Kav")
800            return
801        end
802
803        RavKav_copyField(ENV_REF,GI_REF, "ID number")
804        RavKav_copyField(ENV_REF,GI_REF, "Date of birth")
805
806        local PROFS_REF = nodes.find_first(ENV_REF, {label="Profiles"})
807        if PROFS_REF then
808            RavKav_SummariseProfile(PROFS_REF, 1, GI_REF)
809            RavKav_SummariseProfile(PROFS_REF, 2, GI_REF)
810        end
811    end
812end
813
814function RavKav_summariseEvent(EVENTS_REF, nRec, LT_REF)
815    local EVENT_REC_REF = nodes.find_first(EVENTS_REF, {label="record", id=nRec})
816    if nil == EVENT_REC_REF then return end
817
818    local issuerId = RavKav_getFieldAsNumber(EVENT_REC_REF, "Issuer")
819    if 0 == issuerId then return end
820
821    local EVSUM_REF = nodes.append(LT_REF, {classname="record", label="Event", id=nRec})
822
823
824    -- Description ---------------------------
825    local description = ""
826
827    if RAVKAV_ISSUERS[issuerId] then description = RAVKAV_ISSUERS[issuerId] end
828
829    local lineNumber = RavKav_getFieldAsNumber(EVENT_REC_REF, "Line number")
830    if 0 < lineNumber then
831        description = description.." line "
832        local routeSystemId = RavKav_getFieldAsNumber(EVENT_REC_REF, "Route system")
833        local sLineNum = tostring(lineNumber)
834        if RAVKAV_ROUTES[routeSystemId] then
835            if 15 == issuerId then  --Metropolis
836                local sRouteSystemId = tostring(routeSystemId)
837                local posS, posE = string.find(sLineNum, sRouteSystemId)
838                if 1 == posS then                                       --Does "1234567..." start with "12"?
839                    sLineNum = string.sub(sLineNum, posE + 1, posE + 4) --"1234567..." -> "345"
840                end
841            end
842            description = description..sLineNum..", cluster "..RAVKAV_ROUTES[routeSystemId]
843        else
844            description = description..sLineNum
845        end
846    end
847    if 0 < #description then RavKav_addTextNode(EVSUM_REF, "Description", description) end
848    ------------------------------------------
849
850
851    if 0 < RavKav_getFieldAsNumber(EVENT_REC_REF, "Event time") then
852        RavKav_copyField(EVENT_REC_REF, EVSUM_REF, "Event time")
853    end
854
855    if 2 == issuerId then   --Israel Rail
856        local locationId = RavKav_getFieldAsNumber(EVENT_REC_REF, "Location")
857        if RAVKAV_LOCATIONS[locationId] then
858            RavKav_addTextNode(EVSUM_REF, "Station", RAVKAV_LOCATIONS[locationId])
859        end
860    end
861
862
863    -- Details -------------------------------
864    details = ""
865    local eventType = RavKav_getFieldAsNumber(EVENT_REC_REF, "Event type")
866    local contractId = RavKav_getFieldAsNumber(EVENT_REC_REF, "Contract ID")
867    if 6 ~= eventType and 0 < contractId and 9 > contractId then    --not a transit trip
868        local REF, data, debitedText = RavKav_getField(EVENT_REC_REF, "Debit amount")
869        local debitAmount = 0
870        if data then debitAmount = bytes.tonumber(data) end
871        if 0 < debitAmount then
872            if 1 ~= eventType then  --not an exit event
873                details = RAVKAV_EVENT_TYPES[eventType]
874            end
875            details = details.." contract "..tostring(contractId).." charged "..debitedText
876        else
877            details = RAVKAV_EVENT_TYPES[eventType].." contract "..tostring(contractId)
878        end
879
880        local fareCode = RavKav_getFieldAsNumber(EVENT_REC_REF, "Fare code")
881        if 0 < fareCode then
882            details = details.." (code "..tostring(fareCode)..")"
883        end
884    else
885        details = RAVKAV_EVENT_TYPES[eventType]
886    end
887    if 0 < #details then RavKav_addTextNode(EVSUM_REF, "Details", details) end
888    ------------------------------------------
889end
890
891function RavKav_summariseLatestTravel(APP_REF, SUM_REF)
892	log.print(log.DBG, "Summarising latest travel...")
893    local EVENTS_REF = nodes.find_first(APP_REF, {label="Event logs"})
894    if EVENTS_REF then
895        local LT_REF = nodes.append(SUM_REF, {classname="file", label="Latest Travel"})
896        local numRecords = RavKav_getRecordInfo(EVENTS_REF)
897        local nRec
898        for nRec = 1, numRecords do
899            RavKav_summariseEvent(EVENTS_REF, nRec, LT_REF)
900        end
901    end
902end
903
904function RavKav_summariseTextArray(REC_REF, itmLabel, lookupAry, SM_REF, sumLabel)
905    local summary = ""
906    local ary = RavKav_getFieldsAsNumberAry(REC_REF, itmLabel)
907    local i, id
908    for i, id in ipairs(ary) do
909        if lookupAry[id] then
910            if 0 < #summary then summary = summary.."\n  " end
911            summary = summary..lookupAry[id]
912        end
913    end
914    if 0 < #summary then RavKav_addTextNode(SM_REF, sumLabel, summary) end
915end
916
917function RavKav_summariseIntegerArray(REC_REF, itmLabel, SM_REF, sumLabel)
918    local summary = ""
919    local ary = RavKav_getFieldsAsNumberAry(REC_REF, itmLabel)
920    local i, val
921    for i, val in ipairs(ary) do
922        if 0 < val then
923            if 0 < #summary then summary = summary..", " end
924            summary = summary..tostring(val)
925        end
926    end
927    if 0 < #summary then RavKav_addTextNode(SM_REF, sumLabel, summary) end
928end
929
930function RavKav_summariseContract(CONTRACTS_REF, nRec, VC_REF, IC_REF)
931    local CONTRACT_REC_REF = nodes.find_first(CONTRACTS_REF, {label="record", id=nRec})
932    if nil == CONTRACT_REC_REF then return end
933
934    local issuerId = RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Issuer")
935    if 0 == issuerId then return end
936
937    local contractValid = 0 ~= RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Contract valid")
938    local CT_REF = IC_REF
939    if contractValid then CT_REF = VC_REF end
940
941    local CTSUM_REF = nodes.append(CT_REF, {classname="record", label="Contract", id=nRec})
942
943    -- Description ---------------------------
944    local description = ""
945
946    if RAVKAV_ISSUERS[issuerId] then description = RAVKAV_ISSUERS[issuerId] end
947
948    local ticketType = RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Ticket type")
949    if RAVKAV_TICKET_TYPES[ticketType] then description = description.." "..RAVKAV_TICKET_TYPES[ticketType] end
950
951    if 0 < #description then RavKav_addTextNode(CTSUM_REF, "Description", description) end
952    ------------------------------------------
953
954    RavKav_summariseTextArray(CONTRACT_REC_REF, "Route ID", RAVKAV_ROUTES, CTSUM_REF, "Clusters")
955
956    -- Type of contract ----------------------
957    if 0 < ticketType then
958        local REF, data = RavKav_getField(CONTRACT_REC_REF, "Extended ticket type (ETT)")
959        if data then
960            local ett = bytes.tonumber(data)
961            if RAVKAV_CONTRACT_DESCRIPTIONS[ticketType][ett] then
962                RavKav_addTextNode(CTSUM_REF, "Type of contract", RAVKAV_CONTRACT_DESCRIPTIONS[ticketType][ett])
963            end
964        end
965    end
966    ------------------------------------------
967
968    RavKav_summariseIntegerArray(CONTRACT_REC_REF, "Tariff code", CTSUM_REF, "Tariff codes")
969    RavKav_summariseTextArray(CONTRACT_REC_REF, "Contract type", RAVKAV_CONTRACT_TYPES, CTSUM_REF, "Contract types")
970
971    local journeyInterchangesFlag = 0 ~= RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Journey interchanges flag")
972    if journeyInterchangesFlag then RavKav_addTextNode(CTSUM_REF, "Journey interchanges", "Includes switching/resume") end
973
974    if 0 < RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Restriction duration") then
975        RavKav_copyField(CONTRACT_REC_REF, CTSUM_REF, "Restriction duration")
976    end
977
978    RavKav_copyField(CONTRACT_REC_REF, CTSUM_REF, "Balance")
979
980    if 0x3fff ~= RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Valid from") then  --14 bits of valid from date are inverted
981        RavKav_copyField(CONTRACT_REC_REF, CTSUM_REF, "Valid from")
982    elseif 0 < RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Date of purchase") then
983        RavKav_copyField(CONTRACT_REC_REF, CTSUM_REF, "Date of purchase")
984    end
985
986    RavKav_copyField(CONTRACT_REC_REF, CTSUM_REF, "Valid to")
987
988    if 0 < RavKav_getFieldAsNumber(CONTRACT_REC_REF, "Valid until") then
989        RavKav_copyField(CONTRACT_REC_REF, CTSUM_REF, "Valid until")
990    end
991end
992
993function RavKav_summariseContracts(APP_REF, SUM_REF)
994	log.print(log.DBG, "Summarising contracts...")
995    local CONTRACTS_REF = nodes.find_first(APP_REF, {label="Contracts"})
996    if CONTRACTS_REF then
997        local VC_REF = nodes.append(SUM_REF, {classname="file", label="Valid Contracts"})
998        local IC_REF = nodes.append(SUM_REF, {classname="file", label="Invalid Contracts"})
999        local numRecords = RavKav_getRecordInfo(CONTRACTS_REF)
1000        local nRec
1001        for nRec = 1, numRecords do
1002            RavKav_summariseContract(CONTRACTS_REF, nRec, VC_REF, IC_REF)
1003        end
1004    end
1005end
1006
1007function RavKav_readFiles(APP_REF)
1008	local lid_index
1009	local lid_desc
1010	for lid_index, lid_desc in ipairs(LID_LIST) do
1011		RavKav_readFile(APP_REF, lid_desc[1], lid_desc[2], lid_desc[3])
1012	end
1013end
1014
1015function RavKav_parseRecords(APP_REF)
1016    RavKav_parseGeneralInfo(APP_REF)
1017    local cntrAry = RavKav_parseCounters(APP_REF)
1018    RavKav_parseEventsLog(APP_REF)
1019    RavKav_parseContracts(APP_REF, cntrAry)
1020end
1021
1022function RavKav_summariseRecords(APP_REF, root)
1023    local SUM_REF = nodes.append(root, {classname="block", label="Summary"})
1024    RavKav_summariseGeneralInfo(APP_REF, SUM_REF)
1025    RavKav_summariseLatestTravel(APP_REF, SUM_REF)
1026    RavKav_summariseContracts(APP_REF, SUM_REF)
1027end
1028
1029function RavKav_parseAnswerToSelect(ctx)
1030    local ats
1031    local data
1032
1033    for ats in ctx:find({label="answer to select"}) do
1034        data = nodes.get_attribute(ats,"val")
1035        tlv_parse(ats,data,RAVKAV_IDO)
1036    end
1037end
1038
1039
1040RavKav_parseAnswerToSelect(CARD)
1041RavKav_parseRecords(CARD)
1042RavKav_summariseRecords(CARD, CARD)
1043
1044