/*******************************************************************************
* Copyright (c) 2011, 2014 Ericsson, Ecole Polytechnique de Montreal and others
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthew Khouzam - Initial API and implementation
* Simon Marchi - Initial API and implementation
* Matthew Khouzam - Update for live trace reading support
* Bernd Hufmann - Add method to copy metadata file
*******************************************************************************/
package org.eclipse.tracecompass.ctf.core.trace;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.UUID;
import org.antlr.runtime.ANTLRReaderStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.tree.CommonTree;
import org.antlr.runtime.tree.RewriteCardinalityException;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.ctf.core.CTFException;
import org.eclipse.tracecompass.ctf.parser.CTFLexer;
import org.eclipse.tracecompass.ctf.parser.CTFParser;
import org.eclipse.tracecompass.ctf.parser.CTFParser.parse_return;
import org.eclipse.tracecompass.internal.ctf.core.event.metadata.CtfAntlrException;
import org.eclipse.tracecompass.internal.ctf.core.event.metadata.IOStructGen;
import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException;
import org.eclipse.tracecompass.internal.ctf.core.trace.Utils;
/**
* The CTF trace metadata TSDL file
*
* @version 1.0
* @author Matthew Khouzam
* @author Simon Marchi
*/
public class Metadata {
// ------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------
private static final Charset ASCII_CHARSET = Charset.forName("ASCII"); //$NON-NLS-1$
private static final String TEXT_ONLY_METADATA_HEADER_PREFIX = "/* CTF"; //$NON-NLS-1$
private static final int PREVALIDATION_SIZE = 8;
private static final int BITS_PER_BYTE = Byte.SIZE;
/**
* Name of the metadata file in the trace directory
*/
private static final String METADATA_FILENAME = "metadata"; //$NON-NLS-1$
/**
* Size of the metadata packet header, in bytes, computed by hand.
*/
private static final int METADATA_PACKET_HEADER_SIZE = 37;
// ------------------------------------------------------------------------
// Attributes
// ------------------------------------------------------------------------
/**
* Byte order as detected when reading the TSDL magic number.
*/
private ByteOrder fDetectedByteOrder = null;
/**
* The trace file to which belongs this metadata file.
*/
private final CTFTrace fTrace;
private IOStructGen fTreeParser;
// ------------------------------------------------------------------------
// Constructors
// ------------------------------------------------------------------------
/**
* Constructs a Metadata object.
*
* @param trace
* The trace to which belongs this metadata file.
*/
public Metadata(CTFTrace trace) {
fTrace = trace;
}
/**
* For network streaming
*/
public Metadata() {
fTrace = new CTFTrace();
}
// ------------------------------------------------------------------------
// Getters/Setters/Predicates
// ------------------------------------------------------------------------
/**
* Returns the ByteOrder that was detected while parsing the metadata.
*
* @return The byte order.
*/
public ByteOrder getDetectedByteOrder() {
return fDetectedByteOrder;
}
/**
* Gets the parent trace
*
* @return the parent trace
*/
public CTFTrace getTrace() {
return fTrace;
}
// ------------------------------------------------------------------------
// Operations
// ------------------------------------------------------------------------
/**
* Parse the metadata file.
*
* @throws CTFException
* If there was a problem parsing the metadata
*/
public void parseFile() throws CTFException {
/*
* Reader. It will contain a StringReader if we are using packet-based
* metadata and it will contain a FileReader if we have text-based
* metadata.
*/
try (FileInputStream fis = new FileInputStream(getMetadataPath());
FileChannel metadataFileChannel = fis.getChannel();
/* Check if metadata is packet-based, if not it is text based */
Reader metadataTextInput = (isPacketBased(metadataFileChannel) ? readBinaryMetaData(metadataFileChannel) : new FileReader(fis.getFD()));) {
readMetaDataText(metadataTextInput);
} catch (FileNotFoundException e) {
throw new CTFException("Cannot find metadata file!", e); //$NON-NLS-1$
} catch (IOException | ParseException e) {
throw new CTFException(e);
} catch (RecognitionException | RewriteCardinalityException e) {
throw new CtfAntlrException(e);
}
}
private Reader readBinaryMetaData(FileChannel metadataFileChannel) throws CTFException {
/* Create StringBuffer to receive metadata text */
StringBuffer metadataText = new StringBuffer();
/*
* Read metadata packet one by one, appending the text to the
* StringBuffer
*/
MetadataPacketHeader packetHeader = readMetadataPacket(
metadataFileChannel, metadataText);
while (packetHeader != null) {
packetHeader = readMetadataPacket(metadataFileChannel,
metadataText);
}
/* Wrap the metadata string with a StringReader */
return new StringReader(metadataText.toString());
}
/**
* Executes a weak validation of the metadata. It checks if a file with name
* metadata exists and if one of the following conditions are met:
* <ul>
* <li>For text-only metadata, the file starts with "/* CTF" (without the
* quotes)</li>
* <li>For packet-based metadata, the file starts with correct magic number
* </li>
* </ul>
*
* @param path
* path to CTF trace directory
* @return <code>true</code> if pre-validation is ok else <code>false</code>
* @throws CTFException
* file channel cannot be created
* @since 1.0
*/
public static boolean preValidate(String path) throws CTFException {
String metadataPath = path + Utils.SEPARATOR + METADATA_FILENAME;
File metadataFile = new File(metadataPath);
if (metadataFile.exists() && metadataFile.length() > PREVALIDATION_SIZE) {
try (FileChannel fc = FileChannel.open(metadataFile.toPath(), StandardOpenOption.READ)) {
ByteBuffer bb = ByteBuffer.allocate(PREVALIDATION_SIZE);
bb.clear();
fc.read(bb);
bb.flip();
if (bb.getInt(0) == Utils.TSDL_MAGIC) {
return true;
}
bb.order(ByteOrder.LITTLE_ENDIAN);
if (bb.getInt(0) == Utils.TSDL_MAGIC) {
return true;
}
bb.position(0);
byte bytes[] = new byte[PREVALIDATION_SIZE];
bb.get(bytes);
String text = new String(bytes, ASCII_CHARSET);
return text.startsWith(TEXT_ONLY_METADATA_HEADER_PREFIX);
} catch (IOException e) {
throw new CTFException(e.getMessage(), e);
}
}
return false;
}
/**
* Read the metadata from a formatted TSDL string
*
* @param data
* the data to read
* @throws CTFException
* this exception wraps a ParseException, IOException or
* CtfAntlrException, three exceptions that can be obtained from
* parsing a TSDL file
*/
public void parseText(String data) throws CTFException {
Reader metadataTextInput = new StringReader(data);
try {
readMetaDataText(metadataTextInput);
} catch (IOException | ParseException e) {
throw new CTFException(e);
} catch (RecognitionException | RewriteCardinalityException e) {
throw new CtfAntlrException(e);
}
}
private void readMetaDataText(Reader metadataTextInput) throws IOException, RecognitionException, ParseException {
CommonTree tree = createAST(metadataTextInput);
/* Generate IO structures (declarations) */
fTreeParser = new IOStructGen(tree, NonNullUtils.checkNotNull(fTrace));
fTreeParser.generate();
/* store locally in case of concurrent modification */
ByteOrder detectedByteOrder = getDetectedByteOrder();
if (detectedByteOrder != null && fTrace.getByteOrder() != detectedByteOrder) {
throw new ParseException("Metadata byte order and trace byte order inconsistent."); //$NON-NLS-1$
}
}
/**
* Read a metadata fragment from a formatted TSDL string
*
* @param dataFragment
* the data to read
* @throws CTFException
* this exception wraps a ParseException, IOException or
* CtfAntlrException, three exceptions that can be obtained from
* parsing a TSDL file
*/
public void parseTextFragment(String dataFragment) throws CTFException {
Reader metadataTextInput = new StringReader(dataFragment);
try {
readMetaDataTextFragment(metadataTextInput);
} catch (IOException | ParseException e) {
throw new CTFException(e);
} catch (RecognitionException | RewriteCardinalityException e) {
throw new CtfAntlrException(e);
}
}
private void readMetaDataTextFragment(Reader metadataTextInput) throws IOException, RecognitionException, ParseException {
CommonTree tree = createAST(metadataTextInput);
fTreeParser.setTree(tree);
fTreeParser.generateFragment();
}
private static CommonTree createAST(Reader metadataTextInput) throws IOException,
RecognitionException {
/* Create an ANTLR reader */
ANTLRReaderStream antlrStream;
antlrStream = new ANTLRReaderStream(metadataTextInput);
/* Parse the metadata text and get the AST */
CTFLexer ctfLexer = new CTFLexer(antlrStream);
CommonTokenStream tokens = new CommonTokenStream(ctfLexer);
CTFParser ctfParser = new CTFParser(tokens, false);
parse_return pr = ctfParser.parse();
return pr.getTree();
}
/**
* Determines whether the metadata file is packet-based by looking at the
* TSDL magic number. If it is packet-based, it also gives information about
* the endianness of the trace using the detectedByteOrder attribute.
*
* @param metadataFileChannel
* FileChannel of the metadata file.
* @return True if the metadata is packet-based.
* @throws CTFException
*/
private boolean isPacketBased(FileChannel metadataFileChannel)
throws CTFException {
/*
* Create a ByteBuffer to read the TSDL magic number (default is
* big-endian)
*/
ByteBuffer magicByteBuffer = ByteBuffer.allocate(Utils.TSDL_MAGIC_LEN);
/* Read without changing file position */
try {
metadataFileChannel.read(magicByteBuffer, 0);
} catch (IOException e) {
throw new CTFException("Unable to read metadata file channel.", e); //$NON-NLS-1$
}
/* Get the first int from the file */
int magic = magicByteBuffer.getInt(0);
/* Check if it matches */
if (Utils.TSDL_MAGIC == magic) {
fDetectedByteOrder = ByteOrder.BIG_ENDIAN;
return true;
}
/* Try the same thing, but with little-endian */
magicByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
magic = magicByteBuffer.getInt(0);
if (Utils.TSDL_MAGIC == magic) {
fDetectedByteOrder = ByteOrder.LITTLE_ENDIAN;
return true;
}
return false;
}
private String getMetadataPath() {
/* Path of metadata file = trace directory path + metadata filename */
if (fTrace.getTraceDirectory() == null) {
return ""; //$NON-NLS-1$
}
return fTrace.getTraceDirectory().getPath()
+ Utils.SEPARATOR + METADATA_FILENAME;
}
/**
* Reads a metadata packet from the given metadata FileChannel, do some
* basic validation and append the text to the StringBuffer.
*
* @param metadataFileChannel
* Metadata FileChannel
* @param metadataText
* StringBuffer to which the metadata text will be appended.
* @return A structure describing the header of the metadata packet, or null
* if the end of the file is reached.
* @throws CTFException
*/
private MetadataPacketHeader readMetadataPacket(
FileChannel metadataFileChannel, StringBuffer metadataText)
throws CTFException {
/* Allocate a ByteBuffer for the header */
ByteBuffer headerByteBuffer = ByteBuffer.allocate(METADATA_PACKET_HEADER_SIZE);
/* Read the header */
try {
int nbBytesRead = metadataFileChannel.read(headerByteBuffer);
/* Return null if EOF */
if (nbBytesRead < 0) {
return null;
}
if (nbBytesRead != METADATA_PACKET_HEADER_SIZE) {
throw new CTFException("Error reading the metadata header."); //$NON-NLS-1$
}
} catch (IOException e) {
throw new CTFException("Error reading the metadata header.", e); //$NON-NLS-1$
}
/* Set ByteBuffer's position to 0 */
headerByteBuffer.position(0);
/* Use byte order that was detected with the magic number */
headerByteBuffer.order(fDetectedByteOrder);
MetadataPacketHeader header = new MetadataPacketHeader(headerByteBuffer);
/* Check TSDL magic number */
if (!header.isMagicValid()) {
throw new CTFException("TSDL magic number does not match"); //$NON-NLS-1$
}
/* Check UUID */
if (!fTrace.uuidIsSet()) {
fTrace.setUUID(header.getUuid());
} else if (!fTrace.getUUID().equals(header.getUuid())) {
throw new CTFException("UUID mismatch"); //$NON-NLS-1$
}
/* Extract the text from the packet */
int payloadSize = ((header.getContentSize() / BITS_PER_BYTE) - METADATA_PACKET_HEADER_SIZE);
if (payloadSize < 0) {
throw new CTFException("Invalid metadata packet payload size."); //$NON-NLS-1$
}
int skipSize = (header.getPacketSize() - header.getContentSize()) / BITS_PER_BYTE;
/* Read the payload + the padding in a ByteBuffer */
ByteBuffer payloadByteBuffer = ByteBuffer.allocateDirect(payloadSize
+ skipSize);
try {
metadataFileChannel.read(payloadByteBuffer);
} catch (IOException e) {
throw new CTFException("Error reading metadata packet payload.", e); //$NON-NLS-1$
}
payloadByteBuffer.rewind();
/* Read only the payload from the ByteBuffer into a byte array */
byte payloadByteArray[] = new byte[payloadByteBuffer.remaining()];
payloadByteBuffer.get(payloadByteArray, 0, payloadSize);
/* Convert the byte array to a String */
String str = new String(payloadByteArray, 0, payloadSize, ASCII_CHARSET);
/* Append it to the existing metadata */
metadataText.append(str);
return header;
}
private static class MetadataPacketHeader {
private static final int UUID_SIZE = 16;
private final int fMagic;
private final UUID fUuid;
private final int fChecksum;
private final int fContentSize;
private final int fPacketSize;
private final byte fCompressionScheme;
private final byte fEncryptionScheme;
private final byte fChecksumScheme;
private final byte fCtfMajorVersion;
private final byte fCtfMinorVersion;
public MetadataPacketHeader(ByteBuffer headerByteBuffer) {
/* Read from the ByteBuffer */
fMagic = headerByteBuffer.getInt();
byte[] uuidBytes = new byte[UUID_SIZE];
headerByteBuffer.get(uuidBytes);
fUuid = Utils.makeUUID(uuidBytes);
fChecksum = headerByteBuffer.getInt();
fContentSize = headerByteBuffer.getInt();
fPacketSize = headerByteBuffer.getInt();
fCompressionScheme = headerByteBuffer.get();
fEncryptionScheme = headerByteBuffer.get();
fChecksumScheme = headerByteBuffer.get();
fCtfMajorVersion = headerByteBuffer.get();
fCtfMinorVersion = headerByteBuffer.get();
}
public boolean isMagicValid() {
return fMagic == Utils.TSDL_MAGIC;
}
public UUID getUuid() {
return fUuid;
}
public int getContentSize() {
return fContentSize;
}
public int getPacketSize() {
return fPacketSize;
}
@Override
public String toString() {
/* Only for debugging, shouldn't be externalized */
/* Therefore it cannot be covered by test cases */
return "MetadataPacketHeader [magic=0x" //$NON-NLS-1$
+ Integer.toHexString(fMagic) + ", uuid=" //$NON-NLS-1$
+ fUuid.toString() + ", checksum=" + fChecksum //$NON-NLS-1$
+ ", contentSize=" + fContentSize + ", packetSize=" //$NON-NLS-1$ //$NON-NLS-2$
+ fPacketSize + ", compressionScheme=" + fCompressionScheme //$NON-NLS-1$
+ ", encryptionScheme=" + fEncryptionScheme //$NON-NLS-1$
+ ", checksumScheme=" + fChecksumScheme //$NON-NLS-1$
+ ", ctfMajorVersion=" + fCtfMajorVersion //$NON-NLS-1$
+ ", ctfMinorVersion=" + fCtfMinorVersion + ']'; //$NON-NLS-1$
}
}
/**
* Copies the metadata file to a destination directory.
*
* @param path
* the destination directory
* @return the path to the target file
* @throws IOException
* if an error occurred
*
* @since 1.0
*/
public Path copyTo(final File path) throws IOException {
Path source = FileSystems.getDefault().getPath(fTrace.getTraceDirectory().getAbsolutePath(), METADATA_FILENAME);
Path destPath = FileSystems.getDefault().getPath(path.getAbsolutePath());
return Files.copy(source, destPath.resolve(source.getFileName()));
}
}