Autopsy  4.19.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
ChromeCacheExtractor.java
Go to the documentation of this file.
1 /*
2  *
3  * Autopsy Forensic Browser
4  *
5  * Copyright 2019 Basis Technology Corp.
6  *
7  * Project Contact/Architect: carrier <at> sleuthkit <dot> org
8  *
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  * http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  */
21 package org.sleuthkit.autopsy.recentactivity;
22 
23 import java.io.File;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.RandomAccessFile;
27 import java.nio.ByteBuffer;
28 import java.nio.ByteOrder;
29 import java.nio.channels.FileChannel;
30 import java.nio.charset.Charset;
31 import java.nio.file.Path;
32 import java.nio.file.Paths;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Map.Entry;
42 import java.util.Optional;
43 import java.util.logging.Level;
44 import org.openide.util.NbBundle;
45 import org.openide.util.NbBundle.Messages;
57 import org.sleuthkit.datamodel.AbstractFile;
58 import org.sleuthkit.datamodel.Blackboard;
59 import org.sleuthkit.datamodel.BlackboardArtifact;
60 import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
61 import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE;
62 import org.sleuthkit.datamodel.BlackboardAttribute;
63 import org.sleuthkit.datamodel.Content;
64 import org.sleuthkit.datamodel.DerivedFile;
65 import org.sleuthkit.datamodel.OsAccount;
66 import org.sleuthkit.datamodel.TimeUtilities;
67 import org.sleuthkit.datamodel.TskCoreException;
68 import org.sleuthkit.datamodel.TskData;
69 import org.sleuthkit.datamodel.TskException;
70 
95 final class ChromeCacheExtractor {
96 
97  private final static String DEFAULT_CACHE_PATH_STR = "default/cache"; //NON-NLS
98  private final static String BROTLI_MIMETYPE ="application/x-brotli"; //NON-NLS
99 
100  private final static long UINT32_MASK = 0xFFFFFFFFl;
101 
102  private final static int INDEXFILE_HDR_SIZE = 92*4;
103  private final static int DATAFILE_HDR_SIZE = 8192;
104 
105  private final static Logger logger = Logger.getLogger(ChromeCacheExtractor.class.getName());
106 
107  private static final String VERSION_NUMBER = "1.0.0"; //NON-NLS
108  private final String moduleName;
109 
110  private String absOutputFolderName;
111  private String relOutputFolderName;
112 
113  private final Content dataSource;
114  private final IngestJobContext context;
115  private final DataSourceIngestModuleProgress progressBar;
116  private final IngestServices services = IngestServices.getInstance();
117  private Case currentCase;
118  private FileManager fileManager;
119 
120  // A file table to cache copies of index and data_n files.
121  private final Map<String, FileWrapper> fileCopyCache = new HashMap<>();
122 
123  // A file table to cache the f_* files.
124  private final Map<String, AbstractFile> externalFilesTable = new HashMap<>();
125 
131  final class FileWrapper {
132  private final AbstractFile abstractFile;
133  private final RandomAccessFile fileCopy;
134  private final ByteBuffer byteBuffer;
135 
136  FileWrapper (AbstractFile abstractFile, RandomAccessFile fileCopy, ByteBuffer buffer ) {
137  this.abstractFile = abstractFile;
138  this.fileCopy = fileCopy;
139  this.byteBuffer = buffer;
140  }
141 
142  public RandomAccessFile getFileCopy() {
143  return fileCopy;
144  }
145  public ByteBuffer getByteBuffer() {
146  return byteBuffer;
147  }
148  AbstractFile getAbstractFile() {
149  return abstractFile;
150  }
151  }
152 
153  @NbBundle.Messages({
154  "ChromeCacheExtractor.moduleName=ChromeCacheExtractor",
155  "# {0} - module name",
156  "# {1} - row number",
157  "# {2} - table length",
158  "# {3} - cache path",
159  "ChromeCacheExtractor.progressMsg={0}: Extracting cache entry {1} of {2} entries from {3}"
160  })
161  ChromeCacheExtractor(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar ) {
162  moduleName = Bundle.ChromeCacheExtractor_moduleName();
163  this.dataSource = dataSource;
164  this.context = context;
165  this.progressBar = progressBar;
166  }
167 
168 
174  private void moduleInit() throws IngestModuleException {
175 
176  try {
177  currentCase = Case.getCurrentCaseThrows();
178  fileManager = currentCase.getServices().getFileManager();
179 
180  } catch (NoCurrentCaseException ex) {
181  String msg = "Failed to get current case."; //NON-NLS
182  throw new IngestModuleException(msg, ex);
183  }
184  }
185 
193  private void resetForNewCacheFolder(String cachePath) throws IngestModuleException {
194 
195  fileCopyCache.clear();
196  externalFilesTable.clear();
197 
198  String cacheAbsOutputFolderName = this.getAbsOutputFolderName() + cachePath;
199  File outDir = new File(cacheAbsOutputFolderName);
200  if (outDir.exists() == false) {
201  outDir.mkdirs();
202  }
203 
204  String cacheTempPath = RAImageIngestModule.getRATempPath(currentCase, moduleName, context.getJobId()) + cachePath;
205  File tempDir = new File(cacheTempPath);
206  if (tempDir.exists() == false) {
207  tempDir.mkdirs();
208  }
209  }
210 
217  private void cleanup () {
218 
219  for (Entry<String, FileWrapper> entry : this.fileCopyCache.entrySet()) {
220  Path tempFilePath = Paths.get(RAImageIngestModule.getRATempPath(currentCase, moduleName, context.getJobId()), entry.getKey() );
221  try {
222  entry.getValue().getFileCopy().getChannel().close();
223  entry.getValue().getFileCopy().close();
224 
225  File tmpFile = tempFilePath.toFile();
226  if (!tmpFile.delete()) {
227  tmpFile.deleteOnExit();
228  }
229  } catch (IOException ex) {
230  logger.log(Level.WARNING, String.format("Failed to delete cache file copy %s", tempFilePath.toString()), ex); //NON-NLS
231  }
232  }
233  }
234 
240  private String getAbsOutputFolderName() {
241  return absOutputFolderName;
242  }
243 
249  private String getRelOutputFolderName() {
250  return relOutputFolderName;
251  }
252 
259  void processCaches() {
260 
261  try {
262  moduleInit();
263  } catch (IngestModuleException ex) {
264  String msg = "Failed to initialize ChromeCacheExtractor."; //NON-NLS
265  logger.log(Level.SEVERE, msg, ex);
266  return;
267  }
268 
269  // Find and process the cache folders. There could be one per user
270  try {
271  // Identify each cache folder by searching for the index files in each
272  List<AbstractFile> indexFiles = findIndexFiles();
273 
274  if (indexFiles.size() > 0) {
275  // Create an output folder to save any derived files
276  absOutputFolderName = RAImageIngestModule.getRAOutputPath(currentCase, moduleName, context.getJobId());
277  relOutputFolderName = Paths.get(RAImageIngestModule.getRelModuleOutputPath(currentCase, moduleName, context.getJobId())).normalize().toString();
278 
279  File dir = new File(absOutputFolderName);
280  if (dir.exists() == false) {
281  dir.mkdirs();
282  }
283  }
284 
285  // Process each of the cache folders
286  for (AbstractFile indexFile: indexFiles) {
287 
288  if (context.dataSourceIngestIsCancelled()) {
289  return;
290  }
291 
292  if (indexFile.getSize() > 0) {
293  processCacheFolder(indexFile);
294  }
295  }
296 
297  } catch (TskCoreException ex) {
298  String msg = "Failed to find cache index files"; //NON-NLS
299  logger.log(Level.WARNING, msg, ex);
300  }
301  }
302 
303  @Messages({
304  "ChromeCacheExtract_adding_extracted_files_msg=Chrome Cache: Adding %d extracted files for analysis.",
305  "ChromeCacheExtract_adding_artifacts_msg=Chrome Cache: Adding %d artifacts for analysis.",
306  "ChromeCacheExtract_loading_files_msg=Chrome Cache: Loading files from %s."
307  })
308 
315  private void processCacheFolder(AbstractFile indexFile) {
316 
317  String cacheFolderName = indexFile.getParentPath();
318  Optional<FileWrapper> indexFileWrapper;
319 
320  /*
321  * The first part of this method is all about finding the needed files in the cache
322  * folder and making internal copies/caches of them so that we can later process them
323  * and effeciently look them up.
324  */
325  try {
326  progressBar.progress(String.format(Bundle.ChromeCacheExtract_loading_files_msg(), cacheFolderName));
327  resetForNewCacheFolder(cacheFolderName);
328 
329  // @@@ This is little ineffecient because we later in this call search for the AbstractFile that we currently have
330  // Load the index file into the caches
331  indexFileWrapper = findDataOrIndexFile(indexFile.getName(), cacheFolderName);
332  if (!indexFileWrapper.isPresent()) {
333  String msg = String.format("Failed to find copy cache index file %s", indexFile.getUniquePath());
334  logger.log(Level.WARNING, msg);
335  return;
336  }
337 
338 
339  // load the data files into the internal cache. We do this because we often
340  // jump in between the various data_X files resolving segments
341  for (int i = 0; i < 4; i ++) {
342  Optional<FileWrapper> dataFile = findDataOrIndexFile(String.format("data_%1d",i), cacheFolderName );
343  if (!dataFile.isPresent()) {
344  return;
345  }
346  }
347 
348  // find all f_* files in a single query and load them into the cache
349  // we do this here so that it is a single query instead of hundreds of individual ones
350  findExternalFiles(cacheFolderName);
351 
352  } catch (TskCoreException | IngestModuleException ex) {
353  String msg = "Failed to find cache files in path " + cacheFolderName; //NON-NLS
354  logger.log(Level.WARNING, msg, ex);
355  return;
356  }
357 
358  /*
359  * Now the analysis begins. We parse the index file and that drives parsing entries
360  * from data_X or f_XXXX files.
361  */
362  logger.log(Level.INFO, "{0}- Now reading Cache index file from path {1}", new Object[]{moduleName, cacheFolderName }); //NON-NLS
363 
364  List<AbstractFile> derivedFiles = new ArrayList<>();
365  Collection<BlackboardArtifact> artifactsAdded = new ArrayList<>();
366 
367  ByteBuffer indexFileROBuffer = indexFileWrapper.get().getByteBuffer();
368  IndexFileHeader indexHdr = new IndexFileHeader(indexFileROBuffer);
369 
370  // seek past the header
371  indexFileROBuffer.position(INDEXFILE_HDR_SIZE);
372 
373  try {
374  /* Cycle through index and get the CacheAddress for each CacheEntry. Process each entry
375  * to extract data, add artifacts, etc. from the f_XXXX and data_x files */
376  for (int i = 0; i < indexHdr.getTableLen(); i++) {
377 
378  if (context.dataSourceIngestIsCancelled()) {
379  cleanup();
380  return;
381  }
382 
383  CacheAddress addr = new CacheAddress(indexFileROBuffer.getInt() & UINT32_MASK, cacheFolderName);
384  if (addr.isInitialized()) {
385  progressBar.progress(NbBundle.getMessage(this.getClass(),
386  "ChromeCacheExtractor.progressMsg",
387  moduleName, i, indexHdr.getTableLen(), cacheFolderName) );
388  try {
389  List<DerivedFile> addedFiles = processCacheEntry(addr, artifactsAdded);
390  derivedFiles.addAll(addedFiles);
391  }
392  catch (TskCoreException | IngestModuleException ex) {
393  logger.log(Level.WARNING, String.format("Failed to get cache entry at address %s for file with object ID %d (%s)", addr, indexFile.getId(), ex.getLocalizedMessage())); //NON-NLS
394  }
395  }
396  }
397  } catch (java.nio.BufferUnderflowException ex) {
398  logger.log(Level.WARNING, String.format("Ran out of data unexpectedly reading file %s (ObjID: %d)", indexFile.getName(), indexFile.getId()));
399  }
400 
401  if (context.dataSourceIngestIsCancelled()) {
402  cleanup();
403  return;
404  }
405 
406 
407  // notify listeners of new files and schedule for analysis
408  progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_extracted_files_msg(), derivedFiles.size()));
409  derivedFiles.forEach((derived) -> {
410  services.fireModuleContentEvent(new ModuleContentEvent(derived));
411  });
412  context.addFilesToJob(derivedFiles);
413 
414  // notify listeners about new artifacts
415  progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_artifacts_msg(), artifactsAdded.size()));
416  Blackboard blackboard = currentCase.getSleuthkitCase().getBlackboard();
417  try {
418  blackboard.postArtifacts(artifactsAdded, moduleName);
419  } catch (Blackboard.BlackboardException ex) {
420  logger.log(Level.WARNING, String.format("Failed to post cacheIndex artifacts "), ex); //NON-NLS
421  }
422 
423  cleanup();
424  }
425 
438  private List<DerivedFile> processCacheEntry(CacheAddress cacheAddress, Collection<BlackboardArtifact> artifactsAdded ) throws TskCoreException, IngestModuleException {
439 
440  List<DerivedFile> derivedFiles = new ArrayList<>();
441 
442  // get the path to the corresponding data_X file for the cache entry
443  String cacheEntryFileName = cacheAddress.getFilename();
444  String cachePath = cacheAddress.getCachePath();
445 
446  Optional<FileWrapper> cacheEntryFileOptional = findDataOrIndexFile(cacheEntryFileName, cachePath);
447  if (!cacheEntryFileOptional.isPresent()) {
448  String msg = String.format("Failed to find data file %s", cacheEntryFileName); //NON-NLS
449  throw new IngestModuleException(msg);
450  }
451 
452  // Load the entry to get its metadata, segments, etc.
453  CacheEntry cacheEntry = new CacheEntry(cacheAddress, cacheEntryFileOptional.get() );
454  List<CacheDataSegment> dataSegments = cacheEntry.getDataSegments();
455 
456  // Only process the first payload data segment in each entry
457  // first data segement has the HTTP headers, 2nd is the payload
458  if (dataSegments.size() < 2) {
459  return derivedFiles;
460  }
461  CacheDataSegment dataSegment = dataSegments.get(1);
462 
463  // Name where segment is located (could be diffrent from where entry was located)
464  String segmentFileName = dataSegment.getCacheAddress().getFilename();
465  Optional<AbstractFile> segmentFileAbstractFile = findAbstractFile(segmentFileName, cachePath);
466  if (!segmentFileAbstractFile.isPresent()) {
467  logger.log(Level.WARNING, "Error finding segment file: " + cachePath + "/" + segmentFileName); //NON-NLS
468  return derivedFiles;
469  }
470 
471  boolean isBrotliCompressed = false;
472  if (dataSegment.getType() != CacheDataTypeEnum.HTTP_HEADER && cacheEntry.isBrotliCompressed() ) {
473  isBrotliCompressed = true;
474  }
475 
476 
477  // Make artifacts around the cached item and extract data from data_X file
478  try {
479  AbstractFile cachedItemFile; //
480  /* If the cached data is in a f_XXXX file, we only need to make artifacts. */
481  if (dataSegment.isInExternalFile() ) {
482  cachedItemFile = segmentFileAbstractFile.get();
483  }
484  /* If the data is in a data_X file, we need to extract it out and then make the similar artifacts */
485  else {
486 
487  // Data segments in "data_x" files are saved in individual files and added as derived files
488  String filename = dataSegment.save();
489  String relPathname = getRelOutputFolderName() + dataSegment.getCacheAddress().getCachePath() + filename;
490 
491  // @@@ We should batch these up and do them in one big insert / transaction
492  DerivedFile derivedFile = fileManager.addDerivedFile(filename, relPathname,
493  dataSegment.getDataLength(),
494  cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), // TBD
495  true,
496  segmentFileAbstractFile.get(),
497  "",
498  moduleName,
499  VERSION_NUMBER,
500  "",
501  TskData.EncodingType.NONE);
502 
503  derivedFiles.add(derivedFile);
504  cachedItemFile = derivedFile;
505  }
506 
507  addArtifacts(cacheEntry, cacheEntryFileOptional.get().getAbstractFile(), cachedItemFile, artifactsAdded);
508 
509  // Tika doesn't detect these types. So, make sure they have the correct MIME type */
510  if (isBrotliCompressed) {
511  cachedItemFile.setMIMEType(BROTLI_MIMETYPE);
512  cachedItemFile.save();
513  }
514 
515  } catch (TskException ex) {
516  logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
517  }
518 
519  return derivedFiles;
520  }
521 
531  private void addArtifacts(CacheEntry cacheEntry, AbstractFile cacheEntryFile, AbstractFile cachedItemFile, Collection<BlackboardArtifact> artifactsAdded) throws TskCoreException {
532 
533  // Create a TSK_WEB_CACHE entry with the parent as data_X file that had the cache entry
534  Collection<BlackboardAttribute> webAttr = new ArrayList<>();
535  String url = cacheEntry.getKey() != null ? cacheEntry.getKey() : "";
536  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
537  moduleName, url));
538  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
539  moduleName, NetworkUtils.extractDomain(url)));
540  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
541  moduleName, cacheEntry.getCreationTime()));
542  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HEADERS,
543  moduleName, cacheEntry.getHTTPHeaders()));
544  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
545  moduleName, cachedItemFile.getUniquePath()));
546  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
547  moduleName, cachedItemFile.getId()));
548 
549  BlackboardArtifact webCacheArtifact = cacheEntryFile.newDataArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_CACHE), webAttr);
550  artifactsAdded.add(webCacheArtifact);
551 
552  // Create a TSK_ASSOCIATED_OBJECT on the f_XXX or derived file file back to the CACHE entry
553  BlackboardArtifact associatedObjectArtifact = cachedItemFile.newDataArtifact(
554  new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT),
555  Arrays.asList(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
556  moduleName, webCacheArtifact.getArtifactID())));
557 
558  artifactsAdded.add(associatedObjectArtifact);
559  }
560 
569  private void findExternalFiles(String cachePath) throws TskCoreException {
570 
571  List<AbstractFile> effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS
572  for (AbstractFile abstractFile : effFiles ) {
573  String cacheKey = cachePath + abstractFile.getName();
574  if (cachePath.equals(abstractFile.getParentPath()) && abstractFile.isFile()) {
575  // Don't overwrite an allocated version with an unallocated version
576  if (abstractFile.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)
577  || !externalFilesTable.containsKey(cacheKey)) {
578  this.externalFilesTable.put(cacheKey, abstractFile);
579  }
580  }
581  }
582  }
591  private Optional<AbstractFile> findAbstractFile(String cacheFileName, String cacheFolderName) throws TskCoreException {
592 
593  // see if it is cached
594  String fileTableKey = cacheFolderName + cacheFileName;
595 
596  if (cacheFileName != null) {
597  if (cacheFileName.startsWith("f_") && externalFilesTable.containsKey(fileTableKey)) {
598  return Optional.of(externalFilesTable.get(fileTableKey));
599  }
600  } else {
601  return Optional.empty();
602  }
603 
604  if (fileCopyCache.containsKey(fileTableKey)) {
605  return Optional.of(fileCopyCache.get(fileTableKey).getAbstractFile());
606  }
607 
608  List<AbstractFile> cacheFiles = currentCase.getSleuthkitCase().getFileManager().findFilesExactNameExactPath(dataSource,
609  cacheFileName, cacheFolderName);
610  if (!cacheFiles.isEmpty()) {
611  // Sort the list for consistency. Preference is:
612  // - In correct subfolder and allocated
613  // - In correct subfolder and unallocated
614  // - In incorrect subfolder and allocated
615  Collections.sort(cacheFiles, new Comparator<AbstractFile>() {
616  @Override
617  public int compare(AbstractFile file1, AbstractFile file2) {
618  try {
619  if (file1.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)
620  && ! file2.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
621  return -1;
622  }
623 
624  if (file2.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)
625  && ! file1.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
626  return 1;
627  }
628  } catch (TskCoreException ex) {
629  logger.log(Level.WARNING, "Error getting unique path for file with ID " + file1.getId() + " or " + file2.getId(), ex);
630  }
631 
632  if (file1.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)
633  && ! file2.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) {
634  return -1;
635  }
636  if (file2.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)
637  && ! file1.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) {
638  return 1;
639  }
640 
641  return Long.compare(file1.getId(), file2.getId());
642  }
643  });
644 
645  // The best match will be the first element
646  return Optional.of(cacheFiles.get(0));
647  }
648 
649  return Optional.empty();
650  }
651 
659  private List<AbstractFile> findIndexFiles() throws TskCoreException {
660  return fileManager.findFiles(dataSource, "index", DEFAULT_CACHE_PATH_STR); //NON-NLS
661  }
662 
663 
664 
676  private Optional<FileWrapper> findDataOrIndexFile(String cacheFileName, String cacheFolderName) throws TskCoreException, IngestModuleException {
677 
678  // Check if the file is already in the cache
679  String fileTableKey = cacheFolderName + cacheFileName;
680  if (fileCopyCache.containsKey(fileTableKey)) {
681  return Optional.of(fileCopyCache.get(fileTableKey));
682  }
683 
684  // Use Autopsy to get the AbstractFile
685  Optional<AbstractFile> abstractFileOptional = findAbstractFile(cacheFileName, cacheFolderName);
686  if (!abstractFileOptional.isPresent()) {
687  return Optional.empty();
688  }
689 
690  // Wrap the file so that we can get the ByteBuffer later.
691  // @@@ BC: I think this should nearly all go into FileWrapper and be done lazily and perhaps based on size.
692  // Many of the files are small enough to keep in memory for the ByteBuffer
693 
694  // write the file to disk so that we can have a memory-mapped ByteBuffer
695  AbstractFile cacheFile = abstractFileOptional.get();
696  RandomAccessFile randomAccessFile = null;
697  String tempFilePathname = RAImageIngestModule.getRATempPath(currentCase, moduleName, context.getJobId()) + cacheFolderName + cacheFile.getName(); //NON-NLS
698  try {
699  File newFile = new File(tempFilePathname);
700  ContentUtils.writeToFile(cacheFile, newFile, context::dataSourceIngestIsCancelled);
701 
702  randomAccessFile = new RandomAccessFile(tempFilePathname, "r");
703  FileChannel roChannel = randomAccessFile.getChannel();
704  ByteBuffer cacheFileROBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0,
705  (int) roChannel.size());
706 
707  cacheFileROBuf.order(ByteOrder.nativeOrder());
708  FileWrapper cacheFileWrapper = new FileWrapper(cacheFile, randomAccessFile, cacheFileROBuf );
709 
710  if (!cacheFileName.startsWith("f_")) {
711  fileCopyCache.put(cacheFolderName + cacheFileName, cacheFileWrapper);
712  }
713 
714  return Optional.of(cacheFileWrapper);
715  }
716  catch (IOException ex) {
717 
718  try {
719  if (randomAccessFile != null) {
720  randomAccessFile.close();
721  }
722  }
723  catch (IOException ex2) {
724  logger.log(Level.SEVERE, "Error while trying to close temp file after exception.", ex2); //NON-NLS
725  }
726  String msg = String.format("Error reading/copying Chrome cache file '%s' (id=%d).", //NON-NLS
727  cacheFile.getName(), cacheFile.getId());
728  throw new IngestModuleException(msg, ex);
729  }
730  }
731 
735  final class IndexFileHeader {
736 
737  private final long magic;
738  private final int version;
739  private final int numEntries;
740  private final int numBytes;
741  private final int lastFile;
742  private final int tableLen;
743 
744  IndexFileHeader(ByteBuffer indexFileROBuf) {
745 
746  magic = indexFileROBuf.getInt() & UINT32_MASK;
747 
748  indexFileROBuf.position(indexFileROBuf.position()+2);
749 
750  version = indexFileROBuf.getShort();
751  numEntries = indexFileROBuf.getInt();
752  numBytes = indexFileROBuf.getInt();
753  lastFile = indexFileROBuf.getInt();
754 
755  indexFileROBuf.position(indexFileROBuf.position()+4); // this_id
756  indexFileROBuf.position(indexFileROBuf.position()+4); // stats cache cacheAddress
757 
758  tableLen = indexFileROBuf.getInt();
759  }
760 
761  public long getMagic() {
762  return magic;
763  }
764 
765  public int getVersion() {
766  return version;
767  }
768 
769  public int getNumEntries() {
770  return numEntries;
771  }
772 
773  public int getNumBytes() {
774  return numBytes;
775  }
776 
777  public int getLastFile() {
778  return lastFile;
779  }
780 
781  public int getTableLen() {
782  return tableLen;
783  }
784 
785  @Override
786  public String toString() {
787  StringBuilder sb = new StringBuilder();
788 
789  sb.append(String.format("Index Header:"))
790  .append(String.format("\tMagic = %x" , getMagic()) )
791  .append(String.format("\tVersion = %x" , getVersion()) )
792  .append(String.format("\tNumEntries = %x" , getNumEntries()) )
793  .append(String.format("\tNumBytes = %x" , getNumBytes()) )
794  .append(String.format("\tLastFile = %x" , getLastFile()) )
795  .append(String.format("\tTableLen = %x" , getTableLen()) );
796 
797  return sb.toString();
798  }
799  }
800 
804  enum CacheFileTypeEnum {
805  EXTERNAL,
806  RANKINGS,
807  BLOCK_256,
808  BLOCK_1K,
809  BLOCK_4K,
810  BLOCK_FILES,
811  BLOCK_ENTRIES,
812  BLOCK_EVICTED
813  }
814 
815 
816 
839  final class CacheAddress {
840  // sundry constants to parse the bit fields
841  private static final long ADDR_INITIALIZED_MASK = 0x80000000l;
842  private static final long FILE_TYPE_MASK = 0x70000000;
843  private static final long FILE_TYPE_OFFSET = 28;
844  private static final long NUM_BLOCKS_MASK = 0x03000000;
845  private static final long NUM_BLOCKS_OFFSET = 24;
846  private static final long FILE_SELECTOR_MASK = 0x00ff0000;
847  private static final long FILE_SELECTOR_OFFSET = 16;
848  private static final long START_BLOCK_MASK = 0x0000FFFF;
849  private static final long EXTERNAL_FILE_NAME_MASK = 0x0FFFFFFF;
850 
851  private final long uint32CacheAddr;
852  private final CacheFileTypeEnum fileType;
853  private final int numBlocks;
854  private final int startBlock;
855  private final String fileName;
856  private final int fileNumber;
857 
858  private final String cachePath;
859 
860 
866  CacheAddress(long uint32, String cachePath) {
867 
868  uint32CacheAddr = uint32;
869  this.cachePath = cachePath;
870 
871 
872  // analyze the
873  int fileTypeEnc = (int)(uint32CacheAddr & FILE_TYPE_MASK) >> FILE_TYPE_OFFSET;
874  fileType = CacheFileTypeEnum.values()[fileTypeEnc];
875 
876  if (isInitialized()) {
877  if (isInExternalFile()) {
878  fileNumber = (int)(uint32CacheAddr & EXTERNAL_FILE_NAME_MASK);
879  fileName = String.format("f_%06x", getFileNumber() );
880  numBlocks = 0;
881  startBlock = 0;
882  } else {
883  fileNumber = (int)((uint32CacheAddr & FILE_SELECTOR_MASK) >> FILE_SELECTOR_OFFSET);
884  fileName = String.format("data_%d", getFileNumber() );
885  numBlocks = (int)(uint32CacheAddr & NUM_BLOCKS_MASK >> NUM_BLOCKS_OFFSET);
886  startBlock = (int)(uint32CacheAddr & START_BLOCK_MASK);
887  }
888  }
889  else {
890  fileName = null;
891  fileNumber = 0;
892  numBlocks = 0;
893  startBlock = 0;
894  }
895  }
896 
897  boolean isInitialized() {
898  return ((uint32CacheAddr & ADDR_INITIALIZED_MASK) != 0);
899  }
900 
901  CacheFileTypeEnum getFileType() {
902  return fileType;
903  }
904 
909  String getFilename() {
910  return fileName;
911  }
912 
913  String getCachePath() {
914  return cachePath;
915  }
916 
917  boolean isInExternalFile() {
918  return (fileType == CacheFileTypeEnum.EXTERNAL);
919  }
920 
921  int getFileNumber() {
922  return fileNumber;
923  }
924 
925  int getStartBlock() {
926  return startBlock;
927  }
928 
929  int getNumBlocks() {
930  return numBlocks;
931  }
932 
933  int getBlockSize() {
934  switch (fileType) {
935  case RANKINGS:
936  return 36;
937  case BLOCK_256:
938  return 256;
939  case BLOCK_1K:
940  return 1024;
941  case BLOCK_4K:
942  return 4096;
943  case BLOCK_FILES:
944  return 8;
945  case BLOCK_ENTRIES:
946  return 104;
947  case BLOCK_EVICTED:
948  return 48;
949  default:
950  return 0;
951  }
952  }
953 
954  public long getUint32CacheAddr() {
955  return uint32CacheAddr;
956  }
957 
958  @Override
959  public String toString() {
960  StringBuilder sb = new StringBuilder();
961  sb.append(String.format("CacheAddr %08x : %s : filename %s",
962  uint32CacheAddr,
963  isInitialized() ? "Initialized" : "UnInitialized",
964  getFilename()));
965 
966  if ((fileType == CacheFileTypeEnum.BLOCK_256) ||
967  (fileType == CacheFileTypeEnum.BLOCK_1K) ||
968  (fileType == CacheFileTypeEnum.BLOCK_4K) ) {
969  sb.append(String.format(" (%d blocks starting at %08X)",
970  this.getNumBlocks(),
971  this.getStartBlock()
972  ));
973  }
974 
975  return sb.toString();
976  }
977 
978  }
979 
983  enum CacheDataTypeEnum {
984  HTTP_HEADER,
985  UNKNOWN,
986  };
987 
997  final class CacheDataSegment {
998 
999  private int length;
1000  private final CacheAddress cacheAddress;
1001  private CacheDataTypeEnum type;
1002 
1003  private boolean isHTTPHeaderHint;
1004 
1005  private FileWrapper cacheFileCopy = null;
1006  private byte[] data = null;
1007 
1008  private String httpResponse;
1009  private final Map<String, String> httpHeaders = new HashMap<>();
1010 
1011  CacheDataSegment(CacheAddress cacheAddress, int len) {
1012  this(cacheAddress, len, false);
1013  }
1014 
1015  CacheDataSegment(CacheAddress cacheAddress, int len, boolean isHTTPHeader ) {
1016  this.type = CacheDataTypeEnum.UNKNOWN;
1017  this.length = len;
1018  this.cacheAddress = cacheAddress;
1019  this.isHTTPHeaderHint = isHTTPHeader;
1020  }
1021 
1022  boolean isInExternalFile() {
1023  return cacheAddress.isInExternalFile();
1024  }
1025 
1026  boolean hasHTTPHeaders() {
1027  return this.type == CacheDataTypeEnum.HTTP_HEADER;
1028  }
1029 
1030  String getHTTPHeader(String key) {
1031  return this.httpHeaders.get(key);
1032  }
1033 
1039  String getHTTPHeaders() {
1040  if (!hasHTTPHeaders()) {
1041  return "";
1042  }
1043 
1044  StringBuilder sb = new StringBuilder();
1045  httpHeaders.entrySet().forEach((entry) -> {
1046  if (sb.length() > 0) {
1047  sb.append(" \n");
1048  }
1049  sb.append(String.format("%s : %s",
1050  entry.getKey(), entry.getValue()));
1051  });
1052 
1053  return sb.toString();
1054  }
1055 
1056  String getHTTPRespone() {
1057  return httpResponse;
1058  }
1059 
1065  void extract() throws TskCoreException, IngestModuleException {
1066 
1067  // do nothing if already extracted,
1068  if (data != null) {
1069  return;
1070  }
1071 
1072  // Don't extract data from external files.
1073  if (!cacheAddress.isInExternalFile()) {
1074 
1075  if (cacheAddress.getFilename() == null) {
1076  throw new TskCoreException("Cache address has no file name");
1077  }
1078 
1079  cacheFileCopy = findDataOrIndexFile(cacheAddress.getFilename(), cacheAddress.getCachePath()).get();
1080 
1081  this.data = new byte [length];
1082  ByteBuffer buf = cacheFileCopy.getByteBuffer();
1083  int dataOffset = DATAFILE_HDR_SIZE + cacheAddress.getStartBlock() * cacheAddress.getBlockSize();
1084  if (dataOffset > buf.capacity()) {
1085  return;
1086  }
1087  buf.position(dataOffset);
1088  buf.get(data, 0, length);
1089 
1090  // if this might be a HTPP header, lets try to parse it as such
1091  if ((isHTTPHeaderHint)) {
1092  String strData = new String(data);
1093  if (strData.contains("HTTP")) {
1094 
1095  // Http headers if present, are usually in frst data segment in an entry
1096  // General Parsing algo:
1097  // - Find start of HTTP header by searching for string "HTTP"
1098  // - Skip to the first 0x00 to get to the end of the HTTP response line, this makrs start of headers section
1099  // - Find the end of the header by searching for 0x00 0x00 bytes
1100  // - Extract the headers section
1101  // - Parse the headers section - each null terminated string is a header
1102  // - Each header is of the format "name: value" e.g.
1103 
1104  type = CacheDataTypeEnum.HTTP_HEADER;
1105 
1106  int startOff = strData.indexOf("HTTP");
1107  Charset charset = Charset.forName("UTF-8");
1108  boolean done = false;
1109  int i = startOff;
1110  int hdrNum = 1;
1111 
1112  while (!done) {
1113  // each header is null terminated
1114  int start = i;
1115  while (i < data.length && data[i] != 0) {
1116  i++;
1117  }
1118 
1119  // http headers are terminated by 0x00 0x00
1120  if (i == data.length || data[i+1] == 0) {
1121  done = true;
1122  }
1123 
1124  int len = (i - start);
1125  String headerLine = new String(data, start, len, charset);
1126 
1127  // first line is the http response
1128  if (hdrNum == 1) {
1129  httpResponse = headerLine;
1130  } else {
1131  int nPos = headerLine.indexOf(':');
1132  if (nPos > 0 ) {
1133  String key = headerLine.substring(0, nPos);
1134  String val= headerLine.substring(nPos+1);
1135  httpHeaders.put(key.toLowerCase(), val);
1136  }
1137  }
1138 
1139  i++;
1140  hdrNum++;
1141  }
1142  }
1143  }
1144  }
1145  }
1146 
1147  String getDataString() throws TskCoreException, IngestModuleException {
1148  if (data == null) {
1149  extract();
1150  }
1151  return new String(data);
1152  }
1153 
1154  byte[] getDataBytes() throws TskCoreException, IngestModuleException {
1155  if (data == null) {
1156  extract();
1157  }
1158  return data.clone();
1159  }
1160 
1161  int getDataLength() {
1162  return this.length;
1163  }
1164 
1165  CacheDataTypeEnum getType() {
1166  return type;
1167  }
1168 
1169  CacheAddress getCacheAddress() {
1170  return cacheAddress;
1171  }
1172 
1173 
1182  String save() throws TskCoreException, IngestModuleException {
1183  String fileName;
1184 
1185  if (cacheAddress.isInExternalFile()) {
1186  fileName = cacheAddress.getFilename();
1187  } else {
1188  fileName = String.format("%s__%08x", cacheAddress.getFilename(), cacheAddress.getUint32CacheAddr());
1189  }
1190 
1191  String filePathName = getAbsOutputFolderName() + cacheAddress.getCachePath() + fileName;
1192  save(filePathName);
1193 
1194  return fileName;
1195  }
1196 
1206  void save(String filePathName) throws TskCoreException, IngestModuleException {
1207 
1208  // Save the data to specified file
1209  if (data == null) {
1210  extract();
1211  }
1212 
1213  // Data in external files is not saved in local files
1214  if (!this.isInExternalFile()) {
1215  // write the
1216  try (FileOutputStream stream = new FileOutputStream(filePathName)) {
1217  stream.write(data);
1218  } catch (IOException ex) {
1219  throw new TskCoreException(String.format("Failed to write output file %s", filePathName), ex);
1220  }
1221  }
1222  }
1223 
1224  @Override
1225  public String toString() {
1226  StringBuilder strBuilder = new StringBuilder();
1227  strBuilder.append(String.format("\t\tData type = : %s, Data Len = %d ",
1228  this.type.toString(), this.length ));
1229 
1230  if (hasHTTPHeaders()) {
1231  String str = getHTTPHeader("content-encoding");
1232  if (str != null) {
1233  strBuilder.append(String.format("\t%s=%s", "content-encoding", str ));
1234  }
1235  }
1236 
1237  return strBuilder.toString();
1238  }
1239 
1240  }
1241 
1242 
1246  enum EntryStateEnum {
1247  ENTRY_NORMAL,
1248  ENTRY_EVICTED,
1249  ENTRY_DOOMED
1250  };
1251 
1252 
1253 // Main structure for an entry on the backing storage.
1254 //
1255 // Each entry has a key, identifying the URL the cache entry pertains to.
1256 // If the key is longer than
1257 // what can be stored on this structure, it will be extended on consecutive
1258 // blocks (adding 256 bytes each time), up to 4 blocks (1024 - 32 - 1 chars).
1259 // After that point, the whole key will be stored as a data block or external
1260 // file.
1261 //
1262 // Each entry can have upto 4 data segments
1263 //
1264 // struct EntryStore {
1265 // uint32 hash; // Full hash of the key.
1266 // CacheAddr next; // Next entry with the same hash or bucket.
1267 // CacheAddr rankings_node; // Rankings node for this entry.
1268 // int32 reuse_count; // How often is this entry used.
1269 // int32 refetch_count; // How often is this fetched from the net.
1270 // int32 state; // Current state.
1271 // uint64 creation_time;
1272 // int32 key_len;
1273 // CacheAddr long_key; // Optional cacheAddress of a long key.
1274 // int32 data_size[4]; // We can store up to 4 data streams for each
1275 // CacheAddr data_addr[4]; // entry.
1276 // uint32 flags; // Any combination of EntryFlags.
1277 // int32 pad[4];
1278 // uint32 self_hash; // The hash of EntryStore up to this point.
1279 // char key[256 - 24 * 4]; // null terminated
1280 // };
1281 
1285  final class CacheEntry {
1286 
1287  // each entry is 256 bytes. The last section of the entry, after all the other fields is a null terminated key
1288  private static final int MAX_KEY_LEN = 256-24*4;
1289 
1290  private final CacheAddress selfAddress;
1291  private final FileWrapper cacheFileCopy;
1292 
1293  private final long hash;
1294  private final CacheAddress nextAddress;
1295  private final CacheAddress rankingsNodeAddress;
1296 
1297  private final int reuseCount;
1298  private final int refetchCount;
1299  private final EntryStateEnum state;
1300 
1301  private final long creationTime;
1302  private final int keyLen;
1303 
1304  private final CacheAddress longKeyAddresses; // cacheAddress of the key, if the key is external to the entry
1305 
1306  private final int[] dataSegmentSizes;
1307  private final CacheAddress[] dataSegmentIndexFileEntries;
1308  private List<CacheDataSegment> dataSegments;
1309 
1310  private final long flags;
1311 
1312  private String key; // Key may be found within the entry or may be external
1313 
1314  CacheEntry(CacheAddress cacheAdress, FileWrapper cacheFileCopy ) throws TskCoreException, IngestModuleException {
1315  this.selfAddress = cacheAdress;
1316  this.cacheFileCopy = cacheFileCopy;
1317 
1318  ByteBuffer fileROBuf = cacheFileCopy.getByteBuffer();
1319 
1320  int entryOffset = DATAFILE_HDR_SIZE + cacheAdress.getStartBlock() * cacheAdress.getBlockSize();
1321 
1322  // reposition the buffer to the the correct offset
1323  if (entryOffset < fileROBuf.capacity()) {
1324  fileROBuf.position(entryOffset);
1325  } else {
1326  throw new IngestModuleException("Position seeked in Buffer to big"); // NON-NLS
1327  }
1328 
1329  hash = fileROBuf.getInt() & UINT32_MASK;
1330 
1331  long uint32 = fileROBuf.getInt() & UINT32_MASK;
1332  nextAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1333 
1334  uint32 = fileROBuf.getInt() & UINT32_MASK;
1335  rankingsNodeAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1336 
1337  reuseCount = fileROBuf.getInt();
1338  refetchCount = fileROBuf.getInt();
1339 
1340  int stateVal = fileROBuf.getInt();
1341  if ((stateVal >= 0) && (stateVal < EntryStateEnum.values().length)) {
1342  state = EntryStateEnum.values()[stateVal];
1343  } else {
1344  throw new TskCoreException("Invalid EntryStateEnum value"); // NON-NLS
1345  }
1346  creationTime = (fileROBuf.getLong() / 1000000) - Long.valueOf("11644473600");
1347 
1348  keyLen = fileROBuf.getInt();
1349 
1350  uint32 = fileROBuf.getInt() & UINT32_MASK;
1351  longKeyAddresses = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1352 
1353  dataSegments = null;
1354  dataSegmentSizes= new int[4];
1355  for (int i = 0; i < 4; i++) {
1356  dataSegmentSizes[i] = fileROBuf.getInt();
1357  }
1358  dataSegmentIndexFileEntries = new CacheAddress[4];
1359  for (int i = 0; i < 4; i++) {
1360  dataSegmentIndexFileEntries[i] = new CacheAddress(fileROBuf.getInt() & UINT32_MASK, selfAddress.getCachePath());
1361  }
1362 
1363  flags = fileROBuf.getInt() & UINT32_MASK;
1364  // skip over pad
1365  for (int i = 0; i < 4; i++) {
1366  fileROBuf.getInt();
1367  }
1368 
1369  // skip over self hash
1370  fileROBuf.getInt();
1371 
1372  // get the key
1373  if (longKeyAddresses != null) {
1374  // Key is stored outside of the entry
1375  try {
1376  if (longKeyAddresses.getFilename() != null) {
1377  CacheDataSegment data = new CacheDataSegment(longKeyAddresses, this.keyLen, true);
1378  key = data.getDataString();
1379  }
1380  } catch (TskCoreException | IngestModuleException ex) {
1381  throw new TskCoreException(String.format("Failed to get external key from address %s", longKeyAddresses)); //NON-NLS
1382  }
1383  }
1384  else { // key stored within entry
1385  StringBuilder strBuilder = new StringBuilder(MAX_KEY_LEN);
1386  int keyLen = 0;
1387  while (fileROBuf.remaining() > 0 && keyLen < MAX_KEY_LEN) {
1388  char keyChar = (char)fileROBuf.get();
1389  if (keyChar == '\0') {
1390  break;
1391  }
1392  strBuilder.append(keyChar);
1393  keyLen++;
1394  }
1395 
1396  key = strBuilder.toString();
1397  }
1398  }
1399 
1400  public CacheAddress getCacheAddress() {
1401  return selfAddress;
1402  }
1403 
1404  public long getHash() {
1405  return hash;
1406  }
1407 
1408  public CacheAddress getNextCacheAddress() {
1409  return nextAddress;
1410  }
1411 
1412  public int getReuseCount() {
1413  return reuseCount;
1414  }
1415 
1416  public int getRefetchCount() {
1417  return refetchCount;
1418  }
1419 
1420  public EntryStateEnum getState() {
1421  return state;
1422  }
1423 
1424  public long getCreationTime() {
1425  return creationTime;
1426  }
1427 
1428  public long getFlags() {
1429  return flags;
1430  }
1431 
1432  public String getKey() {
1433  return key;
1434  }
1435 
1444  public List<CacheDataSegment> getDataSegments() throws TskCoreException, IngestModuleException {
1445 
1446  if (dataSegments == null) {
1447  dataSegments = new ArrayList<>();
1448  for (int i = 0; i < 4; i++) {
1449  if (dataSegmentSizes[i] > 0) {
1450  CacheDataSegment cacheData = new CacheDataSegment(dataSegmentIndexFileEntries[i], dataSegmentSizes[i], true );
1451 
1452  cacheData.extract();
1453  dataSegments.add(cacheData);
1454  }
1455  }
1456  }
1457  return dataSegments;
1458  }
1459 
1467  boolean hasHTTPHeaders() {
1468  if ((dataSegments == null) || dataSegments.isEmpty()) {
1469  return false;
1470  }
1471  return dataSegments.get(0).hasHTTPHeaders();
1472  }
1473 
1480  String getHTTPHeader(String key) {
1481  if ((dataSegments == null) || dataSegments.isEmpty()) {
1482  return null;
1483  }
1484  // First data segment has the HTTP headers, if any
1485  return dataSegments.get(0).getHTTPHeader(key);
1486  }
1487 
1493  String getHTTPHeaders() {
1494  if ((dataSegments == null) || dataSegments.isEmpty()) {
1495  return null;
1496  }
1497  // First data segment has the HTTP headers, if any
1498  return dataSegments.get(0).getHTTPHeaders();
1499  }
1500 
1509  boolean isBrotliCompressed() {
1510 
1511  if (hasHTTPHeaders() ) {
1512  String encodingHeader = getHTTPHeader("content-encoding");
1513  if (encodingHeader!= null) {
1514  return encodingHeader.trim().equalsIgnoreCase("br");
1515  }
1516  }
1517 
1518  return false;
1519  }
1520 
1521  @Override
1522  public String toString() {
1523  StringBuilder sb = new StringBuilder();
1524  sb.append(String.format("Entry = Hash: %08x, State: %s, ReuseCount: %d, RefetchCount: %d",
1525  this.hash, this.state.toString(), this.reuseCount, this.refetchCount ))
1526  .append(String.format("\n\tKey: %s, Keylen: %d",
1527  this.key, this.keyLen, this.reuseCount, this.refetchCount ))
1528  .append(String.format("\n\tCreationTime: %s",
1529  TimeUtilities.epochToTime(this.creationTime) ))
1530  .append(String.format("\n\tNext Address: %s",
1531  (nextAddress != null) ? nextAddress.toString() : "None"));
1532 
1533  for (int i = 0; i < 4; i++) {
1534  if (dataSegmentSizes[i] > 0) {
1535  sb.append(String.format("\n\tData %d: cache address = %s, Data = %s",
1536  i, dataSegmentIndexFileEntries[i].toString(),
1537  (dataSegments != null)
1538  ? dataSegments.get(i).toString()
1539  : "Data not retrived yet."));
1540  }
1541  }
1542 
1543  return sb.toString();
1544  }
1545  }
1546 }

Copyright © 2012-2021 Basis Technology. Generated on: Thu Sep 30 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.