/*
* Autopsy Forensic Browser
*
* Copyright 2013-2016 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.casemodule;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.SleuthkitJNI;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskDataException;
/*
* A runnable that adds an image data source to the case database.
*/
class AddImageTask implements Runnable {
private final Logger logger = Logger.getLogger(AddImageTask.class.getName());
private final String deviceId;
private final String imagePath;
private final String timeZone;
private final boolean ignoreFatOrphanFiles;
private final DataSourceProcessorProgressMonitor progressMonitor;
private final DataSourceProcessorCallback callback;
private boolean criticalErrorOccurred;
/*
* The cancellation requested flag and SleuthKit add image process are
* guarded by a monitor (called a lock here to avoid confusion with the
* progress monitor) to synchronize cancelling the process (setting the flag
* and calling its stop method) and calling either its commit or revert
* method. The built-in monitor of the add image process can't be used for
* this because it is already used to synchronize its run (init part),
* commit, revert, and currentDirectory methods.
*
* TODO (AUT-2021): Merge SleuthkitJNI.AddImageProcess and AddImageTask
*/
private final Object tskAddImageProcessLock;
private boolean tskAddImageProcessStopped;
private SleuthkitJNI.CaseDbHandle.AddImageProcess tskAddImageProcess;
/**
* Constructs a runnable task that adds an image to the case database.
*
* @param deviceId An ASCII-printable identifier for the device
* associated with the data source that is
* intended to be unique across multiple cases
* (e.g., a UUID).
* @param imagePath Path to the image file.
* @param timeZone The time zone to use when processing dates
* and times for the image, obtained from
* java.util.TimeZone.getID.
* @param ignoreFatOrphanFiles Whether to parse orphans if the image has a
* FAT filesystem.
* @param progressMonitor Progress monitor to report progress during
* processing.
* @param callback Callback to call when processing is done.
*/
AddImageTask(String deviceId, String imagePath, String timeZone, boolean ignoreFatOrphanFiles, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
this.deviceId = deviceId;
this.imagePath = imagePath;
this.timeZone = timeZone;
this.ignoreFatOrphanFiles = ignoreFatOrphanFiles;
this.callback = callback;
this.progressMonitor = progressMonitor;
tskAddImageProcessLock = new Object();
}
/**
* Adds the image to the case database.
*/
@Override
public void run() {
progressMonitor.setIndeterminate(true);
progressMonitor.setProgress(0);
Case currentCase = Case.getCurrentCase();
List<String> errorMessages = new ArrayList<>();
List<Content> newDataSources = new ArrayList<>();
try {
currentCase.getSleuthkitCase().acquireExclusiveLock();
synchronized (tskAddImageProcessLock) {
tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(timeZone, true, ignoreFatOrphanFiles);
}
Thread progressUpdateThread = new Thread(new ProgressUpdater(progressMonitor, tskAddImageProcess));
progressUpdateThread.start();
runAddImageProcess(errorMessages);
if (null != progressUpdateThread) {
progressUpdateThread.interrupt();
}
commitOrRevertAddImageProcess(currentCase, errorMessages, newDataSources);
progressMonitor.setProgress(100);
} finally {
currentCase.getSleuthkitCase().releaseExclusiveLock();
DataSourceProcessorCallback.DataSourceProcessorResult result;
if (criticalErrorOccurred) {
result = DataSourceProcessorResult.CRITICAL_ERRORS;
} else if (!errorMessages.isEmpty()) {
result = DataSourceProcessorResult.NONCRITICAL_ERRORS;
} else {
result = DataSourceProcessorResult.NO_ERRORS;
}
callback.done(result, errorMessages, newDataSources);
}
}
/*
* Attempts to cancel adding the image to the case database.
*/
public void cancelTask() {
synchronized (tskAddImageProcessLock) {
if (null != tskAddImageProcess) {
try {
/*
* All this does is set a flag that will make the TSK add
* image process exit when the flag is checked between
* processing steps. The state of the flag is not
* accessible, so record it here so that it is known that
* the revert method of the process object needs to be
* called.
*/
tskAddImageProcess.stop();
tskAddImageProcessStopped = true;
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Error cancelling adding image %s to the case database", imagePath), ex); //NON-NLS
}
}
}
}
/**
* Runs the TSK add image process.
*
* @param errorMessages Error messages, if any, are added to this list for
* eventual return via the callback.
*/
private void runAddImageProcess(List<String> errorMessages) {
try {
tskAddImageProcess.run(deviceId, new String[]{imagePath});
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Critical error occurred adding image %s", imagePath), ex); //NON-NLS
criticalErrorOccurred = true;
errorMessages.add(ex.getMessage());
} catch (TskDataException ex) {
logger.log(Level.WARNING, String.format("Non-critical error occurred adding image %s", imagePath), ex); //NON-NLS
errorMessages.add(ex.getMessage());
}
}
/**
* Commits or reverts the results of the TSK add image process. If the
* process was stopped before it completed or there was a critical error the
* results are reverted, otherwise they are committed.
*
* @param currentCase The current case.
* @param errorMessages Error messages, if any, are added to this list for
* eventual return via the callback.
* @param newDataSources If the new image is successfully committed, it is
* added to this list for eventual return via the
* callback.
*
* @return
*/
private void commitOrRevertAddImageProcess(Case currentCase, List<String> errorMessages, List<Content> newDataSources) {
synchronized (tskAddImageProcessLock) {
if (tskAddImageProcessStopped || criticalErrorOccurred) {
try {
tskAddImageProcess.revert();
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Error reverting adding image %s to the case database", imagePath), ex); //NON-NLS
errorMessages.add(ex.getMessage());
criticalErrorOccurred = true;
}
} else {
try {
long imageId = tskAddImageProcess.commit();
if (imageId != 0) {
Image newImage = currentCase.getSleuthkitCase().getImageById(imageId);
String verificationError = newImage.verifyImageSize();
if (!verificationError.isEmpty()) {
errorMessages.add(verificationError);
}
newDataSources.add(newImage);
} else {
String errorMessage = String.format("Error commiting adding image %s to the case database, no object id returned", imagePath); //NON-NLS
logger.log(Level.SEVERE, errorMessage);
errorMessages.add(errorMessage);
criticalErrorOccurred = true;
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Error committing adding image %s to the case database", imagePath), ex); //NON-NLS
errorMessages.add(ex.getMessage());
criticalErrorOccurred = true;
}
}
}
}
/**
* A Runnable that updates the progress monitor with the name of the
* directory currently being processed by the SleuthKit add image process.
*/
private class ProgressUpdater implements Runnable {
private final DataSourceProcessorProgressMonitor progressMonitor;
private final SleuthkitJNI.CaseDbHandle.AddImageProcess tskAddImageProcess;
/**
* Constructs a Runnable that updates the progress monitor with the name
* of the directory currently being processed by the SleuthKit.
*
* @param progressMonitor
* @param tskAddImageProcess
*/
ProgressUpdater(DataSourceProcessorProgressMonitor progressMonitor, SleuthkitJNI.CaseDbHandle.AddImageProcess tskAddImageProcess) {
this.progressMonitor = progressMonitor;
this.tskAddImageProcess = tskAddImageProcess;
}
/**
* Updates the progress monitor with the name of the directory currently
* being processed by the SleuthKit add image process.
*/
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
String currDir = tskAddImageProcess.currentDirectory();
if (currDir != null) {
if (!currDir.isEmpty()) {
progressMonitor.setProgressText(
NbBundle.getMessage(this.getClass(), "AddImageTask.run.progress.adding",
currDir));
}
}
/*
* The sleep here throttles the UI updates and provides a
* non-standard mechanism for completing this task by
* interrupting the thread in which it is running.
*
* TODO (AUT-1870): Replace this with giving the task to a
* java.util.concurrent.ScheduledThreadPoolExecutor that is
* shut down when the main task completes.
*/
Thread.sleep(500);
}
} catch (InterruptedException expected) {
}
}
}
}