Repository: aboutcode-org/container-inspector (language: python)

Issue:
Debian distro not correctly detected with this os-release
This /usr/lib/os-release
```
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
```
was not detected correctly in  rootfs pipeline.

1. we should ensure that we can get to this path too
2. we should be more flexible with the paths (and leading segments)
3. in all cases, checking for system-installed packages should not be dependent on the detected distro. We should try Debian, RPM, etc. and all supported types in all FS and/or layers analyzed in a scancode.io pipeline (which impacts the code here, in scancode-toolkit and in scancode.io) 


Discussion / hints:

Fix patch:
diff --git a/.gitattributes b/.gitattributes
index 04abf53..96c89ce 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,3 @@
 # Ignore all Git auto CR/LF line endings conversions
 * -text
 pyproject.toml export-subst
-
diff --git a/.travis.yml b/.travis.yml
index 7a342df..1b52eb2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,7 +15,7 @@ python:
   - "3.8"
 
 # Scripts to run at install stage
-install: ./configure
+install: ./configure --dev
 
 # Scripts to run at script stage
 script: tmp/bin/pytest
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6d37775..0026ff0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,16 +1,69 @@
 Changelog
 =========
 
-v21.4.26 (2021-04-26)
----------------------
+v21.5.15
+--------
 
-This is a major release. There is no API change.
+This is a major release. 
+
+API changes
+~~~~~~~~~~~
+
+The Image and Layer object structures have changed significantly:
+- legacy parent_id and parent_digest attributes are removed from Image
+
+- new attributes have been added to correctly track the tarball of an image
+  or layer and its extracted location:
+  - "extracted_location" is the absolute path where an image or layer is extracted
+  - "archive_location" is the absolute path to an image or layer archive
+  Therefore we have these attribute renames, additions and deletions:
+    - Image.base_location -> Image.extracted_location
+    - Image.archive_location: added
+    - Layer.extracted_to_location -> Layer.extracted_location
+    - Layer.layer_location -> Layer.archive_location
+    - Layer.layer_sha256 -> Layer.sha256
+  Also:
+    - Layer.layer_id is now the sha256 of the Layer archive
+    - Image.sha256, os_version, variant: added
+
+- the layer_id is now based on the SHA256 of the layer tarball and not based on
+  the UUID-like directory names that contain a "layer.tar" in Docker image.
+- Image.config_digest is now prefixed with "sha256:"
+- All mappings keys are now lowercased recursively, including for labels.
+
+- Dropped support for Python 2. The minimum Python version is now Python 3.6
+- Dropped support for Docker image v1-style format
+- Dropped support for Windows as it was never intended to run on Windows.
+  Windows as a container is a target though.
+- The way Image and Layers archives are extracted has changed significantly.
+  Images are extracted as before keeping symlinks (which are essential to support
+  certain Docker image layouts). In contrast, Layers are now exracted using
+  extractcode and links are ignored.
+
+
+New features
+~~~~~~~~~~~~
 
 - Add new find_root function to find the root of a filesystem
-- Drop support for Python 2. The minimum Python version is now Python 3.6
-- Drop support for Docker imgae v1-style format
-- Add new tests for distro detection and os-release handling
-- The master branch has been renamed to main
+
+- Add new tests for distro detection and os-release handling using a large
+  collection of os-release files
+
+- Layer/Image.to_dict() now accepts a new "exclude_fields" argument with a list
+  of field names to optionally exclude.
+
+- Add experimental support for container images using the OCI layout and add a
+  new Image attribute "image_format" with the value "docker" or "oci"
+
+- Add experimental support for Windows-based containers.
+
+
+Misc
+~~~~
+
+- The experimental fetch module has been removed
+- The master branch has been renamed to main.
+
 
 
 v3.1.2 (2020-07-07)
diff --git a/README.rst b/README.rst
index c76c032..af53c50 100644
--- a/README.rst
+++ b/README.rst
@@ -16,7 +16,8 @@ container-inspector provides utilities to:
 Quick start
 -----------
 
-- Get 3.6+
+- Only runs on POSIX OSes
+- Get Python 3.6+
 - Check out a clone or download of container-inspector, then run: `./configure`.
 - Then run `tmp/bin/container-inspector -h` for help.
 
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 9a4c950..61517b9 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -7,7 +7,7 @@
 
 jobs:
 
-    - template: etc/ci/azure-linux.yml
+    - template: etc/ci/azure-posix.yml
       parameters:
           job_name: ubuntu16_cpython
           image_name: ubuntu-16.04
@@ -15,7 +15,7 @@ jobs:
           test_suites:
               all: tmp/bin/pytest -vvs
 
-    - template: etc/ci/azure-linux.yml
+    - template: etc/ci/azure-posix.yml
       parameters:
           job_name: ubuntu18_cpython
           image_name: ubuntu-18.04
@@ -23,7 +23,7 @@ jobs:
           test_suites:
               all: tmp/bin/pytest -n 2 -vvs
 
-    - template: etc/ci/azure-linux.yml
+    - template: etc/ci/azure-posix.yml
       parameters:
           job_name: ubuntu20_cpython
           image_name: ubuntu-20.04
@@ -31,7 +31,7 @@ jobs:
           test_suites:
               all: tmp/bin/pytest -n 2 -vvs
 
-    - template: etc/ci/azure-mac.yml
+    - template: etc/ci/azure-posix.yml
       parameters:
           job_name: macos1014_cpython
           image_name: macos-10.14
@@ -39,26 +39,10 @@ jobs:
           test_suites:
               all: tmp/bin/pytest -n 2 -vvs
 
-    - template: etc/ci/azure-mac.yml
+    - template: etc/ci/azure-posix.yml
       parameters:
           job_name: macos1015_cpython
           image_name: macos-10.15
           python_versions: ['3.6', '3.7', '3.8', '3.9']
           test_suites:
               all: tmp/bin/pytest -n 2 -vvs
-
-    - template: etc/ci/azure-win.yml
-      parameters:
-          job_name: win2016_cpython
-          image_name: vs2017-win2016
-          python_versions: ['3.6', '3.7', '3.8', '3.9']
-          test_suites:
-              all: tmp\Scripts\pytest -n 2 -vvs
-
-    - template: etc/ci/azure-win.yml
-      parameters:
-          job_name: win2019_cpython
-          image_name: windows-2019
-          python_versions: ['3.6', '3.7', '3.8', '3.9']
-          test_suites:
-              all: tmp\Scripts\pytest -n 2 -vvs
diff --git a/configure b/configure
index 5c57902..25ab0ce 100755
--- a/configure
+++ b/configure
@@ -1,44 +1,169 @@
 #!/usr/bin/env bash
 #
-# Copyright (c) nexB Inc. and others.
+# Copyright (c) nexB Inc. and others. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/ for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
 #
 
 set -e
 #set -x
 
-# source this script for a basic setup and configuration for local development
+################################
+# A configuration script to set things up: 
+# create a virtualenv and install or update thirdparty packages.
+# Source this script for initial configuration
+# Use configure --help for details
+#
+# This script will search for a virtualenv.pyz app in etc/thirdparty/virtualenv.pyz
+# Otherwise it will download the latest from the VIRTUALENV_PYZ_URL default
+################################
+CLI_ARGS=$1
 
-CONFIGURE_ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+################################
+# Defaults. Change these variables to customize this script
+################################
 
+# Requirement arguments passed to pip and used by default or with --dev.
+REQUIREMENTS="--editable ."
+DEV_REQUIREMENTS="--editable .[testing]"
 
-if [[ "$1" == "--clean" ]]; then
-    rm -rf "$CONFIGURE_ROOT_DIR/tmp"
-    exit
+# where we create a virtualenv
+VIRTUALENV_DIR=tmp
+
+# Cleanable files and directories with the --clean option
+CLEANABLE="
+    build
+	tmp"
+
+# extra  arguments passed to pip
+PIP_EXTRA_ARGS=" "
+
+# the URL to download virtualenv.pyz if needed
+VIRTUALENV_PYZ_URL=https://bootstrap.pypa.io/virtualenv.pyz
+################################
+
+
+################################
+# Current directory where this script lives
+CFG_ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+CFG_BIN_DIR=$CFG_ROOT_DIR/$VIRTUALENV_DIR/bin
+
+
+################################
+# Set the quiet flag to empty if not defined 
+if [[ "$CFG_QUIET" == "" ]]; then
+    CFG_QUIET=" "
 fi
 
 
-if [[ "$PYTHON_EXE" == "" ]]; then
-    PYTHON_EXE=python3.6
+################################
+# find a proper Python to run
+# Use environment variables or a file if available.
+# Otherwise the latest Python by default.
+if [[ "$PYTHON_EXECUTABLE" == "" ]]; then
+    # check for a file named PYTHON_EXECUTABLE 
+    if [ -f "$CFG_ROOT_DIR/PYTHON_EXECUTABLE" ]; then
+        PYTHON_EXECUTABLE=$(cat "$CFG_ROOT_DIR/PYTHON_EXECUTABLE")
+    else
+        PYTHON_EXECUTABLE=python3
+    fi
 fi
 
 
-function setup {
-    # create a virtualenv on Python
-    mkdir -p $CONFIGURE_ROOT_DIR/tmp
-    wget -O $CONFIGURE_ROOT_DIR/tmp/virtualenv.pyz https://bootstrap.pypa.io/virtualenv.pyz
-    $PYTHON_EXE $CONFIGURE_ROOT_DIR/tmp/virtualenv.pyz $CONFIGURE_ROOT_DIR/tmp
-    source $CONFIGURE_ROOT_DIR/tmp/bin/activate
-    $CONFIGURE_ROOT_DIR/tmp/bin/pip install --upgrade pip virtualenv setuptools wheel
+################################
+cli_help() {
+    echo An initial configuration script
+    echo "  usage: ./configure [options]"
+    echo
+    echo The default is to configure for regular use. Use --dev for development.
+    echo
+    echo The options are:
+    echo " --clean: clean built and installed files and exit."
+    echo " --dev:   configure the environment for development."
+    echo " --help:  display this help message and exit."
+    echo
+    echo By default, the python interpreter version found in the path is used.
+    echo Alternatively, the PYTHON_EXECUTABLE environment variable can be set to
+    echo configure another Python executable interpreter to use. If this is not
+    echo set, a file named PYTHON_EXECUTABLE containing a single line with the
+    echo path of the Python executable to use will be checked last.
+    set +e
+    exit
 }
 
 
-setup
+clean() {
+    # Remove cleanable file and directories and files from the root dir.
+    echo "* Cleaning ..."
+    for cln in $CLEANABLE;
+        do rm -rf "${CFG_ROOT_DIR:?}/${cln:?}";
+    done
+    set +e
+    exit
+}
 
-$CONFIGURE_ROOT_DIR/tmp/bin/pip install -r requirements_dev.txt
 
-if [ -f "$CONFIGURE_ROOT_DIR/tmp/bin/activate" ]; then
-    source "$CONFIGURE_ROOT_DIR/tmp/bin/activate"
-fi
+create_virtualenv() {
+    # create a virtualenv for Python
+    # Note: we do not use the bundled Python 3 "venv" because its behavior and
+    # presence is not consistent across Linux distro and sometimes pip is not
+    # included either by default. The virtualenv.pyz app cures all these issues.
+
+    VENV_DIR="$1"
+    if [ ! -f "$CFG_BIN_DIR/python" ]; then
+
+        mkdir -p "$CFG_ROOT_DIR/$VENV_DIR"
+
+        if [ -f "$CFG_ROOT_DIR/etc/thirdparty/virtualenv.pyz" ]; then
+            VIRTUALENV_PYZ="$CFG_ROOT_DIR/etc/thirdparty/virtualenv.pyz"
+        else
+            VIRTUALENV_PYZ="$CFG_ROOT_DIR/$VENV_DIR/virtualenv.pyz"
+            wget -O "$VIRTUALENV_PYZ" "$VIRTUALENV_PYZ_URL"
+        fi
+
+        $PYTHON_EXECUTABLE "$VIRTUALENV_PYZ" \
+            --wheel embed --pip embed --setuptools embed \
+            --seeder pip \
+            --never-download \
+            --no-periodic-update \
+            --no-vcs-ignore \
+            $CFG_QUIET \
+            "$CFG_ROOT_DIR/$VENV_DIR"
+    fi
+}
+
+
+install_packages() {
+    # install requirements in virtualenv
+    # note: --no-build-isolation means that pip/wheel/setuptools will not
+    # be reinstalled a second time and reused from the virtualenv and this
+    # speeds up the installation.
+    # We always have the PEP517 build dependencies installed already.
+
+    "$CFG_BIN_DIR/pip" install \
+        --upgrade \
+        --no-build-isolation \
+        $CFG_QUIET \
+        $PIP_EXTRA_ARGS \
+        $1
+}
+
+
+################################
+# Main command line entry point
+CFG_DEV_MODE=0
+CFG_REQUIREMENTS=$REQUIREMENTS
+
+case "$CLI_ARGS" in
+    --help)    cli_help;;
+    --clean)   clean;;
+    --dev)     CFG_REQUIREMENTS="$DEV_REQUIREMENTS" && CFG_DEV_MODE=1;;
+esac
+
+create_virtualenv "$VIRTUALENV_DIR"
+install_packages "$CFG_REQUIREMENTS"
+. "$CFG_BIN_DIR/activate"
 
 set +e
diff --git a/configure.bat b/configure.bat
deleted file mode 100644
index f03ea07..0000000
--- a/configure.bat
+++ /dev/null
@@ -1,121 +0,0 @@
-@echo OFF
-@setlocal
-@rem Copyright (c) nexB Inc. http://www.nexb.com/ - All rights reserved.
-
-@rem ################################
-@rem # A configuration script for Windows
-@rem #
-@rem # The options and (optional) arguments are:
-@rem #  --clean : this is exclusive of anything else and cleans the environment
-@rem #    from built and installed files
-@rem #
-@rem #  --python < path to python.exe> : this must be the first argument and set
-@rem #    the path to the Python executable to use. If < path to python.exe> is
-@rem #    set to "path", then the executable will be the python.exe available
-@rem #    in the PATH.
-@rem ################################
-
-@rem Current directory where this .bat files lives
-set CFG_ROOT_DIR=%~dp0
-@rem path where a configured Python should live in the current virtualenv if installed
-set CONFIGURED_PYTHON=%CFG_ROOT_DIR%tmp\Scripts\python.exe
-set PYTHON_EXECUTABLE=
-
-
-@rem parse command line options and arguments
-:collectopts
-if "%1" EQU "--help" (goto cli_help)
-if "%1" EQU "--clean" (call rmdir /s /q "%CFG_ROOT_DIR%tmp") && call exit /b
-if "%1" EQU "--python" (set PROVIDED_PYTHON=%~2) && shift && shift && goto collectopts
-
-@rem If we have a pre-configured Python in our virtualenv, reuse this as-is and run
-if exist ""%CONFIGURED_PYTHON%"" (
-    set PYTHON_EXECUTABLE=%CONFIGURED_PYTHON%
-    goto run
-)
-
-@rem If we have a command arg for Python use this as-is
-if ""%PROVIDED_PYTHON%""==""path"" (
-    @rem use a bare python available in the PATH
-    set PYTHON_EXECUTABLE=python
-    goto run
-)
-if exist ""%PROVIDED_PYTHON%"" (
-    set PYTHON_EXECUTABLE=%PROVIDED_PYTHON%
-    goto run
-)
-
-
-@rem otherwise we search for a suitable Python interpreter
-:find_python
-@rem First check the existence of the "py" launcher (available in Python 3)
-@rem if we have it, check if we have a py -3 installed with the good version or a py 2.7
-@rem if not, check if we have an old py 2.7
-@rem exist if all fails
-
-where py >nul 2>nul
-if %ERRORLEVEL% == 0 (
-    @rem we have a py launcher, check for the availability of our required Python 3 version
-    py -3.6 --version >nul 2>nul
-    if %ERRORLEVEL% == 0 (
-        set PYTHON_EXECUTABLE=py -3.6
-    ) else (
-        @rem we have no required python 3, let's try python 2:
-        py -2 --version >nul 2>nul
-        if %ERRORLEVEL% == 0 (
-            set PYTHON_EXECUTABLE=py -2
-        ) else (
-            @rem we have py and no python 3 and 2, exit
-            echo * Unable to find an installation of Python.
-            exit /b 1
-        )
-    )
-) else (
-    @rem we have no py launcher, check for a default Python 2 installation
-    if not exist ""%DEFAULT_PYTHON2%"" (
-       echo * Unable to find an installation of Python.
-       exit /b 1
-    ) else (
-        set PYTHON_EXECUTABLE=%DEFAULT_PYTHON2%
-    )
-)
-
-
-:run
-@rem without this things may not always work on Windows 10, but this makes things slower
-set PYTHONDONTWRITEBYTECODE=1
-
-call mkdir "%CFG_ROOT_DIR%tmp"
-call curl -o "%CFG_ROOT_DIR%tmp\virtualenv.pyz" https://bootstrap.pypa.io/virtualenv.pyz
-call %PYTHON_EXECUTABLE% "%CFG_ROOT_DIR%tmp\virtualenv.pyz" "%CFG_ROOT_DIR%tmp"
-call "%CFG_ROOT_DIR%tmp\Scripts\activate"
-call "%CFG_ROOT_DIR%tmp\Scripts\pip" install --upgrade pip virtualenv setuptools wheel
-call "%CFG_ROOT_DIR%tmp\Scripts\pip" install -e .[testing]
-
-@rem Return a proper return code on failure
-if %ERRORLEVEL% neq 0 (
-    exit /b %ERRORLEVEL%
-)
-endlocal
-goto activate
-
-
-:cli_help
-echo A configuration script for Windows
-echo usage: configure [options] [path/to/config/directory]
-echo.
-echo The options and arguments are:
-echo  --clean : this is exclusive of anything else and cleans the environment
-echo    from built and installed files
-echo.
-echo  --python path/to/python.exe : this is set to the path of an alternative
-echo    Python executable to use. If path/to/python.exe is set to "path",
-echo    then the executable will be the python.exe available in the PATH.
-echo.
-
-
-:activate
-@rem Activate the virtualenv
-if exist "%CFG_ROOT_DIR%tmp\Scripts\activate" (
-    "%CFG_ROOT_DIR%tmp\Scripts\activate"
-)
diff --git a/etc/ci/azure-mac.yml b/etc/ci/azure-mac.yml
deleted file mode 100644
index 752ae2e..0000000
--- a/etc/ci/azure-mac.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-parameters:
-    job_name: ''
-    image_name: ''
-    python_versions: []
-    test_suites: {}
-    python_architecture: x64
-
-jobs:
-    - job: ${{ parameters.job_name }}
-
-      pool:
-          vmImage: ${{ parameters.image_name }}
-
-      strategy:
-          matrix:
-            ${{ each pyver in parameters.python_versions }}:
-              ${{ each tsuite in parameters.test_suites }}:
-                 ${{ format('py{0} {1}', pyver, tsuite.key) }}:
-                     python_version: ${{ pyver }}
-                     test_suite_label: ${{ tsuite.key }}
-                     test_suite: ${{ tsuite.value }}
-      steps:
-          - checkout: self
-            fetchDepth: 10
-
-          - task: UsePythonVersion@0
-            inputs:
-                versionSpec: '$(python_version)'
-                architecture: '${{ parameters.python_architecture }}'
-            displayName: 'Install Python $(python_version)'
-
-          - script: ./configure
-            displayName: 'Run Configure'
-
-          - script: $(test_suite)
-            displayName: 'Run $(test_suite_label) tests with py$(python_version) on ${{ parameters.job_name }}'
diff --git a/etc/ci/azure-linux.yml b/etc/ci/azure-posix.yml
similarity index 84%
rename from etc/ci/azure-linux.yml
rename to etc/ci/azure-posix.yml
index 752ae2e..0921d9b 100644
--- a/etc/ci/azure-linux.yml
+++ b/etc/ci/azure-posix.yml
@@ -19,6 +19,7 @@ jobs:
                      python_version: ${{ pyver }}
                      test_suite_label: ${{ tsuite.key }}
                      test_suite: ${{ tsuite.value }}
+
       steps:
           - checkout: self
             fetchDepth: 10
@@ -29,7 +30,11 @@ jobs:
                 architecture: '${{ parameters.python_architecture }}'
             displayName: 'Install Python $(python_version)'
 
-          - script: ./configure
+          - script: |
+                python3 --version
+                python$(python_version) --version
+                echo "python$(python_version)" > PYTHON_EXECUTABLE
+                ./configure --dev
             displayName: 'Run Configure'
 
           - script: $(test_suite)
diff --git a/etc/ci/azure-win.yml b/etc/ci/azure-win.yml
deleted file mode 100644
index afe1686..0000000
--- a/etc/ci/azure-win.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-parameters:
-    job_name: ''
-    image_name: ''
-    python_versions: []
-    test_suites: {}
-    python_architecture: x64
-
-jobs:
-    - job: ${{ parameters.job_name }}
-
-      pool:
-          vmImage: ${{ parameters.image_name }}
-
-      strategy:
-          matrix:
-            ${{ each pyver in parameters.python_versions }}:
-              ${{ each tsuite in parameters.test_suites }}:
-                 ${{ format('py{0} {1}', pyver, tsuite.key) }}:
-                     python_version: ${{ pyver }}
-                     test_suite_label: ${{ tsuite.key }}
-                     test_suite: ${{ tsuite.value }}
-      steps:
-          - checkout: self
-            fetchDepth: 10
-
-          - task: UsePythonVersion@0
-            inputs:
-                versionSpec: '$(python_version)'
-                architecture: '${{ parameters.python_architecture }}'
-            displayName: 'Install Python $(python_version)'
-
-          - script: configure --python path
-            displayName: 'Run Configure'
-
-          - script: $(test_suite)
-            displayName: 'Run $(test_suite_label) tests with py$(python_version) on ${{ parameters.job_name }}'
diff --git a/pyproject.toml b/pyproject.toml
index f1021d2..8eebe91 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,6 +3,8 @@ requires = ["setuptools >= 50", "wheel", "setuptools_scm[toml] >= 4"]
 build-backend = "setuptools.build_meta"
 
 [tool.setuptools_scm]
+# this is used populated when creating a git archive
+# and when there is .git dir and/or there is no git installed
 fallback_version = "v9999.$Format:%h-%cs$"
 
 [tool.pytest.ini_options]
@@ -43,5 +45,5 @@ python_functions = "test"
 addopts = [
     "-rfExXw",
     "--strict-markers",
-    "--doctest-modules",
+    "--doctest-modules"
 ]
diff --git a/setup.cfg b/setup.cfg
index bd3e997..75ef426 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,6 +16,7 @@ url = https://github.com/nexB/container-inspector
 classifiers =
     Development Status :: 5 - Production/Stable
     Intended Audience :: Developers
+    Operating System :: POSIX
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3 :: Only
     Topic :: Software Development
