blob: 62bd9bf72e59983ab0a8c070d935ea1eed834865 [file] [log] [blame]
/*************************************************************************************************************************************************
* Copyright (c) 2015, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
************************************************************************************************************************************************/
package no.nordicsemi.android.dfu.internal;
import com.google.gson.Gson;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import no.nordicsemi.android.dfu.DfuBaseService;
import no.nordicsemi.android.dfu.internal.manifest.FileInfo;
import no.nordicsemi.android.dfu.internal.manifest.Manifest;
import no.nordicsemi.android.dfu.internal.manifest.ManifestFile;
import no.nordicsemi.android.dfu.internal.manifest.SoftDeviceBootloaderFileInfo;
/**
* <p>Reads the firmware files from the a ZIP file. The ZIP file must be either created using the <b>nrf utility</b> tool, available together with Master Control Panel v3.8.0+,
* or follow the backward compatibility syntax: must contain only files with names: application.hex/bin, softdevice.hex/dat or bootloader.hex/bin, optionally also application.dat
* and/or system.dat with init packets.</p>
* <p>The ArchiveInputStream will read only files with types specified by <b>types</b> parameter of the constructor.</p>
*/
public class ArchiveInputStream extends ZipInputStream {
/** The name of the manifest file is fixed. */
private static final String MANIFEST = "manifest.json";
// Those file names are for backwards compatibility mode
private static final String SOFTDEVICE_HEX = "softdevice.hex";
private static final String SOFTDEVICE_BIN = "softdevice.bin";
private static final String BOOTLOADER_HEX = "bootloader.hex";
private static final String BOOTLOADER_BIN = "bootloader.bin";
private static final String APPLICATION_HEX = "application.hex";
private static final String APPLICATION_BIN = "application.bin";
private static final String SYSTEM_INIT = "system.dat";
private static final String APPLICATION_INIT = "application.dat";
/** Contains bytes arrays with BIN files. HEX files are converted to BIN before being added to this map. */
private Map<String, byte[]> entries;
private Manifest manifest;
private byte[] applicationBytes;
private byte[] softDeviceBytes;
private byte[] bootloaderBytes;
private byte[] softDeviceAndBootloaderBytes;
private byte[] systemInitBytes;
private byte[] applicationInitBytes;
private byte[] currentSource;
private int bytesReadFromCurrentSource;
private int softDeviceSize;
private int bootloaderSize;
private int applicationSize;
private int bytesRead;
/**
* <p>
* The ArchiveInputStream read HEX or BIN files from the Zip stream. It may skip some of them, depending on the value of the types parameter.
* This is useful if the DFU service wants to send the Soft Device and Bootloader only, and then the Application in the following connection, despite
* the ZIP file contains all 3 HEX/BIN files.
* When types is equal to {@link DfuBaseService#TYPE_AUTO} all present files are read.
* </p>
* <p>
* Use bit combination of the following types:
* <ul>
* <li>{@link DfuBaseService#TYPE_SOFT_DEVICE}</li>
* <li>{@link DfuBaseService#TYPE_BOOTLOADER}</li>
* <li>{@link DfuBaseService#TYPE_APPLICATION}</li>
* <li>{@link DfuBaseService#TYPE_AUTO}</li>
* </ul>
* </p>
*
* @param stream
* the Zip Input Stream
* @param mbrSize
* The size of the MRB segment (Master Boot Record) on the device. The parser will cut data from addresses below that number from all HEX files.
* @param types
* File types that are to be read from the ZIP. Use {@link DfuBaseService#TYPE_APPLICATION} etc.
* @throws java.io.IOException
*/
public ArchiveInputStream(final InputStream stream, final int mbrSize, final int types) throws IOException {
super(stream);
this.entries = new HashMap<>();
this.bytesRead = 0;
this.bytesReadFromCurrentSource = 0;
try {
/*
* This method reads all entries from the ZIP file and puts them to entries map.
* The 'manifest.json' file, if exists, is converted to the manifestData String.
*/
parseZip(mbrSize);
/*
* Let's read and parse the 'manifest.json' file.
*/
if (manifest != null) {
boolean valid = false;
// Read the application
if (manifest.getApplicationInfo() != null && (types == DfuBaseService.TYPE_AUTO || (types & DfuBaseService.TYPE_APPLICATION) > 0)) {
final FileInfo application = manifest.getApplicationInfo();
applicationBytes = entries.get(application.getBinFileName());
applicationInitBytes = entries.get(application.getDatFileName());
if (applicationBytes == null)
throw new IOException("Application file " + application.getBinFileName() + " not found.");
applicationSize = applicationBytes.length;
currentSource = applicationBytes;
valid = true;
}
// Read the Bootloader
if (manifest.getBootloaderInfo() != null && (types == DfuBaseService.TYPE_AUTO || (types & DfuBaseService.TYPE_BOOTLOADER) > 0)) {
if (systemInitBytes != null)
throw new IOException("Manifest: softdevice and bootloader specified. Use softdevice_bootloader instead.");
final FileInfo bootloader = manifest.getBootloaderInfo();
bootloaderBytes = entries.get(bootloader.getBinFileName());
systemInitBytes = entries.get(bootloader.getDatFileName());
if (bootloaderBytes == null)
throw new IOException("Bootloader file " + bootloader.getBinFileName() + " not found.");
bootloaderSize = bootloaderBytes.length;
currentSource = bootloaderBytes;
valid = true;
}
// Read the Soft Device
if (manifest.getSoftdeviceInfo() != null && (types == DfuBaseService.TYPE_AUTO || (types & DfuBaseService.TYPE_SOFT_DEVICE) > 0)) {
final FileInfo softdevice = manifest.getSoftdeviceInfo();
softDeviceBytes = entries.get(softdevice.getBinFileName());
systemInitBytes = entries.get(softdevice.getDatFileName());
if (softDeviceBytes == null)
throw new IOException("SoftDevice file " + softdevice.getBinFileName() + " not found.");
softDeviceSize = softDeviceBytes.length;
currentSource = softDeviceBytes;
valid = true;
}
// Read the combined Soft Device and Bootloader
if (manifest.getSoftdeviceBootloaderInfo() != null && (types == DfuBaseService.TYPE_AUTO ||
((types & DfuBaseService.TYPE_SOFT_DEVICE) > 0) && (types & DfuBaseService.TYPE_BOOTLOADER) > 0)) {
if (systemInitBytes != null)
throw new IOException("Manifest: The softdevice_bootloader may not be used together with softdevice or bootloader.");
final SoftDeviceBootloaderFileInfo system = manifest.getSoftdeviceBootloaderInfo();
softDeviceAndBootloaderBytes = entries.get(system.getBinFileName());
systemInitBytes = entries.get(system.getDatFileName());
if (softDeviceAndBootloaderBytes == null)
throw new IOException("File " + system.getBinFileName() + " not found.");
softDeviceSize = system.getSoftdeviceSize();
bootloaderSize = system.getBootloaderSize();
currentSource = softDeviceAndBootloaderBytes;
valid = true;
}
if (!valid) {
throw new IOException("Manifest file must specify at least one file.");
}
} else {
/*
* Compatibility mode. The 'manifest.json' file does not exist.
*
* In that case the ZIP file must contain one or more of the following files:
*
* - application.hex/dat
* + application.dat
* - softdevice.hex/dat
* - bootloader.hex/dat
* + system.dat
*/
boolean valid = false;
// Search for the application
if (types == DfuBaseService.TYPE_AUTO || (types & DfuBaseService.TYPE_APPLICATION) > 0) {
applicationBytes = entries.get(APPLICATION_HEX); // the entry bytes has already been converted to BIN, just the name remained.
if (applicationBytes == null)
applicationBytes = entries.get(APPLICATION_BIN);
if (applicationBytes != null) {
applicationSize = applicationBytes.length;
applicationInitBytes = entries.get(APPLICATION_INIT);
currentSource = applicationBytes;
valid = true;
}
}
// Search for theBootloader
if (types == DfuBaseService.TYPE_AUTO || (types & DfuBaseService.TYPE_BOOTLOADER) > 0) {
bootloaderBytes = entries.get(BOOTLOADER_HEX); // the entry bytes has already been converted to BIN, just the name remained.
if (bootloaderBytes == null)
bootloaderBytes = entries.get(BOOTLOADER_BIN);
if (bootloaderBytes != null) {
bootloaderSize = bootloaderBytes.length;
systemInitBytes = entries.get(SYSTEM_INIT);
currentSource = bootloaderBytes;
valid = true;
}
}
// Search for the Soft Device
if (types == DfuBaseService.TYPE_AUTO || (types & DfuBaseService.TYPE_SOFT_DEVICE) > 0) {
softDeviceBytes = entries.get(SOFTDEVICE_HEX); // the entry bytes has already been converted to BIN, just the name remained.
if (softDeviceBytes == null)
softDeviceBytes = entries.get(SOFTDEVICE_BIN);
if (softDeviceBytes != null) {
softDeviceSize = softDeviceBytes.length;
systemInitBytes = entries.get(SYSTEM_INIT);
currentSource = softDeviceBytes;
valid = true;
}
}
if (!valid) {
throw new IOException("The ZIP file must contain an Application, a Soft Device and/or a Bootloader.");
}
}
} finally {
super.close();
}
}
/**
* Reads all files into byte arrays.
* Here we don't know whether the ZIP file is valid.
*
* The ZIP file is valid when contains a 'manifest.json' file and all BIN and DAT files that are specified in the manifest.
*
* For backwards compatibility ArchiveInputStream supports also ZIP archives without 'manifest.json' file
* but than it MUST include at least one of the following files: softdevice.bin/hex, bootloader.bin/hex, application.bin/hex.
* To support the init packet such ZIP file should contain also application.dat and/or system.dat (with the CRC16 of a SD, BL or SD+BL together).
*/
private void parseZip(int mbrSize) throws IOException {
final byte[] buffer = new byte[1024];
String manifestData = null;
ZipEntry ze;
while ((ze = getNextEntry()) != null) {
final String filename = ze.getName();
// Read file content to byte array
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
int count;
while ((count = super.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
byte[] source = baos.toByteArray();
// In case of HEX file convert it to BIN
if (filename.toLowerCase(Locale.US).endsWith("hex")) {
final HexInputStream is = new HexInputStream(source, mbrSize);
source = new byte[is.available()];
is.read(source);
is.close();
}
// Save the file content either as a manifest data or by adding it to entries
if (MANIFEST.equals(filename))
manifestData = new String(source, "UTF-8");
else
entries.put(filename, source);
}
if (manifestData != null) {
final ManifestFile manifestFile = new Gson().fromJson(manifestData, ManifestFile.class);
manifest = manifestFile.getManifest();
}
}
@Override
public void close() throws IOException {
softDeviceBytes = null;
bootloaderBytes = null;
softDeviceBytes = null;
softDeviceAndBootloaderBytes = null;
softDeviceSize = bootloaderSize = applicationSize = 0;
currentSource = null;
bytesRead = bytesReadFromCurrentSource = 0;
super.close();
}
@Override
public int read(final byte[] buffer) throws IOException {
int maxSize = currentSource.length - bytesReadFromCurrentSource;
int size = buffer.length <= maxSize ? buffer.length : maxSize;
System.arraycopy(currentSource, bytesReadFromCurrentSource, buffer, 0, size);
bytesReadFromCurrentSource += size;
if (buffer.length > size) {
if (startNextFile() == null) {
bytesRead += size;
return size;
}
maxSize = currentSource.length;
final int nextSize = buffer.length - size <= maxSize ? buffer.length - size : maxSize;
System.arraycopy(currentSource, 0, buffer, size, nextSize);
bytesReadFromCurrentSource += nextSize;
size += nextSize;
}
bytesRead += size;
return size;
}
/**
* Returns the manifest object if it was specified in the ZIP file.
* @return the manifest object
*/
public Manifest getManifest() {
return manifest;
}
/**
* Returns the content type based on the content of the ZIP file. The content type may be truncated using {@link #setContentType(int)}.
*
* @return a bit field of {@link DfuBaseService#TYPE_SOFT_DEVICE TYPE_SOFT_DEVICE}, {@link DfuBaseService#TYPE_BOOTLOADER TYPE_BOOTLOADER} and {@link DfuBaseService#TYPE_APPLICATION
* TYPE_APPLICATION}
*/
public int getContentType() {
byte b = 0;
if (softDeviceSize > 0)
b |= DfuBaseService.TYPE_SOFT_DEVICE;
if (bootloaderSize > 0)
b |= DfuBaseService.TYPE_BOOTLOADER;
if (applicationSize > 0)
b |= DfuBaseService.TYPE_APPLICATION;
return b;
}
/**
* Truncates the current content type. May be used to hide some files, f.e. to send Soft Device and Bootloader without Application or only the Application.
*
* @param type
* the new type
* @return the final type after truncating
*/
public int setContentType(final int type) {
if (bytesRead > 0)
throw new UnsupportedOperationException("Content type must not be change after reading content");
final int t = getContentType() & type;
if ((t & DfuBaseService.TYPE_SOFT_DEVICE) == 0) {
softDeviceBytes = null;
if (softDeviceAndBootloaderBytes != null) {
softDeviceAndBootloaderBytes = null;
bootloaderSize = 0;
}
softDeviceSize = 0;
}
if ((t & DfuBaseService.TYPE_BOOTLOADER) == 0) {
bootloaderBytes = null;
if (softDeviceAndBootloaderBytes != null) {
softDeviceAndBootloaderBytes = null;
softDeviceSize = 0;
}
bootloaderSize = 0;
}
if ((t & DfuBaseService.TYPE_APPLICATION) == 0) {
applicationBytes = null;
applicationSize = 0;
}
return t;
}
/**
* Sets the currentSource to the new file or to <code>null</code> if the last file has been transmitted.
*
* @return the new source, the same as {@link #currentSource}
*/
private byte[] startNextFile() {
byte[] ret;
if (currentSource == softDeviceBytes && bootloaderBytes != null) {
ret = currentSource = bootloaderBytes;
} else if (currentSource != applicationBytes && applicationBytes != null) {
ret = currentSource = applicationBytes;
} else {
ret = currentSource = null;
}
bytesReadFromCurrentSource = 0;
return ret;
}
@Override
/**
* Returns the number of bytes that has not been read yet. This value includes only firmwares matching the content type set by the construcotor or the {@link #setContentType(int)} method.
*/
public int available() {
return softDeviceSize + bootloaderSize + applicationSize - bytesRead;
}
/**
* Returns the total size of the SoftDevice firmware. In case the firmware was given as a HEX, this method returns the size of the BIN content of the file.
* @return the size of the SoftDevice firmware (BIN part)
*/
public int softDeviceImageSize() {
return softDeviceSize;
}
/**
* Returns the total size of the Bootloader firmware. In case the firmware was given as a HEX, this method returns the size of the BIN content of the file.
* @return the size of the Bootloader firmware (BIN part)
*/
public int bootloaderImageSize() {
return bootloaderSize;
}
/**
* Returns the total size of the Application firmware. In case the firmware was given as a HEX, this method returns the size of the BIN content of the file.
* @return the size of the Application firmware (BIN part)
*/
public int applicationImageSize() {
return applicationSize;
}
/**
* Returns the content of the init file for SoftDevice and/or Bootloader. When both SoftDevice and Bootloader are present in the ZIP file (as two files using the compatibility mode
* or as one file using the new Distribution packet) the system init contains validation data for those two files combined (e.g. the CRC value). This method may return
* <code>null</code> if there is no SoftDevice nor Bootloader in the ZIP or the DAT file is not present there.
* @return the content of the init packet for SoftDevice and/or Bootloader
*/
public byte[] getSystemInit() {
return systemInitBytes;
}
/**
* Returns the content of the init file for the Application or <code>null</code> if no application file in the ZIP, or the DAT file is not provided.
* @return the content of the init packet for Application
*/
public byte[] getApplicationInit() {
return applicationInitBytes;
}
}