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