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

Copyright © 2012-2022 Basis Technology. Generated on: Tue Aug 1 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.