| /********************************************************************************
|
| * 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;
|
| }
|
| }
|
|
|
| }
|
| } |