Autopsy  4.7.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
IngestTasksScheduler.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2012-2018 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.ingest;
20 
21 import java.io.Serializable;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.Deque;
27 import java.util.Iterator;
28 import java.util.LinkedList;
29 import java.util.List;
30 import java.util.TreeSet;
31 import java.util.concurrent.BlockingDeque;
32 import java.util.concurrent.LinkedBlockingDeque;
33 import java.util.logging.Level;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import javax.annotation.concurrent.GuardedBy;
37 import javax.annotation.concurrent.ThreadSafe;
39 import org.sleuthkit.datamodel.AbstractFile;
40 import org.sleuthkit.datamodel.Content;
41 import org.sleuthkit.datamodel.FileSystem;
42 import org.sleuthkit.datamodel.TskCoreException;
43 import org.sleuthkit.datamodel.TskData;
44 
49 @ThreadSafe
50 final class IngestTasksScheduler {
51 
52  private static final int FAT_NTFS_FLAGS = TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT12.getValue() | TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT16.getValue() | TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT32.getValue() | TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_NTFS.getValue();
53  private static final Logger logger = Logger.getLogger(IngestTasksScheduler.class.getName());
54  @GuardedBy("IngestTasksScheduler.this")
55  private static IngestTasksScheduler instance;
56  private final IngestTaskTrackingQueue dataSourceIngestThreadQueue;
57  @GuardedBy("this")
58  private final TreeSet<FileIngestTask> rootFileTaskQueue;
59  @GuardedBy("this")
60  private final Deque<FileIngestTask> pendingFileTaskQueue;
61  private final IngestTaskTrackingQueue fileIngestThreadsQueue;
62 
68  synchronized static IngestTasksScheduler getInstance() {
69  if (IngestTasksScheduler.instance == null) {
70  IngestTasksScheduler.instance = new IngestTasksScheduler();
71  }
72  return IngestTasksScheduler.instance;
73  }
74 
80  private IngestTasksScheduler() {
81  this.dataSourceIngestThreadQueue = new IngestTaskTrackingQueue();
82  this.rootFileTaskQueue = new TreeSet<>(new RootDirectoryTaskComparator());
83  this.pendingFileTaskQueue = new LinkedList<>();
84  this.fileIngestThreadsQueue = new IngestTaskTrackingQueue();
85  }
86 
93  BlockingIngestTaskQueue getDataSourceIngestTaskQueue() {
94  return this.dataSourceIngestThreadQueue;
95  }
96 
103  BlockingIngestTaskQueue getFileIngestTaskQueue() {
104  return this.fileIngestThreadsQueue;
105  }
106 
113  synchronized void scheduleIngestTasks(DataSourceIngestJob job) {
114  if (!job.isCancelled()) {
115  /*
116  * Scheduling of both the data source ingest task and the initial
117  * file ingest tasks for a job must be an atomic operation.
118  * Otherwise, the data source task might be completed before the
119  * file tasks are scheduled, resulting in a potential false positive
120  * when another thread checks whether or not all the tasks for the
121  * job are completed.
122  */
123  this.scheduleDataSourceIngestTask(job);
124  this.scheduleFileIngestTasks(job, Collections.emptyList());
125  }
126  }
127 
133  synchronized void scheduleDataSourceIngestTask(DataSourceIngestJob job) {
134  if (!job.isCancelled()) {
135  DataSourceIngestTask task = new DataSourceIngestTask(job);
136  try {
137  this.dataSourceIngestThreadQueue.putLast(task);
138  } catch (InterruptedException ex) {
139  IngestTasksScheduler.logger.log(Level.INFO, String.format("Ingest tasks scheduler interrupted while blocked adding a task to the data source level ingest task queue (jobId={%d)", job.getId()), ex);
140  Thread.currentThread().interrupt();
141  }
142  }
143  }
144 
153  synchronized void scheduleFileIngestTasks(DataSourceIngestJob job, Collection<AbstractFile> files) {
154  if (!job.isCancelled()) {
155  Collection<AbstractFile> candidateFiles;
156  if (files.isEmpty()) {
157  candidateFiles = getTopLevelFiles(job.getDataSource());
158  } else {
159  candidateFiles = files;
160  }
161  for (AbstractFile file : candidateFiles) {
162  FileIngestTask task = new FileIngestTask(job, file);
163  if (IngestTasksScheduler.shouldEnqueueFileTask(task)) {
164  this.rootFileTaskQueue.add(task);
165  }
166  }
167  shuffleFileTaskQueues();
168  }
169  }
170 
179  synchronized void fastTrackFileIngestTasks(DataSourceIngestJob job, Collection<AbstractFile> files) {
180  if (!job.isCancelled()) {
181  /*
182  * Put the files directly into the queue for the file ingest
183  * threads, if they pass the file filter for the job. The files are
184  * added to the queue for the ingest threads BEFORE the other queued
185  * tasks because the use case for this method is scheduling new
186  * carved or derived files from a higher priority task that is
187  * already in progress.
188  */
189  for (AbstractFile file : files) {
190  FileIngestTask fileTask = new FileIngestTask(job, file);
191  if (shouldEnqueueFileTask(fileTask)) {
192  try {
193  this.fileIngestThreadsQueue.putFirst(fileTask);
194  } catch (InterruptedException ex) {
195  IngestTasksScheduler.logger.log(Level.INFO, String.format("Ingest tasks scheduler interrupted while scheduling file level ingest tasks (jobId={%d)", job.getId()), ex);
196  Thread.currentThread().interrupt();
197  return;
198  }
199  }
200  }
201  }
202  }
203 
210  synchronized void notifyTaskCompleted(DataSourceIngestTask task) {
211  this.dataSourceIngestThreadQueue.taskCompleted(task);
212  }
213 
220  synchronized void notifyTaskCompleted(FileIngestTask task) {
221  this.fileIngestThreadsQueue.taskCompleted(task);
222  shuffleFileTaskQueues();
223  }
224 
233  synchronized boolean tasksForJobAreCompleted(DataSourceIngestJob job) {
234  long jobId = job.getId();
235  return !(this.dataSourceIngestThreadQueue.hasTasksForJob(jobId)
236  || hasTasksForJob(this.rootFileTaskQueue, jobId)
237  || hasTasksForJob(this.pendingFileTaskQueue, jobId)
238  || this.fileIngestThreadsQueue.hasTasksForJob(jobId));
239  }
240 
248  synchronized void cancelPendingTasksForIngestJob(DataSourceIngestJob job) {
249  long jobId = job.getId();
250  IngestTasksScheduler.removeTasksForJob(this.rootFileTaskQueue, jobId);
251  IngestTasksScheduler.removeTasksForJob(this.pendingFileTaskQueue, jobId);
252  }
253 
263  private static List<AbstractFile> getTopLevelFiles(Content dataSource) {
264  List<AbstractFile> topLevelFiles = new ArrayList<>();
265  Collection<AbstractFile> rootObjects = dataSource.accept(new GetRootDirectoryVisitor());
266  if (rootObjects.isEmpty() && dataSource instanceof AbstractFile) {
267  // The data source is itself a file to be processed.
268  topLevelFiles.add((AbstractFile) dataSource);
269  } else {
270  for (AbstractFile root : rootObjects) {
271  List<Content> children;
272  try {
273  children = root.getChildren();
274  if (children.isEmpty()) {
275  // Add the root object itself, it could be an unallocated
276  // space file, or a child of a volume or an image.
277  topLevelFiles.add(root);
278  } else {
279  // The root object is a file system root directory, get
280  // the files within it.
281  for (Content child : children) {
282  if (child instanceof AbstractFile) {
283  topLevelFiles.add((AbstractFile) child);
284  }
285  }
286  }
287  } catch (TskCoreException ex) {
288  logger.log(Level.WARNING, "Could not get children of root to enqueue: " + root.getId() + ": " + root.getName(), ex); //NON-NLS
289  }
290  }
291  }
292  return topLevelFiles;
293  }
294 
325  synchronized private void shuffleFileTaskQueues() {
326  while (this.fileIngestThreadsQueue.isEmpty()) {
327  /*
328  * If the pending file task queue is empty, move the highest
329  * priority root file task, if there is one, into it.
330  */
331  if (this.pendingFileTaskQueue.isEmpty()) {
332  final FileIngestTask rootTask = this.rootFileTaskQueue.pollFirst();
333  if (rootTask != null) {
334  this.pendingFileTaskQueue.addLast(rootTask);
335  }
336  }
337 
338  /*
339  * Try to move the next task from the pending task queue into the
340  * queue for the file ingest threads, if it passes the filter for
341  * the job.
342  */
343  final FileIngestTask pendingTask = this.pendingFileTaskQueue.pollFirst();
344  if (pendingTask == null) {
345  return;
346  }
347  if (shouldEnqueueFileTask(pendingTask)) {
348  try {
349  /*
350  * The task is added to the queue for the ingest threads
351  * AFTER the higher priority tasks that preceded it.
352  */
353  this.fileIngestThreadsQueue.putLast(pendingTask);
354  } catch (InterruptedException ex) {
355  IngestTasksScheduler.logger.log(Level.INFO, "Ingest tasks scheduler interrupted while blocked adding a task to the file level ingest task queue", ex);
356  Thread.currentThread().interrupt();
357  return;
358  }
359  }
360 
361  /*
362  * If the task that was just queued for the file ingest threads has
363  * children, try to queue tasks for the children. Each child task
364  * will go into either the directory queue if it has children of its
365  * own, or into the queue for the file ingest threads, if it passes
366  * the filter for the job.
367  */
368  final AbstractFile file = pendingTask.getFile();
369  try {
370  for (Content child : file.getChildren()) {
371  if (child instanceof AbstractFile) {
372  AbstractFile childFile = (AbstractFile) child;
373  FileIngestTask childTask = new FileIngestTask(pendingTask.getIngestJob(), childFile);
374  if (childFile.hasChildren()) {
375  this.pendingFileTaskQueue.add(childTask);
376  } else if (shouldEnqueueFileTask(childTask)) {
377  try {
378  this.fileIngestThreadsQueue.putLast(childTask);
379  } catch (InterruptedException ex) {
380  IngestTasksScheduler.logger.log(Level.INFO, "Ingest tasks scheduler interrupted while blocked adding a task to the file level ingest task queue", ex);
381  Thread.currentThread().interrupt();
382  return;
383  }
384  }
385  }
386  }
387  } catch (TskCoreException ex) {
388  logger.log(Level.SEVERE, String.format("Error getting the children of %s (objId=%d)", file.getName(), file.getId()), ex); //NON-NLS
389  }
390  }
391  }
392 
402  private static boolean shouldEnqueueFileTask(final FileIngestTask task) {
403  final AbstractFile file = task.getFile();
404 
405  // Skip the task if the file is actually the pseudo-file for the parent
406  // or current directory.
407  String fileName = file.getName();
408 
409  if (fileName.equals(".") || fileName.equals("..")) {
410  return false;
411  }
412 
413  /*
414  * Ensures that all directories, files which are members of the ingest
415  * file filter, and unallocated blocks (when processUnallocated is
416  * enabled) all continue to be processed. AbstractFiles which do not
417  * meet one of these criteria will be skipped.
418  *
419  * An additional check to see if unallocated space should be processed
420  * is part of the FilesSet.fileIsMemberOf() method.
421  *
422  * This code may need to be updated when
423  * TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS comes into use by Autopsy.
424  */
425  if (!file.isDir() && !shouldBeCarved(task) && !fileAcceptedByFilter(task)) {
426  return false;
427  }
428 
429  // Skip the task if the file is one of a select group of special, large
430  // NTFS or FAT file system files.
431  if (file instanceof org.sleuthkit.datamodel.File) {
432  final org.sleuthkit.datamodel.File f = (org.sleuthkit.datamodel.File) file;
433 
434  // Get the type of the file system, if any, that owns the file.
435  TskData.TSK_FS_TYPE_ENUM fsType = TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_UNSUPP;
436  try {
437  FileSystem fs = f.getFileSystem();
438  if (fs != null) {
439  fsType = fs.getFsType();
440  }
441  } catch (TskCoreException ex) {
442  logger.log(Level.SEVERE, "Error querying file system for " + f, ex); //NON-NLS
443  }
444 
445  // If the file system is not NTFS or FAT, don't skip the file.
446  if ((fsType.getValue() & FAT_NTFS_FLAGS) == 0) {
447  return true;
448  }
449 
450  // Find out whether the file is in a root directory.
451  boolean isInRootDir = false;
452  try {
453  AbstractFile parent = f.getParentDirectory();
454  isInRootDir = parent.isRoot();
455  } catch (TskCoreException ex) {
456  logger.log(Level.WARNING, "Error querying parent directory for" + f.getName(), ex); //NON-NLS
457  }
458 
459  // If the file is in the root directory of an NTFS or FAT file
460  // system, check its meta-address and check its name for the '$'
461  // character and a ':' character (not a default attribute).
462  if (isInRootDir && f.getMetaAddr() < 32) {
463  String name = f.getName();
464  if (name.length() > 0 && name.charAt(0) == '$' && name.contains(":")) {
465  return false;
466  }
467  }
468  }
469 
470  return true;
471  }
472 
481  private static boolean shouldBeCarved(final FileIngestTask task) {
482  return task.getIngestJob().shouldProcessUnallocatedSpace() && task.getFile().getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS);
483  }
484 
493  private static boolean fileAcceptedByFilter(final FileIngestTask task) {
494  return !(task.getIngestJob().getFileIngestFilter().fileIsMemberOf(task.getFile()) == null);
495  }
496 
506  synchronized private static boolean hasTasksForJob(Collection<? extends IngestTask> tasks, long jobId) {
507  for (IngestTask task : tasks) {
508  if (task.getIngestJob().getId() == jobId) {
509  return true;
510  }
511  }
512  return false;
513  }
514 
522  private static void removeTasksForJob(Collection<? extends IngestTask> tasks, long jobId) {
523  Iterator<? extends IngestTask> iterator = tasks.iterator();
524  while (iterator.hasNext()) {
525  IngestTask task = iterator.next();
526  if (task.getIngestJob().getId() == jobId) {
527  iterator.remove();
528  }
529  }
530  }
531 
540  private static int countTasksForJob(Collection<? extends IngestTask> queue, long jobId) {
541  int count = 0;
542  for (IngestTask task : queue) {
543  if (task.getIngestJob().getId() == jobId) {
544  count++;
545  }
546  }
547  return count;
548  }
549 
558  synchronized IngestJobTasksSnapshot getTasksSnapshotForJob(long jobId) {
559  return new IngestJobTasksSnapshot(jobId, this.dataSourceIngestThreadQueue.countQueuedTasksForJob(jobId),
560  countTasksForJob(this.rootFileTaskQueue, jobId),
561  countTasksForJob(this.pendingFileTaskQueue, jobId),
562  this.fileIngestThreadsQueue.countQueuedTasksForJob(jobId),
563  this.dataSourceIngestThreadQueue.countRunningTasksForJob(jobId) + this.fileIngestThreadsQueue.countRunningTasksForJob(jobId));
564  }
565 
570  private static class RootDirectoryTaskComparator implements Comparator<FileIngestTask> {
571 
572  @Override
573  public int compare(FileIngestTask q1, FileIngestTask q2) {
574  AbstractFilePriority.Priority p1 = AbstractFilePriority.getPriority(q1.getFile());
575  AbstractFilePriority.Priority p2 = AbstractFilePriority.getPriority(q2.getFile());
576  if (p1 == p2) {
577  return (int) (q2.getFile().getId() - q1.getFile().getId());
578  } else {
579  return p2.ordinal() - p1.ordinal();
580  }
581  }
582 
587  private static class AbstractFilePriority {
588 
590  }
591 
592  enum Priority {
593 
594  LAST, LOW, MEDIUM, HIGH
595  }
596 
597  static final List<Pattern> LAST_PRI_PATHS = new ArrayList<>();
598 
599  static final List<Pattern> LOW_PRI_PATHS = new ArrayList<>();
600 
601  static final List<Pattern> MEDIUM_PRI_PATHS = new ArrayList<>();
602 
603  static final List<Pattern> HIGH_PRI_PATHS = new ArrayList<>();
604 
605  /*
606  * prioritize root directory folders based on the assumption that we
607  * are looking for user content. Other types of investigations may
608  * want different priorities.
609  */
610  static /*
611  * prioritize root directory folders based on the assumption that we
612  * are looking for user content. Other types of investigations may
613  * want different priorities.
614  */ {
615  // these files have no structure, so they go last
616  //unalloc files are handled as virtual files in getPriority()
617  //LAST_PRI_PATHS.schedule(Pattern.compile("^\\$Unalloc", Pattern.CASE_INSENSITIVE));
618  //LAST_PRI_PATHS.schedule(Pattern.compile("^\\Unalloc", Pattern.CASE_INSENSITIVE));
619  LAST_PRI_PATHS.add(Pattern.compile("^pagefile", Pattern.CASE_INSENSITIVE));
620  LAST_PRI_PATHS.add(Pattern.compile("^hiberfil", Pattern.CASE_INSENSITIVE));
621  // orphan files are often corrupt and windows does not typically have
622  // user content, so put them towards the bottom
623  LOW_PRI_PATHS.add(Pattern.compile("^\\$OrphanFiles", Pattern.CASE_INSENSITIVE));
624  LOW_PRI_PATHS.add(Pattern.compile("^Windows", Pattern.CASE_INSENSITIVE));
625  // all other files go into the medium category too
626  MEDIUM_PRI_PATHS.add(Pattern.compile("^Program Files", Pattern.CASE_INSENSITIVE));
627  // user content is top priority
628  HIGH_PRI_PATHS.add(Pattern.compile("^Users", Pattern.CASE_INSENSITIVE));
629  HIGH_PRI_PATHS.add(Pattern.compile("^Documents and Settings", Pattern.CASE_INSENSITIVE));
630  HIGH_PRI_PATHS.add(Pattern.compile("^home", Pattern.CASE_INSENSITIVE));
631  HIGH_PRI_PATHS.add(Pattern.compile("^ProgramData", Pattern.CASE_INSENSITIVE));
632  }
633 
641  static AbstractFilePriority.Priority getPriority(final AbstractFile abstractFile) {
642  if (!abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.FS)) {
643  //quickly filter out unstructured content
644  //non-fs virtual files and dirs, such as representing unalloc space
645  return AbstractFilePriority.Priority.LAST;
646  }
647  //determine the fs files priority by name
648  final String path = abstractFile.getName();
649  if (path == null) {
650  return AbstractFilePriority.Priority.MEDIUM;
651  }
652  for (Pattern p : HIGH_PRI_PATHS) {
653  Matcher m = p.matcher(path);
654  if (m.find()) {
655  return AbstractFilePriority.Priority.HIGH;
656  }
657  }
658  for (Pattern p : MEDIUM_PRI_PATHS) {
659  Matcher m = p.matcher(path);
660  if (m.find()) {
661  return AbstractFilePriority.Priority.MEDIUM;
662  }
663  }
664  for (Pattern p : LOW_PRI_PATHS) {
665  Matcher m = p.matcher(path);
666  if (m.find()) {
667  return AbstractFilePriority.Priority.LOW;
668  }
669  }
670  for (Pattern p : LAST_PRI_PATHS) {
671  Matcher m = p.matcher(path);
672  if (m.find()) {
673  return AbstractFilePriority.Priority.LAST;
674  }
675  }
676  //default is medium
677  return AbstractFilePriority.Priority.MEDIUM;
678  }
679  }
680  }
681 
686  @ThreadSafe
687  private class IngestTaskTrackingQueue implements BlockingIngestTaskQueue {
688 
689  private final BlockingDeque<IngestTask> taskQueue = new LinkedBlockingDeque<>();
690  @GuardedBy("this")
691  private final List<IngestTask> queuedTasks = new LinkedList<>();
692  @GuardedBy("this")
693  private final List<IngestTask> tasksInProgress = new LinkedList<>();
694 
705  void putFirst(IngestTask task) throws InterruptedException {
706  synchronized (this) {
707  this.queuedTasks.add(task);
708  }
709  try {
710  this.taskQueue.putFirst(task);
711  } catch (InterruptedException ex) {
712  synchronized (this) {
713  this.queuedTasks.remove(task);
714  }
715  throw ex;
716  }
717  }
718 
729  void putLast(IngestTask task) throws InterruptedException {
730  synchronized (this) {
731  this.queuedTasks.add(task);
732  }
733  try {
734  this.taskQueue.putLast(task);
735  } catch (InterruptedException ex) {
736  synchronized (this) {
737  this.queuedTasks.remove(task);
738  }
739  throw ex;
740  }
741  }
742 
753  @Override
754  public IngestTask getNextTask() throws InterruptedException {
755  IngestTask task = taskQueue.takeFirst();
756  synchronized (this) {
757  this.queuedTasks.remove(task);
758  this.tasksInProgress.add(task);
759  }
760  return task;
761  }
762 
768  boolean isEmpty() {
769  synchronized (this) {
770  return this.queuedTasks.isEmpty();
771  }
772  }
773 
780  void taskCompleted(IngestTask task) {
781  synchronized (this) {
782  this.tasksInProgress.remove(task);
783  }
784  }
785 
794  boolean hasTasksForJob(long jobId) {
795  synchronized (this) {
796  return IngestTasksScheduler.hasTasksForJob(this.queuedTasks, jobId) || IngestTasksScheduler.hasTasksForJob(this.tasksInProgress, jobId);
797  }
798  }
799 
808  int countQueuedTasksForJob(long jobId) {
809  synchronized (this) {
810  return IngestTasksScheduler.countTasksForJob(this.queuedTasks, jobId);
811  }
812  }
813 
822  int countRunningTasksForJob(long jobId) {
823  synchronized (this) {
824  return IngestTasksScheduler.countTasksForJob(this.tasksInProgress, jobId);
825  }
826  }
827 
828  }
829 
833  static final class IngestJobTasksSnapshot implements Serializable {
834 
835  private static final long serialVersionUID = 1L;
836  private final long jobId;
837  private final long dsQueueSize;
838  private final long rootQueueSize;
839  private final long dirQueueSize;
840  private final long fileQueueSize;
841  private final long runningListSize;
842 
848  IngestJobTasksSnapshot(long jobId, long dsQueueSize, long rootQueueSize, long dirQueueSize, long fileQueueSize, long runningListSize) {
849  this.jobId = jobId;
850  this.dsQueueSize = dsQueueSize;
851  this.rootQueueSize = rootQueueSize;
852  this.dirQueueSize = dirQueueSize;
853  this.fileQueueSize = fileQueueSize;
854  this.runningListSize = runningListSize;
855  }
856 
863  long getJobId() {
864  return jobId;
865  }
866 
873  long getRootQueueSize() {
874  return rootQueueSize;
875  }
876 
883  long getDirectoryTasksQueueSize() {
884  return dirQueueSize;
885  }
886 
887  long getFileQueueSize() {
888  return fileQueueSize;
889  }
890 
891  long getDsQueueSize() {
892  return dsQueueSize;
893  }
894 
895  long getRunningListSize() {
896  return runningListSize;
897  }
898 
899  }
900 
901 }

Copyright © 2012-2016 Basis Technology. Generated on: Mon Jun 18 2018
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.