blob: f5e6095f3919d58b96c38636e627d2e0d7dae172 [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015-2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************/
package org.eclipse.mdm.openatfx.mdf.mdf4;
import static java.lang.Math.min;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import org.asam.ods.T_ExternalReference;
/**
* <p>
* THE ATTACHMENT BLOCK <code>ATBLOCK</code>
* </p>
* The data section of the ATBLOCK contains attachments (binary data)
*
* @author Christian Rechner, Tobias Leemann
*/
class ATBLOCK extends BLOCK {
public static String BLOCK_ID = "##AT";
private long lnkAtNext;
private long lnkTxFilename;
private long lnkTxMIMEtype;
private long lnkMdComment;
private int flags;
private int creatorIndex;
private byte[] md5CheckSum;
private long origSize;
private long embeddedSize;
/**
* Constructor.
*
* @param sbc
* The byte channel pointing to the MDF file.
* @param pos
* The position of the block within the MDF file.
*/
private ATBLOCK(SeekableByteChannel sbc, long pos) {
super(sbc, pos);
}
public long getLnkAtNext() {
return lnkAtNext;
}
public void setLnkAtNext(long lnkAtNext) {
this.lnkAtNext = lnkAtNext;
}
public long getLnkTxFilename() {
return lnkTxFilename;
}
public void setLnkTxFilename(long lnkTxFilename) {
this.lnkTxFilename = lnkTxFilename;
}
public long getLnkTxMIMEtype() {
return lnkTxMIMEtype;
}
public void setLnkTxMIMEtype(long lnkTxMIMEtype) {
this.lnkTxMIMEtype = lnkTxMIMEtype;
}
public long getLnkMdComment() {
return lnkMdComment;
}
public void setLnkMdComment(long lnkMdComment) {
this.lnkMdComment = lnkMdComment;
}
public int getFlags() {
return flags;
}
public void setFlags(int flags) {
this.flags = flags;
}
public int getCreatorIndex() {
return creatorIndex;
}
public void setCreatorIndex(int creatorIndex) {
this.creatorIndex = creatorIndex;
}
public byte[] getMd5CheckSum() {
return md5CheckSum;
}
public void setMd5CheckSum(byte[] md5CheckSum) {
this.md5CheckSum = md5CheckSum;
}
public long getOrigSize() {
return origSize;
}
public void setOrigSize(long origSize) {
this.origSize = origSize;
}
public long getEmbeddedSize() {
return embeddedSize;
}
public void setEmbeddedSize(long embeddedSize) {
this.embeddedSize = embeddedSize;
}
public ATBLOCK getAtNextBlock() throws IOException {
if (lnkAtNext > 0) {
return ATBLOCK.read(sbc, lnkAtNext);
}
return null;
}
public TXBLOCK getTxFilennameBlock() throws IOException {
if (lnkTxFilename > 0) {
return TXBLOCK.read(sbc, lnkTxFilename);
}
return null;
}
public TXBLOCK getTxMIMETypeBlock() throws IOException {
if (lnkTxMIMEtype > 0) {
return TXBLOCK.read(sbc, lnkTxMIMEtype);
}
return null;
}
public TXBLOCK getTxCommentBlock() throws IOException {
if (lnkMdComment > 0) {
return TXBLOCK.read(sbc, lnkMdComment);
}
return null;
}
public MDBLOCK getMdCommentBlock() throws IOException {
if (lnkMdComment > 0) {
return MDBLOCK.read(sbc, lnkMdComment);
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "DTBLOCK [pos=" + getPos() + "]";
}
/**
* Reads a SDBLOCK from the channel starting at current channel position.
*
* @param channel
* The channel to read from.
* @param pos
* The position
* @return The block data.
* @throws IOException
* The exception.
*/
public static ATBLOCK read(SeekableByteChannel channel, long pos) throws IOException {
ATBLOCK block = new ATBLOCK(channel, pos);
// read block header
ByteBuffer bb = ByteBuffer.allocate(24 + 32 + 40); // 24 Head 32 Links
// 40 Data
bb.order(ByteOrder.LITTLE_ENDIAN);
channel.position(pos);
channel.read(bb);
bb.rewind();
// CHAR 4: Block type identifier
block.setId(MDF4Util.readCharsISO8859(bb, 4));
if (!block.getId().equals(BLOCK_ID)) {
throw new IOException("Wrong block type - expected '" + BLOCK_ID + "', found '" + block.getId() + "'");
}
// BYTE 4: Reserved used for 8-Byte alignment
bb.get(new byte[4]);
// UINT64: Length of block
block.setLength(MDF4Util.readUInt64(bb));
// UINT64: Number of links
block.setLinkCount(MDF4Util.readUInt64(bb));
// LINK 1 Pointer to next ATBLOCK (NIL allowed)
block.setLnkAtNext(MDF4Util.readLink(bb));
// LINK 1 Pointer to Text Block with Filename (NIL allowed)
block.setLnkTxFilename(MDF4Util.readLink(bb));
// LINK 1 Pointer to Text Block with MIME Type (TXBLOCK) (NIL allowed)
block.setLnkTxMIMEtype(MDF4Util.readLink(bb));
// LINK 1 Pointer to comment Block MDBLOCK (NIL allowed)
block.setLnkMdComment(MDF4Util.readLink(bb));
// UINT16 flags
block.setFlags(MDF4Util.readUInt16(bb));
// UINT16 Creator Index
block.setCreatorIndex(MDF4Util.readUInt16(bb));
// BYTE 4: Reserved used for 8-Byte alignment
bb.get(new byte[4]);
// BYTE 16: MD5 Checksum
byte[] md5sum = new byte[16];
bb.get(md5sum);
block.setMd5CheckSum(md5sum);
// UINT64 OriginalSize
block.setOrigSize(MDF4Util.readUInt64(bb));
// UINT64 EmbeddedSize
block.setEmbeddedSize(MDF4Util.readUInt64(bb));
return block;
}
/**
* Converts this AT Block and all linked one into dedicated
* external links, where each embedded file is written to disc.
*
* @param target the working directory
* @param subDir name of the target sub directory for exporting embedded files, may be empty
* @return all external links, neither null nor empty
* @throws IOException in case of errors
*/
public T_ExternalReference[] toExtRefs(Path target, String subDir) throws IOException {
List<T_ExternalReference> extRefs = new ArrayList<>();
ATBLOCK block = this;
int count = 0;
while (block != null) {
boolean embedded = (block.flags & 1) != 0;
Path file;
if (embedded) {
file = block.exportEmbedded(subDir.isEmpty() ? target : target.resolve(subDir), ++count);
} else {
file = block.getFilePath(target, true);
}
block.checkMD5(file);
extRefs.add(new T_ExternalReference(block.getDescription(), block.getMimeType(file),
target.relativize(file).toString()));
block = block.getAtNextBlock();
}
return extRefs.stream().toArray(T_ExternalReference[]::new);
}
/**
* Returns the file MIME type if available.
*
* @return the MIME type, may be empty but not null
* @throws IOException in case of errors
*/
private String getMimeType(Path file) throws IOException {
String value = Files.probeContentType(file);
if (lnkTxMIMEtype > 0) {
value = getTxMIMETypeBlock().getTxData();
}
return value == null ? "application/octet-stream" : value.trim();
}
/**
* Returns the file description if available.
*
* @return the file description, may be empty but not null
* @throws IOException in case of errors
*/
private String getDescription() throws IOException {
if (lnkMdComment < 1) {
return "";
}
String value = null;
try {
value = getMdCommentBlock().getMdData();
} catch (IOException e) {
value = getTxCommentBlock().getTxData();
}
return value == null ? "" : value.trim();
}
/**
* Exports given embedded stream into given file.
*
* @param target the target file path, not null and must not exist yet
* @param id the internal files count, used to index files whose name is unknown
* @return the newly created file path, not null
* @throws IOException in case of errors
*/
private Path exportEmbedded(Path target, int id) throws IOException {
if ((flags & 1) == 0) {
throw new IllegalStateException("file is not embedded");
}
Path file;
if (lnkTxFilename > 0) {
file = getFilePath(target, false);
} else {
file = target.resolve("embedded_" + id + ".bin");
}
Files.createDirectories(file.getParent());
EmbeddedStream embeddedStream = new EmbeddedStream(sbc, getPos() + 96, getEmbeddedSize());
boolean compressed = (flags & 1 << 1) != 0;
if (compressed) {
unzip(embeddedStream, file);
} else {
Files.copy(embeddedStream, file);
}
return file;
}
/**
* Exports given compressed embedded inputs stream into given file target.
*
* @param embeddedStream the embedded input stream, not null
* @param target the target file path, not null and must not exist yet
* @throws IOException in case of errors
*/
private void unzip(EmbeddedStream embeddedStream, Path target) throws IOException {
try (InputStream is = embeddedStream; OutputStream os = Files.newOutputStream(target, CREATE_NEW)) {
Inflater inflater = new Inflater();
byte[] writeBuffer = new byte[8192];
byte[] readBuffer = new byte[8192];
int written;
int read = 0;
while(read > -1 && !inflater.finished()) {
while(inflater.needsInput() && (read = is.read(readBuffer)) > 0) {
// feed iflater as long as it needs input and as long as there is data available
inflater.setInput(readBuffer, 0, read);
}
while ((written = inflater.inflate(writeBuffer)) > 0) {
// write decompressed bytes as long inflater is satisfied
os.write(writeBuffer, 0, written);
}
}
os.flush();
} catch (DataFormatException e) {
throw new IOException("failed to decompress embedded file", e);
}
}
/**
* Returns the file path.
*
* @param target the working directory
* @param exists if true, the returned file path must exist
* @return the target file path, not null
* @throws IOException in case of errors
*/
private Path getFilePath(Path target, boolean exists) throws IOException {
if (lnkTxFilename < 1) {
throw new IllegalStateException("file name TXBLOCK is not set");
}
TXBLOCK fileNameTX = getTxFilennameBlock();
Path file = Paths.get(fileNameTX.getTxData());
if (!file.isAbsolute()) {
file = target.resolve(fileNameTX.getTxData());
}
if (exists && !file.toFile().exists()) {
throw new IOException("file '" + file + "' not found.");
}
return file;
}
/**
* Compares calculated MD5 checksum with the stored one.
*
* @param file the file path, not null
* @throws IOException in case of errors
*/
private void checkMD5(Path file) throws IOException {
if ((flags & 1 << 2) == 0) {
// checksum is not available => skip!
return;
}
try {
MessageDigest md = MessageDigest.getInstance("MD5");
try (DigestInputStream is = new DigestInputStream(Files.newInputStream(file), md)) {
byte[] buffer = new byte[1024];
while (is.read(buffer) != -1);
byte[] calculated = is.getMessageDigest().digest();
if (!Arrays.equals(calculated, getMd5CheckSum())) {
throw new IOException("checksum validation failed for file '" + file + "'");
}
}
} catch (NoSuchAlgorithmException e) {
throw new IOException("checksum algorithm 'MD5' not found.");
}
}
/**
* Embedded input stream implementation.
*/
private static final class EmbeddedStream extends InputStream {
private final SeekableByteChannel sbc;
private final long length0;
private long current = 0;
/**
* Constructor.
*
* @param sbc the channel source for byte access, not null
* @param start the channel position this stream internally starts from
* @param length the number of bytes this stream will provide
* @throws IOException in case of errors
*/
public EmbeddedStream(SeekableByteChannel sbc, long start, long length) throws IOException {
this.sbc = sbc;
this.length0 = length;
sbc.position(start);
}
/**
* {@inheritDoc}
*/
@Override
public int read() throws IOException {
if (current >= length0) {
return -1;
}
ByteBuffer buffer = ByteBuffer.allocate(1);
int read = sbc.read(buffer);
if (read > 0) {
current += read;
buffer.rewind();
return buffer.getInt();
} else {
return -1;
}
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] bytes) throws IOException {
if (current >= length0) {
return -1;
}
int read = (int) min(sbc.read(ByteBuffer.wrap(bytes)), length0 - current);
if (read > 0) {
current += read;
return read;
} else {
return -1;
}
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] bytes, int offset, int length) throws IOException {
if (current >= length0) {
return -1;
}
int read = (int) min(sbc.read(ByteBuffer.wrap(bytes, offset, length)), length0 - current);
if (read > 0) {
current += read;
return read;
} else {
return -1;
}
}
}
}