@@ -35,6 +36,8 @@ install_requires =
     click
     attrs
     dockerfile_parse
+    extractcode[full]
+    commoncode
 
 python_requires= >=3.6
 
diff --git a/src/container_inspector/__init__.py b/src/container_inspector/__init__.py
index bfb2059..deffe27 100644
--- a/src/container_inspector/__init__.py
+++ b/src/container_inspector/__init__.py
@@ -13,16 +13,10 @@
 
 import re
 
-__version__ = '21.5.7'
-
-REPOSITORIES_FILE = 'repositories'  # legacy v1
-
 MANIFEST_JSON_FILE = 'manifest.json'
 
-LAYER_VERSION_FILE = 'VERSION'
-LAYER_JSON_FILE = 'json'  # legacy v1
 LAYER_TAR_FILE = 'layer.tar'
-    
+
 EMPTY_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
 EMPTY_DIGEST = 'sha256:' + EMPTY_SHA256
 
diff --git a/src/container_inspector/cli.py b/src/container_inspector/cli.py
index fd31083..9385485 100755
--- a/src/container_inspector/cli.py
+++ b/src/container_inspector/cli.py
@@ -11,7 +11,6 @@
 # CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 
-
 import csv as csv_module
 import json as json_module
 import logging
@@ -101,7 +100,7 @@ def container_inspector(image_path, extract_to=None, csv=False):
 
 
 def _container_inspector(image_path, extract_to=None, csv=False):
-    images = list(get_images_from_dir_or_tarball(image_path, extract_to=extract_to))
+    images = get_images_from_dir_or_tarball(image_path, extract_to=extract_to)
     as_json = not csv
 
     if as_json:
@@ -110,7 +109,7 @@ def _container_inspector(image_path, extract_to=None, csv=False):
     else:
         from io import StringIO
         output = StringIO()
-        flat = list(image.flatten_images(images))
+        flat = list(image.flatten_images_data(images))
         if not flat:
             return
         keys = flat[0].keys()
@@ -126,14 +125,18 @@ def _container_inspector(image_path, extract_to=None, csv=False):
 def get_images_from_dir_or_tarball(image_path, extract_to=None, quiet=False):
     image_loc = os.path.abspath(os.path.expanduser(image_path))
     if path.isdir(image_path):
-        images = list(image.Image.get_images_from_dir(image_loc))
+        images = image.Image.get_images_from_dir(image_loc)
     else:
         # assume tarball
         extract_to = extract_to or tempfile.mkdtemp()
-        images = list(image.Image.get_images_from_tarball(
-            image_loc, target_dir=extract_to, force_extract=True))
+        images = image.Image.get_images_from_tarball(
+            archive_location=image_loc,
+            extracted_location=extract_to,
+            verify=True,
+        )
+
         for img in images:
-            img.extract_layers(target_dir=extract_to)
+            img.extract_layers(extracted_location=extract_to)
         if not quiet:
             click.echo('Extracting image tarball to: {}'.format(extract_to))
     return images
diff --git a/src/container_inspector/distro.py b/src/container_inspector/distro.py
index 7d4ffaa..4d38a66 100755
--- a/src/container_inspector/distro.py
+++ b/src/container_inspector/distro.py
@@ -41,13 +41,15 @@ def logger_debug(*args):
 @attr.attributes
 class Distro(object):
     """
-    Configuration data. Shared definition as found in a layer json file and an image
-    config json file.
+    Configuration data. Shared definition as found in a layer json file and an
+    image config json file.
     """
 
     os = attr.attrib(
         default='linux',
-        metadata=dict(doc='Operating system, default to linux.')
+        metadata=dict(
+            doc='Operating system, default to linux. '
+                'One of: {}'.format(', '.join(os_choices)))
     )
 
     architecture = attr.attrib(
@@ -57,183 +59,215 @@ class Distro(object):
 
     name = attr.attrib(
         default='linux',
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            NAME= A string identifying the operating system, without a version component,
-            and suitable for presentation to the user. If not set, defaults to "NAME=Linux".
-            Example: "NAME=Fedora" or "NAME="Debian GNU/Linux"".
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            NAME= A string identifying the operating system, without a version
+            component, and suitable for presentation to the user. If not set,
+            defaults to "NAME=Linux". Example: "NAME=Fedora" or "NAME="Debian
+            GNU/Linux"".
         ''')
     )
 
     version = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            VERSION= A string identifying the operating system version, excluding any OS
-            name information, possibly including a release code name, and suitable for
-            presentation to the user. This field is optional. Example: "VERSION=17" or
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+
+            VERSION= A string identifying the operating system version,
+            excluding any OS name information, possibly including a release code
+            name, and suitable for presentation to the user. This field is
+            optional. Example: "VERSION=17" or
             "VERSION="17 (Beefy Miracle)"".
         ''')
     )
 
     identifier = attr.attrib(
         default='linux',
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            ID= A lower-case string (no spaces or other characters outside of 0–9, a–z, ".",
-            "_" and "-") identifying the operating system, excluding any version information
-            and suitable for processing by scripts or usage in generated filenames. If not
-            set, defaults to "ID=linux". Example: "ID=fedora" or "ID=debian".
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            ID= A lower-case string (no spaces or other characters outside of
+            0–9, a–z, ".", "_" and "-") identifying the operating system,
+            excluding any version information and suitable for processing by
+            scripts or usage in generated filenames. If not set, defaults to
+            "ID=linux". Example: "ID=fedora" or "ID=debian".
         ''')
     )
 
     id_like = attr.attrib(
         default=attr.Factory(list),
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            This is a list of ids, not a space-separated string.
-            ID_LIKE= A space-separated list of operating system identifiers in the same
-            syntax as the ID= setting. It should list identifiers of operating systems that
-            are closely related to the local operating system in regards to packaging and
-            programming interfaces, for example listing one or more OS identifiers the local
-            OS is a derivative from. An OS should generally only list other OS identifiers
-            it itself is a derivative of, and not any OSes that are derived from it, though
-            symmetric relationships are possible. Build scripts and similar should check
-            this variable if they need to identify the local operating system and the value
-            of ID= is not recognized. Operating systems should be listed in order of how
-            closely the local operating system relates to the listed ones, starting with the
-            closest. This field is optional. Example: for an operating system with
-            "ID=centos", an assignment of "ID_LIKE="rhel fedora"" would be appropriate. For
-            an operating system with "ID=ubuntu", an assignment of "ID_LIKE=debian" is
-            appropriate.
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            This is a list of ids, not a space-separated string. ID_LIKE= A
+            space-separated list of operating system identifiers in the same
+            syntax as the ID= setting. It should list identifiers of operating
+            systems that are closely related to the local operating system in
+            regards to packaging and programming interfaces, for example listing
+            one or more OS identifiers the local OS is a derivative from. An OS
+            should generally only list other OS identifiers it itself is a
+            derivative of, and not any OSes that are derived from it, though
+            symmetric relationships are possible. Build scripts and similar
+            should check this variable if they need to identify the local
+            operating system and the value of ID= is not recognized. Operating
+            systems should be listed in order of how closely the local operating
+            system relates to the listed ones, starting with the closest. This
+            field is optional. Example: for an operating system with
+            "ID=centos", an assignment of "ID_LIKE="rhel fedora"" would be
+            appropriate. For an operating system with "ID=ubuntu", an assignment
+            of "ID_LIKE=debian" is appropriate.
         ''')
     )
 
     version_codename = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            VERSION_CODENAME= A lower-case string (no spaces or other characters outside of
-            0–9, a–z, ".", "_" and "-") identifying the operating system release code name,
-            excluding any OS name information or release version, and suitable for
-            processing by scripts or usage in generated filenames. This field is optional
-            and may not be implemented on all systems. Examples: "VERSION_CODENAME=buster",
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            VERSION_CODENAME= A lower-case string (no spaces or other characters
+            outside of 0–9, a–z, ".", "_" and "-") identifying the operating
+            system release code name, excluding any OS name information or
+            release version, and suitable for processing by scripts or usage in
+            generated filenames. This field is optional and may not be
+            implemented on all systems. Examples: "VERSION_CODENAME=buster",
             "VERSION_CODENAME=xenial"
         ''')
     )
 
     version_id = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            VERSION_ID=A lower-case string (mostly numeric, no spaces or other characters
-            outside of 0–9, a–z, ".", "_" and "-") identifying the operating system version,
-            excluding any OS name information or release code name, and suitable for
-            processing by scripts or usage in generated filenames. This field is optional.
-            Example: "VERSION_ID=17" or "VERSION_ID=11.04".
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            VERSION_ID=A lower-case string (mostly numeric, no spaces or other
+            characters outside of 0–9, a–z, ".", "_" and "-") identifying the
+            operating system version, excluding any OS name information or
+            release code name, and suitable for processing by scripts or usage
+            in generated filenames. This field is optional. Example:
+            "VERSION_ID=17" or "VERSION_ID=11.04".
         ''')
     )
 
     pretty_name = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            PRETTY_NAME=A pretty operating system name in a format suitable for presentation
-            to the user. May or may not contain a release code name or OS version of some
-            kind, as suitable. If not set, defaults to "PRETTY_NAME="Linux"". Example:
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            PRETTY_NAME=A pretty operating system name in a format suitable for
+            presentation to the user. May or may not contain a release code name
+            or OS version of some kind, as suitable. If not set, defaults to
+            "PRETTY_NAME="Linux"". Example:
             "PRETTY_NAME="Fedora 17 (Beefy Miracle)"".
         ''')
     )
 
     cpe_name = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            CPE_NAME=A CPE name for the operating system, in URI binding syntax, following
-            the Common Platform Enumeration Specification as proposed by the NIST. This
-            field is optional. Example: "CPE_NAME="cpe:/o:fedoraproject:fedora:17""
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            CPE_NAME=A CPE name for the operating system, in URI binding syntax,
+            following the Common Platform Enumeration Specification as proposed
+            by the NIST. This field is optional. Example:
+            "CPE_NAME="cpe:/o:fedoraproject:fedora:17""
         ''')
     )
 
     home_url = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            HOME_URL= should refer to the homepage of the operating system, or alternatively
-            some homepage of the specific version of the operating system.
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            HOME_URL= should refer to the homepage of the operating system, or
+            alternatively some homepage of the specific version of the operating
+            system.
         ''')
     )
 
     documentation_url = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            DOCUMENTATION_URL= should refer to the main documentation page for this
-            operating system.
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            DOCUMENTATION_URL= should refer to the main documentation page for
+            this operating system.
         ''')
     )
 
     support_url = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            SUPPORT_URL= should refer to the main support page for the
-            operating system, if there is any. This is primarily intended for operating
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            SUPPORT_URL= should refer to the main support page for the operating
+            system, if there is any. This is primarily intended for operating
             systems which vendors provide support for.
         ''')
     )
 
     bug_report_url = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            BUG_REPORT_URL= should refer to the
-            main bug reporting page for the operating system, if there is any. This is
-            primarily intended for operating systems that rely on community QA.
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            BUG_REPORT_URL= should refer to the main bug reporting page for the
+            operating system, if there is any. This is primarily intended for
+            operating systems that rely on community QA.
         ''')
     )
 
     privacy_policy_url = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            PRIVACY_POLICY_URL= should refer to the main privacy policy page for the
-            operating system, if there is any.
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            PRIVACY_POLICY_URL= should refer to the main privacy policy page for
+            the operating system, if there is any.
         ''')
     )
 
     build_id = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            BUILD_ID=A string uniquely identifying the system image used as the origin for a
-            distribution (it is not updated with system updates). The field can be identical
-            between different VERSION_IDs as BUILD_ID is an only a unique identifier to a
-            specific version. Distributions that release each update as a new version would
-            only need to use VERSION_ID as each build is already distinct based on the
-            VERSION_ID. This field is optional. Example: "BUILD_ID="2013-03-20.3"" or
-            "BUILD_ID=201303203".
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            BUILD_ID=A string uniquely identifying the system image used as the
+            origin for a distribution (it is not updated with system updates).
+            The field can be identical between different VERSION_IDs as BUILD_ID
+            is an only a unique identifier to a specific version. Distributions
+            that release each update as a new version would only need to use
+            VERSION_ID as each build is already distinct based on the
+            VERSION_ID. This field is optional. Example:
+            "BUILD_ID="2013-03-20.3"" or "BUILD_ID=201303203".
         ''')
     )
 
     variant = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            VARIANT= A string identifying a specific variant or edition of the operating
