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