/* * Autopsy Forensic Browser * * Copyright 2011-2014 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.datamodel; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.TimeZone; import java.util.concurrent.Future; import java.util.function.Supplier; import java.util.logging.Level; import java.util.prefs.PreferenceChangeEvent; import java.util.prefs.PreferenceChangeListener; import javax.swing.SwingWorker; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; import org.sleuthkit.datamodel.DerivedFile; import org.sleuthkit.datamodel.Directory; import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.TskException; import org.sleuthkit.datamodel.VirtualDirectory; /** * Static class of utility methods for Content objects */ public final class ContentUtils { private final static Logger logger = Logger.getLogger(ContentUtils.class.getName()); private static boolean displayTimesInLocalTime = UserPreferences.displayTimesInLocalTime(); private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); private static final SimpleDateFormat dateFormatterISO8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); static { UserPreferences.addChangeListener(new PreferenceChangeListener() { @Override public void preferenceChange(PreferenceChangeEvent evt) { if (evt.getKey().equals(UserPreferences.DISPLAY_TIMES_IN_LOCAL_TIME)) { displayTimesInLocalTime = UserPreferences.displayTimesInLocalTime(); } } }); } // don't instantiate private ContentUtils() { throw new AssertionError(); } /** * Convert epoch seconds to a string value in the given time zone * * @param epochSeconds * @param tzone * * @return */ public static String getStringTime(long epochSeconds, TimeZone tzone) { String time = "0000-00-00 00:00:00"; if (epochSeconds != 0) { synchronized (dateFormatter) { dateFormatter.setTimeZone(tzone); time = dateFormatter.format(new java.util.Date(epochSeconds * 1000)); } } return time; } public static String getStringTimeISO8601(long epochSeconds, TimeZone tzone) { String time = "0000-00-00T00:00:00Z"; //NON-NLS if (epochSeconds != 0) { synchronized (dateFormatterISO8601) { dateFormatterISO8601.setTimeZone(tzone); time = dateFormatterISO8601.format(new java.util.Date(epochSeconds * 1000)); } } return time; } /** * Convert epoch seconds to a string value (convenience method) * * @param epochSeconds * @param c * * @return */ public static String getStringTime(long epochSeconds, Content c) { return getStringTime(epochSeconds, getTimeZone(c)); } /** * Convert epoch seconds to a string value (convenience method) in ISO8601 * format such as 2008-07-04T13:45:04Z * * @param epochSeconds * @param c * * @return */ public static String getStringTimeISO8601(long epochSeconds, Content c) { return getStringTimeISO8601(epochSeconds, getTimeZone(c)); } public static TimeZone getTimeZone(Content c) { try { if (!shouldDisplayTimesInLocalTime()) { return TimeZone.getTimeZone("GMT"); } else { final Content dataSource = c.getDataSource(); if ((dataSource != null) && (dataSource instanceof Image)) { Image image = (Image) dataSource; return TimeZone.getTimeZone(image.getTimeZone()); } else { //case such as top level VirtualDirectory return TimeZone.getDefault(); } } } catch (TskException ex) { return TimeZone.getDefault(); } } private static final SystemNameVisitor systemName = new SystemNameVisitor(); public static String getSystemName(Content content) { return content.accept(systemName); } private static class SystemNameVisitor extends ContentVisitor.Default<String> { SystemNameVisitor() { } @Override protected String defaultVisit(Content cntnt) { return cntnt.getName() + ":" + Long.toString(cntnt.getId()); } } private static final int TO_FILE_BUFFER_SIZE = 8192; /** * Reads all the data from any content object and writes (extracts) it to a * file. * * @param content Any content object. * @param outputFile Will be created if it doesn't exist, and overwritten if * it does * @param progress progress bar handle to update, if available. null * otherwise * @param worker the swing worker background thread the process runs * within, or null, if in the main thread, used to handle * task cancellation * @param source true if source file * * @return number of bytes extracted * * @throws IOException if file could not be written */ public static <T> long writeToFile(Content content, java.io.File outputFile, ProgressHandle progress, Future<T> worker, boolean source) throws IOException { InputStream in = new ReadContentInputStream(content); // Get the unit size for a progress bar int unit = (int) (content.getSize() / 100); long totalRead = 0; try (FileOutputStream out = new FileOutputStream(outputFile, false)) { byte[] buffer = new byte[TO_FILE_BUFFER_SIZE]; int len = in.read(buffer); while (len != -1) { // If there is a worker, check for a cancelation if (worker != null && worker.isCancelled()) { break; } out.write(buffer, 0, len); len = in.read(buffer); totalRead += len; // If there is a progress bar and this is the source file, // report any progress if (progress != null && source && totalRead >= TO_FILE_BUFFER_SIZE) { int totalProgress = (int) (totalRead / unit); progress.progress(content.getName(), totalProgress); // If it's not the source, just update the file being processed } else if (progress != null && !source) { progress.progress(content.getName()); } } } finally { in.close(); } return totalRead; } public static void writeToFile(Content content, java.io.File outputFile) throws IOException { writeToFile(content, outputFile, null, null, false); } /** * Reads all the data from any content object and writes (extracts) it to a * file, using a cancellation check instead of a Future object method. * * @param content Any content object. * @param outputFile Will be created if it doesn't exist, and overwritten * if it does * @param cancelCheck A function used to check if the file write process * should be terminated. * @return number of bytes extracted * @throws IOException if file could not be written */ public static long writeToFile(Content content, java.io.File outputFile, Supplier<Boolean> cancelCheck) throws IOException { InputStream in = new ReadContentInputStream(content); long totalRead = 0; try (FileOutputStream out = new FileOutputStream(outputFile, false)) { byte[] buffer = new byte[TO_FILE_BUFFER_SIZE]; int len = in.read(buffer); while (len != -1) { out.write(buffer, 0, len); totalRead += len; if (cancelCheck.get()) { break; } len = in.read(buffer); } } finally { in.close(); } return totalRead; } /** * Helper to ignore the '.' and '..' directories */ public static boolean isDotDirectory(AbstractFile dir) { String name = dir.getName(); return name.equals(".") || name.equals(".."); } /** * Extracts file/folder as given destination file, recursing into folders. * Assumes there will be no collisions with existing directories/files, and * that the directory to contain the destination file already exists. */ public static class ExtractFscContentVisitor<T, V> extends ContentVisitor.Default<Void> { java.io.File dest; ProgressHandle progress; SwingWorker<T, V> worker; boolean source = false; /** * Make new extractor for a specific destination * * @param dest The file/folder visited will be extracted as this * file * @param progress progress bar handle to update, if available. null * otherwise * @param worker the swing worker background thread the process runs * within, or null, if in the main thread, used to * handle task cancellation * @param source true if source file */ public ExtractFscContentVisitor(java.io.File dest, ProgressHandle progress, SwingWorker<T, V> worker, boolean source) { this.dest = dest; this.progress = progress; this.worker = worker; this.source = source; } public ExtractFscContentVisitor(java.io.File dest) { this.dest = dest; } /** * Convenience method to make a new instance for given destination and * extract given content */ public static <T, V> void extract(Content cntnt, java.io.File dest, ProgressHandle progress, SwingWorker<T, V> worker) { cntnt.accept(new ExtractFscContentVisitor<>(dest, progress, worker, true)); } @Override public Void visit(File f) { try { ContentUtils.writeToFile(f, dest, progress, worker, source); } catch (IOException ex) { logger.log(Level.SEVERE, "Trouble extracting file to " + dest.getAbsolutePath(), //NON-NLS ex); } return null; } @Override public Void visit(LayoutFile f) { try { ContentUtils.writeToFile(f, dest, progress, worker, source); } catch (IOException ex) { logger.log(Level.SEVERE, "Trouble extracting unallocated content file to " + dest.getAbsolutePath(), //NON-NLS ex); } return null; } @Override public Void visit(DerivedFile df) { try { ContentUtils.writeToFile(df, dest, progress, worker, source); } catch (IOException ex) { logger.log(Level.SEVERE, "Error extracting derived file to " + dest.getAbsolutePath(), //NON-NLS ex); } return null; } @Override public Void visit(LocalFile lf) { try { ContentUtils.writeToFile(lf, dest, progress, worker, source); } catch (IOException ex) { logger.log(Level.SEVERE, "Error extracting local file to " + dest.getAbsolutePath(), //NON-NLS ex); } return null; } @Override public Void visit(SlackFile f) { try { ContentUtils.writeToFile(f, dest, progress, worker, source); } catch (IOException ex) { logger.log(Level.SEVERE, "Trouble extracting slack file to " + dest.getAbsolutePath(), //NON-NLS ex); } return null; } @Override public Void visit(Directory dir) { return visitDir(dir); } @Override public Void visit(VirtualDirectory dir) { return visitDir(dir); } private java.io.File getFsContentDest(Content fsc) { String path = dest.getAbsolutePath() + java.io.File.separator + fsc.getName(); return new java.io.File(path); } public Void visitDir(AbstractFile dir) { // don't extract . and .. directories if (isDotDirectory(dir)) { return null; } dest.mkdir(); try { int numProcessed = 0; // recurse on children for (Content child : dir.getChildren()) { java.io.File childFile = getFsContentDest(child); ExtractFscContentVisitor<T, V> childVisitor = new ExtractFscContentVisitor<>(childFile, progress, worker, false); // If this is the source directory of an extract it // will have a progress and worker, and will keep track // of the progress bar's progress if (worker != null && worker.isCancelled()) { break; } if (progress != null && source) { progress.progress(child.getName(), numProcessed); } child.accept(childVisitor); numProcessed++; } } catch (TskException ex) { logger.log(Level.SEVERE, "Trouble fetching children to extract.", ex); //NON-NLS } return null; } @Override protected Void defaultVisit(Content content) { throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), "ContentUtils.exception.msg", content.getClass().getSimpleName())); } } /** * Indicates whether or not times should be displayed using local time. * * @return True or false. */ public static boolean shouldDisplayTimesInLocalTime() { return displayTimesInLocalTime; } }