001package jmri.jmrix.can.cbus.swing.bootloader;
002
003import java.io.*;
004import java.util.Optional;
005
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009/**
010 * Class to encapsulate an intel format hex file for a CBUS PIC.
011 *
012 * Assumes hex record addresses are 8-byte aligned and that addresses increase
013 * monotonically.
014 * 
015 * @author Andrew Crosland Copyright (C) 2020, 2022
016 */
017public class HexFile {
018
019    protected String name;
020    protected File file;
021    protected FileInputStream in;
022    protected BufferedInputStream buffIn;
023    // Number of hex records
024    protected static final int MAX_HEX_SIZE = 10000;
025
026    protected int address = 0;
027    protected boolean read;
028    protected int lineNo = 0;
029    private int progStart = 99999999;
030    private int progEnd = 0;
031
032    // Storage for raw program data
033    protected HexRecord [] hexRecords;
034    protected int readIndex = 0;
035    protected int endRecord = 0;
036
037
038    /**
039     * Create a new HexFile object and initialize data to unprogrammed state.
040     *
041     * @param fileName file name to use for the hex file
042     */
043    public HexFile(String fileName) {
044        name = fileName;
045        file = new File(fileName);
046
047        hexRecords = new HexRecord[MAX_HEX_SIZE];
048    }
049
050
051    /**
052     * @return name of the open file
053     */
054    public String getName() {
055        return name;
056    }
057
058
059    /**
060     * Open hex file for reading.
061     *
062     * @throws FileNotFoundException if pre-defined file can't be opened
063     */
064    public void openRd() throws FileNotFoundException {
065        read = true;
066        // Create an input reader based on the file, so we can read its data.
067        in = new FileInputStream(file);
068        buffIn = new BufferedInputStream(in);
069        address = 0;
070    }
071
072
073    /**
074     * Close the currently open file.
075     * <p>
076     * Safe to call if the file was never opened, or has already been closed.
077     * The records already read remain available in memory.
078     */
079    public void close() {
080        try {
081            if (buffIn != null) {
082                buffIn.close();
083            } else if (in != null) {
084                in.close();
085            }
086        } catch (IOException e) {
087            log.warn("Exception closing hex file", e);
088        }
089        buffIn = null;
090        in = null;
091        read = false;
092        name = null;
093    }
094
095
096    /**
097     * DProcess record if required
098     * @param r hex record
099     */
100    protected void checkRecord(HexRecord r) {
101
102    }
103
104    /**
105     * Read one hex record from the file
106     *
107     * @return the hex record
108     * @throws java.io.IOException on read error.
109     */
110    protected HexRecord readOneRecord() throws IOException {
111        HexRecord r;
112        try {
113            r = new HexRecord(this);
114        } catch (IOException e) {
115            log.error("Exception reading hex record", e);
116            throw new IOException(e);
117        }
118
119        return r;
120    }
121
122    /**
123     * Read a hex file.
124     * <p>
125     * Read hex records and store TYPE_DATA records in the array.
126     * <p>
127     * Only data, end of file and extended linear address records are
128     * understood. Any other record type causes the file to be rejected.
129     *
130     * @throws IOException on read error, or if the file contains an
131     *                     unsupported record type, or too many records
132     */
133    public void read() throws IOException {
134        HexRecord r;
135
136        lineNo = 0;
137        endRecord = 0;
138
139        do {
140            if (lineNo >= hexRecords.length) {
141                log.error("Too many records in hex file, maximum is {}", hexRecords.length);
142                throw new IOException("Too many records in hex file, maximum is " + hexRecords.length);
143            }
144            r = readOneRecord();
145            if ((r.type != HexRecord.TYPE_DATA) && (r.type != HexRecord.END) && (r.type != HexRecord.EXT_ADDR)) {
146                // Segment address, start address or unknown record types are not supported
147                log.error("Unsupported hex record type {} at line {}", r.type, lineNo);
148                throw new IOException("Unsupported hex record type " + r.type + " at line " + lineNo);
149            }
150            r.setLineNo(lineNo);
151            hexRecords[lineNo] = r;
152            if (r.type == HexRecord.EXT_ADDR) {
153                // Extended address record so update the record address.
154                // Not kept in the array (lineNo is not incremented) so the
155                // next record overwrites it, only data and end records are stored.
156                address = (r.data[0]& 0xff) * 256 * 65536 + (r.data[1] & 0xff) * 65536;
157//                hexRecords[lineNo].address = address;
158                log.debug("Found extended adress record for address {}", Integer.toHexString(address));
159                continue;
160            }
161            if (r.type == HexRecord.TYPE_DATA) {
162                address = (address & 0xffff0000) + r.getAddress();
163                hexRecords[lineNo].address = address;
164                log.debug("Hex record for address {}", Integer.toHexString(hexRecords[lineNo].address));
165                // Keep track of start and end addresses that have been read
166                if (address < progStart) {
167                    progStart = address;
168                }
169                if ((address + r.len) > progEnd) {
170                    progEnd = address + r.len;
171                }
172            }
173            if (r.type == HexRecord.END) {
174                endRecord = lineNo;
175            }
176            // Process the record now that its full address is known
177            checkRecord(r);
178            lineNo++;
179        } while (r.type != HexRecord.END);
180        log.debug("Done reading hex file");
181    }
182
183
184    /**
185     * Read a character from the hex file
186     * @return the character
187     * @throws IOException from the underlying read operation
188     */
189    public int readChar() throws IOException {
190            return buffIn.read();
191    }
192
193
194    /**
195     * Read a hex byte.
196     *
197     * @return the byte
198     * @throws IOException from the underlying read operation
199     */
200    public int rdHexByte() throws IOException {
201        int hi = rdHexDigit();
202        int lo = rdHexDigit();
203        return hi * 16 + lo;
204    }
205
206
207    /**
208     * Read a single hex digit.
209     *
210     * @return int representing a single hex digit 0 - f.
211     * @throws IOException  from the underlying read operation or if there's an invalid hex digit
212     */
213    private int rdHexDigit() throws IOException {
214        int b = buffIn.read();
215        if ((b >= '0') && (b <= '9')) {
216            b = b - '0';
217        } else if ((b >= 'A') && (b <= 'F')) {
218            b = b - 'A' + 10;
219        } else if ((b >= 'a') && (b <= 'f')) {
220            b = b - 'a' + 10;
221        } else {
222            log.error("Invalid hex digit {}", b);
223            throw new IOException(Bundle.getMessage("HexInvalidDigit"));
224        }
225        return (byte) b;
226    }
227
228
229    /**
230     * Get current address.
231     *
232     * @return int the current address
233     */
234    public int getAddress() {
235        return address;
236    }
237
238
239    /**
240     * Get current file line number
241     *
242     * @return the file number
243     */
244    public int getLineNo() {
245        return lineNo;
246    }
247
248    
249    /**
250     * Return the next TYPE_DATA record from the file
251     * <p>
252     * Once the records read from the file are exhausted, an END record is
253     * returned so that callers stop cleanly.
254     *
255     * @return the next hex record
256     */
257    public HexRecord getNextRecord() {
258        if ((readIndex >= 0) && (readIndex <= endRecord) && (readIndex < hexRecords.length)
259                && (hexRecords[readIndex] != null)) {
260            return hexRecords[readIndex++];
261        }
262        log.warn("No more hex records to read at index {}", readIndex);
263        readIndex = endRecord + 1;
264        return new HexRecord();
265    }
266
267
268    /**
269     * Get the hex record for a given address
270     *
271     * Expected that the address will be the start address of a record but will
272     * return the first record that encompasses the address and increment the
273     * index to point at the next record.
274     * <p>
275     * Only the records actually read from the file are searched.
276     *
277     * @param addr The address
278     * @return the hex record, or empty if no data record contains the address
279     */
280    public Optional<HexRecord> getRecordForAddress(int addr) {
281        readIndex = 0;
282
283        for (int i = 0; (i <= endRecord) && (i < hexRecords.length); i++) {
284            HexRecord r = hexRecords[i];
285            if (r == null) {
286                break;
287            }
288            if ((r.type == HexRecord.TYPE_DATA)
289                    && (addr >= r.address) && (addr < (r.address + r.len))) {
290                readIndex = i + 1;
291                return Optional.of(r);
292            }
293        }
294        // Not found, leave the index pointing at the END record
295        readIndex = endRecord;
296        return Optional.empty();
297    }
298    
299    
300    /**
301     * Get the file parameters
302     * 
303     * Create an invalid parameter set of necessary. Override in hardware specific
304     * implementations.
305     * 
306     * @return CBUS parameters from the file
307     */
308    public CbusParameters getParams() {
309        return new CbusParameters();
310    }
311    
312
313    /**
314     * Return the lowest address read from the hex file
315     *
316     * @return the highest address
317     */
318    public int getProgStart() {
319        return progStart;
320    }
321
322
323    /**
324     * Return the highest address read from the hex file
325     *
326     * @return the highest address
327     */
328    public int getProgEnd() {
329        return progEnd;
330    }
331
332
333    /**
334     * close the open file
335     */
336    public void dispose() {
337        close();
338    }
339
340
341    private static final Logger log = LoggerFactory.getLogger(HexFile.class);
342
343}