/*
* Autopsy Forensic Browser
*
* Copyright 2011 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.directorytree;
import java.awt.Component;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentVisitor;
import org.sleuthkit.datamodel.Directory;
import org.sleuthkit.datamodel.FileSystem;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.LayoutFile;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.VirtualDirectory;
import org.sleuthkit.datamodel.Volume;
import org.sleuthkit.datamodel.VolumeSystem;
/**
* Extracts all the unallocated space as a single file
*/
final class ExtractUnallocAction extends AbstractAction {
private final List<UnallocStruct> LstUnallocs = new ArrayList<UnallocStruct>();
private static final List<String> lockedVols = new ArrayList<String>();
private static final List<Long> lockedImages = new ArrayList<Long>();
private long currentImage = 0L;
private static final Logger logger = Logger.getLogger(ExtractUnallocAction.class.getName());
private boolean isImage = false;
public ExtractUnallocAction(String title, Volume volu) {
super(title);
UnallocStruct us = new UnallocStruct(volu);
LstUnallocs.add(us);
}
public ExtractUnallocAction(String title, Image img) {
super(title);
isImage = true;
currentImage = img.getId();
if (hasVolumeSystem(img)) {
for (Volume v : getVolumes(img)) {
UnallocStruct us = new UnallocStruct(v);
LstUnallocs.add(us);
}
} else {
UnallocStruct us = new UnallocStruct(img);
LstUnallocs.add(us);
}
}
/**
* Writes the unallocated files to
* $CaseDir/Export/ImgName-Unalloc-ImgObjectID-VolumeID.dat
*
* @param e
*/
@Override
public void actionPerformed(ActionEvent e) {
if (LstUnallocs != null && LstUnallocs.size() > 0) {
if (lockedImages.contains(currentImage)) {
MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.notifyMsg.unallocAlreadyBeingExtr.msg"));
//JOptionPane.showMessageDialog(new Frame(), "Unallocated Space is already being extracted on this Image. Please select a different Image.");
return;
}
List<UnallocStruct> copyList = new ArrayList<UnallocStruct>() {
{
addAll(LstUnallocs);
}
};
JFileChooser fc = new JFileChooser() {
@Override
public void approveSelection() {
File f = getSelectedFile();
if (!f.exists() && getDialogType() == SAVE_DIALOG || !f.canWrite()) {
JOptionPane.showMessageDialog(this, NbBundle.getMessage(this.getClass(),
"ExtractUnallocAction.msgDlg.folderDoesntExist.msg"));
return;
}
super.approveSelection();
}
};
fc.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory()));
fc.setDialogTitle(
NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.dlgTitle.selectDirToSaveTo.msg"));
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fc.setAcceptAllFileFilterUsed(false);
int returnValue = fc.showSaveDialog((Component) e.getSource());
if (returnValue == JFileChooser.APPROVE_OPTION) {
String destination = fc.getSelectedFile().getPath();
for (UnallocStruct u : LstUnallocs) {
u.setPath(destination);
if (u.llf != null && u.llf.size() > 0 && !lockedVols.contains(u.getFileName())) {
//Format for single Unalloc File is ImgName-Unalloc-ImgObjectID-VolumeID.dat
if (u.FileInstance.exists()) {
int res = JOptionPane.showConfirmDialog(new Frame(), NbBundle.getMessage(this.getClass(),
"ExtractUnallocAction.confDlg.unallocFileAlreadyExist.msg",
u.getFileName()));
if (res == JOptionPane.YES_OPTION) {
u.FileInstance.delete();
} else {
copyList.remove(u);
}
}
if (!isImage & !copyList.isEmpty()) {
ExtractUnallocWorker uw = new ExtractUnallocWorker(u);
uw.execute();
}
} else {
logger.log(Level.WARNING, "Tried to get unallocated content from volume ID but " + u.VolumeId + u.llf == null ? "its list of unallocated files was null" : "the volume is locked"); //NON-NLS
}
}
if (isImage && !copyList.isEmpty()) {
ExtractUnallocWorker uw = new ExtractUnallocWorker(copyList);
uw.execute();
}
}
}
}
/**
* Gets all the unallocated files in a given Content.
*
* @param c Content to get Unallocated Files from
*
* @return A list<LayoutFile> if it didn't crash List may be empty.
*/
private List<LayoutFile> getUnallocFiles(Content c) {
UnallocVisitor uv = new UnallocVisitor();
try {
List<Content> unallocFiles = c.getChildren();
if (null != unallocFiles && unallocFiles.isEmpty() == false) {
return unallocFiles.get(0).accept(uv); //Launching it on the root directory
}
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Couldn't get a list of Unallocated Files, failed at sending out the visitor ", tce); //NON-NLS
}
return Collections.emptyList();
}
/**
* Private class for dispatching the file IO in a background thread.
*/
private class ExtractUnallocWorker extends SwingWorker<Integer, Integer> {
private ProgressHandle progress;
private boolean canceled = false;
private List<UnallocStruct> lus = new ArrayList<UnallocStruct>();
private File currentlyProcessing;
private int totalSizeinMegs;
long totalBytes = 0;
ExtractUnallocWorker(UnallocStruct us) {
//Getting the total megs this worker is going to be doing
if (!lockedVols.contains(us.getFileName())) {
this.lus.add(us);
totalBytes = us.getSizeInBytes();
totalSizeinMegs = toMb(totalBytes);
lockedVols.add(us.getFileName());
}
}
ExtractUnallocWorker(List<UnallocStruct> lst) {
//Getting the total megs this worker is going to be doing
for (UnallocStruct lu : lst) {
if (!lockedVols.contains(lu.getFileName())) {
totalBytes += lu.getSizeInBytes();
lockedVols.add(lu.getFileName());
this.lus.add(lu);
}
}
totalSizeinMegs = toMb(totalBytes);
lockedImages.add(currentImage);
}
private int toMb(long bytes) {
if (bytes > 1024 && (bytes / 1024.0) <= Double.MAX_VALUE) {
double Mb = ((bytes / 1024.0) / 1024.0);//Bytes -> Megabytes
if (Mb <= Integer.MAX_VALUE) {
return (int) Math.ceil(Mb);
}
}
return 0;
}
@Override
protected Integer doInBackground() {
try {
progress = ProgressHandle.createHandle(
NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.progress.extractUnalloc.title"), new Cancellable() {
@Override
public boolean cancel() {
logger.log(Level.INFO, "Canceling extraction of unallocated space"); //NON-NLS
canceled = true;
if (progress != null) {
progress.setDisplayName(NbBundle.getMessage(this.getClass(),
"ExtractUnallocAction.progress.displayName.cancelling.text"));
}
return true;
}
});
int MAX_BYTES = 8192;
byte[] buf = new byte[MAX_BYTES]; //read 8kb at a time
//Begin the actual File IO
progress.start(totalSizeinMegs);
int kbs = 0; //Each completion of the while loop adds one to kbs. 16kb * 64 = 1mb.
int mbs = 0; //Increments every 128th tick of kbs
for (UnallocStruct u : this.lus) {
currentlyProcessing = u.getFile();
logger.log(Level.INFO, "Writing Unalloc file to " + currentlyProcessing.getPath()); //NON-NLS
OutputStream dos = new FileOutputStream(currentlyProcessing);
long bytes = 0;
int i = 0;
while (i < u.getLayouts().size() && bytes != u.getSizeInBytes()) {
LayoutFile f = u.getLayouts().get(i);
long offsetPerFile = 0L;
int bytesRead;
while (offsetPerFile != f.getSize() && !canceled) {
if (++kbs % 128 == 0) {
mbs++;
progress.progress(NbBundle.getMessage(this.getClass(),
"ExtractUnallocAction.processing.counter.msg",
mbs, totalSizeinMegs), mbs - 1);
}
bytesRead = f.read(buf, offsetPerFile, MAX_BYTES);
offsetPerFile += bytesRead;
dos.write(buf, 0, bytesRead);
}
bytes += f.getSize();
i++;
}
dos.flush();
dos.close();
if (canceled) {
u.getFile().delete();
logger.log(Level.INFO, "Canceled extraction of " + u.getFileName() + " and deleted file"); //NON-NLS
} else {
logger.log(Level.INFO, "Finished writing unalloc file " + u.getFile().getPath()); //NON-NLS
}
}
progress.finish();
} catch (IOException ioe) {
logger.log(Level.WARNING, "Could not create Unalloc File; error writing file", ioe); //NON-NLS
return -1;
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Could not create Unalloc File; error getting image info", tce); //NON-NLS
return -1;
}
return 1;
}
@Override
protected void done() {
if (isImage) {
lockedImages.remove(currentImage);
}
for (UnallocStruct u : lus) {
lockedVols.remove(u.getFileName());
}
try {
get();
if (!canceled && !lus.isEmpty()) {
MessageNotifyUtil.Notify.info(NbBundle.getMessage(this.getClass(),
"ExtractUnallocAction.done.notifyMsg.completedExtract.title"),
NbBundle.getMessage(this.getClass(),
"ExtractUnallocAction.done.notifyMsg.completedExtract.msg",
lus.get(0).getFile().getParent()));
}
} catch (InterruptedException | ExecutionException ex) {
MessageNotifyUtil.Notify.error(
NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.done.errMsg.title"),
NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.done.errMsg.msg", ex.getMessage()));
} // catch and ignore if we were cancelled
catch (java.util.concurrent.CancellationException ex) {
}
}
}
/**
* Determines if an image has a volume system or not.
*
* @param img The Image to analyze
*
* @return True if there are Volume Systems present
*/
private boolean hasVolumeSystem(Image img) {
try {
return (img.getChildren().get(0) instanceof VolumeSystem);
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Unable to determine if image has a volume system, extraction may be incomplete", tce); //NON-NLS
return false;
}
}
/**
* Gets the volumes on an given image.
*
* @param img The image to analyze
*
* @return A list of volumes from the image. Returns an empty list if no
* matches.
*/
private List<Volume> getVolumes(Image img) {
List<Volume> lstVol = new ArrayList<Volume>();
try {
for (Content v : img.getChildren().get(0).getChildren()) {
if (v instanceof Volume) {
lstVol.add((Volume) v);
}
}
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Could not get volume information from image. Extraction may be incomplete", tce); //NON-NLS
}
return lstVol;
}
/**
* Private visitor class for going through a Content file and grabbing
* unallocated files.
*/
private static class UnallocVisitor extends ContentVisitor.Default<List<LayoutFile>> {
/**
* If the volume has no FileSystem, then it will call this method to
* return the single instance of unallocated space.
*
* @param lf the LayoutFile the visitor encountered
*
* @return A list<LayoutFile> of size 1
*/
@Override
public List<LayoutFile> visit(final org.sleuthkit.datamodel.LayoutFile lf) {
return new ArrayList<LayoutFile>() {
{
add(lf);
}
};
}
/**
* If the visitor finds a FileSystem, it will filter the results for
* directories and return on the Root Dir.
*
* @param fs the FileSystem the visitor encountered
*
* @return A list<LayoutFile> containing the layout files from
* subsequent Visits(), or an empty list
*/
@Override
public List<LayoutFile> visit(FileSystem fs) {
try {
for (Content c : fs.getChildren()) {
if (((AbstractFile) c).isRoot()) {
return c.accept(this);
}
}
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Couldn't get a list of Unallocated Files, failed at visiting FileSystem " + fs.getId(), tce); //NON-NLS
}
return Collections.emptyList();
}
/**
* LayoutDirectory has all the Layout(Unallocated) files
*
* @param vd VirtualDirectory the visitor encountered
*
* @return A list<LayoutFile> containing all the LayoutFile in ld, or an
* empty list.
*/
@Override
public List<LayoutFile> visit(VirtualDirectory vd) {
try {
List<LayoutFile> lflst = new ArrayList<>();
for (Content layout : vd.getChildren()) {
lflst.add((LayoutFile) layout);
}
return lflst;
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Could not get list of Layout Files, failed at visiting Layout Directory", tce); //NON-NLS
}
return Collections.emptyList();
}
/**
* The only time this visitor should ever encounter a directory is when
* parsing over Root
*
* @param dir the directory this visitor encountered
*
* @return A list<LayoutFile> containing LayoutFiles encountered during
* subsequent Visits(), or an empty list.
*/
@Override
public List<LayoutFile> visit(Directory dir) {
try {
for (Content c : dir.getChildren()) {
if (c instanceof VirtualDirectory) {
return c.accept(this);
}
}
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Couldn't get a list of Unallocated Files, failed at visiting Directory " + dir.getId(), tce); //NON-NLS
}
return Collections.emptyList();
}
@Override
protected List<LayoutFile> defaultVisit(Content cntnt) {
return Collections.emptyList();
}
}
/**
* Comparator for sorting lists of LayoutFiles based on their Object ID
* Ensures that the single Unalloc File is in proper order, and that the
* bytes are continuous.
*/
private class SortObjId implements Comparator<LayoutFile> {
@Override
public int compare(LayoutFile o1, LayoutFile o2) {
if (o1.getId() == o2.getId()) {
return 0;
}
if (o1.getId() > o2.getId()) {
return 1;
} else {
return -1;
}
}
}
/**
* Private class for assisting in the running the action over an image with
* multiple volumes.
*/
private class UnallocStruct {
private List<LayoutFile> llf;
private long SizeInBytes;
private long VolumeId;
private long ImageId;
private String ImageName;
private String FileName;
private File FileInstance;
/**
* Contingency constructor in event no VolumeSystem exists on an Image.
*
* @param img Image file to be analyzed
*/
UnallocStruct(Image img) {
this.llf = getUnallocFiles(img);
Collections.sort(llf, new SortObjId());
this.VolumeId = 0;
this.ImageId = img.getId();
this.ImageName = img.getName();
this.FileName = this.ImageName + "-Unalloc-" + this.ImageId + "-" + 0 + ".dat"; //NON-NLS
this.FileInstance = new File(Case.getCurrentCase().getExportDirectory() + File.separator + this.FileName);
this.SizeInBytes = calcSizeInBytes();
}
/**
* Default constructor for extracting info from Volumes.
*
* @param volu Volume file to be analyzed
*/
UnallocStruct(Volume volu) {
try {
this.ImageName = volu.getDataSource().getName();
this.ImageId = volu.getDataSource().getId();
this.VolumeId = volu.getId();
} catch (TskCoreException tce) {
logger.log(Level.WARNING, "Unable to properly create ExtractUnallocAction, extraction may be incomplete", tce); //NON-NLS
this.ImageName = "";
this.ImageId = 0;
}
this.FileName = this.ImageName + "-Unalloc-" + this.ImageId + "-" + VolumeId + ".dat"; //NON-NLS
this.FileInstance = new File(Case.getCurrentCase().getExportDirectory() + File.separator + this.FileName);
this.llf = getUnallocFiles(volu);
Collections.sort(llf, new SortObjId());
this.SizeInBytes = calcSizeInBytes();
}
//Getters
int size() {
return llf.size();
}
private long calcSizeInBytes() {
long size = 0L;
for (LayoutFile f : llf) {
size += f.getSize();
}
return size;
}
long getSizeInBytes() {
return this.SizeInBytes;
}
long getVolumeId() {
return this.VolumeId;
}
long getImageId() {
return this.ImageId;
}
String getImageName() {
return this.ImageName;
}
List<LayoutFile> getLayouts() {
return this.llf;
}
String getFileName() {
return this.FileName;
}
File getFile() {
return this.FileInstance;
}
void setPath(String path) {
this.FileInstance = new File(path + File.separator + this.FileName);
}
}
}