001package jmri.jmrit;
002
003import java.io.*;
004import java.util.ArrayList;
005
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import jmri.util.StringUtil;
010
011/**
012 * Models (and provides utility functions for) board memory as expressed in .hex
013 * files and .DMF files.
014 * <p>
015 * Provides mechanisms to read and interpret firmware update files into an
016 * internal data structure. Provides mechanisms to in create firmware update
017 * files from an internal data structure. Provides mechanisms to allow other
018 * agents to access the data in the internal data structures for the purpose of
019 * sending the data to the device to be updated. Supports the Intel "I8HEX" file
020 * format and a derivative ".dmf" file format created by Digitrax.
021 * <p>
022 * Support for the Intel "I8HEX" format includes support for record types "00"
023 * and "01". The "I8HEX" format implements records with a LOAD OFFSET field of
024 * 16 bits. To support the full 24-bit addressing range provided by the LocoNet
025 * messaging protocol for firmware updates, this class is able to interpret
026 * record type "04" (Extended Linear Address) records for input files with
027 * 16-bit LOAD OFFSET fields. Record type "04" are typically found in the Intel
028 * "I32HEX" 32-bit addressing format. Because the class supports only 24 bits of
029 * address, interpretation of the "04" record type requires that the upper 8
030 * bits of the 16-bit data field be 0.
031 * <p>
032 * Support for some .hex files emitted by some tool-sets requires support for
033 * the Extended Segment Address record type (record type "02"), which may be
034 * used in I16HEX format files. This version of the {@link #readHex} method
035 * supports the Extended Segment Address record type ONLY when the segment
036 * specified in the data field is 0x0000.
037 * <p>
038 * Support for the Digitrax ".DMF" format is an extension to the "I8HEX"
039 * support. This extension supports interpretation of the 24-bit LOAD OFFSET
040 * fields used in .DFM files. The class does not allow files with 24-bit LOAD
041 * OFFSET fields to use the "04" (Extended Linear Address) record type unless
042 * its data field is 0x0000.
043 * <p>
044 * Support for the ".DMF" format allows capture of Key/Value pairs which may be
045 * embedded in special comments within a .DMF file. This support is enabled for
046 * I8HEX files.
047 * <p>
048 * The class treats the information within a file's records as having
049 * "big-endian" address values in the record LOAD OFFSET field. The INFO or DATA
050 * field information is interpreted as 8-bit values, with the left-most value in
051 * the INFO or DATA field corresponding to the address specified by the record's
052 * LOAD OFFSET field plus the influence of the most recent previous Extended
053 * Linear Address record, if any.
054 * <p>
055 * The INFO or DATA field for Extended Linear Address records is interpreted as
056 * a big-endian value, where bits 7 thru 0 of the data field value are used as
057 * bits 23 thru 16 of the effective address, while bits 15 thru 0 of the
058 * effective address are from the 16-bit LOAD OFFSET of each data record. Bits
059 * 15 thru 8 of the Extended Linear Address record INFO or DATA field must be 0
060 * because of the 24-bit address limitation of this implementation.
061 * <p>
062 * The class does not have to know anything about filenames or filename
063 * extensions. Instead, to read a file, an instantiating method will create a
064 * {@link File} object and pass that object to {@link #readHex}.
065 * Similarly, when writing the contents of data storage to a file, the
066 * instantiating method will create a {@link File} and an associated
067 * {@link Writer} and pass the {@link Writer} object to
068 * {@link #writeHex}. The mechanisms implemented within this class do not
069 * know about or care about the filename or its extension and do not use that
070 * information as part of its file interpretation or file creation.
071 * <p>
072 * The class is implemented with a maximum of 24 bits of address space, with up
073 * to 256 pages of up to 65536 bytes per page. A "sparse" implementation of
074 * memory is modeled, where only occupied pages are allocated within the Java
075 * system's memory.
076 * <hr>
077 * The Intel "Hexadecimal Object File Format File Format Specification"
078 * uses the following terms for the fields of the record:
079 * <dl>
080 * <dt>RECORD MARK</dt><dd>first character of a record.  ':'</dd>
081 * 
082 * <dt>RECLEN</dt><dd>a two-character specifier of the number of bytes of information 
083 *          in the "INFO or DATA" field.  Immediately follows the RECORD 
084 *          MARK charcter. Since each byte within the "INFO or DATA" field is 
085 *          represented by two ASCII characters, the data field contains twice
086 *          the RECLEN value number of ASCII characters.</dd>
087 * 
088 * <dt>LOAD OFFSET</dt><dd>specifies the 16-bit starting load offset of the data bytes.
089 *          This applies only to "Data" records, so this class requires that
090 *          this field must encode 0x0000 for all other record types.  The LOAD
091 *          OFFSET field immediately follows the RECLEN field.
092 * <p>
093 *          Note that for the 24-bit addressing format used with ".DMF" 
094 *          files, this field is a 24-bit starting load offset, represented by
095 *          six ASCII characters, rather than the four ASCII characters 
096 *          specified in the Intel specification.</dd>
097 * 
098 * <dt>RECTYP</dt><dd>RECord TYPe - indicates the record type for this record.  The 
099 *          RECTYPE field immediately follows the LOAD OFFSET field.</dd>
100 * 
101 * <dt>INFO or DATA</dt><dd>(Optional) field containing information or data which is 
102 *          appropriate to the RECTYP.  Immediately follows the RECTYP field.
103 *          contains RECLEN times 2 characters, where consecutive pairs of
104 *          characters represent one byte of info or data.</dd>
105 * 
106 * <dt>CHKSUM</dt><dd>8-bit Checksum, computed using the hexadecimal byte values represented 
107 *          by the character pairs in RECLEN, LOAD OFFSET, RECTYP, and INFO 
108 *          or DATA fields, such that the computed sum, when added to the 
109 *          CKSUM value, sums to an 8-bit value of 0x00.</dd>
110 * </dl>
111 * This information based on the Intel document "Hexadecimal Object File Format
112 * Specification", Revision A, January 6, 1988.
113 * <p>
114 * Mnemonically, a properly formatted record would appear as:
115 * <pre>
116 *     :lloooott{dd}cc
117 * where:
118 *      ':'     is the RECORD MARK
119 *      "ll"    is the RECLEN
120 *      "oooo"  is the 16-bit LOAD OFFSET
121 *      "tt"    is the RECTYP
122 *      "{dd}"  is the INFO or DATA field, containing zero or more pairs of 
123 *                  characters of Info or Data associated with the record
124 *      "cc"    is the CHKSUM
125 * </pre>
126 * <p>
127 * and a few examples of complaint records would be:
128 * <ul>
129 *     <li>:02041000FADE07
130 *     <li>:020000024010AC
131 *     <li>:00000001FF
132 * </ul>
133 *
134 * @author Bob Jacobsen Copyright (C) 2005, 2008
135 * @author B. Milhaupt Copyright (C) 2014, 2017
136 */
137public class MemoryContents {
138
139    // Class (static) variables
140
141    /* For convenience, a page of local storage of data is sized to equal one 
142     * "segment" within an input file.  As such, the terms "page" and "segment" 
143     * are used interchangeably throughout here.
144     * 
145     * The number of pages is chosen to match the 24-bit address space.
146     */
147    private static final int DEFAULT_MEM_VALUE = -1;
148    private static final int PAGESIZE = 0x10000;
149    private static final int PAGES = 256;
150
151    private static final int RECTYP_DATA_RECORD = 0;
152    private static final String STRING_DATA_RECTYP = StringUtil.twoHexFromInt(RECTYP_DATA_RECORD);
153    private static final int RECTYP_EXTENDED_SEGMENT_ADDRESS_RECORD = 2;
154    private static final int RECTYP_EXTENDED_LINEAR_ADDRESS_RECORD = 4;
155    private static final int RECTYP_EOF_RECORD = 1;
156    private static final int CHARS_IN_RECORD_MARK = 1;
157    private static final int CHARS_IN_RECORD_LENGTH = 2;
158    private static final int CHARS_IN_RECORD_TYPE = 2;
159    private static final int CHARS_IN_EACH_DATA_BYTE = 2;
160    private static final int CHARS_IN_CHECKSUM = 2;
161    private static final int CHARS_IN_24_BIT_ADDRESS = 6;
162    private static final int CHARS_IN_16_BIT_ADDRESS = 4;
163
164    private static final char LEADING_CHAR_COMMENT = '#'; // NOI18N
165    private static final char LEADING_CHAR_KEY_VALUE = '!'; // NOI18N
166    private static final char LEADING_CHAR_RECORD_MARK = ':'; // NOI18N
167
168    // Instance variables
169    /**
170     * Firmware data storage
171     *
172     * Implemented as a two-dimensional array where the first dimension
173     * represents the "page" number, and the second dimension represents the
174     * byte within the page of {@link #PAGESIZE} bytes.
175     */
176    private final int[][] pageArray;
177    private int currentPage;
178    private int lineNum;
179    private boolean hasData;
180    private int curExtLinAddr;
181    private int curExtSegAddr;
182
183    /**
184     * Storage for Key/Value comment information extracted from key/value
185     * comments within a .DMF or .hex file
186     */
187    private ArrayList<String> keyValComments = new ArrayList<String>(1);
188
189    /**
190     * Defines the LOAD OFFSET field type used/expected for records in "I8HEX"
191     * and ".DMF" file formats.
192     * <p>
193     * When reading a file using the {@link #readHex} method, the value is
194     * inferred from the first record and then used to validate the remaining
195     * records in the file.
196     * <p>
197     * This value must be properly set before invoking the {@link #writeHex}
198     * method.
199     */
200    private LoadOffsetFieldType loadOffsetFieldType = LoadOffsetFieldType.UNDEFINED;
201
202    /**
203     */
204    public MemoryContents() {
205        pageArray = new int[PAGES][];
206        currentPage = -1;
207        hasData = false;
208        curExtLinAddr = 0;
209        curExtSegAddr = 0;
210        keyValComments = new ArrayList<String>(1);
211    }
212
213    private boolean isPageInitialized(int page) {
214        return (pageArray[page] != null);
215    }
216
217    /**
218     * Initialize a single page of data storage, if and only if the page has not
219     * been initialized already.
220     *
221     */
222    private void initPage(int page) {
223        if (pageArray[page] != null) {
224            if (log.isDebugEnabled()) {
225                log.debug("Method initPage was previously invoked for page {}", page);
226            }
227            return;
228        }
229
230        int[] largeArray = new int[PAGESIZE];
231        for (int i = 0; i < PAGESIZE; i++) {
232            largeArray[i] = DEFAULT_MEM_VALUE;  // default contents
233        }
234        pageArray[page] = largeArray;
235    }
236
237    /**
238     * Perform a read of a .hex file information into JAVA memory. Assumes that
239     * the file is of the Intel "I8HEX" format or the similar Digitrax ".DMF"
240     * format. Automatically infers the file type. Performs various checks upon
241     * the incoming data to help ensure proper interpretation of the file and to
242     * help detect corrupted files. Extracts "key/value" pair information from
243     * comments for use by the invoking method.
244     * <p>
245     * Integrity checks include:
246     * <ul>
247     * <li>Identification of LOAD OFFSET field type from first record
248     * <li>Verification that all subsequent records use the same LOAD OFFSET
249     * field type
250     * <li>Verification of checksum found at the end of each record
251     * <li>Verification of supported record types
252     * <li>Flagging of lines which are neither comment lines or records
253     * <li>Identification of a missing EOF record
254     * <li>Identification of any record after an EOF record
255     * <li>Identification of a file without any data record
256     * <li>Identification of any records which have extra characters after the
257     * checksum
258     * </ul>
259     * <p>
260     * When reading the file, {@link #readHex} infers the addressing format
261     * from the first record found in the file, and future records are
262     * interpreted using that addressing format. It is not necessary to
263     * pre-configure the addressing format before reading the file. This is a
264     * departure from previous versions of this method.
265     * <p>
266     * Blank lines are allowed and are ignored.
267     * <p>
268     * This code supports reading of files containing comments. Comment lines
269     * which begin with '#' are ignored.
270     * <p>
271     * Comment lines which * begin with '!' may encode Key/Value pair
272     * information. Such Key/Value pair information is used within the .DMF
273     * format to provide configuration information for firmware update
274     * mechanism. This class also extracts key/value pair comments "I8HEX"
275     * format files. After successful completion of the {@link #readHex} call,
276     * then the {@link #extractValueOfKey(String keyName)} method may be used to inspect individual key values.
277     * <p>
278     * Key/Value pair definition comment lines are of the format:
279     * <p>
280     * {@code ! KeyName: Value}
281     *
282     * @param filename string containing complete filename with path
283     * @throws FileNotFoundException               if the file does not exist
284     * @throws MemoryFileRecordLengthException     if a record line is too long
285     *                                             or short
286     * @throws MemoryFileChecksumException         if a record checksum does not
287     *                                             match the computed record
288     *                                             checksum
289     * @throws MemoryFileUnknownRecordType         if a record contains an
290     *                                             unsupported record type
291     * @throws MemoryFileRecordContentException    if a record contains
292     *                                             inappropriate characters
293     * @throws MemoryFileNoEOFRecordException      if a file does not contain an
294     *                                             EOF record
295     * @throws MemoryFileNoDataRecordsException    if a file does not contain
296     *                                             any data records
297     * @throws MemoryFileRecordFoundAfterEOFRecord if a file contains records
298     *                                             after the EOF record
299     * @throws MemoryFileAddressingRangeException  if a file contains an
300     *                                             Extended Linear Address
301     *                                             record outside of the
302     *                                             supported address range
303     * @throws IOException                         if a file cannot be opened
304     *                                             via newBufferedReader
305     */
306    public void readHex(String filename) throws FileNotFoundException,
307            MemoryFileRecordLengthException, MemoryFileChecksumException,
308            MemoryFileUnknownRecordType, MemoryFileRecordContentException,
309            MemoryFileNoDataRecordsException, MemoryFileNoEOFRecordException,
310            MemoryFileRecordFoundAfterEOFRecord, MemoryFileAddressingRangeException,
311            IOException {
312        readHex(new File(filename));
313    }
314
315    /**
316     * Perform a read of a .hex file information into JAVA memory. Assumes that
317     * the file is of the Intel "I8HEX" format or the similar Digitrax ".DMF"
318     * format. Automatically infers the file type. Performs various checks upon
319     * the incoming data to help ensure proper interpretation of the file and to
320     * help detect corrupted files. Extracts "key/value" pair information from
321     * comments for use by the invoking method.
322     * <p>
323     * Integrity checks include:
324     * <ul>
325     * <li>Identification of LOAD OFFSET field type from first record
326     * <li>Verification that all subsequent records use the same LOAD OFFSET
327     * field type
328     * <li>Verification of checksum found at the end of each record
329     * <li>Verification of supported record types
330     * <li>Flagging of lines which are neither comment lines or records
331     * <li>Identification of a missing EOF record
332     * <li>Identification of any record after an EOF record
333     * <li>Identification of a file without any data record
334     * <li>Identification of any records which have extra characters after the
335     * checksum
336     * </ul><p>
337     * When reading the file, {@link #readHex} infers the addressing format
338     * from the first record found in the file, and future records are
339     * interpreted using that addressing format. It is not necessary to
340     * pre-configure the addressing format before reading the file. This is a
341     * departure from previous versions of this method.
342     * <p>
343     * Blank lines are allowed and are ignored.
344     * <p>
345     * This code supports reading of files containing comments. Comment lines
346     * which begin with '#' are ignored.
347     * <p>
348     * Comment lines which * begin with '!' may encode Key/Value pair
349     * information. Such Key/Value pair information is used within the .DMF
350     * format to provide configuration information for firmware update
351     * mechanism. This class also extracts key/value pair comments "I8HEX"
352     * format files. After successful completion of this method,
353     * then the {@code #extractValueOfKey(String keyName)} method may be used to inspect individual key values.
354     * <p>
355     * Key/Value pair definition comment lines are of the format:
356     * <p>
357     * {@code ! KeyName: Value}
358     *
359     * @param file file to read
360     * @throws FileNotFoundException               if the file does not exist
361     * @throws MemoryFileRecordLengthException     if a record line is too long
362     *                                             or short
363     * @throws MemoryFileChecksumException         if a record checksum does not
364     *                                             match the computed record
365     *                                             checksum
366     * @throws MemoryFileUnknownRecordType         if a record contains an
367     *                                             unsupported record type
368     * @throws MemoryFileRecordContentException    if a record contains
369     *                                             inappropriate characters
370     * @throws MemoryFileNoEOFRecordException      if a file does not contain an
371     *                                             EOF record
372     * @throws MemoryFileNoDataRecordsException    if a file does not contain
373     *                                             any data records
374     * @throws MemoryFileRecordFoundAfterEOFRecord if a file contains records
375     *                                             after the EOF record
376     * @throws MemoryFileAddressingRangeException  if a file contains an
377     *                                             Extended Linear Address
378     *                                             record outside of the
379     *                                             supported address range
380     * @throws IOException                         if a file cannot be opened
381     *                                             via newBufferedReader
382     */
383    public void readHex(File file) throws FileNotFoundException,
384            MemoryFileRecordLengthException, MemoryFileChecksumException,
385            MemoryFileUnknownRecordType, MemoryFileRecordContentException,
386            MemoryFileNoDataRecordsException, MemoryFileNoEOFRecordException,
387            MemoryFileRecordFoundAfterEOFRecord, MemoryFileAddressingRangeException,
388            IOException {
389        BufferedReader fileStream;
390        try {
391            fileStream = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
392        } catch (IOException ex) {
393            throw new FileNotFoundException(ex.toString());
394        }
395        
396        this.clear();   // Ensure that the information storage is clear of any 
397                        // previous contents
398        currentPage = 0;
399        loadOffsetFieldType = LoadOffsetFieldType.UNDEFINED;
400        boolean foundDataRecords = false;
401        boolean foundEOFRecord = false;
402
403        keyValComments.clear();  // ensure that no key/value pair values are retained 
404        //from a previous invocation.
405
406        lineNum = 0;
407        // begin reading the file
408        try {
409            //byte bval;
410            int ival;
411            String line;
412            while ((line = fileStream.readLine()) != null) {
413                // this loop reads one line per turn
414                lineNum++;
415
416                // decode line type
417                int len = line.length();
418                if (len < 1) {
419                    continue; // skip empty lines
420                }
421                if (line.charAt(0) == LEADING_CHAR_COMMENT) {
422                    // human comment.  Ignore it.
423                } else if (line.charAt(0) == LEADING_CHAR_KEY_VALUE) {
424                    // machine comment; store it to allow for key/value extraction
425                    keyValComments.add(line);
426                } else if (line.charAt(0) == LEADING_CHAR_RECORD_MARK) {
427                    // hex file record - determine LOAD OFFSET field type (if not yet 
428                    // then interpret the record based on its RECTYP
429
430                    int indexOfLastAddressCharacter;
431                    if (loadOffsetFieldType == LoadOffsetFieldType.UNDEFINED) {
432                        // Infer the file's LOAD OFFSET field type from the first record.
433                        // It is sufficient to infer the LOAD OFFSET field type once, then 
434                        // interpret all future records as the same type without 
435                        // checking the type again, because the checksum verfication
436                        // uses the LOAD OFFSET field type as part of the 
437                        // checksum verification.
438
439                        loadOffsetFieldType = inferRecordAddressType(line);
440
441                        if ((isLoadOffsetType16Bits())
442                                && (isLoadOffsetType24Bits())) {
443                            // could not infer a valid addressing type.
444                            String message = "Could not infer addressing type from" // NOI18N
445                                    + " line " + lineNum + "."; // NOI18N
446                            logError(message);
447                            throw new MemoryFileRecordContentException(message);
448                        }
449                    }
450
451                    // Determine the index of the last character of the line which 
452                    // contains LOAD OFFSET field info
453                    indexOfLastAddressCharacter = charsInAddress() + 2;
454                    if (indexOfLastAddressCharacter < 0) {
455                        // unknown LOAD OFFSET field type - cannot continue.
456                        String message = "Fell thru with unknown loadOffsetFieldType value " // NOI18N
457                                + loadOffsetFieldType + " for line" + lineNum + "."; // NOI18N
458                        logError(message);
459                        throw new MemoryFileAddressingRangeException(message);
460                    }
461
462                    // extract the RECTYP.
463                    int recordType = Integer.valueOf(line.substring(indexOfLastAddressCharacter + 1,
464                            indexOfLastAddressCharacter + 3), 16).intValue();
465                    if (log.isDebugEnabled()) {
466                        log.debug("RECTYP = 0x{}", Integer.toHexString(recordType));
467                    }
468
469                    // verify record character count
470                    int count = extractRecLen(line);
471                    if (len != CHARS_IN_RECORD_MARK + CHARS_IN_RECORD_LENGTH
472                            + charsInAddress()
473                            + CHARS_IN_RECORD_TYPE
474                            + (count * CHARS_IN_EACH_DATA_BYTE) + CHARS_IN_CHECKSUM) {
475                        // line length error - invalid record or invalid data 
476                        // length byte or incorrect LOAD OFFSET field type
477                        String message
478                                = "Data record line length is incorrect for " // NOI18N
479                                + "inferred addressing type and for data " // NOI18N
480                                + "count field in line " + lineNum;// NOI18N
481                        logError(message);
482                        throw new MemoryFileRecordLengthException(message);
483                    }
484
485                    // verify the checksum now that we know the RECTYP.
486                    // Do this by calculating the checksum of all characters on 
487                    //line (except the ':' record mark), which should result in 
488                    // a computed checksum value of 0
489                    int computedChecksum = calculate8BitChecksum(line.substring(CHARS_IN_RECORD_MARK));
490                    if (computedChecksum != 0x00) {
491                        // line's checksum is incorrect.  Find checksum of 
492                        // all but the checksum bytes
493                        computedChecksum = calculate8BitChecksum(
494                                line.substring(
495                                        CHARS_IN_RECORD_MARK,
496                                        line.length()
497                                        - CHARS_IN_RECORD_MARK
498                                        - CHARS_IN_CHECKSUM + 1)
499                        );
500                        int expectedChecksum = Integer.parseInt(line.substring(line.length() - 2), 16);
501                        String message = "Record checksum error in line " // NOI18N
502                                + lineNum
503                                + " - computed checksum = 0x" // NOI18N
504                                + Integer.toHexString(computedChecksum)
505                                + ", expected checksum = 0x" // NOI18N
506                                + Integer.toHexString(expectedChecksum)
507                                + "."; // NOI18N
508                        logError(message);
509                        throw new MemoryFileChecksumException(message);
510                    }
511
512                    if (recordType == RECTYP_DATA_RECORD) {
513                        // Record Type 0x00
514                        if (foundEOFRecord) {
515                            // problem - data record happened after an EOF record was parsed
516                            String message = "Found a Data record in line " // NOI18N
517                                    + lineNum + " after the EOF record"; // NOI18N
518                            logError(message);
519                            throw new MemoryFileRecordFoundAfterEOFRecord(message);
520                        }
521
522                        int recordAddress = extractLoadOffset(line);
523
524                        recordAddress &= (isLoadOffsetType24Bits())
525                                ? 0x00FFFFFF : 0x0000FFFF;
526
527                        // compute effective address (assumes cannot have 
528                        // non-zero values in both curExtLinAddr and 
529                        // curExtSegAddr)
530                        int effectiveAddress = recordAddress + curExtLinAddr + curExtSegAddr;
531
532                        if (addressAndCountIsOk(effectiveAddress, count) == false) {
533                            // data crosses memory boundary that can be mis-interpreted.
534                            // So refuse the file.
535                            String message = "Data crosses boundary which could lead to " // NOI18N
536                                    + " mis-interpretation.  Aborting read at line " // NOI18N
537                                    + line;
538                            logError(message);
539                            throw new MemoryFileAddressingRangeException(message);
540                        }
541
542                        int effectivePage = effectiveAddress / PAGESIZE;
543                        if (!isPageInitialized(effectivePage)) {
544                            initPage(effectivePage);
545                            log.debug("effective address 0x{} is causing change to segment 0x{}", // NOI18N
546                                    Integer.toHexString(effectiveAddress),
547                                    Integer.toHexString(effectivePage));
548                        }
549                        int effectiveOffset = effectiveAddress % PAGESIZE;
550
551                        log.debug("Effective address 0x{}, effective page 0x{}, effective offset 0x{}",
552                                Integer.toHexString(effectiveAddress),
553                                Integer.toHexString(effectivePage),
554                                Integer.toHexString(effectiveOffset));
555                        for (int i = 0; i < count; ++i) {
556                            int startIndex = indexOfLastAddressCharacter + 3 + (i * 2);
557                            // parse as hex into integer, then convert to byte
558                            ival = Integer.valueOf(line.substring(startIndex, startIndex + 2), 16).intValue();
559                            pageArray[effectivePage][effectiveOffset++] = ival;
560                            hasData = true;
561                        }
562                        foundDataRecords = true;
563
564                    } else if (recordType == RECTYP_EXTENDED_SEGMENT_ADDRESS_RECORD) {
565                        // parse Extended Segment Address record to check for
566                        // validity
567                        if (foundEOFRecord) {
568                            String message
569                                    = "Found a Extended Segment Address record in line " // NOI18N
570                                    + lineNum
571                                    + " after the EOF record"; // NOI18N
572                            logError(message);
573                            throw new MemoryFileRecordFoundAfterEOFRecord(message);
574                        }
575
576                        int datacount = extractRecLen(line);
577                        if (datacount != 2) {
578                            String message = "Extended Segment Address record " // NOI18N
579                                    + "did not have 16 bits of data content." // NOI18N
580                                    + lineNum;
581                            logError(message);
582                            throw new MemoryFileRecordContentException(message);
583                        }
584                        int startpoint = indexOfLastAddressCharacter + 3;
585                        // compute page number from '20-bit segment address' in record
586                        int newPage = 16 * Integer.valueOf(line.substring(startpoint,
587                                (startpoint + 2 * datacount)), 16).intValue();
588
589                        // check for an allowed segment value
590                        if (newPage != 0) {
591                            String message = "Unsupported Extended Segment Address " // NOI18N
592                                    + "Record data value 0x" // NOI18N
593                                    + Integer.toHexString(newPage)
594                                    + " in line " + lineNum; // NOI18N
595                            logError(message);
596                            throw new MemoryFileAddressingRangeException(message);
597                        }
598                        curExtLinAddr = 0;
599                        curExtSegAddr = newPage;
600                        if (newPage != currentPage) {
601                            currentPage = newPage;
602                            initPage(currentPage);
603                        }
604
605                    } else if (recordType == RECTYP_EXTENDED_LINEAR_ADDRESS_RECORD) {
606                        // Record Type 0x04
607                        if (foundEOFRecord) {
608                            String message
609                                    = "Found a Extended Linear Address record in line " // NOI18N
610                                    + lineNum
611                                    + " after the EOF record"; // NOI18N
612                            logError(message);
613                            throw new MemoryFileRecordFoundAfterEOFRecord(message);
614                        }
615
616                        // validate that LOAD OFFSET field of record is all zeros.
617                        if (extractLoadOffset(line) != 0) {
618                            String message = "Extended Linear Address record has " // NOI18N
619                                    + "non-zero LOAD OFFSET field." // NOI18N
620                                    + lineNum;
621                            logError(message);
622                            throw new MemoryFileRecordContentException(message);
623                        }
624
625                        // Allow non-zero Extended Linear Address value ONLY if 16-bit addressing!
626                        int datacount = extractRecLen(line);
627                        if (datacount != 2) {
628                            String message = "Expect data payload length of 2, " // NOI18N
629                                    + "found RECLEN value of " + // NOI18N
630                                    +extractRecLen(line)
631                                    + " in line " + lineNum; // NOI18N
632                            logError(message);
633                            throw new MemoryFileRecordContentException(message);
634                        }
635                        int startpoint = indexOfLastAddressCharacter + 3;
636                        int tempPage = Integer.valueOf(line.substring(startpoint,
637                                (startpoint + 2 * datacount)), 16).intValue();
638
639                        if ((tempPage != 0) && (isLoadOffsetType24Bits())) {
640                            // disallow non-zero extended linear address if 24-bit addressing
641                            String message = "Extended Linear Address record with non-zero" // NOI18N
642                                    + "data field in line " // NOI18N
643                                    + lineNum
644                                    + " is not allowed in files using " // NOI18N
645                                    + "24-bit LOAD OFFSET field.";  // NOI18N
646                            logError(message); // NOI18N
647                            throw new MemoryFileRecordContentException(message);
648                        } else if (tempPage < PAGES) {
649                            curExtLinAddr = tempPage * 65536;
650                            curExtSegAddr = 0;
651                            currentPage = tempPage;
652                            initPage(currentPage);
653                            if (log.isDebugEnabled()) {
654                                log.debug("New page 0x{}", Integer.toHexString(currentPage)); // NOI18N
655                            } // NOI18N
656                        } else {
657                            String message = "Page number 0x" // NOI18N
658                                    + Integer.toHexString(tempPage)
659                                    + " specified in line number " // NOI18N
660                                    + lineNum
661                                    + " is beyond the supported 24-bit address range."; // NOI18N;
662                            logError(message);
663                            throw new MemoryFileAddressingRangeException(message);
664                        }
665
666                    } else if (recordType == RECTYP_EOF_RECORD) {
667                        if ((extractRecLen(line) != 0)
668                                || (extractLoadOffset(line) != 0)) {
669                            String message = "Illegal EOF record form in line " // NOI18N
670                                    + lineNum;
671                            logError(message);
672                            throw new MemoryFileRecordContentException(message);
673                        }
674
675                        foundEOFRecord = true;
676                        continue; // not record we need to handle
677                    } else {
678                        String message = "Unknown RECTYP 0x" // NOI18N
679                                + Integer.toHexString(recordType)
680                                + " was found in line " // NOI18N
681                                + lineNum + ".  Aborting file read."; // NOI18N
682                        logError(message);
683                        throw new MemoryFileUnknownRecordType(message);
684                    }
685                    // end parsing hex file record
686                } else {
687                    String message = "Unknown line type in line " + lineNum + "."; // NOI18N
688                    logError(message);
689                    throw new MemoryFileUnknownRecordType(message);
690                }
691            }
692        } catch (IOException e) {
693            log.error("Exception reading file", e);
694        } // NOI18N
695        finally {
696            try {
697                fileStream.close();
698            } catch (IOException e2) {
699                log.error("Exception closing file", e2);
700            } // NOI18N
701        }
702        if (!foundDataRecords) {
703            String message = "No Data Records found in file - aborting."; // NOI18N
704            logError(message);
705            throw new MemoryFileNoDataRecordsException(message);
706        } else if (!foundEOFRecord) {  // found Data Records, but no EOF
707            String message = "No EOF Record found in file - aborting."; // NOI18N
708            logError(message);
709            throw new MemoryFileNoEOFRecordException(message);
710        }
711    }
712
713    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
714        justification="pass Error String directly.")
715    private void logError(String errorToLog) {
716        log.error(errorToLog);
717    }
718    
719    /**
720     * Sends a character stream of an image of a programmatic representation of
721     * memory in the Intel "I8HEX" file format to a Writer.
722     * <p>
723     * Number of bytes of data per data record is fixed at 16. Does not write
724     * any comment information to the file.
725     * <p>
726     * This method generates only RECTYPs "00" and "01", and does not generate
727     * any comment lines in its output.
728     *
729     * @param w Writer to which the character stream is sent
730     * @throws IOException                         upon file access problem
731     * @throws MemoryFileAddressingFormatException if unsupported addressing
732     *                                             format
733     */
734    public void writeHex(Writer w) throws IOException, MemoryFileAddressingFormatException {
735        writeHex(w, 16);
736    }
737
738    /**
739     * Sends a character stream of key/value pairs (if requested) and an image
740     * of a programmatic representation of memory in either the Intel "I8HEX" or
741     * Digitrax ".DMF" file format to a Writer.
742     * <p>
743     * When selected for writing, the key/value pairs are provided at the
744     * beginning of the character stream. Note that comments of the key/value
745     * format implemented here is not in compliance with the "I8HEX" format.
746     * <p>
747     * The "I8HEX" format is used when the {@link #loadOffsetFieldType} is
748     * configured for 16-bit addresses in the record LOAD OFFSET field. The
749     * ".DMF" format is used when the {@link #loadOffsetFieldType} is
750     * configured for 24-bit addresses in the record LOAD OFFSET field.
751     * <p>
752     * The method generates only RECTYPs "00" and "01", and does not generate
753     * any comment lines in its output.
754     *
755     * @param writer       Writer to which the character stream is sent
756     * @param writeKeyVals determines whether key/value pairs (if any) are
757     *                     written at the beginning of the stream
758     * @param blockSize    is the maximum number of bytes defined in a data
759     *                     record
760     * @throws IOException                         upon file access problem
761     * @throws MemoryFileAddressingFormatException if unsupported addressing
762     *                                             format
763     */
764    @SuppressWarnings("javadoc")
765    public void writeHex(Writer writer, boolean writeKeyVals, int blockSize)
766            throws IOException, MemoryFileAddressingFormatException {
767        if (writeKeyVals) {
768            writeComments(writer);
769        }
770        writeHex(writer, blockSize);
771    }
772
773    /**
774     * Sends a character stream of an image of a programmatic representation of
775     * memory in either the Intel "I8HEX" or Digitrax ".DMF" file format to a
776     * Writer.
777     * <p>
778     * The "I8HEX" format is used when the{@link #loadOffsetFieldType} is
779     * configured for 16-bit addresses in the record LOAD OFFSET field. The
780     * ".DMF" format is used when the {@link #loadOffsetFieldType} is
781     * configured for 24-bit addresses in the record LOAD OFFSET field.
782     * <p>
783     * The method generates only RECTYPs "00" and "01", and does not generate
784     * any comment lines in its output.
785     *
786     * @param writer    Writer to which the character stream is sent
787     * @param blockSize is the maximum number of bytes defined in a data record
788     * @throws IOException                         upon file access problem
789     * @throws MemoryFileAddressingFormatException if unsupported addressing
790     *                                             format
791     */
792    private void writeHex(Writer writer, int blockSize)
793            throws IOException, MemoryFileAddressingFormatException {
794        int blocksize = blockSize; // number of bytes per record in .hex file
795        // validate Address format selection
796        if ((!isLoadOffsetType16Bits())
797                && (!isLoadOffsetType24Bits())) {
798            String message = "Invalid loadOffsetFieldType at writeHex invocation"; // NOI18N
799            log.error(message);
800            throw new MemoryFileAddressingFormatException(message);
801        }
802
803        for (int segment = 0; segment < PAGES; ++segment) {
804            if (pageArray[segment] != null) {
805                if ((segment != 0) && (isLoadOffsetType16Bits())) {
806                    // write an extended linear address record for 16-bit LOAD OFFSET field size files only
807                    StringBuffer output = new StringBuffer(":0200000400"); // NOI18N
808                    output.append(StringUtil.twoHexFromInt(segment));
809
810                    int checksum = calculate8BitChecksum(output.substring(CHARS_IN_RECORD_MARK));
811                    output.append(StringUtil.twoHexFromInt(checksum));
812                    output.append("\n"); // NOI18N
813
814                    writer.write(output.toString());
815                }
816                for (int i = 0; i < pageArray[segment].length - blocksize + 1; i += blocksize) {
817                    if (log.isDebugEnabled()) {
818                        log.debug("write at 0x{}", Integer.toHexString(i)); // NOI18N
819                    }
820                    // see if need to write the current block
821                    boolean write = false;
822                    int startOffset = -1;
823
824                    // Avoid producing a record which spans the natural alignment of
825                    // addresses with respect to blocksize.  In other words, do not produce
826                    // a data record that spans both sides of an Address which is a natural
827                    // mulitple of blocksize.
828                    for (int j = i; j < (i + blocksize) - ((i + blocksize) % blocksize); j++) {
829                        if (pageArray[segment][j] >= 0) {
830                            write = true;
831                            if (startOffset < 0) {
832                                startOffset = j;
833                                if (log.isDebugEnabled()) {
834                                    log.debug("startOffset = 0x{}", Integer.toHexString(startOffset)); // NOI18N
835                                }
836                            }
837                        }
838                        if (((write == true) && (j == i + (blocksize - 1)))
839                                || ((write == true) && (pageArray[segment][j] < 0))) {
840                            // got to end of block size, or got a gap in the data
841                            // need to write out at least a partial block of data
842                            int addressForAddressField = startOffset;
843                            if (isLoadOffsetType24Bits()) {
844                                addressForAddressField += segment * PAGESIZE;
845                            }
846                            int addrMostSByte = (addressForAddressField) / 65536;
847                            int addrMidSByte = ((addressForAddressField) - (65536 * addrMostSByte)) / 256;
848                            int addrLeastSByte = (addressForAddressField) - (256 * addrMidSByte) - (65536 * addrMostSByte);
849                            int count = j - startOffset;
850                            if ( j == i + (blocksize - 1) ) {
851                                count++;
852                            }
853                            if (log.isDebugEnabled()) {
854                                log.debug("Writing Address {} ({}bit Address) count {}", startOffset, isLoadOffsetType24Bits() ? "24" : "16", count);
855                            }
856
857                            StringBuffer output = new StringBuffer(":"); // NOI18N
858                            output.append(StringUtil.twoHexFromInt(count));
859                            if (isLoadOffsetType24Bits()) {
860                                output.append(StringUtil.twoHexFromInt(addrMostSByte));
861                            }
862                            output.append(StringUtil.twoHexFromInt(addrMidSByte));
863                            output.append(StringUtil.twoHexFromInt(addrLeastSByte));
864                            output.append(STRING_DATA_RECTYP);
865
866                            for (int k = 0; k < count; ++k) {
867                                int val = pageArray[segment][startOffset + k];
868                                output.append(StringUtil.twoHexFromInt(val));
869                            }
870                            int checksum = calculate8BitChecksum(output.substring(CHARS_IN_RECORD_MARK));
871                            output.append(StringUtil.twoHexFromInt(checksum));
872                            output.append("\n"); // NOI18N
873                            writer.write(output.toString());
874                            write = false;
875                            startOffset = -1;
876                        }
877                    }
878                    if (!write) {
879                        continue; // no, we don't
880                    }
881                }
882            }
883        }
884        // write last line & close
885        writer.write((isLoadOffsetType24Bits()) ? ":0000000001FF\n" : ":00000001FF\n"); // NOI18N
886        writer.flush();
887    }
888
889    /**
890     * Return the address of the next location containing data, including the
891     * location in the argument
892     *
893     * @param location indicates the address from which the next location is
894     *                 determined
895     * @return the next location
896     */
897    public int nextContent(int location) {
898        currentPage = location / PAGESIZE;
899        int offset = location % PAGESIZE;
900        for (; currentPage < PAGES; currentPage++) {
901            if (pageArray[currentPage] != null) {
902                for (; offset < pageArray[currentPage].length; offset++) {
903                    if (pageArray[currentPage][offset] != DEFAULT_MEM_VALUE) {
904                        return offset + currentPage * PAGESIZE;
905                    }
906                }
907            }
908            offset = 0;
909        }
910        return -1;
911    }
912
913    /**
914     * Modifies the programmatic representation of memory to reflect a specified
915     * value.
916     *
917     * @param location location within programmatic representation of memory to
918     *                 modify
919     * @param value    value to be placed at location within programmatic
920     *                 representation of memory
921     */
922    public void setLocation(int location, int value) {
923        currentPage = location / PAGESIZE;
924
925        pageArray[currentPage][location % PAGESIZE] = value;
926        hasData = true;
927    }
928
929    /**
930     * Queries the programmatic representation of memory to determine if
931     * location is represented.
932     *
933     * @param location location within programmatic representation of memory to
934     *                 inspect
935     * @return true if location exists within programmatic representation of
936     *         memory
937     */
938    public boolean locationInUse(int location) {
939        currentPage = location / PAGESIZE;
940        if (pageArray[currentPage] == null) {
941            return false;
942        }
943        try {
944            return pageArray[currentPage][location % PAGESIZE] != DEFAULT_MEM_VALUE;
945        } catch (Exception e) {
946            log.error("error in locationInUse {} {}", currentPage, location, e); // NOI18N
947            return false;
948        }
949    }
950
951    /**
952     * Returns the value from the programmatic representation of memory for the
953     * specified location. Returns -1 if the specified location is not currently
954     * represented in the programmatic representation of memory.
955     *
956     * @param location location within programmatic representation of memory to
957     *                 report
958     * @return value found at the specified location.
959     */
960    public int getLocation(int location) {
961        currentPage = location / PAGESIZE;
962        if (pageArray[currentPage] == null) {
963            log.error("Error in getLocation(0x{}): accessed uninitialized page {}", Integer.toHexString(location), currentPage);
964            return DEFAULT_MEM_VALUE;
965        }
966        try {
967            return pageArray[currentPage][location % PAGESIZE];
968        } catch (Exception e) {
969            log.error("Error in getLocation(0x{}); computed (current page 0x{}): exception ", Integer.toHexString(location), Integer.toHexString(currentPage), e); // NOI18N
970            return 0;
971        }
972    }
973
974    /**
975     * Reports whether the object has not been initialized with any data.
976     *
977     * @return false if object contains data, true if no data stored in object.
978     */
979    public boolean isEmpty() {
980        return !hasData;
981    }
982
983    /**
984     * Infers addressing type from contents of string containing a record.
985     * <p>
986     * Returns ADDRESSFIELDSIZEUNKNOWN if
987     * <ul>
988     * <li>the recordString does not begin with ':'
989     * <li>the length of recordString is not appropriate to define an integral
990     * number of bytes
991     * <li>the recordString checksum does not match a checksum computed for the
992     * recordString
993     * <li>if the record type extracted after inferring the addressing type is
994     * an unsupported record type
995     * <li>if the length of recordString did not match the length expected for
996     * the inferred addressing type.
997     * <ul>
998     *
999     * @param recordString the ASCII record, including the leading ':'
1000     * @return the inferred addressing type, or ADDRESSFIELDSIZEUNKNOWN if the
1001     *         addressing type cannot be inferred
1002     */
1003    private LoadOffsetFieldType inferRecordAddressType(String recordString) {
1004        if (recordString.charAt(0) != LEADING_CHAR_RECORD_MARK) {
1005            log.error("Cannot infer record addressing type because line {} is not a record.", lineNum); // NOI18N
1006            return LoadOffsetFieldType.ADDRESSFIELDSIZEUNKNOWN;
1007        }
1008        String r = recordString.substring(CHARS_IN_RECORD_MARK);  // create a string without the leading ':'
1009        int len = r.length();
1010        if (((len + 1) / 2) != (len / 2)) {
1011            // Not an even number of characters in the line (after removing the ':'
1012            // character), so must be a bad record.
1013            log.error("Cannot infer record addressing type because line {} does not have the correct number of characters.", lineNum); // NOI18N
1014            return LoadOffsetFieldType.ADDRESSFIELDSIZEUNKNOWN;
1015        }
1016
1017        int datalen = Integer.parseInt(r.substring(0, 2), 16);
1018        int checksumInRecord = Integer.parseInt(r.substring(len - 2, len), 16);
1019
1020        // Compute the checksum of the record
1021        int calculatedChecksum = calculate8BitChecksum(recordString.substring(CHARS_IN_RECORD_MARK,
1022                recordString.length() - CHARS_IN_CHECKSUM));
1023
1024        // Return if record checksum value does not match calculated checksum
1025        if (calculatedChecksum != checksumInRecord) {
1026            log.error("Cannot infer record addressing type because line {} does not have the correct checksum (expect 0x{}, found CHKSUM = 0x{})", lineNum, Integer.toHexString(calculatedChecksum), Integer.toHexString(checksumInRecord)); // NOI18N
1027            return LoadOffsetFieldType.ADDRESSFIELDSIZEUNKNOWN;
1028        }
1029
1030        // Checksum is ok, so can check length of line versus address size.
1031        // Compute expected line lengths based on possible address sizes
1032        int computedLenIf16Bit = 2 + 4 + 2 + (datalen * 2) + 2;
1033        int computedLenIf24Bit = computedLenIf16Bit + 2;
1034
1035        // Determine if record line length matches any of the expected line lengths
1036        if (computedLenIf16Bit == len) {
1037            //inferred 16-bit addressing based on length.  Check the record type.
1038            if (isSupportedRecordType(Integer.parseInt(r.substring(6, 8), 16))) {
1039                return LoadOffsetFieldType.ADDRESSFIELDSIZE16BITS;
1040            } else {
1041                log.error("Cannot infer record addressing type in line {} because record type is an unsupported record type.", lineNum); // NOI18N
1042                return LoadOffsetFieldType.ADDRESSFIELDSIZEUNKNOWN;
1043            }
1044        }
1045
1046        if (computedLenIf24Bit == len) {
1047            //inferred 24-bit addressing based on length.  Check the record type.
1048            if (isSupportedRecordType(Integer.parseInt(r.substring(8, 10), 16))) {
1049                return LoadOffsetFieldType.ADDRESSFIELDSIZE24BITS;
1050            } else {
1051                log.error("Cannot infer record addressing type in line {} because record type is an unsupported record type.", lineNum); // NOI18N
1052                return LoadOffsetFieldType.ADDRESSFIELDSIZEUNKNOWN;
1053            }
1054        }
1055
1056        // Record length did not match a calculated line length for any supported
1057        // addressing type.  Report unknown record addressing type.
1058        return LoadOffsetFieldType.ADDRESSFIELDSIZEUNKNOWN;
1059    }
1060
1061    /**
1062     * Calculates an 8-bit checksum value from a string which uses sequential
1063     * pairs of ASCII characters to encode the hexadecimal values of a sequence
1064     * of bytes.
1065     * <p>
1066     * When used to calculate the checksum of a record in I8HEX or similar
1067     * format, the infoString parameter is expected to include only those
1068     * characters which are used for calculation of the checksum. The "record
1069     * mark" at the beginning of a record should not be included in the
1070     * infoString. Similarly, the checksum at the end of a record should
1071     * generally not be included in the infoString.
1072     * <p>
1073     * An example infoString value might be: 020000040010
1074     * <p>
1075     * In case of an invalid infoString, the returned checksum is -1.
1076     * <p>
1077     * If using this method to verify the checksum of a record, the infoString
1078     * should include the record Checksum characters. Then the invoking method
1079     * may check for a non-zero return value to indicate a checksum error.
1080     *
1081     * @param infoString a string of characters for which the checksum is
1082     *                   calculated
1083     * @return the calculated 8-bit checksum, or -1 if not a valid infoString
1084     */
1085    private int calculate8BitChecksum(String infoString) {
1086        // check length of record content for an even number of characters
1087        int len = infoString.length();
1088        if (((len + 1) / 2) != (len / 2)) {
1089            return -1;
1090        }
1091
1092        // Compute the checksum of the record, omitting the last two characters.
1093        int calculatedChecksum = 0;
1094        for (int i = 0; i < len; i += 2) {
1095            calculatedChecksum += Integer.parseInt(infoString.substring(i, i + 2), 16);
1096        }
1097        // Safely remove extraneous bits from the calculated checksum to create an 
1098        // 8-bit result.
1099        return (0xFF & (0x100 - (calculatedChecksum & 0xFF)));
1100    }
1101
1102    /**
1103     * Determines if a given amount of data will pass a segment boundary when
1104     * added to the memory image beginning at a given address.
1105     *
1106     * @param addr  address for begin of a sequence of bytes
1107     * @param count number of bytes
1108     * @return true if string of bytes will not cross into another page, else
1109     *         false.
1110     */
1111    private boolean addressAndCountIsOk(int addr, int count) {
1112        int beginPage = addr / PAGESIZE;
1113        int endPage = ((addr + count - 1) / PAGESIZE);
1114        log.debug("Effective Record Addr = 0x{} count = {} BeginPage = {} endpage = {}", Integer.toHexString(addr), count, beginPage, endPage); // NOI18N
1115        return (beginPage == endPage);
1116    }
1117
1118    /**
1119     * Finds the Value for a specified Key if that Key is found in the list of
1120     * Key/Value pair comment lines. The list of Key/Value pair comment lines is
1121     * created while the input file is processed.
1122     * <p>
1123     * Key/value pair information is extractable only from comments of the form:
1124     * <p>
1125     * {@code ! Key/Value}
1126     *
1127     * @param keyName Key/value comment line, including the leading "! "
1128     * @return String containing Key name
1129     */
1130    public String extractValueOfKey(String keyName) {
1131        for (int i = 0; i < keyValComments.size(); i++) {
1132            String t = keyValComments.get(i);
1133            String targetedKey = "! " + keyName + ": "; // NOI18N
1134            if (t.startsWith(targetedKey)) {
1135                int f = t.indexOf(": "); // NOI18N
1136                String value = t.substring(f + 2, t.length());
1137                if (log.isDebugEnabled()) {
1138                    log.debug("Key {} was found in firmware image with value '{}'", keyName, value); // NOI18N
1139                }
1140                return value;
1141            }
1142        }
1143        if (log.isDebugEnabled()) {
1144            log.debug("Key {} is not defined in firmware image", keyName); // NOI18N
1145        }
1146        return null;
1147
1148    }
1149
1150    /**
1151     * Finds the index of the specified key within the array containing
1152     * key/value comments
1153     *
1154     * @param keyName Key to search for in the internal storage
1155     * @return index in the arraylist for the specified key, or -1 if the key is
1156     *         not found in the list
1157     */
1158    private int findKeyCommentIndex(String keyName) {
1159        for (int i = 0; i < keyValComments.size(); i++) {
1160            String t = keyValComments.get(i);
1161            String targetedKey = "! " + keyName + ": "; // NOI18N
1162            if (t.startsWith(targetedKey)) {
1163                return i;
1164            }
1165        }
1166        if (log.isDebugEnabled()) {
1167            log.debug("Did not find key {}", keyName); // NOI18N
1168        }
1169        return -1;
1170    }
1171
1172    /**
1173     * Updates the internal key/value storage to reflect the parameters. If the
1174     * key already exists, its value is updated based on the parameter. If the
1175     * key does not exist, a new key/value pair comment is added to the
1176     * key/value storage list.
1177     *
1178     * @param keyName key to use
1179     * @param value   value to store
1180     */
1181    public void addKeyValueComment(String keyName, String value) {
1182        int keyIndex;
1183        if ((keyIndex = findKeyCommentIndex(keyName)) < 0) {
1184            // key does not already exist.  Can simply add the key/value comment
1185            keyValComments.add("! " + keyName + ": " + value + "\n"); // NOI18N
1186            return;
1187        }
1188        log.warn("Key {} already exists in key/value set.  Overriding previous value!", keyName); // NOI18N
1189        keyValComments.set(keyIndex, "! " + keyName + ": " + value + "\n"); // NOI18N
1190    }
1191
1192    public enum LoadOffsetFieldType {
1193
1194        UNDEFINED,
1195        ADDRESSFIELDSIZE16BITS,
1196        ADDRESSFIELDSIZE24BITS,
1197        ADDRESSFIELDSIZEUNKNOWN
1198    }
1199
1200    /**
1201     * Configures the Addressing format used in the LOAD OFFSET field when
1202     * writing to a .hex file using the {@link #writeHex} method.
1203     * <p>
1204     * Note that the {@link #readHex} method infers the addressing format
1205     * from the first record in the file and updates the stored address format
1206     * based on the format found in the file.
1207     *
1208     * @param addressingType addressing type to use
1209     */
1210    public void setAddressFormat(LoadOffsetFieldType addressingType) {
1211        loadOffsetFieldType = addressingType;
1212    }
1213
1214    /**
1215     * Returns the current addressing format setting. The current setting is
1216     * established by the last occurrence of the {@link #setAddressFormat}
1217     * method or {@link #readHex} method invocation.
1218     *
1219     * @return the current Addressing format setting
1220     */
1221    public LoadOffsetFieldType getCurrentAddressFormat() {
1222        return loadOffsetFieldType;
1223    }
1224
1225    /**
1226     * Writes key/data pair information to an output file
1227     * <p>
1228     * Since the key/value metadata is typically presented at the beginning of a
1229     * firmware file, the method would typically be invoked before invocation of
1230     * the writeHex method.
1231     * @param writer Writer to which the character stream is sent
1232     * @throws IOException if problems writing data to file
1233     */
1234    public void writeComments(Writer writer) throws IOException {
1235        for (String s : keyValComments) {
1236            writer.write(s);
1237        }
1238    }
1239
1240    private boolean isLoadOffsetType24Bits() {
1241        return loadOffsetFieldType == LoadOffsetFieldType.ADDRESSFIELDSIZE24BITS;
1242    }
1243
1244    private boolean isLoadOffsetType16Bits() {
1245        return loadOffsetFieldType == LoadOffsetFieldType.ADDRESSFIELDSIZE16BITS;
1246    }
1247
1248    private boolean isSupportedRecordType(int recordType) {
1249        switch (recordType) {
1250            case RECTYP_DATA_RECORD:
1251            case RECTYP_EXTENDED_LINEAR_ADDRESS_RECORD:
1252            case RECTYP_EOF_RECORD:
1253            case RECTYP_EXTENDED_SEGMENT_ADDRESS_RECORD:
1254                return true;
1255            default:
1256                return false;
1257        }
1258    }
1259
1260    private int extractRecLen(String line) {
1261        return Integer.valueOf(line.substring(CHARS_IN_RECORD_MARK,
1262                CHARS_IN_RECORD_MARK + CHARS_IN_RECORD_LENGTH), 16).intValue();
1263    }
1264
1265    private int charsInAddress() {
1266        if (isLoadOffsetType24Bits()) {
1267            return CHARS_IN_24_BIT_ADDRESS;
1268        } else if (isLoadOffsetType16Bits()) {
1269            return CHARS_IN_16_BIT_ADDRESS;
1270        } else {
1271            return -999;
1272        }
1273    }
1274
1275    private int extractLoadOffset(String line) {
1276        return Integer.parseInt(
1277                line.substring(CHARS_IN_RECORD_MARK + CHARS_IN_RECORD_LENGTH,
1278                        CHARS_IN_RECORD_MARK + CHARS_IN_RECORD_LENGTH + charsInAddress()), 16);
1279    }
1280    
1281    /**
1282     * Generalized class from which detailed exceptions are derived.
1283     */
1284    public class MemoryFileException extends jmri.JmriException {
1285
1286        public MemoryFileException() {
1287            super();
1288        }
1289
1290        public MemoryFileException(String s) {
1291            super(s);
1292        }
1293    }
1294
1295    /**
1296     * An exception for a record which has incorrect checksum.
1297     */
1298    public class MemoryFileChecksumException extends MemoryFileException {
1299
1300        public MemoryFileChecksumException() {
1301            super();
1302        }
1303
1304        public MemoryFileChecksumException(String s) {
1305            super(s);
1306        }
1307    }
1308
1309    /**
1310     * An exception for a record containing a record type which is not
1311     * supported.
1312     */
1313    public class MemoryFileUnknownRecordType extends MemoryFileException {
1314
1315        public MemoryFileUnknownRecordType() {
1316            super();
1317        }
1318
1319        public MemoryFileUnknownRecordType(String s) {
1320            super(s);
1321        }
1322    }
1323
1324    /**
1325     * An exception for a record which has content which cannot be parsed.
1326     * <p>
1327     * Possible examples may include records which include characters other than
1328     * ASCII characters associated with hexadecimal digits and the initial ':'
1329     * character, trailing spaces, etc.
1330     */
1331    public class MemoryFileRecordContentException extends MemoryFileException {
1332
1333        public MemoryFileRecordContentException() {
1334            super();
1335        }
1336
1337        public MemoryFileRecordContentException(String s) {
1338            super(s);
1339        }
1340    }
1341
1342    /**
1343     * An exception for a data record where there are too many or too few
1344     * characters versus the number of characters expected based on the record
1345     * type field, LOAD OFFSET field size, and data count field.
1346     */
1347    public class MemoryFileRecordLengthException extends MemoryFileException {
1348
1349        public MemoryFileRecordLengthException() {
1350            super();
1351        }
1352
1353        public MemoryFileRecordLengthException(String s) {
1354            super(s);
1355        }
1356    }
1357
1358    /**
1359     * An exception for an unsupported addressing format
1360     */
1361    public class MemoryFileAddressingFormatException extends MemoryFileException {
1362
1363        public MemoryFileAddressingFormatException() {
1364            super();
1365        }
1366
1367        public MemoryFileAddressingFormatException(String s) {
1368            super(s);
1369        }
1370    }
1371
1372    /**
1373     * An exception for an address outside of the supported range
1374     */
1375    public class MemoryFileAddressingRangeException extends MemoryFileException {
1376
1377        public MemoryFileAddressingRangeException() {
1378            super();
1379        }
1380
1381        public MemoryFileAddressingRangeException(String s) {
1382            super(s);
1383        }
1384    }
1385
1386    /**
1387     * An exception for a file with no data records
1388     */
1389    public class MemoryFileNoDataRecordsException extends MemoryFileException {
1390
1391        public MemoryFileNoDataRecordsException() {
1392            super();
1393        }
1394
1395        public MemoryFileNoDataRecordsException(String s) {
1396            super(s);
1397        }
1398    }
1399
1400    /**
1401     * An exception for a file without an end-of-file record
1402     */
1403    public class MemoryFileNoEOFRecordException extends MemoryFileException {
1404
1405        public MemoryFileNoEOFRecordException() {
1406            super();
1407        }
1408
1409        public MemoryFileNoEOFRecordException(String s) {
1410            super(s);
1411        }
1412    }
1413
1414    /**
1415     * An exception for a file containing at least one record after the EOF
1416     * record
1417     */
1418    public class MemoryFileRecordFoundAfterEOFRecord extends MemoryFileException {
1419
1420        public MemoryFileRecordFoundAfterEOFRecord() {
1421            super();
1422        }
1423
1424        public MemoryFileRecordFoundAfterEOFRecord(String s) {
1425            super(s);
1426        }
1427    }
1428
1429    /**
1430     * Summarize contents
1431     */
1432    @Override
1433    public String toString() {
1434        StringBuffer retval = new StringBuffer("Pages occupied: "); // NOI18N
1435        for (int page=0; page<PAGES; page++) {
1436            if (isPageInitialized(page)) {
1437                retval.append(page);
1438                retval.append(" ");
1439            }
1440        }
1441        return new String(retval);
1442    }
1443
1444    /**
1445     * Clear out an imported Firmware File.
1446     * 
1447     * This may be used, when the instantiating object has evaluated the contents of 
1448     * a firmware file and found it to be inappropriate for updating to a device, 
1449     * to clear out the firmware image so that there is no chance that it can be
1450     * updated to the device.
1451     * 
1452     */
1453    public void clear() {
1454        log.info("Clearing a MemoryContents object by program request.");
1455        currentPage = -1;
1456        hasData = false;
1457        curExtLinAddr = 0;
1458        curExtSegAddr = 0;
1459        keyValComments = new ArrayList<String>(1);
1460        for (int i = 0 ; i < pageArray.length; ++i) {
1461            pageArray[i] = null;
1462        }
1463        
1464    }
1465
1466    private final static Logger log = LoggerFactory.getLogger(MemoryContents.class);
1467}