-            system suitable for presentation to the user. This field may be used to inform
-            the user that the configuration of this system is subject to a specific
-            divergent set of rules or default configuration settings. This field is optional
-            and may not be implemented on all systems. Examples: "VARIANT="Server Edition"",
-            "VARIANT="Smart Refrigerator Edition"" Note: this field is for display purposes
-            only. The VARIANT_ID field should be used for making programmatic decisions.
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            VARIANT= A string identifying a specific variant or edition of the
+            operating system suitable for presentation to the user. This field
+            may be used to inform the user that the configuration of this system
+            is subject to a specific divergent set of rules or default
+            configuration settings. This field is optional and may not be
+            implemented on all systems. Examples: "VARIANT="Server Edition"",
+            "VARIANT="Smart Refrigerator Edition"" Note: this field is for
+            display purposes only. The VARIANT_ID field should be used for
+            making programmatic decisions.
         ''')
     )
 
     variant_id = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            VARIANT_ID= A lower-case string (no spaces or other characters outside of 0–9,
-            a–z, ".", "_" and "-"), identifying a specific variant or edition of the
-            operating system. This may be interpreted by other packages in order to
-            determine a divergent default configuration. This field is optional and may not
-            be implemented on all systems. Examples: "VARIANT_ID=server",
-            "VARIANT_ID=embedded"
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            VARIANT_ID= A lower-case string (no spaces or other characters
+            outside of 0–9, a–z, ".", "_" and "-"), identifying a specific
+            variant or edition of the operating system. This may be interpreted
+            by other packages in order to determine a divergent default
+            configuration. This field is optional and may not be implemented on
+            all systems. Examples: "VARIANT_ID=server", "VARIANT_ID=embedded"
         ''')
     )
 
     logo = attr.attrib(
         default=None,
-        metadata=dict(doc='''Based on os-release: https://www.freedesktop.org/software/systemd/man/os-release.html
-            LOGO= A string, specifying the name of an icon as defined by freedesktop.org
-            Icon Theme Specification. This can be used by graphical applications to display
-            an operating system's or distributor's logo. This field is optional and may not
-            necessarily be implemented on all systems. Examples: "LOGO=fedora-logo", "LOGO
+        metadata=dict(doc='''Based on os-release:
+            https://www.freedesktop.org/software/systemd/man/os-release.html
+            LOGO= A string, specifying the name of an icon as defined by
+            freedesktop.org Icon Theme Specification. This can be used by
+            graphical applications to display an operating system's or
+            distributor's logo. This field is optional and may not necessarily
+            be implemented on all systems. Examples: "LOGO=fedora-logo", "LOGO
             =distributor-logo-opensuse"
         ''')
     )
@@ -254,13 +288,12 @@ def to_dict(self):
         return attr.asdict(self)
 
     @classmethod
-    def from_file(cls, location):
+    def from_os_release_file(cls, location):
         """
-        Return a Distro built from an os-release file.
+        Return a Distro built from a Linux os-release file.
         """
-
         data = parse_os_release(location) or {}
-        kwargs = dict(
+        new_data = dict(
             # This idiom looks a tad wierd but we want to always get a linux as
             # default even if the poped value is an empty string or None
             os=data.pop('OS', 'linux') or 'linux',
@@ -291,21 +324,66 @@ def from_file(cls, location):
         # note: we poped all known key/value pairs above.
         # therefore the remainder are unknown, extra data.
         if data:
-            kwargs['extra_data'] = data
+            new_data['extra_data'] = data
 
-        return cls(**kwargs)
+        return cls(**new_data)
+
+    from_file = from_os_release_file
 
     @classmethod
-    def from_rootfs(cls, location):
+    def from_rootfs(cls, location, base_distro=None):
         """
-        Return a Distro built from an os-release file in the root filesystem at
-        `location`.
+        Return a Distro discovered from the rootfs at `location`.
+        Return None if no OS was found.
+
+        Use the optional ``base_distro`` Distro object attributes as a base and
+        to guide discovery. If provided ``base_distro`` may be returned as-is
+        if no extra OS details were found.
         """
-        # /etc/os-release has precedence over /usr/lib/os-release.
+        finders = {
+            'linux': cls.find_linux_details,
+            'windows': cls.find_windows_details,
+            'freebsd': cls.find_freebsd_details,
+        }
+
+        for finder_os, finder in finders.items():
+            if base_distro and base_distro.os != finder_os:
+                continue
+
+            found = finder(location)
+            if found:
+                return base_distro.merge(found)
+
+        if base_distro:
+            return base_distro
+
+    @classmethod
+    def find_linux_details(cls, location):
+        """
+        Find a linux distro details using the os-release file and return a
+        Distro object or None.
+        """
+        # note: /etc/os-release has precedence over /usr/lib/os-release.
         for candidate_path in ('etc/os-release', 'usr/lib/os-release',):
             os_release = path.join(location, candidate_path)
             if path.exists(os_release):
-                return cls.from_file(os_release)
+                return cls.from_os_release_file(location=os_release)
+
+    @classmethod
+    def find_windows_details(cls, location):
+        """
+        Find a Windows installation details and return a Distro object or None.
+        """
+        return cls(
+            os='windows',
+            identifier='identifier',
+        )
+
+    @classmethod
+    def find_freebsd_details(cls, location):
+        """
+        Find a FreeBSDinstallation details and return a Distro object or None.
+        """
 
     def categories(self):
         """
@@ -337,6 +415,17 @@ def categories(self):
             ),
         )
 
+    def merge(self, other_distro):
+        """
+        Return a new distro based on this Distro data updated with non-empty
+        values from the ``other_distro`` Distro object.
+        """
+        existing = self.to_dict()
+        if other_distro:
+            other_non_empty = {k: v for k, v in other_distro.to_dict().items() if v}
+            existing.update(other_non_empty)
+        return type(self)(**existing)
+
 
 def parse_os_release(location):
     """
@@ -379,6 +468,7 @@ def get_alpine_details():
     release is in /etc/alpine-release
     See /etc/apk/repositories to get a list of base repo URLS
     """
+    pass
 
 
 def get_rpm_details():
@@ -387,6 +477,7 @@ def get_rpm_details():
     repos baseurl.
     /etc/redhat-release contains a version
     """
+    pass
 
 
 def get_fedora_details():
@@ -420,6 +511,7 @@ def get_suse_details():
 
     /etc/products.d/openSUSE.prod is an XML file with many details
     """
+    pass
 
 
 def get_rhel_details():
@@ -428,6 +520,7 @@ def get_rhel_details():
     /usr/lib/swidtag/redhat.com/com.redhat.RHEL-8.0-x86_64.swidtag
     /usr/lib/swidtag/redhat.com/com.redhat.RHEL-8-x86_64.swidtag
     """
+    pass
 
 
 def get_centos_details():
@@ -444,6 +537,7 @@ def get_centos_details():
 
     presence of /usr/share/doc/centos-release-5/ dir may help for older centos
     """
+    pass
 
 
 def get_distroless_details():
@@ -464,6 +558,7 @@ def get_distroless_details():
     SUPPORT_URL="https://github.com/GoogleContainerTools/distroless/blob/master/README.md"
     BUG_REPORT_URL="https://github.com/GoogleContainerTools/distroless/issues/new"
     """
+    pass
 
 
 def get_busybox_details():
@@ -480,3 +575,4 @@ def get_busybox_details():
     /etc/passwd
     /etc/shadow
     """
+    pass
diff --git a/src/container_inspector/dockerfile.py b/src/container_inspector/dockerfile.py
index 33f5c3d..26260d6 100755
--- a/src/container_inspector/dockerfile.py
+++ b/src/container_inspector/dockerfile.py
@@ -18,13 +18,11 @@
 import dockerfile_parse
 import os
 
-
 logger = logging.getLogger(__name__)
 # un-comment these lines to enable logging
 # logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
 # logger.setLevel(logging.DEBUG)
 
-
 """
 Analysis helper for Docker Dockerfiles.
 """
@@ -223,7 +221,6 @@ def map_image_to_dockerfile(image, dockerfile):
     from_image_name_tag = from_base['value'].strip()
     from_image_name, _, from_image_tag = from_image_name_tag.partition(':')
 
-
     # align layers and dockerfile lines, from top to bottom
     aligned = map(None, reversed(image.layers), reversed(dockerfile['instructions']))
 
diff --git a/src/container_inspector/fetch/__init__.py b/src/container_inspector/fetch/__init__.py
deleted file mode 100644
index 10a02e8..0000000
--- a/src/container_inspector/fetch/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) nexB Inc. and others. All rights reserved.
-# http://nexb.com and https://github.com/nexB/container-inspector/
-#
-# This software is licensed under the Apache License version 2.0.#
-#
-# You may not use this software except in compliance with the License.
-# You may obtain a copy of the License at:
-#     http://apache.org/licenses/LICENSE-2.0
-# Unless required by applicable law or agreed to in writing, software distributed
-# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
-# CONDITIONS OF ANY KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations under the License.
-
-from __future__ import print_function
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
-from .docker_image import DockerRegistry
-from .docker_image import normalize_docker_registry_name
-from .docker_image import DOCKER_REGISTRY
\ No newline at end of file
diff --git a/src/container_inspector/fetch/docker_image.py b/src/container_inspector/fetch/docker_image.py
deleted file mode 100644
index 06fffb8..0000000
--- a/src/container_inspector/fetch/docker_image.py
+++ /dev/null
@@ -1,518 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding:UTF-8 -*-
-# Copyright (c) 2020 Nicolas Iooss
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-"""Get information about Docker images
-
-This retrieves information about a Docker image and can download an extract it,
-even when the host appears not to be compatible with the container (contrary to
-docker pull, podman pull, buildah, etc.).
-
-Usage example:
-
-    # List tags of official Alpine and Debian images
-    # (https://hub.docker.com/_/alpine and https://hub.docker.com/_/debian)
-    docker_image.py -l alpine debian
-
-    # Download Centos 8 image
-    docker_image.py -o docker_cache.out centos:8
-
-    # Download Official Microsoft SQL Server Express Edition images for Windows Containers
-    # (https://hub.docker.com/r/microsoft/mssql-server-windows-express/)
-    docker_image.py -o docker_cache.out microsoft/mssql-server-windows-express
-
-    # Download Official images for Microsoft SQL Server on Linux for Docker Engine
-    # (https://hub.docker.com/_/microsoft-mssql-server)
-    docker_image.py -o docker_cache.out -r mcr.microsoft.com mssql/server:latest-ubuntu
-
-Documentation:
-* https://docs.docker.com/registry/spec/api/
-  Specification of Docker registry API
-* https://docs.docker.com/registry/spec/auth/scope/
-  Token Scope Documentation
-
-@author: Nicolas Iooss
-@license: MIT
-"""
-import argparse
-import hashlib
-import json
-import logging
-from pathlib import Path
-import os
-import re
-
-import requests
-
-
-# pylint: disable=invalid-name
-logger = logging.getLogger(__name__)
-
-
-DOCKER_REGISTRY = 'https://registry-1.docker.io'
-
-
-def normalize_docker_registry_name(image_name):
-    """Normalize the name of an image for Docker registry"""
-    if image_name.startswith('_/'):
-        # On Docker Hub, "_/name" means "library/name"
-        return 'library/{}'.format(image_name[2:])
-
-    if '/' not in image_name:
-        # Official images are located in library
-        return 'library/{}'.format(image_name)
-
-    return image_name
-
-
-def split_auth_header(header):
-    """Split comma-separate key=value fields from a WWW-Authenticate header"""
-    fields = {}
-    remaining = header.strip()
-    while remaining:
-        matches = re.match(r'^([0-9a-zA-Z]+)="([^"]+)"(?:,\s*(.+))?$', remaining)
-        if not matches:
-            # Without quotes
-            matches = re.match(r'^([0-9a-zA-Z]+)=([^",]+)(?:,\s*(.+))?$', remaining)
-        if not matches:
-            raise ValueError("Unable to parse auth header {}".format(repr(header)))
-        key, value, remaining = matches.groups()
-        fields[key] = value
-    return fields
-
-
-class DockerRegistry:
-    """Handle information about a connection to a docker registry"""
-    def __init__(self, url, trust_filesystem=False):
-        url = url.rstrip('/')
-        # Prepend https:// automatically
-        if '://' not in url:
-            url = f'https://{url}'
-
-        self.url = url
-        self.headers = {}
-        self.last_auth_scope = None
-        self.trust_filesystem = trust_filesystem
-
-        # Set of layer digests that have already been validated in cache
-        # Dictionary of "sha256:xxxxx" -> size of layer
-        self.cached_layer_digests = {}
-
-        # Authenticate with the registry, using /v2/ ping response
-        # cf. https://github.com/containers/image/blob/v5.1.0/docker/docker_client.go#L508
-        # and https://github.com/containers/image/pull/211#issuecomment-273426236
-        # ... or not. Such a request does not work on gcr.io with an empty scope,
-        # so rely on requests using ping(scope=...) to authenticate in a
-        # "lazy evaluation way" with the registry.
-        # self.ping()
-
-    def normalize_name_if_needed(self, image_name):
-        """Normalize the name of an image for Docker registry, if the registry is Docker's one"""
-        if self.url == DOCKER_REGISTRY:
-            return normalize_docker_registry_name(image_name)
-        return image_name
-
-    def ping(self, scope=None):
-        """Ping the registry, authenticating if needed"""
-        if 'Authorization' in self.headers and scope != self.last_auth_scope:
-            # Invalidate the authentication header
-            del self.headers['Authorization']
-
-        ping_url = '{}/v2/'.format(self.url)
-        ping_response = requests.get(ping_url, headers=self.headers, allow_redirects=False)
-        if ping_response.status_code == 200:
-            logger.debug("Ping OK, result=%r", ping_response.json())
-            return ping_response.json()
-
-        if ping_response.status_code == 401:
-            # Authenticate to the registry
-            auth_header = ping_response.headers['WWW-Authenticate']
-            kind, fields_str = auth_header.split(' ', 1)
-            if kind == 'Bearer':
-                logger.debug("Authenticating to %r with scope=%r", fields_str, scope)
-                fields = split_auth_header(fields_str)
-                auth_realm = fields['realm']
-                auth_service = fields.get('service')
-
-                if not auth_realm.startswith('https://'):
-                    logger.error("Authentication realm %r is not HTTPS", auth_realm)
-                    raise ValueError("Authentication realm is not HTTPS, refusing to use it")
-                auth_params = {}
-                if auth_service:
-                    auth_params['service'] = auth_service
-                if scope:
-                    auth_params['scope'] = scope
-
-                auth_response = requests.get(auth_realm, params=auth_params, allow_redirects=False)
-                if auth_response.status_code != 200:
-                    logger.error("Unexpected auth response code %d", auth_response.status_code)
-                    raise ValueError("Unexpected auth response")
-
-                # Extract the authentication token from the response and put it in an Authorization header
-                auth_resp_json = auth_response.json()
-                auth_token = auth_resp_json['token']
-                self.headers['Authorization'] = 'Bearer {}'.format(auth_token)
-                self.last_auth_scope = scope
-
-                # Try pinging again
-                ping_response = requests.get(ping_url, headers=self.headers, allow_redirects=False)
-                if ping_response.status_code != 200:
-                    logger.error("Unsuccessful authenticated ping response code %d", ping_response.status_code)
-                    raise ValueError("Unsuccessful authenticated ping response")
-                # https://registry-1.docker.io replies with '{}'
-                # https://gcr.io replies with ''
-                # https://quay.io replies with 'true'
-                logger.debug("Authentication OK, ping result=%r", ping_response.content)
-                return ping_response.content
-
-            raise ValueError("Unexpected authentication header kind {}".format(repr(auth_header)))
-
-        logger.error("Unexpected ping response code %d", ping_response.status_code)
-        raise ValueError("Unexpected ping response")
-
-    def get_with_scope(self, url, scope, headers=None, allow_redirects=False, stream=False):
-        """Perform a HTTP GET request with the specified scope"""
-        assert scope
-        if self.last_auth_scope != scope:
-            self.ping(scope=scope)
-        used_headers = self.headers
-        if headers:
-            used_headers = used_headers.copy()
-            used_headers.update(headers)
-        response = requests.get(
-            url,
-            headers=used_headers,
-            allow_redirects=allow_redirects,
-            stream=stream)
-        if response.status_code == 401:
-            # Launch an authentication procedure (the token may have expired)
-            logger.debug("Trying to authenticate again, to get %r", url)
-            self.last_auth_scope = None
-            self.ping(scope=scope)
-
-            # Recompute the headers with the new authentication header value
-            used_headers = self.headers
-            if headers:
-                used_headers = used_headers.copy()
-                used_headers.update(headers)
-            response = requests.get(
-                url,
-                headers=used_headers,
-                allow_redirects=allow_redirects,
-                stream=stream)
-        return response
-
-    def list_tags(self, image_name):
-        """List the available tags of an image"""
-        image_name = self.normalize_name_if_needed(image_name)
-        response = self.get_with_scope(
-            '{}/v2/{}/tags/list'.format(self.url, image_name),
-            scope='repository:{}:pull'.format(image_name),
-            allow_redirects=True)
-        if response.status_code != 200:
-            logger.error("Unable to retrieve the tags of image %r: HTTP error %d",
-                         image_name, response.status_code)
-            logger.error("... Response JSON: %r", response.json())
-            raise ValueError("HTTP error {}".format(response.status_code))
-
-        data = response.json()
-        if data['name'] != image_name:
-            logger.error("Received data %r", data)
-            raise ValueError("Mismatched image name in data: {} != {}".format(
-                repr(data['name']), repr(image_name)))
-        return data['tags']
-
-    def get_manifest(self, image_name, tag_name):
-        """Retrieve the manifest of an image with the specified tag"""
-        image_name = self.normalize_name_if_needed(image_name)
-        response = self.get_with_scope(
-            '{}/v2/{}/manifests/{}'.format(self.url, image_name, tag_name),
-            scope='repository:{}:pull'.format(image_name),
-            headers={
-                # Request Image Manifest Version 2, Schema 2, if available
-                'Accept': (
-                    'application/vnd.docker.distribution.manifest.v2+json, ' +
-                    'application/vnd.docker.distribution.manifest.v1+json'
-                ),
-            },
-            allow_redirects=True)
-        if response.status_code != 200:
-            logger.error("Unable to retrieve the manifest of %r:%r: HTTP error %d",
-                         image_name, tag_name, response.status_code)
-            logger.error("... Response JSON: %r", response.json())
-            raise ValueError("HTTP error {}".format(response.status_code))
-
-        content_type = response.headers['content-type']
-        if content_type == 'application/vnd.docker.distribution.manifest.v1+json':
-            # Return a v1 manifest
-            manifest = response.json()
-            return {'v1': manifest}
-
-        if content_type == 'application/vnd.docker.distribution.manifest.v2+json':
-            # Return a v2 manifest, plus configuration
-            manifest = response.json()
-            result = {'v2': manifest}
-            try:
-                config_type = manifest['config']['mediaType']
-            except KeyError:
-                pass
-            else:
-                if config_type == 'application/vnd.docker.container.image.v1+json':
-                    # Request the configuration
-                    response = self.get_with_scope(
-                        '{}/v2/{}/manifests/{}'.format(self.url, image_name, tag_name),
-                        scope='repository:{}:pull'.format(image_name),
-                        headers={'Accept': config_type},
-                        allow_redirects=True)
-                    if response.status_code != 200:
-                        # This may happen, for example with the following error:
-                        # {'errors': [{'code': 'MANIFEST_INVALID', 'message': 'manifest invalid', 'detail': {}}]}
-                        # In this case, the v2 manifest is still valid, even though it does not have a config
-                        logger.warning("Unable to retrieve the manifest v2 config of %r:%r: HTTP error %d",
-                                       image_name, tag_name, response.status_code)
-                        logger.warning("... Response JSON: %r", response.json())
-                    else:
-                        result['config'] = response.json()
-                else:
-                    raise ValueError("Unimplemented manifest v2 config type {}".format(repr(config_type)))
-            return result
-
-        raise ValueError("Unimplemented manifest content type {}".format(repr(content_type)))
-
-    def download_layer(self, image_name, digest_name, output_path, urls=None):
-        """Download a layer according to the specified digest"""
-        # If the layer is already known, return its size
-        cached_layer_size = self.cached_layer_digests.get(digest_name)
-        if cached_layer_size:
-            logger.info("Using already-validated %d bytes from %s",
-                        cached_layer_size, output_path)
-            return cached_layer_size
-
-        # If the output already exists, checks its content
-        if output_path.exists():
-            if self.trust_filesystem:
-                # Trust that the file holds the right content
-                total_size = output_path.stat().st_size
-                logger.info("Trusting cached %d bytes from %s", total_size, output_path)
-                self.cached_layer_digests[digest_name] = total_size
-                return total_size
-
-            computed_digest = hashlib.sha256()
-            total_size = 0
-            with output_path.open('rb') as fout:
-                while True:
-                    chunk = fout.read(4096)
-                    if not chunk:
-                        break
-                    computed_digest.update(chunk)
-                    total_size += len(chunk)
-            resulting_name = 'sha256:{}'.format(computed_digest.hexdigest())
-            if resulting_name != digest_name:
-                raise ValueError("Invalid file {} with mismatched SHA256 digest: {} != {}".format(
-                    output_path, repr(resulting_name), repr(digest_name)))
-            logger.info("Validated cached %d bytes from %s", total_size, output_path)
-            self.cached_layer_digests[digest_name] = total_size
-            return total_size
-
-        image_name = self.normalize_name_if_needed(image_name)
-
-        # Download into a ".part" file
-        output_part_path = output_path.with_name(output_path.name + '.part')
-
-        if not urls:
-            # Allow redirections, here
-            response = self.get_with_scope(
-                '{}/v2/{}/blobs/{}'.format(self.url, image_name, digest_name),
-                scope='repository:{}:pull'.format(image_name),
-                allow_redirects=True,
-                stream=True)
-        else:
-            # Use other URLs that the registry
-            for foreign_url in urls:
-                response = self.get_with_scope(
-                    foreign_url,
-                    scope='repository:{}:pull'.format(image_name),
-                    allow_redirects=True,
-                    stream=True)
-                if response.status_code == 200:
-                    break
-                logger.warning("Unable to retrieve layer %r:%r from %r: HTTP error %d",
-                               image_name, digest_name, foreign_url, response.status_code)
-                try:
-                    logger.warning("... Response JSON: %r", response.json())
-                except json.JSONDecodeError:
-                    # Some HTTP 404 errors do not provide a JSON response
-                    logger.warning("... Response (not JSON): %r", response.content)
-                # Continuing with other foreign URL...
-                continue
-            if response.status_code != 200:
-                logger.error("Unable to retrieve layer %r:%r using foreign URL: HTTP error %d",
-                             image_name, digest_name, response.status_code)
-                raise ValueError("HTTP error {}".format(response.status_code))
-
-        if response.status_code != 200:
-            logger.error("Unable to retrieve layer %r:%r: HTTP error %d",
-                         image_name, digest_name, response.status_code)
-            logger.error("... Response JSON: %r", response.json())
-            raise ValueError("HTTP error {}".format(response.status_code))
-
-        total_size = 0
-        computed_digest = hashlib.sha256()
-        with output_part_path.open('wb') as fout:
-            for chunk in response.iter_content(chunk_size=4096):
-                computed_digest.update(chunk)
-                while chunk:
-                    written = fout.write(chunk)
-                    if written == 0:
-                        raise IOError("Unable to write to layer output file {}".format(output_part_path))
-                    total_size += written
-                    chunk = chunk[written:]
-        resulting_name = 'sha256:{}'.format(computed_digest.hexdigest())
-        if resulting_name != digest_name:
-            raise ValueError("Mismatched SHA256 digest: {} != {}".format(
-                repr(resulting_name), repr(digest_name)))
-        os.replace(output_part_path, output_path)
-        logger.info("Downloaded %d bytes to %s", total_size, output_path)
-        self.cached_layer_digests[digest_name] = total_size
-        return total_size
-
-    def download_image(self, image_name, tag_name, output_path):
-        """Download an image into the given output directory and return its manifest"""
-        # Start by downloading the manifest
-        image_name = self.normalize_name_if_needed(image_name)
-        manifest = self.get_manifest(image_name, tag_name)
-
-        # Save the manifest somewhere
-        manifest_escaped_name = '{}__{}.manifest.json'.format(image_name, tag_name)
-        manifest_escaped_name = manifest_escaped_name.replace('/', '__')
-        manifest_escaped_name = manifest_escaped_name.replace('\\', '__')
-        manifest_escaped_name = manifest_escaped_name.replace(':', '__')
-        if not re.match(r'^[0-9A-Za-z_.-]+$', manifest_escaped_name):
-            logger.error("Unescaped characters present in name %r from %r:%r",
-                         manifest_escaped_name, image_name, tag_name)
-            raise ValueError("Unable to save manifest to {}".format(repr(manifest_escaped_name)))
-        with (output_path / manifest_escaped_name).open('w') as fout:
-            json.dump(manifest, fout, indent=2)
-            fout.write('\n')
-        logger.info("Saved %s:%s manifest to %r", image_name, tag_name, manifest_escaped_name)
-
-        # Grab all layers
-        downloaded_layers = set()
-        if 'v2' in manifest:
-            for layer in manifest['v2']['layers']:
-                expected_size = layer['size']
-                digest_name = layer['digest']
-                media_type = layer['mediaType']
-                if digest_name in downloaded_layers:
-                    continue
-                if not re.match(r'^[0-9a-z]+:[0-9a-f]+$', digest_name):
-                    raise ValueError("Unexpected layer digest format: {}".format(repr(digest_name)))
-
-                # Retrieve foreign URLs
-                foreign_urls = None
-                if media_type == 'application/vnd.docker.image.rootfs.diff.tar.gzip':
-                    pass
-                elif media_type == 'application/vnd.docker.image.rootfs.foreign.diff.tar.gzip':
-                    foreign_urls = layer.get('urls')
-                    if not foreign_urls:
-                        logger.warning("No foreign URLs for foreign layer %r", layer)
-                else:
-                    logger.warning("Unknown layer media type: %r", layer)
-
-                layer_path = output_path / (digest_name.replace(':', '_') + '.tar.gz')
-                downloaded_size = self.download_layer(image_name, digest_name, layer_path, urls=foreign_urls)
-                if downloaded_size != expected_size:
-                    raise ValueError("Mismatched downloaded size for layer {}: {} != {}".format(
-                        digest_name, expected_size, downloaded_size))
-                downloaded_layers.add(digest_name)
-
-        for v1_key in ('v1', 'config'):
-            if v1_key not in manifest:
-                continue
-            for layer in manifest[v1_key]['fsLayers']:
-                digest_name = layer['blobSum']
-                if digest_name in downloaded_layers:
-                    continue
-                if not re.match(r'^[0-9a-z]+:[0-9a-f]+$', digest_name):
-                    raise ValueError("Unexpected layer digest format: {}".format(repr(digest_name)))
-                layer_path = output_path / (digest_name.replace(':', '_') + '.tar.gz')
-                self.download_layer(image_name, digest_name, layer_path)
-                downloaded_layers.add(digest_name)
-
-        return manifest
-
-
-def main(argv=None):
-    parser = argparse.ArgumentParser(description="Get information about Docker images")
-    parser.add_argument('image', metavar="IMAGE", nargs='+', type=str,
-                        help="Docker image to use")
-    parser.add_argument('-d', '--debug', action='store_true',
-                        help="show debug messages")
-    parser.add_argument('-l', '--list-tags', action='store_true',
-                        help="List image tags")
-    parser.add_argument('-o', '--output', type=Path,
-                        help="Output directory to download layers to")
-    parser.add_argument('-r', '--registry', type=str, default=DOCKER_REGISTRY,
-                        help="Docker registry to use (default: {})".format(DOCKER_REGISTRY))
-    parser.add_argument('-t', '--tag', nargs='+', type=str,
-                        help="Tags to use (default: latest)")
-    parser.add_argument('-T', '--trust-filesystem', action='store_true',
-                        help="Blindly trust layer files that are already present in the output directory")
-    args = parser.parse_args(argv)
-
-    logging.basicConfig(format='[%(levelname)s] %(message)s',
-                        level=logging.DEBUG if args.debug else logging.INFO)
-
-    registry = DockerRegistry(args.registry, trust_filesystem=args.trust_filesystem)
-    if args.list_tags:
-        for image in args.image:
-            tags = registry.list_tags(image)
-            print("Tags of image {}:".format(repr(image)))
-            for tag in tags:
-                print("  {}".format(tag))
-        return
-
-    if args.output:
-        args.output.mkdir(exist_ok=True)
-
-    for image in args.image:
-        tag_names = args.tag
-        if ':' in image:
-            # Overwrite the tag if the image name contains a colon
-            image, tag = image.split(':')
-            tag_names = (tag, )
-        elif not tag_names:
-            # Use "latest" by default
-            tag_names = ('latest', )
-
-        for tag in tag_names:
-            if not args.output:
-                # Without an output directory, print the manifest
-                print("Manifest of {}:{}:".format(image, tag))
-                manifest = registry.get_manifest(image, tag)
-                print(json.dumps(manifest, indent=2))
-            else:
-                # With an output directory, save the manifest and download layers
-                logger.info("Downloading %s:%s...", image, tag)
-                registry.download_image(image, tag, args.output)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/container_inspector/fetch/docker_image.py.ABOUT b/src/container_inspector/fetch/docker_image.py.ABOUT
deleted file mode 100644
index 74e7ea5..0000000
--- a/src/container_inspector/fetch/docker_image.py.ABOUT
+++ /dev/null
@@ -1,13 +0,0 @@
-about_resource: docker_image.py
-download_url: https://raw.githubusercontent.com/fishilico/shared/54adb42a34381b6fb8710ba59488c7e5e6e9c3c7/python/network/docker_image.py
-homepage_url: https://github.com/fishilico/shared
-license_expression: mit
-copyright: Copyright (c) Nicolas Iooss
-attribute: yes
-checksum_md5: 93ac137e7bf41d97f831d52f960def0f
-checksum_sha1: 922ced125cf229cfc101d2eb8f138fa77b151ff6
-package_url: pkg:github/fishilico/shared@54adb42a34381b6fb8710ba59488c7e5e6e9c3c7#python/network/docker_image.py
-licenses:
-  - key: mit
-    name: MIT License
-    file: mit.LICENSE
diff --git a/src/container_inspector/fetch/mit.LICENSE b/src/container_inspector/fetch/mit.LICENSE
deleted file mode 100644
index e662c78..0000000
--- a/src/container_inspector/fetch/mit.LICENSE
+++ /dev/null
@@ -1,5 +0,0 @@
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/src/container_inspector/image.py b/src/container_inspector/image.py
index c4ec5c1..2f9c973 100755
--- a/src/container_inspector/image.py
+++ b/src/container_inspector/image.py
@@ -13,17 +13,18 @@
 
 import logging
 import os
-from os import path
 
 import attr
 
+from commoncode.fileutils import delete
+
 from container_inspector import MANIFEST_JSON_FILE
 
+from container_inspector.distro import Distro
+from container_inspector import utils
 from container_inspector.utils import as_bare_id
 from container_inspector.utils import load_json
 from container_inspector.utils import sha256_digest
-from container_inspector import utils
-from container_inspector.distro import Distro
 
 logger = logging.getLogger(__name__)
 # un-comment these lines to enable logging
@@ -35,19 +36,28 @@ def logger_debug(*args):
     return logger.debug(' '.join(isinstance(a, str) and a or repr(a) for a in args))
 
 """
-Objects to handle Docker repositories, images and layers data in v1.1 and v1.2 format.
+Objects to handle Docker and OCI images and Layers.
+
+
+Supported formats:
+- docker v1.1 and v1.2
+- OCI (not yet)
+
+The objects supported here are:
+- Image: which is a Docker image that contains manifest and layers
+  - Layer: which is rootfs slice or "diff"
+    - Resource: which repesent a file or directory inside a Layer
 
 The Docker Image Specifications are at:
-- https://github.com/moby/moby/blob/master/image/spec/v1.md
+- https://github.com/moby/moby/blob/master/image/spec/v1.md (no longer supported)
 - https://github.com/moby/moby/blob/master/image/spec/v1.1.md
 - https://github.com/moby/moby/blob/master/image/spec/v1.2.md
 
 The OCI specs:
 - https://github.com/opencontainers/image-spec/blob/master/spec.md
-https://github.com/opencontainers/image-spec/blob/master/image-layout.md
+- https://github.com/opencontainers/image-spec/blob/master/image-layout.md
 - https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
 
-
 The Docker Manifest Specifications are at:
 - https://github.com/docker/distribution/blob/master/docs/spec/deprecated-schema-v1.md
 - https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md
@@ -55,18 +65,6 @@ def logger_debug(*args):
 
 The OCI specs:
 - https://github.com/opencontainers/image-spec/blob/master/manifest.md
-
-
-The model shape is:
-    - Registry
-      - repositories as a list of Repository
-      - Repository
-        - images_by_id with {id: Image}
-        - layers_by_id with {id: Layer}
-        - Image
-          - layers as a list of Layer
-          - Layer
-            - tarball and files
 """
 
 
@@ -75,20 +73,24 @@ class ToDictMixin(object):
     A mixin to add an to_dict() method to an attr-based class.
     """
 
-    def to_dict(self):
-        return attr.asdict(self)
+    def to_dict(self, exclude_fields=()):
+        if exclude_fields:
+            filt = lambda attr, value: attr.name not in exclude_fields
+        else:
+            filt = lambda attr, value: True
+        return attr.asdict(self, filter=filt)
 
 
-def flatten_images(images):
+def flatten_images_data(images):
     """
     Yield mapping for each layer of each image of an `images` list of Image.
     This is a flat data structure for CSV and tabular output.
-    If with_history, include the image history if available and aligned.
     """
 
     for img in images:
         base_data = dict(
-            image_dir=img.base_location,
+            image_extracted_location=img.extracted_location,
+            image_archive_location=img.archive_location,
             image_id=img.image_id,
             image_tags=','.join(img.tags),
         )
@@ -96,24 +98,21 @@ def flatten_images(images):
             layer_data = dict(base_data)
             layer_data['is_empty_layer'] = layer.is_empty_layer
             layer_data['layer_id'] = layer.layer_id
-            layer_data['layer_sha256'] = layer.layer_sha256
+            layer_data['layer_sha256'] = layer.sha256
             layer_data['author'] = layer.author
             layer_data['created_by'] = layer.created_by
             layer_data['created'] = layer.created
             layer_data['comment'] = layer.comment
-            if layer.layer_id:
-                ld = path.join(img.base_location, layer.layer_id)
-            else:
-                ld = None
-            layer_data['layer_location'] = ld
+            layer_data['layer_extracted_location'] = layer.extracted_location
+            layer_data['layer_archive_location'] = layer.archive_location
             yield layer_data
 
 
 @attr.attributes
 class ConfigMixin(object):
     """
-    Configuration data. Shared definition as found in a layer json file and an image
-    config json file.
+    Configuration data. Shared definition as found in a layer json file and an
+    image config json file.
     """
     docker_version = attr.attrib(
         default=None,
@@ -125,9 +124,19 @@ class ConfigMixin(object):
         metadata=dict(doc='Operating system.')
     )
 
+    os_version = attr.attrib(
+        default=None,
+        metadata=dict(doc='Operating system version.')
+    )
+
     architecture = attr.attrib(
         default=None,
-        metadata=dict(doc='architecture.')
+        metadata=dict(doc='Architecture.')
+    )
+
+    variant = attr.attrib(
+        default=None,
+        metadata=dict(doc='Architecture variant.')
     )
 
     created = attr.attrib(
@@ -147,158 +156,211 @@ class ConfigMixin(object):
 
     labels = attr.attrib(
         default=attr.Factory(list),
-        metadata=dict(doc='List of labels for this layer merged from the original config and container_config.')
+        metadata=dict(doc=
+            'List of labels for this layer merged from the '
+            'original config and container_config.'
+        )
     )
 
     @classmethod
     def from_config_data(cls, data):
         """
-        Return a mapping of `data` suitable to use as kwargs from a layer
-        or an image data mapping.
+        Return a mapping of `data` suitable to use as kwargs from a layer or an
+        image config data mapping.
         """
+        data = utils.lower_keys(data)
+
         config = data.get('config', {})
         container_config = data.get('container_config', {})
 
         return dict(
             docker_version=data.get('docker_version'),
             os=data.get('os'),
+            os_version=data.get('os.version'),
             architecture=data.get('architecture'),
+            variant=data.get('variant'),
             created=data.get('created'),
-            author=config.get('Author') or config.get('author'),
+            author=config.get('author'),
             comment=data.get('comment'),
             labels=utils.get_labels(config, container_config),
         )
 
 
 @attr.attributes
-class Image(ToDictMixin, ConfigMixin):
+class ArchiveMixin:
     """
-    A container image with pointers to its layers.
+    An object such as an Image or Layer that has an extracted_location that is a
+    directory where files exists extracted and an archive_location which is the
+    location of the original tarball archive for this object.
     """
 
-    base_location = attr.attrib(
+    extracted_location = attr.attrib(
         default=None,
-        metadata=dict(doc='The directory location where this images is found')
+        metadata=dict(doc=
+            'Absolute directory location where this Archive is extracted.'
+        )
     )
 
-    image_id = attr.attrib(
+    archive_location = attr.attrib(
         default=None,
-        metadata=dict(doc='The id for this image. '
-                      'This is the base name of the config json file '
-                      'and is the same as non-prefixed digest for the config JSON file.'
-                      'For legacy v1.0 images, this is the ID available in a '
-                      'repositories JSON which is the top layer_id.')
+        metadata=dict(doc=
+            'Absolute directory location of this Archive original archive.'
+            'May be empty if this was created from an extracted_location directory.'
+        )
     )
 
-    parent_digest = attr.attrib(
+    sha256 = attr.attrib(
         default=None,
-        metadata=dict(doc='The digest of the parent for this image.')
+        metadata=dict(doc='SHA256 digest of this archive (if there is an archive.)')
     )
 
-    config_digest = attr.attrib(
+    def set_sha256(self):
+        """
+        Compute and set the sha256 attribute.
+        Set to None if ``archive_location`` is not set for this object.
+        """
+        if self.archive_location and not self.sha256:
+            self.sha256 = sha256_digest(self.archive_location)
+
+
+@attr.attributes
+class Image(ArchiveMixin, ConfigMixin, ToDictMixin):
+    """
+    A container image with pointers to its layers.
+    Image objects can be created from these inputs:
+    - an image tarball in docker format (e.g. "docker save").
+    - a directory that contains an extracted image tarball in these layouts.
+    OCI format is not yet supported.
+    """
+
+    image_format = attr.attrib(
         default=None,
-        metadata=dict(doc='The digest of the config JSON file for this image. '
-                          'This is supposed to be the same as the id. '
-                          'Not available for legacy V1.0 images')
+        metadata=dict(doc=
+            'Format of this this image as of one of: "docker" or "oci".'
+        )
     )
 
-    layers = attr.attrib(
-        default=attr.Factory(list),
-        metadata=dict(
-            doc='A list of layer objects from bottom to top, excluding empty '
-                'layers. This is really the "rootfs"')
+    image_id = attr.attrib(
+        default=None,
+        metadata=dict(doc=
+            'Id for this image. '
+            'This is the base name of the config json file '
+            'and is the same as a non-prefixed digest for the config JSON file.'
+        )
     )
 
-    tags = attr.attrib(
-        default=attr.Factory(list),
-        metadata=dict(doc='List of tags for this image as strings of "user/name:version".')
+    config_digest = attr.attrib(
+        default=None,
+        metadata=dict(doc=
+            'Digest of the config JSON file for this image. '
+            'This is supposed to be the same as the id. '
+        )
     )
 
-    history = attr.attrib(
+    tags = attr.attrib(
         default=attr.Factory(list),
-        metadata=dict(
-            doc='List of mapping for the layers history. '
-                'All layers are included including empty layers.'
+        metadata=dict(doc=
+            'List of tags for this image".'
         )
     )
-
     distro = attr.attrib(
         default=None,
-        metadata=dict(doc='The Distro object for this image.')
+        metadata=dict(doc='Distro object for this image.')
     )
 
-    extracted_to_location = attr.attrib(
-        default=None,
-        metadata=dict(doc='The directory where this image has been extracted to.')
+    layers = attr.attrib(
+        default=attr.Factory(list),
+        metadata=dict(doc=
+            'List of Layer objects ordered from bottom to top, excluding empty '
+            'layers."'
+        )
     )
 
+    history = attr.attrib(
+        default=attr.Factory(list),
+        metadata=dict(doc='List of mapping for the layers history.')
+    )
+
+    def __attrs_post_init__(self, *args, **kwargs):
+        if not self.extracted_location:
+            raise TypeError('Image.extracted_location is a required argument')
+
+        self.set_sha256()
+
+        if not self.image_format:
+            self.image_format = self.find_format(self.extracted_location)
+
     @property
     def top_layer(self):
         """
-        The top layer for this image.
+        Top layer for this image.
         """
         return self.layers[-1]
 
     @property
     def bottom_layer(self):
         """
-        The bottom layer for this image.
+        Bottom layer for this image.
         """
         return self.layers[0]
 
-    def extract_layers(self, target_dir, force_extract=False):
+    def extract_layers(self, extracted_location):
         """
-        Extract each layer tarball to the `target_dir` directory. A layer is
-        extracted to its own directory named after its `layer_id`.
-
-        If `force_extract` is False, do not extract a layer if its extraction
-        directory already exists.
+        Extract all layer archives to the `extracted_location` directory.
+        Each layer is extracted to its own directory named after its `layer_id`.
         """
-        self.extracted_to_location = target_dir
         for layer in self.layers:
-            layer.extract(
-                target_dir=self.extracted_to_location,
-                use_layer_id=True,
-                force_extract=force_extract
-            )
+            exloc = os.path.join(extracted_location, layer.layer_id)
+            layer.extract(extracted_location=exloc)
 
     def get_layers_resources(self, with_dir=False):
         """
         Yield a Resource for each file in each layer.
+        extract_layers() must have been called first.
         """
         for layer in self.layers:
-            for resource in layer.get_resources(with_dir):
+            for resource in layer.get_resources(with_dir=with_dir):
                 yield resource
 
     def get_and_set_distro(self):
         """
-        Return a Distro object for this image. Raise exceptions if it cannot be built.
+        Return a Distro object for this image. Raise exceptions if it cannot be
+        built.
         """
         bottom_layer = self.bottom_layer
-        if not bottom_layer.extracted_to_location:
+        if not bottom_layer.extracted_location:
             raise Exception('The image has not been extracted.')
 
-        self.distro = Distro.from_rootfs(bottom_layer.extracted_to_location)
+        distro = Distro(
+            os=self.os,
+            architecture=self.architecture,
+        )
+        if self.os_version:
+            distro.version = self.os_version
+
+        self.distro = Distro.from_rootfs(
+            location=bottom_layer.extracted_location,
+            base_distro=distro,
+        )
+
         return self.distro
 
     def cleanup(self):
         """
-        Removed extracted layer files from self.extracted_to_location.
+        Removed extracted layer files from self.extracted_location.
         """
-        if self.extracted_to_location:
-            utils.delete(self.extracted_to_location)
+        if self.extracted_location:
+            delete(self.extracted_location)
 
         for layer in self.layers:
-            layer.extracted_to_location = None
+            layer.extracted_location = None
 
-        self.extracted_to_location = None
+        self.extracted_location = None
 
     def squash(self, target_dir):
         """
-        Extract and squash all the layers of this image as a single rootfs rooted in the `target_dir` directory.
-        If `use_layer_id` is True, extract in a dir named ``target_dir/layer_id/`
-        Cache the location where this layer was last extracted in the
-        self.extracted_to_location attribute
+        Extract and squash all the layers of this image as a single merged
+        rootfs directory rooted in the `target_dir` directory.
         """
         from container_inspector import rootfs
         rootfs.rebuild_rootfs(self, target_dir)
@@ -306,9 +368,11 @@ def squash(self, target_dir):
     def get_installed_packages(self, packages_getter):
         """
         Yield tuples of unique (package_url, package, layer) for installed
-        packages found in that image's layers using the `packages_getter`
-        function or callable. A package is reported in the layer its package_url
-        is first seen.
+        packages found in each of this image layers using the `packages_getter`
+        function or callable. A package is reported in the layer where its
+        package_url is first seen as installed. Further instances of the exact
+        same package found in the installed package database in following layers
+        are not reported.
 
         The `packages_getter()` function should:
 
@@ -321,9 +385,9 @@ def get_installed_packages(self, packages_getter):
           toolkit packagedcode.models.Package class or some nested mapping with
           the same structure).
 
-        An `packages_getter` function would typically query the
-        system packages database (such as an RPM database or similar) to collect
-        the list of installed system packages.
+        An `packages_getter` function would typically query the system packages
+        database (such as an RPM database or similar) to collect the list of
+        installed system packages.
         """
         seen_packages = set()
         for layer in self.layers:
@@ -334,121 +398,176 @@ def get_installed_packages(self, packages_getter):
                 yield purl, package, layer
 
     @staticmethod
-    def get_images_from_tarball(location, target_dir, force_extract=False):
+    def extract(archive_location, extracted_location):
+        """
+        Extract the image archive tarball at ``archive_location`` to
+        ``extracted_location``.
+        """
+        utils.extract_tar_keeping_symlinks(
+            location=archive_location,
+            target_dir=extracted_location,
+        )
+
+    @staticmethod
+    def get_images_from_tarball(
+        archive_location,
+        extracted_location,
+        verify=True,
+    ):
+        """
+        Return a list of Images found in the tarball at `archive_location` that
+        will be extracted to `extracted_location`. The tarball must be in the
+        format of a "docker save" command tarball.
+
+        If `verify` is True, perform extra checks on the config data and layers
+        checksums.
+        """
+        Image.extract(
+            archive_location=archive_location,
+            extracted_location=extracted_location,
+        )
+
+        return Image.get_images_from_dir(
+            extracted_location=extracted_location,
+            archive_location=archive_location,
+            verify=verify,
+        )
+
+    @staticmethod
+    def get_images_from_dir(
+        extracted_location,
+        archive_location=None,
+        verify=True,
+    ):
         """
-        Yield Image objects found in the tarball at `location` that will be
-        extracted to `target_dir`. The tarball must be in the format of a "docker
-        save" command tarball.
-        If `force_extract` is False, do not extract to target_dir if target_dir
-        already exists
+        Return a list of Image found in the directory at `extracted_location`
+        that can be either a in "docker save" or OCI format.
+
+        If `verify` is True, perform extra checks on the config data and layers
+        checksums.
         """
-        if force_extract or not os.path.exists(target_dir):
-            utils.extract_tar(location, target_dir)
-        return Image.get_images_from_dir(target_dir)
+        if not os.path.isdir(extracted_location):
+            raise Exception(f'Not a directory: {extracted_location}')
+
+        image_format = Image.find_format(extracted_location)
+        if image_format == 'docker':
+            return Image.get_docker_images_from_dir(
+                extracted_location=extracted_location,
+                archive_location=archive_location,
+                verify=verify,
+        )
+
+        if image_format == 'docker':
+            return Image.get_oci_images_from_dir(
+                extracted_location=extracted_location,
+                archive_location=archive_location,
+                verify=verify,
+        )
+
+        raise Exception(
+            f'Unknown container image format {image_format} '
+            f'at {extracted_location}'
+        )
 
     @staticmethod
-    def get_images_from_dir(location, verify=False):
+    def get_docker_images_from_dir(
+        extracted_location,
+        archive_location=None,
+        verify=True,
+    ):
         """
-        Yield Image objects found in a base directory at `location`. The
-        directory must contain a manifest.json and must be in the same format as
-        a "docker save" extracted to `location`.
+        Return a list of Image objects found in the directory at
+        `extracted_location`. The directory must contain a Docker manifest.json and
+        must be in the same format as a "docker save" extracted to
+        `extracted_location`.
+
         If `verify` is True, perform extra checks on the config data and layers
         checksums.
 
         The "manifest.json" JSON file for format v1.1/1.2. of a saved Docker
-        image contains a mapping with this shape:
+        image contains a mapping with this shape for one or more images:
 
         - The `Config` field references another JSON file in same directory
           that includes the image detailed data.
         - The `RepoTags` field lists references pointing to this image.
         - The `Layers` field points to the filesystem changeset tars, e.g. the
           path to the layer.tar files as a list of paths.
-        - An optional `Parent` field references the imageID (as a sha256-prefixed
-          digest?) of the parent image. This parent must be part of the same
-          `manifest.json` file.
 
         For example:
 
         [
-            {'Config': '7043867122e704683c9eaccd7e26abcd5bc9fea413ddfeae66166697bdcbde1f.json',
+            {'Config': '7043867122e704683c9eaccd7e26abcd5bc9fea413ddf.json',
              'Layers': [
-                 '768d4f50f65f00831244703e57f64134771289e3de919a576441c9140e037ea2/layer.tar',
-                 '6a630e46a580e8b2327fc45d9d1f4734ccaeb0afaa094e0f45722a5f1c91e009/layer.tar',
+                 '768d4f50f65f00831244703e57f64134771289e3de919a57/layer.tar',
+                 '6a630e46a580e8b2327fc45d9d1f4734ccaeb0afaa094e0f/layer.tar',
                  ]
              'RepoTags': ['user/image:version'],
-             "Parent": "sha256:5a00e6ccb81ef304e1bb9995ea9605f199aa96659a44237d58ca96982daf9af8"
-             },
-
-            {'Config': '7043867122e704683c9eaccd7e26abcd5bc9fea413ddfeae66166697bdcbde1f.json',
-             'Layers': [
-                 '768d4f50f65f00831244703e57f64134771289e3de919a576441c9140e037ea2/layer.tar',
-                 '6a630e46a580e8b2327fc45d9d1f4734ccaeb0afaa094e0f45722a5f1c91e009/layer.tar',
-                 ]
-             'RepoTags': ['user/image:version']
              },
+            ....
         ]
         """
-        if not path.isdir(location):
-            raise Exception(f'Not a directory: {location}')
+        if not os.path.isdir(extracted_location):
+            raise Exception(f'Not a directory: {extracted_location}')
 
-        manifest_loc = path.join(location, MANIFEST_JSON_FILE)
-        # NOTE: we are only looking at V1.1/2 repos layout for now and not the legacy v1.0.
-        if not path.exists(manifest_loc):
-            raise Exception(f'manifest.json file missing in {location}')
+        manifest_loc = os.path.join(extracted_location, MANIFEST_JSON_FILE)
+        # NOTE: we are only looking at V1.1/2 repos layout for now and not the
+        # legacy v1.0.
+        if not os.path.exists(manifest_loc):
+            raise Exception(f'manifest.json file missing in {extracted_location}')
 
         manifest = load_json(manifest_loc)
 
+        images = []
         for manifest_config in manifest:
-            yield Image.from_manifest_config(
-                base_location=location,
-                manifest_config=manifest_config,
-                verify_config=verify,
-                verify_layers_on_disk=verify,
-            )
+            images.append(
+                Image.from_docker_manifest_config(
+                    extracted_location=extracted_location,
+                    archive_location=archive_location,
+                    manifest_config=manifest_config,
+                    verify=verify,
+            ))
+
+        return images
 
     @staticmethod
-    def from_manifest_config(
-        base_location,
+    def from_docker_manifest_config(
+        extracted_location,
         manifest_config,
-        verify_config=False,
-        verify_layers_on_disk=False
+        archive_location=None,
+        verify=True,
     ):
         """
-        Return an Image object built from `manifest_config` data mapping
-        (obtained from a manifest.json) and the `base_location` directory that
-        contains the `manifest.json` and each image JSON config file.
+        Return an Image object built from a Docker `manifest_config` data
+        mapping (obtained from a manifest.json) and the `extracted_location`
+        directory that contains the manifest.json and each image JSON config
+        file.
+
+        If `verify` is True, perform extra checks on the config data and layers
+        checksums.
+        Raise Exception on errors.
 
         The `manifest_config["Config"]` contains a path to JSON config file that
         is named after its SHA256 checksum and there is one such file for each
         image.
 
-        A `manifest_config` has this shape:
-          {'Config': '7043867122e704683c9eaccd7e26abcd5bc9fea413ddfeae66166697bdcbde1f.json',
+        A manifest.json `manifest_config` attribute has this shape:
+          {'Config': '7043867122e704683c9eaccd7e26abcd.json',
            'Layers': [
-               '768d4f50f65f00831244703e57f64134771289e3de919a576441c9140e037ea2/layer.tar',
-               '6a630e46a580e8b2327fc45d9d1f4734ccaeb0afaa094e0f45722a5f1c91e009/layer.tar',
+               '768d4f50f65f00831244703e57f64134771289/layer.tar',
+               '6a630e46a580e8b2327fc45d9d1f4734ccaeb0/layer.tar',
                ]
            'RepoTags': ['user/image:version'],
-           "Parent": "sha256:5a00e6ccb81ef304e1bb9995ea9605f199aa96659a44237d58ca96982daf9af8"
-           },
+           }
 
-          {'Config': '7043867122e704683c9eaccd7e26abcd5bc9fea413ddfeae66166697bdcbde1f.json',
-           'Layers': [
-               '768d4f50f65f00831244703e57f64134771289e3de919a576441c9140e037ea2/layer.tar',
-               '6a630e46a580e8b2327fc45d9d1f4734ccaeb0afaa094e0f45722a5f1c91e009/layer.tar',
-               ]
-           'RepoTags': ['user/image:version']
-           },
-
-
-        Each JSON config file has this shape:
+        Each JSON config file referenced in the Config attribute such as the
+        file above named: 7043867122e704683c9eaccd7e26abcd.json file has this shape:
          {
             'docker_version': '1.8.2',
             'os': 'linux',
             'architecture': 'amd64',
             'author': '<author name>',
             'created': '2016-09-30T10:16:27.109917034Z',
-            'container': '1ee508bc7a35150c9e5924097a31dfb4b6b2ca1260deb6fd14cb03c53764e40b',
+            'container': '1ee508bc7a35150c9e5924097a31dfb4',
 
             # The `image_config` and `container_config` mappings are essentially
             # similar: image_config is the runtime image_config and
@@ -474,71 +593,87 @@ def from_manifest_config(
 
             'rootfs': {
                 'diff_ids': [
-                    'sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef',
-                    'sha256:2436bc321ced91d2f3052a98ff886a2feed0788eb524b2afeb48099d084c33f5',
-                    'sha256:cd141a5beb0ec83004893dfea6ea8508c6d09a0634593c3f35c0d433898c9322',]
+                    'sha256:5f70bf18a086007016e948b04aed3b82103a3',
+                    'sha256:2436bc321ced91d2f3052a98ff886a2feed07',
+                    'sha256:cd141a5beb0ec83004893dfea6ea8508c6d09',]
                 'type': u'layers'
             }
          }
         """
-        config_file = manifest_config.get('Config') or ''
-        config_file_loc = path.join(base_location, config_file)
-        if not path.exists(config_file_loc):
+        manifest_config = utils.lower_keys(manifest_config)
+
+        config_file = manifest_config.get('config') or ''
+        config_file_loc = os.path.join(extracted_location, config_file)
+        if not os.path.exists(config_file_loc):
             raise Exception(
                 f'Invalid configuration. Missing Config file: {config_file_loc}')
 
-        image_id, _ = path.splitext(path.basename(config_file_loc))
+        image_id, _ = os.path.splitext(os.path.basename(config_file_loc))
         config_digest = sha256_digest(config_file_loc)
-        if verify_config:
-            if image_id != as_bare_id(config_digest):
-                logger.warning('WARNING: img config digest is not consistent.')
-                config_digest = f'sha256:{image_id}'
+        if verify and image_id != as_bare_id(config_digest):
+            raise Exception(
+                f'Image config {config_file_loc} SHA256:{image_id} is not '
+                f'consistent with actual computed value SHA256: {config_digest}'
+            )
 
-        layer_paths = manifest_config.get('Layers') or []
-        layers_locations = [
-            path.join(base_location, layer_path) for layer_path in layer_paths]
+        config_digest = f'sha256:{image_id}'
 
-        parent_digest = manifest_config.get('Parent')
-        tags = manifest_config.get('RepoTags') or []
+        # "Layers" can be either a path to the layer.tar:
+        # "d388bee71bbf28f77042d89b353bacd14506227/layer.tar"
 
-        image_config = load_json(config_file_loc)
-        rootfs = image_config.get('rootfs')
+        # Or with a linked format (e.g. skopeo) where the layer.tar above is a
+        # link to a tarball named after its sha256 and located at the root
+        # 5f70bf18a086007016e948b04aed3b82103a36be.tar
 
-        assert rootfs['type'] == 'layers', (
-            f'Unknown type for img rootfs: expecting "layers": {config_file_loc}')
+        layer_paths = manifest_config.get('layers') or []
+        layers_archive_locs = [
+            os.path.join(extracted_location, lp) for lp in layer_paths]
+
+        tags = manifest_config.get('repotags') or []
+
+        image_config = utils.lower_keys(load_json(config_file_loc))
+        rootfs = image_config['rootfs']
+        rt = rootfs['type']
+        if rt != 'layers':
+            raise Exception(
+                f'Unknown type for image rootfs: expecting "layers" and '
+                f'not {rt} in {config_file_loc}'
+            )
 
         # TODO: add support for empty tarball as this may not work if there is a
         # diff for an empty layer with a digest for some EMPTY content e.g.
         # e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
 
-        layers_sha256s = [as_bare_id(layer_sha256) for layer_sha256 in rootfs['diff_ids']]
-        layer_locs_and_sha256s = list(zip(layers_locations, layers_sha256s))
+        layers_sha256s = [as_bare_id(lsha256) for lsha256 in rootfs['diff_ids']]
+        layer_arch_locs_and_sha256s = zip(layers_archive_locs, layers_sha256s)
 
         layers = []
-        for layer_location, layer_sha256 in layer_locs_and_sha256s:
-            layer_size = path.getsize(layer_location)
-
-            if verify_layers_on_disk:
-                on_disk_layer_sha256 = sha256_digest(layer_location)
-                assert layer_sha256 == on_disk_layer_sha256, (
-                    f'Layer SHA256 at {layer_location} does not match its config "diff_id"')
-
-            lay = Layer(
-                layer_sha256=layer_sha256,
-                layer_location=layer_location,
-                layer_size=layer_size,
-            )
-            layers.append(lay)
+        for layer_archive_loc, layer_sha256 in layer_arch_locs_and_sha256s:
+
+            if verify:
+                on_disk_layer_sha256 = sha256_digest(layer_archive_loc)
+                if layer_sha256 != on_disk_layer_sha256:
+                    raise Exception(
+                        f'Layer archive: SHA256:{on_disk_layer_sha256}\n at '
+                        f'{layer_archive_loc} does not match \n'
+                        f'its "diff_id": SHA256:{layer_sha256}'
+                    )
+
+            layers.append(Layer(
+                archive_location=layer_archive_loc,
+                sha256=layer_sha256,
+            ))
 
         history = image_config.get('history') or {}
         assign_history_to_layers(history, layers)
 
         img = Image(
-            base_location=base_location,
+            image_format='docker',
+            extracted_location=extracted_location,
+            archive_location=archive_location,
             image_id=image_id,
             layers=layers,
             config_digest=config_digest,
-            parent_digest=parent_digest,
             history=history,
             tags=tags,
             **ConfigMixin.from_config_data(image_config)
@@ -546,6 +681,176 @@ def from_manifest_config(
 
         return img
 
+    @staticmethod
+    def find_format(extracted_location):
+        """
+        Rreturn the format of the image at ``extracted_location`` as one of:
+        - docker (which is for the docker v2 format)
+        - oci (which is for the OCI format)
+        """
+        clue_files_by_image_format = {
+            'docker': ('manifest.json', 'repositories',),
+            'oci': ('blobs', 'index.json', 'oci-layout',)
+         }
+
+        files = os.listdir(extracted_location)
+        for image_format, clues in clue_files_by_image_format.items():
+            if all(c in files for c in clues):
+                return image_format
+
+    @staticmethod
+    def get_oci_images_from_dir(
+        extracted_location,
+        archive_location=None,
+        verify=True,
+    ):
+        """
+        Return a list of Images created from OCI images found at
+        `extracted_location` that is a directory where an OCI image tarball has
+        been extracted.
+
+        index.json
+        oci-layout
+        blobs/sha256
+            # at least three files, one being a tarball. Each named after their sha256
+            /17dc2d6ad713655494f3a90a06a5479c62108
+            /cdce9ebeb6e8364afeac430fe7a886ca89a90
+            /540db60ca9383eac9e418f78490994d0af424
+
+        index.json:
+
+        {
+          "schemaVersion": 2,
+          "manifests": [
+            {
+              "mediaType": "application/vnd.oci.image.manifest.v1+json",
+              "digest": "sha256:17dc2d6ad713655494f3a90",
+              "size": 348
+            }
+          ]
+        }
+        which points to a blob:
+
+        Then in 17dc2d6ad713655494f3a90a06a5479c62108 which is JSON
+        and points to a manifest and a layers
+        {
+          "schemaVersion": 2,
+          "config": {
+            "mediaType": "application/vnd.oci.image.config.v1+json",
+            "digest": "sha256:cdce9ebeb6e8364afeac430fe7",
+            "size": 585
+          },
+          "layers": [
+            {
+              "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+              "digest": "sha256:540db60ca9383eac9e418f78",
+              "size": 2811969
+            }
+          ]
+        }
+        And this cdce9ebeb6e8364afeac430fe
+        is a JSON with essentially the same image_config contenet as the Docker format:
+
+        {
+          "created": "2021-04-14T19:19:39.643236135Z",
+          "architecture": "amd64",
+          "os": "linux",
+          "config": {
+            "Env": [
+              "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+            ],
+            "Cmd": [
+              "/bin/sh"
+            ]
+          },
+          "rootfs": {
+            "type": "layers",
+            "diff_ids": [
+              "sha256:b2d5eeeaba3a22b9b8aa97261957974a6"
+            ]
+          },
+          "history": [
+            {
+              "created": "2021-04-14T19:19:39.267885491Z",
+              "created_by": "/bin/sh -c #(nop) ADD file:8ec69d882e7f29f0652 in / "
+            },
+            {
+              "created": "2021-04-14T19:19:39.643236135Z",
+              "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
+              "empty_layer": true
+            }
+          ]
+        }
+        """
+        index_loc = os.path.join(extracted_location, 'index.json')
+        index = load_json(index_loc)
+        index = utils.lower_keys(index)
+        if index['schemaversion'] != 2:
+            raise Exception(
+                f'Unsupported OCI index schema version in {index_loc}. '
+                'Only 2 is supported.'
+            )
+
+        images = []
+        for manifest_data in index['manifests']:
+            mediatype = manifest_data['mediatype']
+            if mediatype != 'application/vnd.oci.image.manifest.v1+json':
+                raise Exception(
+                    f'Unsupported OCI index media type {mediatype} in {index_loc}.'
+                )
+            manifest_digest = manifest_data['digest']
+            manifest_sha256 = as_bare_id(manifest_digest)
+            manifest_loc = get_oci_blob(
+                extracted_location, manifest_sha256, verify=verify)
+            manifest = load_json(manifest_loc)
+
+            config_digest = manifest['config']['digest']
+            config_sha256 = as_bare_id(config_digest)
+            config_loc = get_oci_blob(
+                extracted_location, config_sha256, verify=verify)
+            config = load_json(config_loc)
+
+            layers = []
+            for layer in manifest['layers']:
+                layer_digest = layer['digest']
+                layer_sha256 = as_bare_id(layer_digest)
+                layer_arch_loc = get_oci_blob(
+                    extracted_location, layer_sha256, verify=verify)
+                layers.append(Layer(
+                    archive_location=layer_arch_loc,
+                    sha256=layer_sha256,
+                ))
+
+            history = config.get('history') or {}
+            assign_history_to_layers(history, layers)
+
+            images.append(Image(
+                image_format='oci',
+                extracted_location=extracted_location,
+                archive_location=archive_location,
+                image_id=config_sha256,
+                layers=layers,
+                config_digest=config_digest,
+                history=history,
+                **ConfigMixin.from_config_data(config)
+            ))
+
+        return images
+
+
+def get_oci_blob(extracted_location, sha256, verify=True):
+        loc = os.path.join(extracted_location, 'blobs', 'sha256', sha256)
+        if not os.path.exists(loc):
+            raise Exception(f'Missing OCI image file {loc}')
+        if verify:
+            on_disk_sha256 = sha256_digest(loc)
+            if sha256 != on_disk_sha256:
+                raise Exception(
+                    f'For {loc} on disk SHA256:{on_disk_sha256} does not '
+                    f'match its expected index SHA256:{sha256}'
+                )
+        return loc
+
 
 def assign_history_to_layers(history, layers):
     """
@@ -588,7 +893,7 @@ def assign_history_to_layers(history, layers):
     fields = 'author', 'created', 'created_by', 'comment'
 
     for hist, layer in zip(non_empty_history, non_empty_layers):
-
+        hist = utils.lower_keys(hist)
         for field in fields:
             value = hist.get(field)
             if value:
@@ -599,17 +904,19 @@ def assign_history_to_layers(history, layers):
 class Resource(ToDictMixin):
     path = attr.attrib(
         default=None,
-        metadata=dict(doc='The root-relative path for this Resource.')
+        metadata=dict(doc='Rootfs-relative path for this Resource.')
     )
 
     layer_path = attr.attrib(
         default=None,
-        metadata=dict(doc='The relative path including the layer prefix.')
+        metadata=dict(doc=
+            'Rootfs-relative path with the addition of the layer id as a prefix.'
+        )
     )
 
     location = attr.attrib(
         default=None,
-        metadata=dict(doc='The absolute location for this Resource.')
+        metadata=dict(doc='Absolute location of this Resource.')
     )
 
     is_file = attr.ib(
@@ -624,45 +931,43 @@ class Resource(ToDictMixin):
 
 
 @attr.attributes
-class Layer(ToDictMixin, ConfigMixin):
+class Layer(ArchiveMixin, ConfigMixin, ToDictMixin):
     """
-    A layer object represents a slice of a root filesyetem.
+    A layer object represents a slice of a root filesystem in a container image.
     """
-    format_version = '1.0'
-
-    layer_location = attr.attrib(
-        default=None,
-        metadata=dict(doc='The base directory for this layer.')
-    )
 
-    layer_sha256 = attr.attrib(
+    layer_id = attr.attrib(
         default=None,
-        metadata=dict(doc='The SHA256 digest of the layer.tar file for this layer.')
+        metadata=dict(doc=
+            'Id for this layer which must be set to the SHA256 of its archive.'
+        )
     )
 
-    layer_size = attr.attrib(
+    size = attr.attrib(
         default=0,
-        metadata=dict(doc='Size in byte of the layer.tar archive')
+        metadata=dict(doc='Size in byte of the layer archive')
     )
 
-    layer_id = attr.attrib(
-        default=None,
-        metadata=dict(doc='The id for this layer. aka. its tarball parent directory name ')
+    is_empty_layer = attr.attrib(
+        default=False,
+        metadata=dict(doc=
+            'True if this is an empty layer. An empty layer has no content.'
+        )
     )
 
     author = attr.attrib(
         default=None,
-        metadata=dict(doc='The author of this layer.')
+        metadata=dict(doc='Author of this layer.')
     )
 
     created = attr.attrib(
         default=None,
-        metadata=dict(doc='The date/timestamp for when this layer was created.')
+        metadata=dict(doc='Date/timestamp for when this layer was created.')
     )
 
     created_by = attr.attrib(
         default=None,
-        metadata=dict(doc='The command for this layer.')
+        metadata=dict(doc='Command used to create this layer.')
     )
 
     comment = attr.attrib(
@@ -670,81 +975,61 @@ class Layer(ToDictMixin, ConfigMixin):
         metadata=dict(doc='A comment for this layer.')
     )
 
-    is_empty_layer = attr.attrib(
-        default=False,
-        metadata=dict(doc='True if this is an empty layer. An empty layer has no tarball.')
-    )
-
-    parent_id = attr.attrib(
-        default=None,
-        metadata=dict(doc='parent layer id. LEGACY V10 format.')
-    )
+    def __attrs_post_init__(self, *args, **kwargs):
+        if not self.archive_location:
+            raise TypeError('Layer.archive_location is a required argument')
 
-    extracted_to_location = attr.attrib(
-        default=None,
-        metadata=dict(doc='The directory where this layer has been extracted to '
-            'as a plain rootfs.')
-    )
+        self.set_sha256()
+        self.layer_id = self.sha256
 
-    def __attrs_post_init__(self, *args, **kwargs):
-        if self.layer_location and not self.layer_id:
-            # reconstruct that id from the path
-            self.layer_id = path.basename(path.dirname(self.layer_location))
+        if not self.size:
+            self.size = os.path.getsize(self.archive_location)
 
-    def extract(self, target_dir, use_layer_id=True, force_extract=False):
+    def extract(self, extracted_location):
         """
-        Extract layer tarball to `target_dir` directory.
-        If `use_layer_id` is True, extract in a dir named ``target_dir/layer_id/``
-        Cache the location where this layer was last extracted in the
-        ``self.extracted_to_location`` attribute.
-
-        If `force_extract` is False, do not extract if self.extracted_to_location
-        already exists
+        Extract this layer archive in the `extracted_location` directory and set
+        this Layer ``extracted_location`` attribute to ``extracted_location``.
         """
-        if use_layer_id:
-            self.extracted_to_location = path.join(target_dir, self.layer_id)
-
-        if force_extract or not os.path.exists(self.extracted_to_location):
-            utils.extract_tar(self.layer_location, self.extracted_to_location)
+        self.extracted_location = extracted_location
+        utils.extract_tar(
+            location=self.archive_location,
+            target_dir=extracted_location,
+        )
 
-    def get_resources(self, with_dir=False):
+    def get_resources(self, with_dir=False, walker=os.walk):
         """
-        Yield a Resource for each file in that layer.
+        Yield a Resource for each file in this layer, omit directories if
+        ``with_dir`` is False.
         """
-        if not self.extracted_to_location:
+        if not self.extracted_location:
             raise Exception('The layer has not been extracted.')
 
-        for top, dirs, files in os.walk(self.extracted_to_location):
+        def build_resource(_top, _name, _is_file):
+            _loc = os.path.join(top, _name)
+            _path = _loc.replace(self.extracted_location, '')
+            _layer_path = os.path.join(self.layer_id, _path.lstrip('/'))
+
+            return Resource(
+                location=_loc,
+                path=_path,
+                layer_path=_layer_path,
+                is_file=_is_file,
+                is_symlink=os.path.islink(_loc),
+            )
+
+        for top, dirs, files in walker(self.extracted_location):
             for f in files:
-                location = os.path.join(top, f)
-                path = location.replace(self.extracted_to_location, '')
-                layer_path = os.path.join(self.layer_id, path.lstrip('/'))
-                yield Resource(
-                    location=location,
-                    path=path,
-                    layer_path=layer_path,
-                    is_file=True,
-                    is_symlink=os.path.islink(location),
-                )
+                yield build_resource(top, f, _is_file=True)
             if with_dir:
                 for d in dirs:
-                    location = os.path.join(top, d)
-                    path = location.replace(self.extracted_to_location, '')
-                    layer_path = os.path.join(self.layer_id, path.lstrip('/'))
-                    yield Resource(
-                        location=location,
-                        path=path,
-                        layer_path=layer_path,
-                        is_file=False,
-                        is_symlink=os.path.islink(location),
-                    )
+                    yield build_resource(top, d, _is_file=True)
 
     def get_installed_packages(self, packages_getter):
         """
-        Yield tuples of (package_url, package) for installed packages found in
-        that layer using the `packages_getter` function or callable.
+        Yield tuples of (package_url, package) for installed system packages
+        found in this layer using the `packages_getter` function or callable.
 
-        The `packages_getter()` function should:
+        The `packages_getter()` function or callable should:
 
         - accept a first argument string that is the root directory of
           filesystem of this the layer
@@ -755,4 +1040,4 @@ def get_installed_packages(self, packages_getter):
           toolkit packagedcode.models.Package class or some nested mapping with
           the same structure).
         """
-        return packages_getter(self.extracted_to_location)
+        return packages_getter(self.extracted_location)
diff --git a/src/container_inspector/rootfs.py b/src/container_inspector/rootfs.py
index 034d6d8..593d75e 100755
--- a/src/container_inspector/rootfs.py
+++ b/src/container_inspector/rootfs.py
@@ -13,11 +13,10 @@
 
 import logging
 import os
-from os import path
 import tempfile
 
-from container_inspector import LAYER_TAR_FILE
-from container_inspector import utils
+from commoncode.fileutils import copytree
+from commoncode.fileutils import delete
 
 logger = logging.getLogger(__name__)
 # un-comment these lines to enable logging
@@ -37,13 +36,14 @@ class InconsistentLayersError(Exception):
 
 def rebuild_rootfs(img, target_dir):
     """
-    Extract and merge all layers of the `image` Image in `target_dir`.
-    Extraction is done in sequence from bottom (root) to top (latest layer)
-    and the "whiteout" unionfs/overlayfs procedure is applied at each step
-    as per the OCI spec:
-        https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
+    Extract and merge or "squash" all layers of the `image` Image in a single
+    rootfs in `target_dir`. Extraction is done in sequence from the bottom (root
+    or initial) layer to the top (or latest) layer and the "whiteouts"
+    unionfs/overlayfs procedure is applied at each step as per the OCI spec:
+    https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
 
     Return a list of deleted "whiteout" files.
+    Raise an Exception on errrors.
 
     The extraction process consists of these steps:
      - extract the layer in a temp directory
@@ -54,121 +54,164 @@ def rebuild_rootfs(img, target_dir):
 
     See also some related implementations and links:
     https://github.com/moby/moby/blob/d1f470946/pkg/archive/whiteouts.go
-    https://github.com/virt-manager/virt-bootstrap/commit/8a7e752d6614f8425874adbbdab16443ee0b1d9b
+    https://github.com/virt-manager/virt-bootstrap/blob/8a7e752d/src/virtBootstrap/whiteout.py
     https://github.com/goldmann/docker-squash
 
-
     https://github.com/moby/moby/blob/master/image/spec/v1.md
     https://github.com/moby/moby/blob/master/image/spec/v1.1.md
     https://github.com/moby/moby/blob/master/image/spec/v1.2.md
     """
 
-    assert path.isdir(target_dir)
-    assert path.exists(target_dir)
-    extract_errors = []
+    assert os.path.isdir(target_dir)
+
     # log  deletions
     deletions = []
 
     for layer_num, layer in enumerate(img.layers):
-        layer_id = layer.layer_id
-        layer_tarball = path.join(img.base_location, layer_id, LAYER_TAR_FILE)
-        logger.debug('Extracting layer {layer_num} tarball: {layer_tarball}'.format(**locals()))
-        temp_target = tempfile.mkdtemp('container_inspector-docker')
+        logger.debug(
+            f'Extracting layer {layer_num} - {layer.layer_id} '
+            f'tarball: {layer.archive_location}'
+        )
 
         # 1. extract a layer to temp.
         # Note that we are not preserving any special file and any file permission
-        utils.extract_tar(location=layer_tarball, target_dir=temp_target)
-        logger.debug('  Extracted layer to: {}'.format(temp_target))
+        extracted_loc = tempfile.mkdtemp('container_inspector-docker')
+        layer.extract(extracted_location=extracted_loc)
+        logger.debug(f'  Extracted layer to: {extracted_loc}')
 
         # 2. find whiteouts in that layer.
-        layer_whiteouts = list(find_whiteouts(temp_target))
-        logger.debug('  Merging extracted layers and applying AUFS whiteouts/deletes')
-        logger.debug('  Whiteouts:\n' + '     \n'.join(map(repr, layer_whiteouts)))
+        whiteouts = list(find_whiteouts(extracted_loc))
+        logger.debug('  Merging extracted layers and applying unionfs whiteouts')
+        logger.debug('  Whiteouts:\n' + '     \n'.join(map(repr, whiteouts)))
 
         # 3. remove whiteouts in the previous layer stack (e.g. the WIP rootfs)
-        for layer_whiteout_marker, target_whiteable_path in layer_whiteouts:
-            logger.debug('    Deleting whiteout dir or file: {target_whiteable_path}'.format(**locals()))
-            whiteable = path.join(target_dir, target_whiteable_path)
-            utils.delete(whiteable)
+        for whiteout_marker_loc, whiteable_path in whiteouts:
+            logger.debug(f'    Deleting dir or file with whiteout marker: {whiteout_marker_loc}')
+            whiteable_loc = os.path.join(target_dir, whiteable_path)
+            delete(whiteable_loc)
             # also delete the whiteout marker file
-            utils.delete(layer_whiteout_marker)
-            deletions.extend(target_whiteable_path)
+            delete(whiteout_marker_loc)
+            deletions.append(whiteable_loc)
 
         # 4. finall copy/overwrite the extracted layer over the WIP rootfs
-        logger.debug('  Moving extracted layer from: {temp_target} to: {target_dir}'.format(**locals()))
-        utils.copytree(temp_target, target_dir)
-        logger.debug('  Moved layer to: {}'.format(target_dir))
-        utils.delete(temp_target)
+        logger.debug(f'  Moving extracted layer from: {extracted_loc} to: {target_dir}')
+        copytree(extracted_loc, target_dir)
+        logger.debug(f'  Moved layer to: {target_dir}')
+        delete(extracted_loc)
 
     return deletions
 
 
 WHITEOUT_EXPLICIT_PREFIX = '.wh.'
-WHITEOUT_OPAQUE_PREFIX = ('.wh..wh.opq')
+WHITEOUT_OPAQUE_PREFIX = '.wh..wh.opq'
 
 
-def get_whiteout_marker_type(file_name):
+def is_whiteout_marker(path):
     """
-    Return the type of whiteout or False if the file_name is a whiteout marker file.
+    Return True if the ``path`` is a whiteout marker file.
+
+    For example::
+    >>> is_whiteout_marker('.wh.somepath')
+    True
+    >>> is_whiteout_marker('.wh..wh.opq')
+    True
+    >>> is_whiteout_marker('somepath.wh.')
+    False
+    >>> is_whiteout_marker('somepath/.wh.foo')
+    True
+    >>> is_whiteout_marker('somepath/.wh.foo/')
+    True
     """
-    if file_name == WHITEOUT_OPAQUE_PREFIX:
-        return WHITEOUT_OPAQUE_PREFIX
-    if file_name.startswith(WHITEOUT_EXPLICIT_PREFIX):
-        return WHITEOUT_EXPLICIT_PREFIX
+    file_name = path and os.path.basename(path.strip('/')) or ''
+    return file_name.startswith(WHITEOUT_EXPLICIT_PREFIX)
 
 
-def get_whiteable_path(location):
+def get_whiteable_path(path):
     """
-    Given a location path string, return the whiteable path for this `location`
-    or None.
+    Return the whiteable path for ``path`` or None if this not a whiteable path.
+    TODO: Handle OSses with case-insensitive FS (e.g. Windows)
     """
-    file_name = path.basename(location)
-    parent_dir = path.dirname(location)
+    file_name = os.path.basename(path)
+    parent_dir = os.path.dirname(path)
 
-    wh_marker_type = get_whiteout_marker_type(file_name)
-
-    if wh_marker_type == WHITEOUT_OPAQUE_PREFIX:
-        # Opaque whiteouts means the whole parent direvtory should be removed
+    if file_name == WHITEOUT_OPAQUE_PREFIX:
+        # Opaque whiteouts means the whole parent directory should be removed
         # https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
         return parent_dir
 
-    elif wh_marker_type == WHITEOUT_EXPLICIT_PREFIX:
+    if file_name.startswith(WHITEOUT_EXPLICIT_PREFIX):
         # Explicit, file-only whiteout
         # https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
         _, _, real_file_name = file_name.rpartition(WHITEOUT_EXPLICIT_PREFIX)
-        return path.join(parent_dir, real_file_name)
+        return os.path.join(parent_dir, real_file_name)
 
 
-def find_whiteouts(base_location):
-    """
-    Yield tuple of (whiteout location, `base_location`-relative path of things to delete)
-    found in the list of `locations` path strings.
-    Does not access filesystem.
+def find_whiteouts(root_location, walker=os.walk):
     """
-    for top, _dirs, files in os.walk(base_location, base_location):
-        for f in files:
-            location = path.join(top, f)
-            whiteable_path = get_whiteable_path(location)
-            if whiteable_path:
-                relative_path = whiteable_path.replace(base_location, '').strip('/')
-                yield location, relative_path
-
+    Yield a two-tuple of:
+     - whiteout marker file location
+     - corresponding file or directory path, relative to the root_location
 
-LINUX_PATHS = set(['usr', 'etc', 'var', 'home', 'sbin', 'sys', 'lib', 'bin', 'vmlinuz', ])
-WINDOWS_PATHS = set(['Program Files', 'Windows', ])
+    found under the `root_location` directory.
 
-
-def find_root(location, max_depth=3, root_paths=LINUX_PATHS, min_paths=2, _walker=os.walk):
+    `_walker` is a callable that behaves the same as `os.walk() and is used
+    for testing`
+    """
+    for top, _dirs, files in walker(root_location):
+        for fil in files:
+            whiteout_marker_loc = os.path.join(top, fil)
+            whiteable_path = get_whiteable_path(whiteout_marker_loc)
+            if whiteable_path:
+                whiteable_path = whiteable_path.replace(
+                    root_location, '').strip(os.path.sep)
+                yield whiteout_marker_loc, whiteable_path
+
+# Set of well known file and directory paths found at the root of a filesystem
+
+
+LINUX_PATHS = set([
+    'usr',
+    'etc',
+    'var',
+    'home',
+    'sbin',
+    'sys',
+    'lib',
+    'bin',
+    'vmlinuz',
+])
+
+WINDOWS_PATHS = set([
+    'Program Files',
+    'Program Files(x86)',
+    'Windows',
+    'ProgramData',
+    'Users',
+    '$Recycle.Bin',
+    'PerfLogs',
+    'System Volume Information',
+])
+
+
+def find_root(
+    location,
+    max_depth=3,
+    root_paths=LINUX_PATHS,
+    min_paths=2,
+    walker=os.walk,
+):
     """
     Return the first likely location of the root of a filesystem found in the
     `location` directory and looking down up to `max_depth` directory levels
-    deep. If `max_depth` == 0, look at full depth. Search for well known
-    directories listed in the `root_paths` set. A root directory is return as
-    found if at least `min_paths` exists as filenames or directories under it.
-    `_walker` is a callable that behaves the same as `os.walk() and is used
-    mostly for tests`
+    deep below the location directory. If `max_depth` == 0, look at full depth.
+    Search for well known directories listed in the `root_paths` set. A root
+    directory is return as found if at least `min_paths` exists as filenames or
+    directories under it.
+
+    `walker` is a callable that behaves the same as `os.walk() and is used
+    for testing`
     """
-    for depth, (top, dirs, files) in enumerate(_walker(location), 1):
+    for depth, (top, dirs, files) in enumerate(walker(location), 1):
         matches = len(set(dirs + files) & root_paths)
         if matches >= min_paths:
             return top
diff --git a/src/container_inspector/utils.py b/src/container_inspector/utils.py
index a18f557..f496175 100755
--- a/src/container_inspector/utils.py
+++ b/src/container_inspector/utils.py
@@ -14,12 +14,11 @@
 import json
 import logging
 import hashlib
-from os import path
-from os import remove as os_remove
-from os import rmdir as os_rmdir
-import shutil
 import os
 
+from commoncode import fileutils
+
+from extractcode.extract import extract_file
 
 logger = logging.getLogger(__name__)
 # un-comment these lines to enable logging
@@ -37,20 +36,22 @@ def load_json(location):
     return data
 
 
-def get_command(cmd):
+def get_command(cmds):
     """
-    Clean the `cmd` list of command elements as found in the layer history.
+    Clean the `cmds` list of command strings as found in a Docker image layer
+    history.
     """
     # FIXME: this need to be cleaned further
-    return cmd and ' '.join(
-        [c for c in cmd if not c.startswith(('/bin/sh', '-c',))]) or ''
+    cmds = cmds or []
+    cmds = [c for c in cmds if not c.startswith(('/bin/sh', '-c',))]
+    return ' '.join(cmds)
 
 
 def sha256_digest(location):
     """
     Return a SHA256 checksum for the file content at location.
     """
-    if os.path.exists(location):
+    if location and os.path.exists(location):
         with open(location, 'rb') as loc:
             sha256 = hashlib.sha256(loc.read())
         return str(sha256.hexdigest())
@@ -69,106 +70,73 @@ def as_bare_id(string):
 
 def get_labels(config, container_config):
     """
-    Return a mapping of unique labels from the merged config and container_config
-    mappings
+    Return a sorted mapping of unique labels from the merged config and
+    container_config mappings
     """
-    labels = set()
+    labels = {}
 
-    config_labels = config.get('Labels', {}) or {}
+    config = lower_keys(config)
+    config_labels = config.get('labels', {}) or {}
     labels.update(config_labels.items())
 
-    container_labels = (container_config.get('Labels', {}) or {})
+    container_config = lower_keys(container_config)
+    container_labels = container_config.get('labels', {}) or {}
     labels.update(container_labels.items())
-    return dict(sorted(labels))
+    return dict(sorted(labels.items()))
 
 
 def extract_tar(location, target_dir):
     """
     Extract a tar archive at `location` in the `target_dir` directory.
-    Ignore special device files.
+    Ignore special device files and symlinks and hardlinks.
     Do not preserve the permissions and owners.
-    Raise exceptions on possible problematic relative paths.
+    Raise an Exception on error.
     """
-    import tarfile
-    with tarfile.open(location) as tarball:
-        # never extract character device, block and fifo files:
-        # we extract dirs, files and links
-        to_extract = [tinfo for tinfo in tarball.getmembers() if not tinfo.isdev()]
-        # force a u+rwx on all files
-        for tinfo in to_extract:
-            tinfo.mode = 0o700
-            # no absolute nor relative paths:
-            # if tinfo.name != path.normpath(tinfo.name).lstrip('./\\'):
-            #     raise Exception('Illegal tar member file path: {}'.format(tinfo.name))
-            # if tinfo.linkname != path.normpath(tinfo.linkname).lstrip('./\\'):
-            #     raise Exception('Illegal tar member file path link from: {} to: {}'.format(tinfo.name, tinfo.linkname))
-        tarball.extractall(target_dir, members=to_extract)
-
+    errors = []
+    fileutils.create_dir(target_dir)
+    for event in extract_file(location, target_dir):
+        if event.done:
+            errors.extend(event.errors)
 
-def _rm_handler(function, path, excinfo):  # @UnusedVariable
-    """
-    shutil.rmtree handler invoked on error when deleting a directory tree.
-    This retries deleting once before giving up.
-    """
-    if function == os_rmdir:
-        try:
-            shutil.rmtree(path, True)
-        except Exception:
-            pass
+    if errors:
+        raise Exception(f'Failed to extract: {location} to: {target_dir}', *errors)
 
-        if path.exists(path):
-            logger.warning('Failed to delete directory %s', path)
 
-    elif function == os_remove:
-        try:
-            delete(path, _err_handler=None)
-        except:
-            pass
-
-        if path.exists(path):
-            logger.warning('Failed to delete file %s', path)
-
-
-def delete(location, _err_handler=_rm_handler):
-    """
-    Delete a directory or file at `location` recursively. Similar to "rm -rf"
-    in a shell or a combo of os.remove and shutil.rmtree.
+def extract_tar_keeping_symlinks(location, target_dir):
     """
-    if not location:
-        return
-
-    if path.exists(location) or path.islink(location):
-        if path.isdir(location):
-            shutil.rmtree(location, False, _rm_handler)
-        else:
-            os_remove(location)
-
-
-def copytree(src, dst):
-    """
-    Copy recursively the `src` directory to the `dst` directory. If `dst` is an
-    existing directory, files in `dst` may be overwritten during the copy.
-    """
-    names = os.listdir(src)
-
-    if not os.path.exists(dst):
-        os.makedirs(dst)
-
-    for name in names:
-        srcname = os.path.join(src, name)
-        dstname = os.path.join(dst, name)
-
-        if path.isdir(srcname):
-            copytree(srcname, dstname)
-        elif path.isfile(srcname):
-            copyfile(srcname, dstname)
-
-
-def copyfile(src, dst):
-    """
-    Copy src file to dst file
+    Extract a tar archive at `location` in the `target_dir` directory.
+    Ignore special device files. Keep symlinks and hardlinks
+    Do not preserve the permissions and owners.
+    Raise exceptions on possible problematic relative paths.
     """
-    assert path.isfile(src)
-    if path.isdir(dst):
-        dst = path.join(dst, path.basename(src))
-    shutil.copyfile(src, dst)
+    fileutils.create_dir(target_dir)
+    import tarfile
+    with tarfile.open(location) as tarball:
+        # never extract character device, block and fifo files:
+        # we extract dirs, files and links only
+        for tinfo in tarball:
+            if tinfo.isdev():
+                continue
+            tarball.extract(
+                member=tinfo,
+                path=target_dir,
+                set_attrs=False,
+            )
+
+
+def lower_keys(mapping):
+    """
+    Return a new ``mapping`` modified such that all keys are lowercased strings.
+    Fails with an Exception if a key is not a string-like obect.
+    Perform this operation recursively on nested mapping and lists.
+
+    For example::
+    >>> lower_keys({'baZ': 'Amd64', 'Foo': {'Bar': {'ABC': 'bAr'}}})
+    {'baz': 'Amd64', 'foo': {'bar': {'abc': 'bAr'}}}
+    """
+    new_mapping = {}
+    for key, value in mapping.items():
+        if isinstance(value, dict):
+            value = lower_keys(value)
+        new_mapping[key.lower()] = value
+    return new_mapping


Test patch:
diff --git a/tests/data/cli/hello-world.tar-inventory-from-dir-expected.json b/tests/data/cli/hello-world.tar-inventory-from-dir-expected.json
index fef3076..ae7fe92 100644
--- a/tests/data/cli/hello-world.tar-inventory-from-dir-expected.json
+++ b/tests/data/cli/hello-world.tar-inventory-from-dir-expected.json
@@ -1,38 +1,44 @@
 [
   {
+    "extracted_location": null,
+    "archive_location": null,
+    "sha256": null,
     "docker_version": "17.03.1-ce",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-06-14T19:29:01.037740325Z",
     "author": null,
     "comment": null,
     "labels": {},
-    "base_location": null,
+    "image_format": "docker",
     "image_id": "1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
-    "parent_digest": null,
-    "config_digest": "1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
+    "config_digest": "sha256:1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
+    "tags": [
+      "hello-world:latest"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": null,
+        "archive_location": null,
+        "sha256": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": null,
-        "layer_sha256": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
-        "layer_size": 3584,
-        "layer_id": "3e0554cb0efadb678332292bb7835495fbb0c71af7e01bd4f7d11e64fe3d54df",
+        "layer_id": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
+        "size": 3584,
+        "is_empty_layer": false,
         "author": null,
         "created": "2017-06-14T19:28:14.683824199Z",
         "created_by": "/bin/sh -c #(nop) COPY file:b65349dad8105cbef74456e9c0c9da5d001e9eb2ade4b3c21e107909aee5b25a in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "hello-world:latest"
-    ],
     "history": [
       {
         "created": "2017-06-14T19:28:14.683824199Z",
@@ -43,8 +49,6 @@
         "created_by": "/bin/sh -c #(nop)  CMD [\"/hello\"]",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   }
 ]
\ No newline at end of file
diff --git a/tests/data/cli/hello-world.tar-inventory-from-tarball-expected.json b/tests/data/cli/hello-world.tar-inventory-from-tarball-expected.json
index fef3076..de63baf 100644
--- a/tests/data/cli/hello-world.tar-inventory-from-tarball-expected.json
+++ b/tests/data/cli/hello-world.tar-inventory-from-tarball-expected.json
@@ -1,38 +1,44 @@
 [
   {
+    "extracted_location": null,
+    "archive_location": null,
+    "sha256": "4d99bd478599e97b8fc8c647d7c6b1b4c3cf1d445949d5f20b08de9ca427cd2f",
     "docker_version": "17.03.1-ce",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-06-14T19:29:01.037740325Z",
     "author": null,
     "comment": null,
     "labels": {},
-    "base_location": null,
+    "image_format": "docker",
     "image_id": "1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
-    "parent_digest": null,
-    "config_digest": "1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
+    "config_digest": "sha256:1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
+    "tags": [
+      "hello-world:latest"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": null,
+        "archive_location": null,
+        "sha256": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": null,
-        "layer_sha256": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
-        "layer_size": 3584,
-        "layer_id": "3e0554cb0efadb678332292bb7835495fbb0c71af7e01bd4f7d11e64fe3d54df",
+        "layer_id": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
+        "size": 3584,
+        "is_empty_layer": false,
         "author": null,
         "created": "2017-06-14T19:28:14.683824199Z",
         "created_by": "/bin/sh -c #(nop) COPY file:b65349dad8105cbef74456e9c0c9da5d001e9eb2ade4b3c21e107909aee5b25a in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "hello-world:latest"
-    ],
     "history": [
       {
         "created": "2017-06-14T19:28:14.683824199Z",
@@ -43,8 +49,6 @@
         "created_by": "/bin/sh -c #(nop)  CMD [\"/hello\"]",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   }
 ]
\ No newline at end of file
diff --git a/tests/data/cli/she-image_from_scratch-1.0.tar-inventory-from-tarball-expected.json b/tests/data/cli/she-image_from_scratch-1.0.tar-inventory-from-tarball-expected.json
index 96f50f9..926b465 100644
--- a/tests/data/cli/she-image_from_scratch-1.0.tar-inventory-from-tarball-expected.json
+++ b/tests/data/cli/she-image_from_scratch-1.0.tar-inventory-from-tarball-expected.json
@@ -1,91 +1,100 @@
 [
   {
+    "extracted_location": null,
+    "archive_location": null,
+    "sha256": "2ff7a11e7e5425f04d757c3dcf38203df22ba08941061781eeda261b0ff54936",
     "docker_version": "18.09.7",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2020-06-02T07:19:55.133752465Z",
     "author": null,
     "comment": null,
     "labels": {
-      "Some other label": ""
+      "some other label": ""
     },
-    "base_location": null,
+    "image_format": "docker",
     "image_id": "6d190579c3d7ccc07188cd204310927f553f2ebe1faf868169365f3eccf9c8ab",
-    "parent_digest": null,
-    "config_digest": "6d190579c3d7ccc07188cd204310927f553f2ebe1faf868169365f3eccf9c8ab",
+    "config_digest": "sha256:6d190579c3d7ccc07188cd204310927f553f2ebe1faf868169365f3eccf9c8ab",
+    "tags": [
+      "she/image_from_scratch:1.0"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": null,
+        "archive_location": null,
+        "sha256": "1f9928556cf4d8e357642820b93a68343ae4bf8eeba76a041b2ea3a4257c5a5a",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": null,
-        "layer_sha256": "1f9928556cf4d8e357642820b93a68343ae4bf8eeba76a041b2ea3a4257c5a5a",
-        "layer_size": 2048,
-        "layer_id": "390f462bd3d4a0f900a4c431a53dc8830461b9e703ba348a2700dcc46f720c02",
+        "layer_id": "1f9928556cf4d8e357642820b93a68343ae4bf8eeba76a041b2ea3a4257c5a5a",
+        "size": 2048,
+        "is_empty_layer": false,
         "author": "You Myself and I.",
         "created": "2020-06-02T07:19:53.24191627Z",
         "created_by": "/bin/sh -c #(nop) ADD file:bc85f38b0c798897406a4c35fe08d06599df3831fca6db0cbd279e5d71bf9514 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": null,
+        "archive_location": null,
+        "sha256": "57a9ef5e4048962620a6311018be2010c179c9853e878a20080aeaed97f42826",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": null,
-        "layer_sha256": "57a9ef5e4048962620a6311018be2010c179c9853e878a20080aeaed97f42826",
-        "layer_size": 5632,
-        "layer_id": "24ff9e455c2811b9f3ba124276b5f2bb0111555057fdcc5b528c471597186fc3",
+        "layer_id": "57a9ef5e4048962620a6311018be2010c179c9853e878a20080aeaed97f42826",
+        "size": 5632,
+        "is_empty_layer": false,
         "author": "You Myself and I.",
         "created": "2020-06-02T07:19:53.514748579Z",
         "created_by": "/bin/sh -c #(nop) COPY dir:dce95f07b795ec49ef6ddbcc12f24aaad9af237726904837c77b5ed2967e349e in /additions ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": null,
+        "archive_location": null,
+        "sha256": "0676ff439070aa2108b9bc3904f04265f83a639b4e6fe3e45c9549fd7bbe3369",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": null,
-        "layer_sha256": "0676ff439070aa2108b9bc3904f04265f83a639b4e6fe3e45c9549fd7bbe3369",
-        "layer_size": 5120,
-        "layer_id": "7295cea29559ce1e63515273dbd67ac770ef613b9b697473c1f95a0c494cfd80",
+        "layer_id": "0676ff439070aa2108b9bc3904f04265f83a639b4e6fe3e45c9549fd7bbe3369",
+        "size": 5120,
+        "is_empty_layer": false,
         "author": "You Myself and I.",
         "created": "2020-06-02T07:19:54.191141634Z",
         "created_by": "/bin/sh -c #(nop) COPY dir:dce95f07b795ec49ef6ddbcc12f24aaad9af237726904837c77b5ed2967e349e in /additions ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": null,
+        "archive_location": null,
+        "sha256": "85dc439f0d28eceba0cfc2b883f3e99a37e60961e2686acf8b85c09e67facb01",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": null,
-        "layer_sha256": "85dc439f0d28eceba0cfc2b883f3e99a37e60961e2686acf8b85c09e67facb01",
-        "layer_size": 2560,
-        "layer_id": "649ef32010e339b304136b22154e13f984f88f528c8120a17291396b9264ccc0",
+        "layer_id": "85dc439f0d28eceba0cfc2b883f3e99a37e60961e2686acf8b85c09e67facb01",
+        "size": 2560,
+        "is_empty_layer": false,
         "author": "You Myself and I.",
         "created": "2020-06-02T07:19:54.826380355Z",
         "created_by": "/bin/sh -c #(nop) ADD file:bc85f38b0c798897406a4c35fe08d06599df3831fca6db0cbd279e5d71bf9514 in /additions/hello ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "she/image_from_scratch:1.0"
-    ],
     "history": [
       {
         "created": "2020-06-02T07:19:52.945579677Z",
@@ -131,8 +140,6 @@
         "created_by": "/bin/sh -c #(nop)  LABEL Some other label=",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   }
 ]
\ No newline at end of file
diff --git a/tests/data/image/she-image_from_scratch-1.0.tar b/tests/data/image/she-image_from_scratch-1.0.tar
new file mode 100644
index 0000000..177efb7
Binary files /dev/null and b/tests/data/image/she-image_from_scratch-1.0.tar differ
diff --git a/tests/data/image/windows-mini-image.tar.gz b/tests/data/image/windows-mini-image.tar.gz
new file mode 100644
index 0000000..0fb6ad7
Binary files /dev/null and b/tests/data/image/windows-mini-image.tar.gz differ
diff --git a/tests/data/image/windows-mini-image.tar.gz.expected.json b/tests/data/image/windows-mini-image.tar.gz.expected.json
new file mode 100644
index 0000000..50afebe
--- /dev/null
+++ b/tests/data/image/windows-mini-image.tar.gz.expected.json
@@ -0,0 +1,66 @@
+{
+  "extracted_location": "",
+  "archive_location": "",
+  "sha256": "f3f60bf21091013c476cecbabe4c5579a180fd557c38b296c6d151c4f586b35c",
+  "docker_version": null,
+  "os": "windows",
+  "os_version": "10.0.19042.985",
+  "architecture": "amd64",
+  "variant": null,
+  "created": "2021-04-30T07:20:45.4345264+00:00",
+  "author": null,
+  "comment": null,
+  "labels": {},
+  "image_format": "docker",
+  "image_id": "67f0a0c2d8efc23f33d6978e9a2f039e2cbddae683a0b7918b74ace10467935b",
+  "config_digest": "sha256:67f0a0c2d8efc23f33d6978e9a2f039e2cbddae683a0b7918b74ace10467935b",
+  "tags": [],
+  "distro": {
+    "os": "windows",
+    "architecture": "amd64",
+    "name": "linux",
+    "version": "10.0.19042.985",
+    "identifier": "identifier",
+    "id_like": [],
+    "version_codename": null,
+    "version_id": null,
+    "pretty_name": null,
+    "cpe_name": null,
+    "home_url": null,
+    "documentation_url": null,
+    "support_url": null,
+    "bug_report_url": null,
+    "privacy_policy_url": null,
+    "build_id": null,
+    "variant": null,
+    "variant_id": null,
+    "logo": null,
+    "extra_data": {}
+  },
+  "layers": [
+    {
+      "extracted_location": "e4e02f4caaa37d28ad4b66babf9d64098e81f08c0be24ea427614eafcbbdc3de",
+      "archive_location": "e4e02f4caaa37d28ad4b66babf9d64098e81f08c0be24ea427614eafcbbdc3de.tar",
+      "sha256": "e4e02f4caaa37d28ad4b66babf9d64098e81f08c0be24ea427614eafcbbdc3de",
+      "docker_version": null,
+      "os": null,
+      "os_version": null,
+      "architecture": null,
+      "variant": null,
+      "labels": [],
+      "layer_id": "e4e02f4caaa37d28ad4b66babf9d64098e81f08c0be24ea427614eafcbbdc3de",
+      "size": 22016,
+      "is_empty_layer": false,
+      "author": null,
+      "created": "2021-04-30T07:20:45.4345264+00:00",
+      "created_by": "Apply image 20H2-amd64",
+      "comment": null
+    }
+  ],
+  "history": [
+    {
+      "created": "2021-04-30T07:20:45.4345264+00:00",
+      "created_by": "Apply image 20H2-amd64"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/data/repos/hello-world.tar.flatten.expected.json b/tests/data/repos/hello-world.tar.flatten.expected.json
index 3d362ab..0038c8f 100644
--- a/tests/data/repos/hello-world.tar.flatten.expected.json
+++ b/tests/data/repos/hello-world.tar.flatten.expected.json
@@ -1,15 +1,17 @@
 [
   {
-    "image_dir": "",
+    "image_extracted_location": "",
+    "image_archive_location": "",
     "image_id": "1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
     "image_tags": "hello-world:latest",
     "is_empty_layer": false,
-    "layer_id": "3e0554cb0efadb678332292bb7835495fbb0c71af7e01bd4f7d11e64fe3d54df",
+    "layer_id": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
     "layer_sha256": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
     "author": null,
     "created_by": "/bin/sh -c #(nop) COPY file:b65349dad8105cbef74456e9c0c9da5d001e9eb2ade4b3c21e107909aee5b25a in / ",
     "created": "2017-06-14T19:28:14.683824199Z",
     "comment": null,
-    "layer_location": "3e0554cb0efadb678332292bb7835495fbb0c71af7e01bd4f7d11e64fe3d54df"
+    "layer_extracted_location": "",
+    "layer_archive_location": "layer.tar"
   }
 ]
\ No newline at end of file
diff --git a/tests/data/repos/hello-world.tar.registry.expected.json b/tests/data/repos/hello-world.tar.registry.expected.json
index 0e4fe35..7490d31 100644
--- a/tests/data/repos/hello-world.tar.registry.expected.json
+++ b/tests/data/repos/hello-world.tar.registry.expected.json
@@ -1,38 +1,44 @@
 [
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": null,
     "docker_version": "17.03.1-ce",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-06-14T19:29:01.037740325Z",
     "author": null,
     "comment": null,
     "labels": {},
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
-    "parent_digest": null,
-    "config_digest": "1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
+    "config_digest": "sha256:1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57",
+    "tags": [
+      "hello-world:latest"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
-        "layer_size": 3584,
-        "layer_id": "3e0554cb0efadb678332292bb7835495fbb0c71af7e01bd4f7d11e64fe3d54df",
+        "layer_id": "45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a",
+        "size": 3584,
+        "is_empty_layer": false,
         "author": null,
         "created": "2017-06-14T19:28:14.683824199Z",
         "created_by": "/bin/sh -c #(nop) COPY file:b65349dad8105cbef74456e9c0c9da5d001e9eb2ade4b3c21e107909aee5b25a in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "hello-world:latest"
-    ],
     "history": [
       {
         "created": "2017-06-14T19:28:14.683824199Z",
@@ -43,8 +49,6 @@
         "created_by": "/bin/sh -c #(nop)  CMD [\"/hello\"]",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   }
 ]
\ No newline at end of file
diff --git a/tests/data/repos/images.tar.gz.expected.json b/tests/data/repos/images.tar.gz.expected.json
index 4b5c56f..e92f042 100644
--- a/tests/data/repos/images.tar.gz.expected.json
+++ b/tests/data/repos/images.tar.gz.expected.json
@@ -1,8 +1,13 @@
 [
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": null,
     "docker_version": "1.12.6",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-01-18T09:49:05.022760406Z",
     "author": null,
     "comment": null,
@@ -19,83 +24,87 @@
       "org.label-schema.vendor": "Microscaling Systems",
       "org.label-schema.version": "0.9.1"
     },
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "4ac5c955a6dc9dc709fa2ce8dd21c45292afd9e4af38dcd1fb239ed546d19233",
-    "parent_digest": null,
-    "config_digest": "4ac5c955a6dc9dc709fa2ce8dd21c45292afd9e4af38dcd1fb239ed546d19233",
+    "config_digest": "sha256:4ac5c955a6dc9dc709fa2ce8dd21c45292afd9e4af38dcd1fb239ed546d19233",
+    "tags": [
+      "microscaling/microscaling:latest"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "7cbcbac42c44c6c38559e5df3a494f44987333c8023a40fec48df2fce1fc146b",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "7cbcbac42c44c6c38559e5df3a494f44987333c8023a40fec48df2fce1fc146b",
-        "layer_size": 30,
-        "layer_id": "d388bee71bbf28f77042d89b353bacd14506227a39b6b1098afe457f0a0b608e",
+        "layer_id": "7cbcbac42c44c6c38559e5df3a494f44987333c8023a40fec48df2fce1fc146b",
+        "size": 30,
+        "is_empty_layer": false,
         "author": null,
         "created": "2016-12-27T18:17:13.762716133Z",
         "created_by": "/bin/sh -c #(nop) ADD file:eeed5f514a35d18fcd9cbfe6c40c582211020bffdd53e4799018d33826fe5067 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "d242f1731c55e0f057e183146de867e820dd2ef575125ec36b008340a3acc65e",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "d242f1731c55e0f057e183146de867e820dd2ef575125ec36b008340a3acc65e",
-        "layer_size": 30,
-        "layer_id": "e439273044a3b34ba206c3598cd007dbba34db5af98d40718d585869af68602e",
+        "layer_id": "d242f1731c55e0f057e183146de867e820dd2ef575125ec36b008340a3acc65e",
+        "size": 30,
+        "is_empty_layer": false,
         "author": "Ross Fairbanks \"ross@microscaling.com\"",
         "created": "2017-01-03T13:15:58.410035553Z",
         "created_by": "/bin/sh -c apk update &&     apk upgrade &&     apk add $BUILD_PACKAGES &&     rm -rf /var/cache/apk/*",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "d43ffef6b2712ef8ecdd86866e543b21ef8843742bf7c73a308a973534fa6c3f",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "d43ffef6b2712ef8ecdd86866e543b21ef8843742bf7c73a308a973534fa6c3f",
-        "layer_size": 30,
-        "layer_id": "ca473f7411dd74e2d4781fc552d75d3b63c830ff66a253eec6bf5a6261c031b2",
+        "layer_id": "d43ffef6b2712ef8ecdd86866e543b21ef8843742bf7c73a308a973534fa6c3f",
+        "size": 30,
+        "is_empty_layer": false,
         "author": "Ross Fairbanks \"ross@microscaling.com\"",
         "created": "2017-01-17T11:17:46.675078318Z",
         "created_by": "/bin/sh -c #(nop) COPY multi:0847be51b158e2902e11d406f498a1d8d58ede5fbab01297b446d84b42667433 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "76ad2c2330f19d6f16fdf86e7b10cc2c1a8160746ffa1c4e3e46c75661f4bdcd",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "76ad2c2330f19d6f16fdf86e7b10cc2c1a8160746ffa1c4e3e46c75661f4bdcd",
-        "layer_size": 30,
-        "layer_id": "325919382e7b94a21ef80452c2fb599f5a03364e8559da0eca1359ad6ac394a0",
+        "layer_id": "76ad2c2330f19d6f16fdf86e7b10cc2c1a8160746ffa1c4e3e46c75661f4bdcd",
+        "size": 30,
+        "is_empty_layer": false,
         "author": "Ross Fairbanks \"ross@microscaling.com\"",
         "created": "2017-01-17T11:17:48.829523581Z",
         "created_by": "/bin/sh -c chmod +x /microscaling",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "microscaling/microscaling:latest"
-    ],
     "history": [
       {
         "created": "2016-12-27T18:17:13.762716133Z",
@@ -164,14 +173,17 @@
         "created_by": "/bin/sh -c #(nop)  CMD [\"/microscaling\"]",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   },
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": null,
     "docker_version": "19.03.8",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2020-10-11T21:21:00.914818853Z",
     "author": null,
     "comment": null,
@@ -189,117 +201,123 @@
       "org.opencontainers.image.vendor": "Open Web Application Security Project",
       "org.opencontainers.image.version": "8.1.2"
     },
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "2e92ac90bc1e459ee9d2d72a8bcd3787895d3d7d2d75391e620cea6f20b61315",
-    "parent_digest": null,
-    "config_digest": "2e92ac90bc1e459ee9d2d72a8bcd3787895d3d7d2d75391e620cea6f20b61315",
+    "config_digest": "sha256:2e92ac90bc1e459ee9d2d72a8bcd3787895d3d7d2d75391e620cea6f20b61315",
+    "tags": [
+      "bkimminich/juice-shop-ctf:latest"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "3e207b409db364b595ba862cdc12be96dcdad8e36c59a03b7b3b61c946a5741a",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "3e207b409db364b595ba862cdc12be96dcdad8e36c59a03b7b3b61c946a5741a",
-        "layer_size": 30,
-        "layer_id": "2ba5d0825bcb0d843f975f7ba52db62531dbc73df083fae3d355c1a22d4a4bdc",
+        "layer_id": "3e207b409db364b595ba862cdc12be96dcdad8e36c59a03b7b3b61c946a5741a",
+        "size": 30,
+        "is_empty_layer": false,
         "author": null,
         "created": "2020-04-24T01:05:03.608058404Z",
         "created_by": "/bin/sh -c #(nop) ADD file:b91adb67b670d3a6ff9463e48b7def903ed516be66fc4282d22c53e41512be49 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "09c52b6fbc483eb8e2d244a916da54fb3990cdaa575cab35edfbb27e132929cb",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "09c52b6fbc483eb8e2d244a916da54fb3990cdaa575cab35edfbb27e132929cb",
-        "layer_size": 30,
-        "layer_id": "c9d345514b7a7f4e4d14748c6a7a6cb408f117fccce87ef80bf72429b5360c44",
+        "layer_id": "09c52b6fbc483eb8e2d244a916da54fb3990cdaa575cab35edfbb27e132929cb",
+        "size": 30,
+        "is_empty_layer": false,
         "author": null,
         "created": "2020-10-08T16:23:14.227103847Z",
         "created_by": "/bin/sh -c addgroup -g 1000 node     && adduser -u 1000 -G node -s /bin/sh -D node     && apk add --no-cache         libstdc++     && apk add --no-cache --virtual .build-deps         curl     && ARCH= && alpineArch=\"$(apk --print-arch)\"       && case \"${alpineArch##*-}\" in         x86_64)           ARCH='x64'           CHECKSUM=\"b8b33ec1d2f6463ded7c02bf29ef4141ff4aabb2c0e0ba14cf16ac7c0a6a31a5\"           ;;         *) ;;       esac   && if [ -n \"${CHECKSUM}\" ]; then     set -eu;     curl -fsSLO --compressed \"https://unofficial-builds.nodejs.org/download/release/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\";     echo \"$CHECKSUM  node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\" | sha256sum -c -       && tar -xJf \"node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\" -C /usr/local --strip-components=1 --no-same-owner       && ln -s /usr/local/bin/node /usr/local/bin/nodejs;   else     echo \"Building from source\"     && apk add --no-cache --virtual .build-deps-full         binutils-gold         g++         gcc         gnupg         libgcc         linux-headers         make         python3     && for key in       4ED778F539E3634C779C87C6D7062848A1AB005C       94AE36675C464D64BAFA68DD7434390BDBE9B9C5       1C050899334244A8AF75E53792EF661D867B9DFA       71DCFD284A79C3B38668286BC97EC7A07EDE3FC1       8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600       C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8       C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C       DD8F2338BAE7501E3DD5AC78C273792F7D83545D       A48C2BEE680E841632CD4E44F07496B3EB3C1762       108F52B48DB57BB0CC439B2997B01419BD92F80A       B9E2F5981AA6E0CD28160D9FF13993A75599653C     ; do       gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys \"$key\" ||       gpg --batch --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys \"$key\" ||       gpg --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys \"$key\" ;     done     && curl -fsSLO --compressed \"https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION.tar.xz\"     && curl -fsSLO --compressed \"https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc\"     && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc     && grep \" node-v$NODE_VERSION.tar.xz\\$\" SHASUMS256.txt | sha256sum -c -     && tar -xf \"node-v$NODE_VERSION.tar.xz\"     && cd \"node-v$NODE_VERSION\"     && ./configure     && make -j$(getconf _NPROCESSORS_ONLN) V=     && make install     && apk del .build-deps-full     && cd ..     && rm -Rf \"node-v$NODE_VERSION\"     && rm \"node-v$NODE_VERSION.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt;   fi   && rm -f \"node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\"   && apk del .build-deps   && node --version   && npm --version",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "55141db9edb2a13ee593cff8c80e883e672e388c8686fd94a4f2518f21de1d32",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "55141db9edb2a13ee593cff8c80e883e672e388c8686fd94a4f2518f21de1d32",
-        "layer_size": 30,
-        "layer_id": "cbfc5b91e94f389bd405928c3be3367feea459d5917bde9d8b2792b96ae789ef",
+        "layer_id": "55141db9edb2a13ee593cff8c80e883e672e388c8686fd94a4f2518f21de1d32",
+        "size": 30,
+        "is_empty_layer": false,
         "author": null,
         "created": "2020-10-08T16:23:16.985023204Z",
         "created_by": "/bin/sh -c apk add --no-cache --virtual .build-deps-yarn curl gnupg tar   && for key in     6A010C5166006599AA17F08146C2130DFD2497F5   ; do     gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys \"$key\" ||     gpg --batch --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys \"$key\" ||     gpg --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys \"$key\" ;   done   && curl -fsSLO --compressed \"https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz\"   && curl -fsSLO --compressed \"https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz.asc\"   && gpg --batch --verify yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz   && mkdir -p /opt   && tar -xzf yarn-v$YARN_VERSION.tar.gz -C /opt/   && ln -s /opt/yarn-v$YARN_VERSION/bin/yarn /usr/local/bin/yarn   && ln -s /opt/yarn-v$YARN_VERSION/bin/yarnpkg /usr/local/bin/yarnpkg   && rm yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz   && apk del .build-deps-yarn   && yarn --version",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "01f37c950ed43fd0ecc47d0a72949201594f650bd63861cc6e6ac8097ca600bf",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "01f37c950ed43fd0ecc47d0a72949201594f650bd63861cc6e6ac8097ca600bf",
-        "layer_size": 30,
-        "layer_id": "bf1ad815335aca28dc1b64e4a9cad7f0bd9f7e9ee8162dce166150b4f1169b0e",
+        "layer_id": "01f37c950ed43fd0ecc47d0a72949201594f650bd63861cc6e6ac8097ca600bf",
+        "size": 30,
+        "is_empty_layer": false,
         "author": null,
         "created": "2020-10-08T16:23:17.192305843Z",
         "created_by": "/bin/sh -c #(nop) COPY file:238737301d47304174e4d24f4def935b29b3069c03c72ae8de97d94624382fce in /usr/local/bin/ ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "08dc907515cbda226cd872c2c79d087eb226fd27182b6b1315306aade51f963d",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "08dc907515cbda226cd872c2c79d087eb226fd27182b6b1315306aade51f963d",
-        "layer_size": 30,
-        "layer_id": "5b2fc24305d353053e3a6aa7a613e75240e34cfede725f4bd4c466468cd3c48c",
+        "layer_id": "08dc907515cbda226cd872c2c79d087eb226fd27182b6b1315306aade51f963d",
+        "size": 30,
+        "is_empty_layer": false,
         "author": null,
         "created": "2020-10-11T21:20:59.851868447Z",
         "created_by": "/bin/sh -c #(nop) COPY dir:5bd634490f1e144602e7b39a1ebcb69af1748a34673299424a7fe6f10334bfa1 in /juice-shop-ctf ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "5b4096031e4780d4c3010335ede79886786ec89d22c2bd85642a30beac682ec9",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "5b4096031e4780d4c3010335ede79886786ec89d22c2bd85642a30beac682ec9",
-        "layer_size": 30,
-        "layer_id": "ee512e2eb28e302e80a1e0bfad85ee3b18adc84a80e88ceac4325c13e3e083ec",
+        "layer_id": "5b4096031e4780d4c3010335ede79886786ec89d22c2bd85642a30beac682ec9",
+        "size": 30,
+        "is_empty_layer": false,
         "author": null,
         "created": "2020-10-11T21:21:00.668316194Z",
         "created_by": "/bin/sh -c #(nop) WORKDIR /data",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "bkimminich/juice-shop-ctf:latest"
-    ],
     "history": [
       {
         "created": "2020-04-24T01:05:03.608058404Z",
@@ -375,8 +393,6 @@
         "created_by": "/bin/sh -c #(nop)  ENTRYPOINT [\"npx\" \"/juice-shop-ctf/bin/juice-shop-ctf.js\"]",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   }
 ]
\ No newline at end of file
diff --git a/tests/data/repos/imagesv11.tar.expected.json b/tests/data/repos/imagesv11.tar.expected.json
index 17fd4ef..99e30b9 100644
--- a/tests/data/repos/imagesv11.tar.expected.json
+++ b/tests/data/repos/imagesv11.tar.expected.json
@@ -1,83 +1,93 @@
 [
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": "79e479cd111b4276a2ff68f9cbaf64436556aac1d79a7df0b5527070bb774f47",
     "docker_version": "1.13.0",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-02-01T09:45:44.138970446Z",
     "author": null,
     "comment": "initial import",
     "labels": {},
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e",
-    "parent_digest": null,
-    "config_digest": "4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e",
+    "config_digest": "sha256:4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e",
+    "tags": [
+      "me/test_image_tar:1.0"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
-        "layer_size": 102400,
-        "layer_id": "de331f94dc3592a39e495432d39ca14ed0cf04a6861c1e7714ff410e8a0225b4",
+        "layer_id": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
+        "size": 102400,
+        "is_empty_layer": false,
         "author": null,
         "created": "2017-02-01T09:45:44.138970446Z",
         "created_by": null,
-        "comment": "initial import",
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": "initial import"
       }
     ],
-    "tags": [
-      "me/test_image_tar:1.0"
-    ],
     "history": [
       {
         "created": "2017-02-01T09:45:44.138970446Z",
         "comment": "initial import"
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   },
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": "79e479cd111b4276a2ff68f9cbaf64436556aac1d79a7df0b5527070bb774f47",
     "docker_version": "1.13.0",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-02-01T09:54:08.599892527Z",
     "author": null,
     "comment": null,
     "labels": {
-      "Some other label": ""
+      "some other label": ""
     },
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1",
-    "parent_digest": null,
-    "config_digest": "c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1",
+    "config_digest": "sha256:c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1",
+    "tags": [
+      "she/image_from_scratch:1.0"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
-        "layer_size": 2048,
-        "layer_id": "a699c18e2119e668e5f1264e97559af65d2111ac0790b6832efeb450fc5193fc",
+        "layer_id": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
+        "size": 2048,
+        "is_empty_layer": false,
         "author": "You Myself and I.",
         "created": "2017-02-01T09:54:08.42333374Z",
         "created_by": "/bin/sh -c #(nop) ADD file:4cf1ddc0e2f550529d17989d68a7968b07a51044244d4f432e778929d1a83a48 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "she/image_from_scratch:1.0"
-    ],
     "history": [
       {
         "created": "2017-02-01T09:54:08.215083283Z",
@@ -96,63 +106,68 @@
         "created_by": "/bin/sh -c #(nop)  LABEL Some other label=",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   },
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": "79e479cd111b4276a2ff68f9cbaf64436556aac1d79a7df0b5527070bb774f47",
     "docker_version": "1.13.0",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-02-01T09:52:31.061930779Z",
     "author": null,
     "comment": null,
     "labels": {
-      "Some label": ""
+      "some label": ""
     },
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23",
-    "parent_digest": null,
-    "config_digest": "12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23",
+    "config_digest": "sha256:12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23",
+    "tags": [
+      "you/secondimage:1.2"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
-        "layer_size": 102400,
-        "layer_id": "f2323825799160c4c1e508c1189dc285c7928677747b5a679020e9b438843127",
+        "layer_id": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
+        "size": 102400,
+        "is_empty_layer": false,
         "author": null,
         "created": "2017-02-01T09:45:44.138970446Z",
         "created_by": null,
-        "comment": "initial import",
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": "initial import"
       },
       {
+        "extracted_location": "",
+        "archive_location": "layer.tar",
+        "sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "layer.tar",
-        "layer_sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
-        "layer_size": 2048,
-        "layer_id": "1466996a39f8d2af81b92fad37ef15c04fb2ca64b43945a00f6f5943913f6b96",
+        "layer_id": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
+        "size": 2048,
+        "is_empty_layer": false,
         "author": "Me Myself and I.",
         "created": "2017-02-01T09:52:30.86379643Z",
         "created_by": "/bin/sh -c #(nop) ADD file:4cf1ddc0e2f550529d17989d68a7968b07a51044244d4f432e778929d1a83a48 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "you/secondimage:1.2"
-    ],
     "history": [
       {
         "created": "2017-02-01T09:45:44.138970446Z",
@@ -175,8 +190,6 @@
         "created_by": "/bin/sh -c #(nop)  LABEL Some label=",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   }
 ]
\ No newline at end of file
diff --git a/tests/data/repos/imagesv11.tar.registry.expected.json b/tests/data/repos/imagesv11.tar.registry.expected.json
index ad8baa0..0021f82 100644
--- a/tests/data/repos/imagesv11.tar.registry.expected.json
+++ b/tests/data/repos/imagesv11.tar.registry.expected.json
@@ -81,7 +81,6 @@
         "comment": "initial import", 
         "config": {}, 
         "image_id": "4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e", 
-        "parent_digest": null, 
         "config_digest": "sha256:4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e", 
         "top_layer_id": "de331f94dc3592a39e495432d39ca14ed0cf04a6861c1e7714ff410e8a0225b4", 
         "top_layer_digest": "sha256:c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9", 
@@ -119,7 +118,6 @@
         "comment": null, 
         "config": {}, 
         "image_id": "12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23", 
-        "parent_digest": null, 
         "config_digest": "sha256:12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23", 
         "top_layer_id": "1466996a39f8d2af81b92fad37ef15c04fb2ca64b43945a00f6f5943913f6b96", 
         "top_layer_digest": "sha256:None", 
@@ -208,7 +206,6 @@
         "comment": null, 
         "config": {}, 
         "image_id": "c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1", 
-        "parent_digest": null, 
         "config_digest": "sha256:c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1", 
         "top_layer_id": "a699c18e2119e668e5f1264e97559af65d2111ac0790b6832efeb450fc5193fc", 
         "top_layer_digest": "sha256:2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62", 
diff --git a/tests/data/repos/imagesv11_with_tar_at_root.tar.expected.json b/tests/data/repos/imagesv11_with_tar_at_root.tar.expected.json
index cf49403..dedded5 100644
--- a/tests/data/repos/imagesv11_with_tar_at_root.tar.expected.json
+++ b/tests/data/repos/imagesv11_with_tar_at_root.tar.expected.json
@@ -80,7 +80,6 @@
       "comment": "initial import", 
       "config": {}, 
       "image_id": "4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e", 
-      "parent_digest": null, 
       "config_digest": "sha256:4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e", 
       "top_layer_id": "de331f94dc3592a39e495432d39ca14ed0cf04a6861c1e7714ff410e8a0225b4", 
       "top_layer_digest": "sha256:c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9", 
@@ -118,7 +117,6 @@
       "comment": null, 
       "config": {}, 
       "image_id": "12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23", 
-      "parent_digest": null, 
       "config_digest": "sha256:12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23", 
       "top_layer_id": "1466996a39f8d2af81b92fad37ef15c04fb2ca64b43945a00f6f5943913f6b96", 
       "top_layer_digest": "sha256:None", 
@@ -207,7 +205,6 @@
       "comment": null, 
       "config": {}, 
       "image_id": "c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1", 
-      "parent_digest": null, 
       "config_digest": "sha256:c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1", 
       "top_layer_id": "a699c18e2119e668e5f1264e97559af65d2111ac0790b6832efeb450fc5193fc", 
       "top_layer_digest": "sha256:2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62", 
diff --git a/tests/data/repos/imagesv11_with_tar_at_root.tar.registry.expected.json b/tests/data/repos/imagesv11_with_tar_at_root.tar.registry.expected.json
index 885c61f..217d366 100644
--- a/tests/data/repos/imagesv11_with_tar_at_root.tar.registry.expected.json
+++ b/tests/data/repos/imagesv11_with_tar_at_root.tar.registry.expected.json
@@ -1,83 +1,93 @@
 [
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": null,
     "docker_version": "1.13.0",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-02-01T09:45:44.138970446Z",
     "author": null,
     "comment": "initial import",
     "labels": {},
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e",
-    "parent_digest": null,
-    "config_digest": "4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e",
+    "config_digest": "sha256:4d955704d3bfe618753e3f969db87e2b60dd4206132a701f23de1dfe0325664e",
+    "tags": [
+      "me/test_image_tar:1.0"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "de331f94dc3592a39e495432d39ca14ed0cf04a6861c1e7714ff410e8a0225b4.tar",
+        "sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "de331f94dc3592a39e495432d39ca14ed0cf04a6861c1e7714ff410e8a0225b4.tar",
-        "layer_sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
-        "layer_size": 2048,
-        "layer_id": "imagesv11_with_tar_at_root.tar",
+        "layer_id": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
+        "size": 2048,
+        "is_empty_layer": false,
         "author": null,
         "created": "2017-02-01T09:45:44.138970446Z",
         "created_by": null,
-        "comment": "initial import",
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": "initial import"
       }
     ],
-    "tags": [
-      "me/test_image_tar:1.0"
-    ],
     "history": [
       {
         "created": "2017-02-01T09:45:44.138970446Z",
         "comment": "initial import"
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   },
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": null,
     "docker_version": "1.13.0",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-02-01T09:54:08.599892527Z",
     "author": null,
     "comment": null,
     "labels": {
-      "Some other label": ""
+      "some other label": ""
     },
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1",
-    "parent_digest": null,
-    "config_digest": "c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1",
+    "config_digest": "sha256:c1edec6e1f0ad79e45f68e963b9e74c0ab77915845ab7743b053c248f1ad64b1",
+    "tags": [
+      "she/image_from_scratch:1.0"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "a699c18e2119e668e5f1264e97559af65d2111ac0790b6832efeb450fc5193fc.tar",
+        "sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "a699c18e2119e668e5f1264e97559af65d2111ac0790b6832efeb450fc5193fc.tar",
-        "layer_sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
-        "layer_size": 2048,
-        "layer_id": "imagesv11_with_tar_at_root.tar",
+        "layer_id": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
+        "size": 2048,
+        "is_empty_layer": false,
         "author": "You Myself and I.",
         "created": "2017-02-01T09:54:08.42333374Z",
         "created_by": "/bin/sh -c #(nop) ADD file:4cf1ddc0e2f550529d17989d68a7968b07a51044244d4f432e778929d1a83a48 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "she/image_from_scratch:1.0"
-    ],
     "history": [
       {
         "created": "2017-02-01T09:54:08.215083283Z",
@@ -96,63 +106,68 @@
         "created_by": "/bin/sh -c #(nop)  LABEL Some other label=",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   },
   {
+    "extracted_location": "",
+    "archive_location": "",
+    "sha256": null,
     "docker_version": "1.13.0",
     "os": "linux",
+    "os_version": null,
     "architecture": "amd64",
+    "variant": null,
     "created": "2017-02-01T09:52:31.061930779Z",
     "author": null,
     "comment": null,
     "labels": {
-      "Some label": ""
+      "some label": ""
     },
-    "base_location": "",
+    "image_format": "docker",
     "image_id": "12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23",
-    "parent_digest": null,
-    "config_digest": "12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23",
+    "config_digest": "sha256:12372b2d7bc16952cc4fbff7534ea9888292f51179c6645b286667d2310bfe23",
+    "tags": [
+      "you/secondimage:1.2"
+    ],
+    "distro": null,
     "layers": [
       {
+        "extracted_location": "",
+        "archive_location": "f2323825799160c4c1e508c1189dc285c7928677747b5a679020e9b438843127.tar",
+        "sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "f2323825799160c4c1e508c1189dc285c7928677747b5a679020e9b438843127.tar",
-        "layer_sha256": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
-        "layer_size": 2048,
-        "layer_id": "imagesv11_with_tar_at_root.tar",
+        "layer_id": "c2c0564e4d42ab2b8e947861d9324a6ab087818731e818bc8667c7523507c4f9",
+        "size": 2048,
+        "is_empty_layer": false,
         "author": null,
         "created": "2017-02-01T09:45:44.138970446Z",
         "created_by": null,
-        "comment": "initial import",
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": "initial import"
       },
       {
+        "extracted_location": "",
+        "archive_location": "1466996a39f8d2af81b92fad37ef15c04fb2ca64b43945a00f6f5943913f6b96.tar",
+        "sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
         "docker_version": null,
         "os": null,
+        "os_version": null,
         "architecture": null,
+        "variant": null,
         "labels": [],
-        "layer_location": "1466996a39f8d2af81b92fad37ef15c04fb2ca64b43945a00f6f5943913f6b96.tar",
-        "layer_sha256": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
-        "layer_size": 2048,
-        "layer_id": "imagesv11_with_tar_at_root.tar",
+        "layer_id": "2d3d307589a202188a779ec6d094a39a396d7d48d6fef273b902af4389bcfd62",
+        "size": 2048,
+        "is_empty_layer": false,
         "author": "Me Myself and I.",
         "created": "2017-02-01T09:52:30.86379643Z",
         "created_by": "/bin/sh -c #(nop) ADD file:4cf1ddc0e2f550529d17989d68a7968b07a51044244d4f432e778929d1a83a48 in / ",
-        "comment": null,
-        "is_empty_layer": false,
-        "parent_id": null,
-        "extracted_to_location": null
+        "comment": null
       }
     ],
-    "tags": [
-      "you/secondimage:1.2"
-    ],
     "history": [
       {
         "created": "2017-02-01T09:45:44.138970446Z",
@@ -175,8 +190,6 @@
         "created_by": "/bin/sh -c #(nop)  LABEL Some label=",
         "empty_layer": true
       }
-    ],
-    "distro": null,
-    "extracted_to_location": null
+    ]
   }
 ]
\ No newline at end of file
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 88516b9..4e52b1c 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -27,11 +27,11 @@ def clean_images_data(images):
     Clean an Image.to_dict() for testing
     """
     for i in images:
-        i['base_location'] = None
-        i['extracted_to_location'] = None
+        i['extracted_location'] = None
+        i['archive_location'] = None
         for l in i['layers']:
-            l['layer_location'] = None
-            l['extracted_to_location'] = None
+            l['extracted_location'] = None
+            l['archive_location'] = None
     return images
 
 
@@ -67,7 +67,7 @@ def test_squash_single_layer(self):
             image_path=test_dir,
             extract_directory=target_dir)
 
-        results = sorted([p.replace(target_dir, '') 
+        results = sorted([p.replace(target_dir, '')
             for p in fileutils.resource_iter(target_dir)])
         expected = ['/hello']
         assert expected == results
@@ -78,9 +78,10 @@ def test_squash_multiple_layers(self):
 
         cli._container_inspector_squash(
             image_path=test_dir,
-            extract_directory=target_dir)
+            extract_directory=target_dir,
+        )
 
-        results = sorted([p.replace(target_dir, '') 
+        results = sorted([p.replace(target_dir, '')
             for p in fileutils.resource_iter(target_dir)])
         expected = [
             '/additions',
diff --git a/tests/test_distro.py b/tests/test_distro.py
index 9703fc1..5bfc351 100644
--- a/tests/test_distro.py
+++ b/tests/test_distro.py
@@ -35,12 +35,12 @@ def test_parse_os_release(self):
             result = parse_os_release(test_file)
             check_expected(result, expected, regen=False)
 
-    def test_distro_from_file(self):
+    def test_distro_from_os_release_file(self):
         test_dir = self.get_test_loc('distro/os-release')
 
         for test_file in resource_iter(test_dir, with_dirs=False):
             if test_file.endswith('-expected.json'):
                 continue
             expected = test_file + '-distro-expected.json'
-            result = Distro.from_file(test_file).to_dict()
+            result = Distro.from_os_release_file(test_file).to_dict()
             check_expected(result, expected, regen=False)
diff --git a/tests/test_image.py b/tests/test_image.py
index 63f989f..0af5663 100644
--- a/tests/test_image.py
+++ b/tests/test_image.py
@@ -16,7 +16,7 @@
 from commoncode.testcase import FileBasedTesting
 
 from container_inspector.image import Image
-from container_inspector.image import flatten_images
+from container_inspector.image import flatten_images_data
 
 from utilities import check_expected
 from utilities import clean_image
@@ -26,67 +26,121 @@ class TestImages(FileBasedTesting):
     test_data_dir = path.join(path.dirname(__file__), 'data')
 
     def test_Image(self):
-        Image()
+        try:
+            Image()
+            self.fail('Exception not caught')
+        except TypeError as e:
+            assert str(e) == 'Image.extracted_location is a required argument'
+
+    def test_Image_with_dir(self):
+        test_dir = self.get_temp_dir()
+        Image(extracted_location=test_dir)
 
     def test_Image_get_images_from_tarball(self):
         test_tarball = self.get_test_loc('repos/imagesv11.tar')
         extract_dir = self.get_temp_dir()
-        expected = path.join(self.get_test_loc('repos'), 'imagesv11.tar.expected.json')
-        result = [clean_image(i).to_dict()
-            for i in Image.get_images_from_tarball(test_tarball, extract_dir, force_extract=True)]
+        expected = path.join(
+            self.get_test_loc('repos'),
+            'imagesv11.tar.expected.json',
+        )
+
+        images = Image.get_images_from_tarball(
+            archive_location=test_tarball,
+            extracted_location=extract_dir,
+            verify=False,
+        )
+        result = [clean_image(i).to_dict() for i in images]
+        check_expected(result, expected, regen=False)
+
+    def test_Image_get_images_from_tarball_windows(self):
+        test_tarball = self.get_test_loc('image/windows-mini-image.tar.gz')
+        extract_dir = self.get_temp_dir()
+        expected = path.join(
+            self.get_test_loc('image'),
+            'windows-mini-image.tar.gz.expected.json',
+        )
+
+        image = Image.get_images_from_tarball(
+            archive_location=test_tarball,
+            extracted_location=extract_dir,
+            verify=False,
+        )[0]
+        layer_extracted_location = self.get_temp_dir()
+        image.extract_layers(extracted_location=layer_extracted_location)
+        image.get_and_set_distro()
+        result = clean_image(image).to_dict()
         check_expected(result, expected, regen=False)
 
     def test_Image_get_images_from_dir(self):
         test_tarball = self.get_test_loc('repos/imagesv11.tar')
         test_dir = self.extract_test_tar(test_tarball)
-        expected = path.join(self.get_test_loc('repos'), 'imagesv11.tar.expected.json')
-        result = [clean_image(i).to_dict()
-            for i in Image.get_images_from_dir(test_dir)]
+        expected = path.join(
+            self.get_test_loc('repos'),
+            'imagesv11.tar.expected.json',
+        )
+        images = Image.get_images_from_dir(
+            extracted_location=test_dir,
+            archive_location=test_tarball,
+        )
+        result = [clean_image(i).to_dict() for i in images]
         check_expected(result, expected, regen=False)
 
     def test_Image_get_images_from_dir_from_hello_world(self):
         test_arch = self.get_test_loc('repos/hello-world.tar')
         test_dir = self.extract_test_tar(test_arch)
-        expected = path.join(self.get_test_loc('repos'), 'hello-world.tar.registry.expected.json')
-        result = [clean_image(i).to_dict()
-            for i in Image.get_images_from_dir(test_dir)]
+        expected = path.join(
+            self.get_test_loc('repos'),
+            'hello-world.tar.registry.expected.json',
+        )
+        images = Image.get_images_from_dir(test_dir)
+        result = [clean_image(i).to_dict() for i in images]
         check_expected(result, expected, regen=False)
 
-    def test_Image_get_images_from_dir_then_flatten_images(self):
+    def test_Image_get_images_from_dir_then_flatten_images_data(self):
         test_arch = self.get_test_loc('repos/hello-world.tar')
         test_dir = self.extract_test_tar(test_arch)
-        expected = path.join(self.get_test_loc('repos'), 'hello-world.tar.flatten.expected.json')
+        expected = path.join(
+            self.get_test_loc('repos'),
+            'hello-world.tar.flatten.expected.json',
+        )
         images = [clean_image(i) for i in Image.get_images_from_dir(test_dir)]
-        result = list(flatten_images(images))
+        result = list(flatten_images_data(images))
         check_expected(result, expected, regen=False)
 
     def test_Image_get_images_from_dir_with_direct_at_root_layerid_dot_tar_tarball(self):
         test_arch = self.get_test_loc('repos/imagesv11_with_tar_at_root.tar')
         test_dir = self.extract_test_tar(test_arch)
-        expected = path.join(self.get_test_loc('repos'), 'imagesv11_with_tar_at_root.tar.registry.expected.json')
-        result = [clean_image(i).to_dict()
-            for i in Image.get_images_from_dir(test_dir)]
+        expected = path.join(
+            self.get_test_loc('repos'),
+            'imagesv11_with_tar_at_root.tar.registry.expected.json',
+        )
+        images = Image.get_images_from_dir(test_dir, verify=False)
+        result = [clean_image(i).to_dict() for i in images]
         check_expected(result, expected, regen=False)
 
     def test_Image_get_images_from_dir_with_verify(self):
         test_arch = self.get_test_loc('repos/hello-world.tar')
         test_dir = self.extract_test_tar(test_arch)
-        list(Image.get_images_from_dir(test_dir, verify=True))
+        Image.get_images_from_dir(test_dir, verify=True)
 
     def test_Image_get_images_from_dir_with_anotations(self):
         test_arch = self.get_test_loc('repos/images.tar.gz')
         test_dir = self.extract_test_tar(test_arch)
         expected = path.join(self.get_test_loc('repos'), 'images.tar.gz.expected.json')
-        result = [clean_image(i).to_dict()
-            for i in Image.get_images_from_dir(test_dir)]
+        images = Image.get_images_from_dir(test_dir, verify=False)
+        result = [clean_image(i).to_dict() for i in images]
         check_expected(result, expected, regen=False)
 
     def test_Image_get_images_from_dir_with_verify_fails_if_invalid_checksum(self):
         test_arch = self.get_test_loc('repos/images.tar.gz')
         test_dir = self.extract_test_tar(test_arch)
         try:
-            list(Image.get_images_from_dir(test_dir, verify=True))
+            Image.get_images_from_dir(test_dir, verify=True)
             self.fail('Exception not raised')
-        except AssertionError as e:
-            assert str(e).startswith('Layer SHA256')
- 
\ No newline at end of file
+        except Exception as e:
+            assert str(e).startswith('Layer archive: SHA256:')
+
+    def test_Image_find_format(self):
+        test_arch = self.get_test_loc('image/she-image_from_scratch-1.0.tar')
+        test_dir = self.extract_test_tar(test_arch)
+        assert Image.find_format(test_dir) == 'docker'
diff --git a/tests/test_rootfs.py b/tests/test_rootfs.py
index bc5d713..d3a7a5d 100644
--- a/tests/test_rootfs.py
+++ b/tests/test_rootfs.py
@@ -26,7 +26,7 @@ class TestRootfs(testcase.FileBasedTesting):
 
     def test_rebuild_rootfs_simple(self):
         test_dir = self.extract_test_tar('rootfs/hello-world.tar')
-        img = list(image.Image.get_images_from_dir(test_dir))[0]
+        img = image.Image.get_images_from_dir(test_dir)[0]
         target_dir = self.get_temp_dir()
         rebuild_rootfs(img, target_dir)
         results = sorted([p.replace(target_dir, '')
@@ -36,7 +36,7 @@ def test_rebuild_rootfs_simple(self):
 
     def test_image_squash_simple(self):
         test_dir = self.extract_test_tar('rootfs/hello-world.tar')
-        img = list(image.Image.get_images_from_dir(test_dir))[0]
+        img = image.Image.get_images_from_dir(test_dir)[0]
         target_dir = self.get_temp_dir()
         img.squash(target_dir)
         results = sorted([p.replace(target_dir, '')
@@ -46,7 +46,7 @@ def test_image_squash_simple(self):
 
     def test_rebuild_rootfs_with_delete(self):
         test_dir = self.extract_test_tar('rootfs/she-image_from_scratch-1.0.tar')
-        img = list(image.Image.get_images_from_dir(test_dir))[0]
+        img = image.Image.get_images_from_dir(test_dir)[0]
         target_dir = self.get_temp_dir()
         rebuild_rootfs(img, target_dir)
         results = sorted([p.replace(target_dir, '')
@@ -147,6 +147,34 @@ def test_rebuild_rootfs_multilayers(self):
         ]
         assert expected == results
 
+    def test_rootfs_can_find_whiteouts(self):
+
+        def mock_walker(root):
+            opj = os.path.join
+            return [
+                (root, ['bin', 'usr'], []),
+                (opj(root, 'bin'), [], ['.wh..wh.opq']),
+                (opj(root, 'usr'), ['lib'], ['foo', '.wh.bar']),
+            ]
+
+        results = list(rootfs.find_whiteouts('baz', walker=mock_walker))
+        expected = [
+            ('baz/bin/.wh..wh.opq', 'bin'),
+            ('baz/usr/.wh.bar', 'usr/bar'),
+        ]
+        assert results == expected
+
+    def test_rootfs_can_find_whiteouts_none(self):
+
+        def mock_walker(root):
+            return [
+                (root, ['bin', 'usr'], []),
+                (os.path.join(root, 'usr'), ['lib'], ['foo', 'bar']),
+            ]
+
+        results = list(rootfs.find_whiteouts('baz', walker=mock_walker))
+        assert results == []
+
     def test_rootfs_can_find_root(self):
 
         def mock_walker(root):
@@ -155,7 +183,7 @@ def mock_walker(root):
                 (os.path.join(root, 'usr'), ['lib'], ['foo', 'bar']),
             ]
 
-        assert rootfs.find_root('baz', _walker=mock_walker) == 'baz'
+        assert rootfs.find_root('baz', walker=mock_walker) == 'baz'
 
     def test_rootfs_can_find_no_root(self):
 
@@ -165,7 +193,7 @@ def mock_walker(root):
                 (os.path.join(root, 'usr'), ['lib'], ['foo', 'bar']),
             ]
 
-        assert rootfs.find_root('baz', _walker=mock_walker) is None
+        assert rootfs.find_root('baz', walker=mock_walker) is None
 
     def test_rootfs_can_find_no_root_with_single_match(self):
 
@@ -175,7 +203,7 @@ def mock_walker(root):
                 (os.path.join(root, 'usr'), ['lib'], ['foo', 'bar']),
             ]
 
-        assert rootfs.find_root('baz', _walker=mock_walker) is None
+        assert rootfs.find_root('baz', walker=mock_walker) is None
 
     def test_rootfs_does_respects_max_depth(self):
         test_dir = self.extract_test_tar('rootfs/find_root.tar.gz')
diff --git a/tests/test_utils.py b/tests/test_utils.py
deleted file mode 100644
index c4bbdfd..0000000
--- a/tests/test_utils.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright (c) nexB Inc. and others. All rights reserved.
-# http://nexb.com and https://github.com/nexB/container-inspector/
-#
-# This software is licensed under the Apache License version 2.0.#
-#
-# You may not use this software except in compliance with the License.
-# You may obtain a copy of the License at:
-#     http://apache.org/licenses/LICENSE-2.0
-# Unless required by applicable law or agreed to in writing, software distributed
-# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
-# CONDITIONS OF ANY KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations under the License.
-
-import os
-
-from commoncode.testcase import FileBasedTesting
diff --git a/tests/utilities.py b/tests/utilities.py
index 06d43cf..534197d 100644
--- a/tests/utilities.py
+++ b/tests/utilities.py
@@ -12,7 +12,7 @@
 # specific language governing permissions and limitations under the License.
 
 import json
-from os import path
+import os
 
 
 def check_expected(result, expected, regen=False):
@@ -27,23 +27,16 @@ def check_expected(result, expected, regen=False):
     with open(expected) as ex:
         expected = json.loads(ex.read())
 
-    assert expected == result
+    assert result == expected
 
 
 def clean_image(image):
     """
     Clean `image` data for test purpose
     """
-    image.base_location = ''
+    image.extracted_location = ''
+    image.archive_location = ''
     for layer in image.layers:
-        clean_layer(layer)
-
+        layer.extracted_location = os.path.basename(layer.extracted_location or '')
+        layer.archive_location = os.path.basename(layer.archive_location or '')
     return image
-
-
-def clean_layer(layer):
-    """
-    Clean `layer` data for test purpose
-    """
-    layer.layer_location = path.basename(layer.layer_location)
-    return layer