[
  {
    "path": ".binny.yaml",
    "content": "tools:\n  # we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)\n  - name: binny\n    version:\n      want: v0.9.0\n    method: github-release\n    with:\n      repo: anchore/binny\n\n  # used for linting\n  - name: golangci-lint\n    version:\n      want: v1.64.8\n    method: github-release\n    with:\n      repo: golangci/golangci-lint\n\n  # used for showing the changelog at release\n  - name: glow\n    version:\n      want: v2.1.0\n    method: github-release\n    with:\n      repo: charmbracelet/glow\n\n  # used to release all artifacts\n  - name: goreleaser\n    version:\n      want: v2.8.1\n    method: github-release\n    with:\n      repo: goreleaser/goreleaser\n\n  # used at release to generate the changelog\n  - name: chronicle\n    version:\n      want: v0.8.0\n    method: github-release\n    with:\n      repo: anchore/chronicle\n\n  # used during static analysis for license compliance\n  - name: bouncer\n    version:\n      want: v0.4.0\n    method: github-release\n    with:\n      repo: wagoodman/go-bouncer\n\n  # used for running all local and CI tasks\n  - name: task\n    version:\n      want: v3.42.1\n    method: github-release\n    with:\n      repo: go-task/task\n\n  # used for triggering a release\n  - name: gh\n    version:\n      want: v2.69.0\n    method: github-release\n    with:\n      repo: cli/cli\n"
  },
  {
    "path": ".bouncer.yaml",
    "content": "permit:\n  - BSD.*\n  - MIT.*\n  - Apache.*\n  - MPL.*\n  - ISC\n  - WTFPL\n  - Unlicense\n\nignore-packages:\n  # crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Library\n  - crypto/internal/boring\n\n"
  },
  {
    "path": ".data/.dive-ci",
    "content": "---\nplugins:\n  - plugin1\n\nrules:\n  # If the efficiency is measured below X%, mark as failed. (expressed as a percentage between 0-1)\n  lowestEfficiency: 0.95\n\n  # If the amount of wasted space is at least X or larger than X, mark as failed. (expressed in B, KB, MB, and GB)\n  highestWastedBytes: 20Mb\n\n  # If the amount of wasted space makes up for X% of the image, mark as failed. (fail if the threshold is met or crossed; expressed as a percentage between 0-1)\n  highestUserWastedPercent: 0.5\n\n  plugin1/rule1: error\n"
  },
  {
    "path": ".data/Dockerfile.example",
    "content": "FROM busybox:latest\nADD README.md /somefile.txt\nRUN mkdir -p /root/example/really/nested\nRUN cp /somefile.txt /root/example/somefile1.txt\nRUN chmod 444 /root/example/somefile1.txt\nRUN cp /somefile.txt /root/example/somefile2.txt\nRUN cp /somefile.txt /root/example/somefile3.txt\nRUN mv /root/example/somefile3.txt /root/saved.txt\nRUN cp /root/saved.txt /root/.saved.txt\nRUN rm -rf /root/example/\nADD .scripts/ /root/.data/\nRUN cp /root/saved.txt /tmp/saved.again1.txt\nRUN cp /root/saved.txt /root/.data/saved.again2.txt\nRUN chmod +x /root/saved.txt\nRUN chmod 421 /root\n"
  },
  {
    "path": ".data/Dockerfile.minimal",
    "content": "FROM scratch\nCOPY README.md /README.md\n"
  },
  {
    "path": ".data/Dockerfile.test-image",
    "content": "FROM busybox:latest\nADD README.md /somefile.txt\nRUN mkdir -p /root/example/really/nested\nRUN cp /somefile.txt /root/example/somefile1.txt\nRUN chmod 444 /root/example/somefile1.txt\nRUN cp /somefile.txt /root/example/somefile2.txt\nRUN cp /somefile.txt /root/example/somefile3.txt\nRUN mv /root/example/somefile3.txt /root/saved.txt\nRUN cp /root/saved.txt /root/.saved.txt\nRUN rm -rf /root/example/\nADD .scripts/ /root/.data/\nRUN cp /root/saved.txt /tmp/saved.again1.txt\nRUN cp /root/saved.txt /root/.data/saved.again2.txt\nRUN chmod +x /root/saved.txt\n"
  },
  {
    "path": ".dockerignore",
    "content": "/.git\n/.data\n/.cover\n/dist\n!/dist/dive_linux_amd64\n/ui\n/internal/utils\n/image\n/cmd\n/build\ncoverage.txt\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: ['wagoodman']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Something isn't working as expected\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**What happened**:\n\n**What you expected to happen**:\n\n**How to reproduce it (as minimally and precisely as possible)**:\n\n**Anything else we need to know?**:\n\n**Environment**:\n- OS version\n- Docker version (if applicable)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Got an idea for a new feature? Let us know!\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**What would you like to be added**:\n\n**Why is this needed**:\n\n**Additional context**:\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/actions/bootstrap/action.yaml",
    "content": "name: \"Bootstrap\"\ndescription: \"Bootstrap all tools and dependencies\"\ninputs:\n  go-version:\n    description: \"Go version to install\"\n    required: true\n    default: \"1.24.x\"\n  cache-key-prefix:\n    description: \"Prefix all cache keys with this value\"\n    required: true\n    default: \"efa04b89c1b1\"\n  bootstrap-apt-packages:\n    description: \"Space delimited list of tools to install via apt\"\n    default: \"\"\n\nruns:\n  using: \"composite\"\n  steps:\n    - uses: actions/setup-go@v5\n      with:\n        go-version: ${{ inputs.go-version }}\n\n    - name: Restore tool cache\n      id: tool-cache\n      uses: actions/cache@v4\n      with:\n        path: ${{ github.workspace }}/.tmp\n        key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }}\n\n    - name: (cache-miss) Bootstrap project tools\n      shell: bash\n      if: steps.tool-cache.outputs.cache-hit != 'true'\n      run: make tools\n\n    - name: (cache-miss) Bootstrap go dependencies\n      shell: bash\n      if: steps.go-mod-cache.outputs.cache-hit != 'true'\n      run: go mod download -x\n\n    - name: Install apt packages\n      if: inputs.bootstrap-apt-packages != ''\n      shell: bash\n      run: |\n        DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }}\n"
  },
  {
    "path": ".github/dependabot.yaml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/scripts/coverage.py",
    "content": "#!/usr/bin/env python3\nimport subprocess\nimport sys\nimport shlex\n\n\nclass bcolors:\n    HEADER = '\\033[95m'\n    OKBLUE = '\\033[94m'\n    OKCYAN = '\\033[96m'\n    OKGREEN = '\\033[92m'\n    WARNING = '\\033[93m'\n    FAIL = '\\033[91m'\n    ENDC = '\\033[0m'\n    BOLD = '\\033[1m'\n    UNDERLINE = '\\033[4m'\n\n\nif len(sys.argv) < 3:\n    print(\"Usage: coverage.py [threshold] [go-coverage-report]\")\n    sys.exit(1)\n\n\nthreshold = float(sys.argv[1])\nreport = sys.argv[2]\n\n\nargs = shlex.split(f\"go tool cover -func {report}\")\np = subprocess.run(args, capture_output=True, text=True)\n\npercent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace(\"%\", \"\"))\nprint(f\"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}\")\n\nif percent_coverage < threshold:\n    print(f\"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}\")\n    sys.exit(1)\n"
  },
  {
    "path": ".github/scripts/trigger-release.sh",
    "content": "#!/usr/bin/env bash\nset -eu\n\nbold=$(tput bold)\nnormal=$(tput sgr0)\n\nif ! [ -x \"$(command -v gh)\" ]; then\n    echo \"The GitHub CLI could not be found. To continue follow the instructions at https://github.com/cli/cli#installation\"\n    exit 1\nfi\n\ngh auth status\n\n# we need all of the git state to determine the next version. Since tagging is done by\n# the release pipeline it is possible to not have all of the tags from previous releases.\ngit fetch --tags\n\n# populates the CHANGELOG.md and VERSION files\necho \"${bold}Generating changelog...${normal}\"\nmake changelog 2> /dev/null\n\nNEXT_VERSION=$(cat VERSION)\n\nif [[ \"$NEXT_VERSION\" == \"\" ||  \"${NEXT_VERSION}\" == \"(Unreleased)\" ]]; then\n    echo \"Could not determine the next version to release. Exiting...\"\n    exit 1\nfi\n\nwhile true; do\n    read -p \"${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] \" yn\n    case $yn in\n        [Yy]* ) echo; break;;\n        [Nn]* ) echo; echo \"Cancelling release...\"; exit;;\n        * ) echo \"Please answer yes or no.\";;\n    esac\ndone\n\necho \"${bold}Kicking off release for ${NEXT_VERSION}${normal}...\"\necho\ngh workflow run release.yaml -f version=${NEXT_VERSION}\n\necho\necho \"${bold}Waiting for release to start...${normal}\"\nsleep 10\n\nset +e\n\necho \"${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')\"\nid=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')\ngh run watch $id --exit-status || (echo ; echo \"${bold}Logs of failed step:${normal}\" && GH_PAGER=\"\" gh run view $id --log-failed)\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: \"Release\"\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: tag the latest commit on main with the given version (prefixed with v)\n        required: true\n\njobs:\n  quality-gate:\n    environment: release\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Check if tag already exists\n        # note: this will fail if the tag already exists\n        run: |\n          [[ \"${{ github.event.inputs.version }}\" == v* ]] || (echo \"version '${{ github.event.inputs.version }}' does not have a 'v' prefix\" && exit 1)\n          git tag ${{ github.event.inputs.version }}\n\n      - name: Check static analysis results\n        uses: fountainhead/action-wait-for-check@v1.2.0\n        id: static-analysis\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/validations.yaml)\n          checkName: \"Static analysis\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check unit test results\n        uses: fountainhead/action-wait-for-check@v1.2.0\n        id: unit\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/validations.yaml)\n          checkName: \"Unit tests (ubuntu-latest)\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check acceptance test results (linux)\n        uses: fountainhead/action-wait-for-check@v1.2.0\n        id: acceptance-linux\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/validations.yaml)\n          checkName: \"Acceptance tests (Linux)\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check acceptance test results (mac)\n        uses: fountainhead/action-wait-for-check@v1.2.0\n        id: acceptance-mac\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/validations.yaml)\n          checkName: \"Acceptance tests (Mac)\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check acceptance test results (windows)\n        uses: fountainhead/action-wait-for-check@v1.2.0\n        id: acceptance-windows\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/validations.yaml)\n          checkName: \"Acceptance tests (Windows)\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Quality gate\n        if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' || steps.acceptance-windows.outputs.conclusion != 'success'\n        run: |\n          echo \"Static Analysis Status: ${{ steps.static-analysis.conclusion }}\"\n          echo \"Unit Test Status: ${{ steps.unit.outputs.conclusion }}\"\n          echo \"Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}\"\n          echo \"Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}\"\n          echo \"Acceptance Test (Windows) Status: ${{ steps.acceptance-windows.outputs.conclusion }}\"\n\n          false\n\n  release:\n    needs: [quality-gate]\n    runs-on: ubuntu-latest\n    permissions:\n      # for tagging\n      contents: write\n      # for pushing container images\n      packages: write\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772  #v3.4.0\n        with:\n          registry: docker.io\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772  #v3.4.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Tag release\n        run: |\n          git tag ${{ github.event.inputs.version }}\n          git push origin --tags\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build & publish release artifacts\n        run: make ci-release\n        env:\n          # for creating the release (requires write access to content)\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          # for updating brew formula in wagoodman/homebrew-dive\n          TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}\n\n      - name: Smoke test published image\n        run: make ci-test-docker-image\n"
  },
  {
    "path": ".github/workflows/validations.yaml",
    "content": "name: \"Validations\"\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  Static-Analysis:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Static analysis\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Run static analysis\n        run: make static-analysis\n\n  Unit-Test:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Unit tests\"\n    strategy:\n      matrix:\n        platform:\n          - ubuntu-latest\n    #         - macos-latest # todo: mac runners are expensive minute-wise\n    #         - windows-latest # todo: support windows\n\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Run unit tests\n        run: make unit\n\n  Build-Snapshot-Artifacts:\n    name: \"Build snapshot artifacts\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build snapshot artifacts\n        run: make snapshot\n\n      - run: docker images wagoodman/dive\n\n      # todo: compare against known json output in shared volume\n      - name: Test production image\n        run: make ci-test-docker-image\n\n      # why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).\n      # see https://github.com/actions/upload-artifact/issues/199 for more info\n      - name: Upload snapshot artifacts\n        uses: actions/cache/save@v4\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n      # ... however the cache trick doesn't work on windows :(\n      - uses: actions/upload-artifact@v4\n        with:\n          name: windows-artifacts\n          path: snapshot/dive_windows_amd64_v1/dive.exe\n\n  Acceptance-Linux:\n    name: \"Acceptance tests (Linux)\"\n    needs: [Build-Snapshot-Artifacts]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@master\n\n      - name: Download snapshot build\n        uses: actions/cache/restore@v4\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n      - name: Test linux run\n        run: make ci-test-linux-run\n\n      - name: Test DEB package installation\n        run: make ci-test-deb-package-install\n\n      - name: Test RPM package installation\n        run: make ci-test-rpm-package-install\n\n  Acceptance-Mac:\n    name: \"Acceptance tests (Mac)\"\n    needs: [Build-Snapshot-Artifacts]\n    runs-on: macos-latest\n    steps:\n      - uses: actions/checkout@master\n\n      - name: Download snapshot build\n        uses: actions/cache/restore@v4\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n      - name: Test darwin run\n        run: make ci-test-mac-run\n\n  Acceptance-Windows:\n    name: \"Acceptance tests (Windows)\"\n    needs: [Build-Snapshot-Artifacts]\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@master\n\n      - uses: actions/download-artifact@v4\n        with:\n          name: windows-artifacts\n\n      - name: Test windows run\n        run: make ci-test-windows-run\n"
  },
  {
    "path": ".gitignore",
    "content": "# app configs\n.dive.yaml\n\n# misc\n/.image\n*.log\nCHANGELOG.md\nVERSION\n\n# IDEs\n/.idea\n/.vscode\n\n# tooling\n/bin\n/.tool-versions\n/.tmp\n/.tool\n/.mise.toml\n/.task\n/go.work\n/go.work.sum\n\n# builds\n/dist\n/snapshot\n\n# testing\n.cover\ncoverage.txt\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n/tmp\n/build\n/_vendor*\n/vendor\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "# TODO: enable this when we have coverage on docstring comments\n#issues:\n#  # The list of ids of default excludes to include or disable.\n#  include:\n#    - EXC0002 # disable excluding of issues about comments from golint\n\nlinters-settings:\n  funlen:\n    # Checks the number of lines in a function.\n    # If lower than 0, disable the check.\n    # Default: 60\n    # TODO: drop this down over time...\n    lines: 110\n    # Checks the number of statements in a function.\n    # If lower than 0, disable the check.\n    # Default: 40\n    statements: 60\n\n# TODO: use the default linters for now, but include these over time\n#linters:\n#  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint\n#  disable-all: true\n#  enable:\n#    - asciicheck\n#    - bodyclose\n#    - depguard\n#    - dogsled\n#    - dupl\n#    - errcheck\n#    - exportloopref\n#    - funlen\n#    - gocognit\n#    - goconst\n#    - gocritic\n#    - gocyclo\n#    - gofmt\n#    - goimports\n#    - goprintffuncname\n#    - gosec\n#    - gosimple\n#    - govet\n#    - ineffassign\n#    - misspell\n#    - nakedret\n#    - nolintlint\n#    - revive\n#    - staticcheck\n#    - stylecheck\n#    - typecheck\n#    - unconvert\n#    - unparam\n#    - unused\n#    - whitespace\n\n# do not enable...\n#    - gochecknoglobals\n#    - gochecknoinits    # this is too aggressive\n#    - godot\n#    - godox\n#    - goerr113\n#    - golint       # deprecated\n#    - gomnd        # this is too aggressive\n#    - interfacer   # this is a good idea, but is no longer supported and is prone to false positives\n#    - lll          # without a way to specify per-line exception cases, this is not usable\n#    - maligned     # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations\n#    - nestif\n#    - prealloc     # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code\n#    - scopelint    # deprecated\n#    - testpackage\n#    - wsl          # this doesn't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)\n#    - varcheck     # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter.  Replaced by unused.\n#    - deadcode     # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter.  Replaced by unused.\n#    - structcheck  # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter.  Replaced by unused.\n#    - rowserrcheck # we're not using sql.Rows at all in the codebase\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "version: 2\n\nrelease:\n  # If set to auto, will mark the release as not ready for production in case there is an indicator for this in the\n  # tag e.g. v1.0.0-rc1 .If set to true, will mark the release as not ready for production.\n  prerelease: auto\n\n  # If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing.\n  draft: false\n\nenv:\n  # required to support multi architecture docker builds\n  - DOCKER_CLI_EXPERIMENTAL=enabled\n  - CGO_ENABLED=0\n\nbuilds:\n  - binary: dive\n    dir: ./cmd/dive\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - windows\n      - darwin\n      - linux\n    goarch:\n      - amd64\n      - arm64\n      - ppc64le\n    ldflags:\n      -w\n      -s\n      -extldflags '-static'\n      -X main.version={{.Version}}\n      -X main.gitCommit={{.Commit}}\n      -X main.buildDate={{.Date}}\n      -X main.gitDescription={{.Summary}}\n\nbrews:\n  - repository:\n      owner: wagoodman\n      name: homebrew-dive\n      token: \"{{.Env.TAP_GITHUB_TOKEN}}\"\n    homepage: &project_url \"https://github.com/wagoodman/dive/\"\n    description: &description \"A tool for exploring layers in a docker image\"\n\narchives:\n  - format: tar.gz\n    format_overrides:\n      - goos: windows\n        format: zip\n\nnfpms:\n  - license: MIT\n    maintainer: Alex Goodman\n    homepage: *project_url\n    description: *description\n    formats:\n      - rpm\n      - deb\n\ndockers:\n  # docker.io amd64\n  - &dockerhub_amd64\n    id: docker-amd64\n    ids:\n      - dive\n    use: buildx\n    goarch: amd64\n    image_templates:\n      - docker.io/wagoodman/dive:latest\n      - docker.io/wagoodman/dive:v{{.Version}}-amd64\n    build_flag_templates:\n      - \"--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}\"\n      - \"--platform=linux/amd64\"\n      - \"--label=org.opencontainers.image.created={{.Date}}\"\n      - \"--label=org.opencontainers.image.title={{.ProjectName}}\"\n      - \"--label=org.opencontainers.image.description=A tool for exploring layers in a docker image\"\n      - \"--label=org.opencontainers.image.url={{.GitURL}}\"\n      - \"--label=org.opencontainers.image.source={{.GitURL}}\"\n      - \"--label=org.opencontainers.image.version={{.Version}}\"\n      - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n      - \"--label=org.opencontainers.image.licenses=MIT\"\n      - \"--label=org.opencontainers.image.authors=Alex Goodman <@wagoodman>\"\n\n  # docker.io arm64\n  - &dockerhub_arm64\n    id: docker-arm64\n    ids:\n      - dive\n    use: buildx\n    goarch: arm64\n    image_templates:\n      - docker.io/wagoodman/dive:v{{.Version}}-arm64\n    build_flag_templates:\n      - \"--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}\"\n      - \"--platform=linux/arm64/v8\"\n      - \"--label=org.opencontainers.image.created={{.Date}}\"\n      - \"--label=org.opencontainers.image.title={{.ProjectName}}\"\n      - \"--label=org.opencontainers.image.description=A tool for exploring layers in a docker image\"\n      - \"--label=org.opencontainers.image.url={{.GitURL}}\"\n      - \"--label=org.opencontainers.image.source={{.GitURL}}\"\n      - \"--label=org.opencontainers.image.version={{.Version}}\"\n      - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n      - \"--label=org.opencontainers.image.licenses=MIT\"\n      - \"--label=org.opencontainers.image.authors=Alex Goodman <@wagoodman>\"\n\n  # ghcr.io amd64\n  - id: ghcr-amd64\n    <<: *dockerhub_amd64\n    image_templates:\n      - ghcr.io/wagoodman/dive:v{{.Version}}-amd64\n\n  # ghcr.io arm64\n  - id: ghcr-arm64\n    <<: *dockerhub_arm64\n    image_templates:\n      - ghcr.io/wagoodman/dive:v{{.Version}}-arm64\n\ndocker_manifests:\n  # docker.io manifests\n  - name_template: docker.io/wagoodman/dive:latest\n    image_templates: &dockerhub_images\n      - docker.io/wagoodman/dive:v{{.Version}}-amd64\n      - docker.io/wagoodman/dive:v{{.Version}}-arm64\n\n  - name_template: docker.io/wagoodman/dive:v{{.Major}}\n    image_templates: *dockerhub_images\n\n  - name_template: docker.io/wagoodman/dive:v{{.Major}}.{{.Minor}}\n    image_templates: *dockerhub_images\n\n  - name_template: docker.io/wagoodman/dive:v{{.Version}}\n    image_templates: *dockerhub_images\n\n  # ghcr.io manifests\n  - name_template: ghcr.io/wagoodman/dive:latest\n    image_templates: &ghcr_images\n      - ghcr.io/wagoodman/dive:v{{.Version}}-amd64\n      - ghcr.io/wagoodman/dive:v{{.Version}}-arm64\n\n  - name_template: ghcr.io/wagoodman/dive:v{{.Major}}\n    image_templates: *ghcr_images\n\n  - name_template: ghcr.io/wagoodman/dive:v{{.Major}}.{{.Minor}}\n    image_templates: *ghcr_images\n\n  - name_template: ghcr.io/wagoodman/dive:v{{.Version}}\n    image_templates: *ghcr_images\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:3.21 AS base\n\nARG DOCKER_CLI_VERSION=${DOCKER_CLI_VERSION}\nRUN wget -O- https://download.docker.com/linux/static/stable/$(uname -m)/docker-${DOCKER_CLI_VERSION}.tgz | \\\n    tar -xzf - docker/docker --strip-component=1 -C /usr/local/bin\n\nCOPY dive /usr/local/bin/\n\n# though we could make this a multi-stage image and copy the binary to scratch, this image is small enough\n# and users are expecting to be able to exec into it\nENTRYPOINT [\"/usr/local/bin/dive\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Alex Goodman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "OWNER = wagoodman\nPROJECT = dive\n\nTOOL_DIR = .tool\nBINNY = $(TOOL_DIR)/binny\nTASK = $(TOOL_DIR)/task\n\n.DEFAULT_GOAL := make-default\n\n## Bootstrapping targets #################################\n\n# note: we need to assume that binny and task have not already been installed\n$(BINNY):\n\t@mkdir -p $(TOOL_DIR)\n\t@curl -sSfL https://raw.githubusercontent.com/anchore/binny/main/install.sh | sh -s -- -b $(TOOL_DIR)\n\n# note: we need to assume that binny and task have not already been installed\n.PHONY: task\n$(TASK) task: $(BINNY)\n\t@$(BINNY) install task -q\n\n# this is a bootstrapping catch-all, where if the target doesn't exist, we'll ensure the tools are installed and then try again\n%:\n\t@make --silent $(TASK)\n\t@$(TASK) $@\n\n## Shim targets #################################\n\n.PHONY: make-default\nmake-default: $(TASK)\n\t@# run the default task in the taskfile\n\t@$(TASK)\n\n# for those of us that can't seem to kick the habit of typing `make ...` lets wrap the superior `task` tool\nTASKS := $(shell bash -c \"test -f $(TASK) && $(TASK) -l | grep '^\\* ' | cut -d' ' -f2 | tr -d ':' | tr '\\n' ' '\" ) $(shell bash -c \"test -f $(TASK) && $(TASK) -l | grep 'aliases:' | cut -d ':' -f 3 | tr '\\n' ' ' | tr -d ','\")\n\n.PHONY: $(TASKS)\n$(TASKS): $(TASK)\n\t@$(TASK) $@\n\n## actual targets\n\nci-test-windows-run:\n\tdive.exe --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci\n\nhelp: $(TASK)\n\t@$(TASK) -l\n"
  },
  {
    "path": "README.md",
    "content": "# dive\n[![GitHub release](https://img.shields.io/github/release/wagoodman/dive.svg)](https://github.com/wagoodman/dive/releases/latest)\n[![Validations](https://github.com/wagoodman/dive/actions/workflows/validations.yaml/badge.svg)](https://github.com/wagoodman/dive/actions/workflows/validations.yaml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/wagoodman/dive)](https://goreportcard.com/report/github.com/wagoodman/dive)\n[![License: MIT](https://img.shields.io/badge/License-MIT%202.0-blue.svg)](https://github.com/wagoodman/dive/blob/main/LICENSE)\n[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat)](https://www.paypal.me/wagoodman)\n\n**A tool for exploring a Docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.**\n\n\n![Image](.data/demo.gif)\n\nTo analyze a Docker image simply run dive with an image tag/id/digest:\n```bash\ndive <your-image-tag>\n```\n\nor you can dive with Docker directly:\n```\nalias dive=\"docker run -ti --rm  -v /var/run/docker.sock:/var/run/docker.sock docker.io/wagoodman/dive\"\ndive <your-image-tag>\n\n# for example\ndive nginx:latest\n```\n\nor if you want to build your image then jump straight into analyzing it:\n```bash\ndive build -t <some-tag> .\n```\n\nBuilding on macOS (supporting only the Docker container engine):\n\n```bash\ndocker run --rm -it \\\n      -v /var/run/docker.sock:/var/run/docker.sock \\\n      -v  \"$(pwd)\":\"$(pwd)\" \\\n      -w \"$(pwd)\" \\\n      -v \"$HOME/.dive.yaml\":\"$HOME/.dive.yaml\" \\\n      docker.io/wagoodman/dive:latest build -t <some-tag> .\n```\n\nAdditionally you can run this in your CI pipeline to ensure you're keeping wasted space to a minimum (this skips the UI):\n```\nCI=true dive <your-image>\n```\n\n![Image](.data/demo-ci.png)\n\n**This is beta quality!** *Feel free to submit an issue if you want a new feature or find a bug :)*\n\n## Basic Features\n\n**Show Docker image contents broken down by layer**\n\nAs you select a layer on the left, you are shown the contents of that layer combined with all previous layers on the right. Also, you can fully explore the file tree with the arrow keys.\n\n**Indicate what's changed in each layer**\n\nFiles that have changed, been modified, added, or removed are indicated in the file tree. This can be adjusted to show changes for a specific layer, or aggregated changes up to this layer.\n\n**Estimate \"image efficiency\"**\n\nThe lower left pane shows basic layer info and an experimental metric that will guess how much wasted space your image contains. This might be from duplicating files across layers, moving files across layers, or not fully removing files. Both a percentage \"score\" and total wasted file space is provided.\n\n**Quick build/analysis cycles**\n\nYou can build a Docker image and do an immediate analysis with one command:\n`dive build -t some-tag .`\n\nYou only need to replace your `docker build` command with the same `dive build`\ncommand.\n\n**CI Integration**\n\nAnalyze an image and get a pass/fail result based on the image efficiency and wasted space. Simply set `CI=true` in the environment when invoking any valid dive command.\n\n**Multiple Image Sources and Container Engines Supported**\n\nWith the `--source` option, you can select where to fetch the container image from:\n```bash\ndive <your-image> --source <source>\n```\nor\n```bash\ndive <source>://<your-image>\n```\n\nWith valid `source` options as such:\n- `docker`: Docker engine (the default option)\n- `docker-archive`: A Docker Tar Archive from disk\n- `podman`: Podman engine (linux only)\n\n## Installation\n\n**Ubuntu/Debian**\n\nUsing debs:\n```bash\nDIVE_VERSION=$(curl -sL \"https://api.github.com/repos/wagoodman/dive/releases/latest\" | grep '\"tag_name\":' | sed -E 's/.*\"v([^\"]+)\".*/\\1/')\ncurl -fOL \"https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb\"\nsudo apt install ./dive_${DIVE_VERSION}_linux_amd64.deb\n```\n\nUsing snap:\n```bash\nsudo snap install docker\nsudo snap install dive\nsudo snap connect dive:docker-executables docker:docker-executables\nsudo snap connect dive:docker-daemon docker:docker-daemon\n```\n\n> [!CAUTION]\n> The Snap method is not recommended if you installed Docker via `apt-get`, since it might break your existing Docker daemon.\n> \n> See also: https://github.com/wagoodman/dive/issues/546\n\n\n**RHEL/Centos**\n```bash\nDIVE_VERSION=$(curl -sL \"https://api.github.com/repos/wagoodman/dive/releases/latest\" | grep '\"tag_name\":' | sed -E 's/.*\"v([^\"]+)\".*/\\1/')\ncurl -fOL \"https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.rpm\"\nrpm -i dive_${DIVE_VERSION}_linux_amd64.rpm\n```\n\n**Arch Linux**\n\nAvailable in the [extra repository](https://archlinux.org/packages/extra/x86_64/dive/) and can be installed via [pacman](https://wiki.archlinux.org/title/Pacman):\n\n```bash\npacman -S dive\n```\n\n**Mac**\n\nIf you use [Homebrew](https://brew.sh):\n\n```bash\nbrew install dive\n```\n\nIf you use [MacPorts](https://www.macports.org):\n\n```bash\nsudo port install dive\n```\n\nOr download the latest Darwin build from the [releases page](https://github.com/wagoodman/dive/releases/latest).\n\n**Windows**\n\nIf you use [Chocolatey](https://chocolatey.org)\n\n```powershell\nchoco install dive\n```\n\nIf you use [scoop](https://scoop.sh/)\n\n```powershell\nscoop install main/dive\n```\n\nIf you use [winget](https://learn.microsoft.com/en-gb/windows/package-manager/):\n\n```powershell\nwinget install --id wagoodman.dive\n```\n\nOr download the latest Windows build from the [releases page](https://github.com/wagoodman/dive/releases/latest).\n\n**Go tools**\nRequires Go version 1.10 or higher.\n\n```bash\ngo install github.com/wagoodman/dive@latest\n```\n*Note*: installing in this way you will not see a proper version when running `dive -v`.\n\n**Nix/NixOS**\n\nOn NixOS:\n```bash\nnix-env -iA nixos.dive\n```\nOn non-NixOS (Linux, Mac)\n```bash\nnix-env -iA nixpkgs.dive\n```\n\n**X-CMD**\n\n[x-cmd](https://www.x-cmd.com/) is a **toolbox for Posix Shell**, offering a lightweight package manager built using shell and awk.\n```sh\nx env use dive\n```\n\n**Docker**\n```bash\ndocker pull docker.io/wagoodman/dive\n# or alternatively\ndocker pull ghcr.io/wagoodman/dive\n```\n\nWhen running you'll need to include the Docker socket file:\n```bash\ndocker run --rm -it \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    docker.io/wagoodman/dive:latest <dive arguments...>\n```\n\nDocker for Windows (showing PowerShell compatible line breaks; collapse to a single line for Command Prompt compatibility)\n```bash\ndocker run --rm -it `\n    -v /var/run/docker.sock:/var/run/docker.sock `\n    docker.io/wagoodman/dive:latest <dive arguments...>\n```\n\n**Note:** depending on the version of docker you are running locally you may need to specify the docker API version as an environment variable:\n```bash\n   DOCKER_API_VERSION=1.37 dive ...\n```\nor if you are running with a docker image:\n```bash\ndocker run --rm -it \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -e DOCKER_API_VERSION=1.37 \\\n    docker.io/wagoodman/dive:latest <dive arguments...>\n```\nif you are using an alternative runtime (Colima etc) then you may need to specify the docker host as an environment variable in order to pull local images:\n```bash\n   export DOCKER_HOST=$(docker context inspect -f '{{ .Endpoints.docker.Host }}')\n```\n\n## CI Integration\n\nWhen running dive with the environment variable `CI=true` then the dive UI will be bypassed and will instead analyze your docker image, giving it a pass/fail indication via return code. Currently there are three metrics supported via a `.dive-ci` file that you can put at the root of your repo:\n```\nrules:\n  # If the efficiency is measured below X%, mark as failed.\n  # Expressed as a ratio between 0-1.\n  lowestEfficiency: 0.95\n\n  # If the amount of wasted space is at least X or larger than X, mark as failed.\n  # Expressed in B, KB, MB, and GB.\n  highestWastedBytes: 20MB\n\n  # If the amount of wasted space makes up for X% or more of the image, mark as failed.\n  # Note: the base image layer is NOT included in the total image size.\n  # Expressed as a ratio between 0-1; fails if the threshold is met or crossed.\n  highestUserWastedPercent: 0.20\n```\nYou can override the CI config path with the `--ci-config` option.\n\n## KeyBindings\n\nKey Binding                                | Description\n-------------------------------------------|---------------------------------------------------------\n<kbd>Ctrl + C</kbd> or <kbd>Q</kbd>        | Exit\n<kbd>Tab</kbd>                             | Switch between the layer and filetree views\n<kbd>Ctrl + F</kbd>                        | Filter files\n<kbd>ESC</kbd>                             | Close filter files\n<kbd>PageUp</kbd> or <kbd>U</kbd>          | Scroll up a page\n<kbd>PageDown</kbd> or <kbd>D</kbd>        | Scroll down a page\n<kbd>Up</kbd> or <kbd>K</kbd>              | Move up one line within a page\n<kbd>Down</kbd> or <kbd>J</kbd>            | Move down one line within a page\n<kbd>Ctrl + A</kbd>                        | Layer view: see aggregated image modifications\n<kbd>Ctrl + L</kbd>                        | Layer view: see current layer modifications\n<kbd>Space</kbd>                           | Filetree view: collapse/uncollapse a directory\n<kbd>Ctrl + Space</kbd>                    | Filetree view: collapse/uncollapse all directories\n<kbd>Ctrl + A</kbd>                        | Filetree view: show/hide added files\n<kbd>Ctrl + R</kbd>                        | Filetree view: show/hide removed files\n<kbd>Ctrl + M</kbd>                        | Filetree view: show/hide modified files\n<kbd>Ctrl + U</kbd>                        | Filetree view: show/hide unmodified files\n<kbd>Ctrl + B</kbd>                        | Filetree view: show/hide file attributes\n<kbd>PageUp</kbd> or <kbd>U</kbd>          | Filetree view: scroll up a page\n<kbd>PageDown</kbd> or <kbd>D</kbd>        | Filetree view: scroll down a page\n\n## UI Configuration\n\nNo configuration is necessary, however, you can create a config file and override values:\n```yaml\n# supported options are \"docker\" and \"podman\"\ncontainer-engine: docker\n# continue with analysis even if there are errors parsing the image archive\nignore-errors: false\nlog:\n  enabled: true\n  path: ./dive.log\n  level: info\n\n# Note: you can specify multiple bindings by separating values with a comma.\n# Note: UI hinting is derived from the first binding\nkeybinding:\n  # Global bindings\n  quit: ctrl+c\n  toggle-view: tab\n  filter-files: ctrl+f, ctrl+slash\n  close-filter-files: esc\n  up: up,k\n  down: down,j\n  left: left,h\n  right: right,l\n\n  # Layer view specific bindings\n  compare-all: ctrl+a\n  compare-layer: ctrl+l\n\n  # File view specific bindings\n  toggle-collapse-dir: space\n  toggle-collapse-all-dir: ctrl+space\n  toggle-added-files: ctrl+a\n  toggle-removed-files: ctrl+r\n  toggle-modified-files: ctrl+m\n  toggle-unmodified-files: ctrl+u\n  toggle-filetree-attributes: ctrl+b\n  page-up: pgup,u\n  page-down: pgdn,d\n\ndiff:\n  # You can change the default files shown in the filetree (right pane). All diff types are shown by default.\n  hide:\n    - added\n    - removed\n    - modified\n    - unmodified\n\nfiletree:\n  # The default directory-collapse state\n  collapse-dir: false\n\n  # The percentage of screen width the filetree should take on the screen (must be >0 and <1)\n  pane-width: 0.5\n\n  # Show the file attributes next to the filetree\n  show-attributes: true\n\nlayer:\n  # Enable showing all changes from this layer and every previous layer\n  show-aggregated-changes: false\n\n```\n\ndive will search for configs in the following locations:\n- `$XDG_CONFIG_HOME/dive/*.yaml`\n- `$XDG_CONFIG_DIRS/dive/*.yaml`\n- `~/.config/dive/*.yaml`\n- `~/.dive.yaml`\n\n`.yml` can be used instead of `.yaml` if desired.\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Release process\n\n\n## Creating a release\n\n**Trigger a new release with `make release`**. \n\nAt this point you'll see a preview changelog in the terminal. If you're happy with the \nchangelog, press `y` to continue, otherwise you can abort and adjust the labels on the \nPRs and issues to be included in the release and re-run the release trigger command.\n\n\n## Retracting a release\n\nIf a release is found to be problematic, it can be retracted with the following steps:\n\n- Deleting the GitHub Release\n- Untag the docker images in the `docker.io` registry\n- Revert the brew formula in [`wagoodman/homebrew-dive`](https://github.com/wagoodman/homebrew-dive) to point to the previous release\n- Add a new `retract` entry in the go.mod for the versioned release\n\n**Note**: do not delete release tags from the git repository since there may already be references to the release\nin the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there\nwill be a warning when users try to pull the new release).\n"
  },
  {
    "path": "Taskfile.yaml",
    "content": "\nversion: \"3\"\nvars:\n  OWNER: wagoodman\n  PROJECT: dive\n\n  # static file dirs\n  TOOL_DIR: .tool\n  TMP_DIR: .tmp\n\n  # TOOLS\n  BINNY: \"{{ .TOOL_DIR }}/binny\"\n  CHRONICLE: \"{{ .TOOL_DIR }}/chronicle\"\n  GORELEASER: \"{{ .TOOL_DIR }}/goreleaser\"\n  GOLANGCI_LINT: \"{{ .TOOL_DIR }}/golangci-lint\"\n  TASK: \"{{ .TOOL_DIR }}/task\"\n  BOUNCER: \"{{ .TOOL_DIR }}/bouncer\"\n  GLOW: \"{{ .TOOL_DIR }}/glow\"\n\n  # used for changelog generation\n  CHANGELOG: CHANGELOG.md\n  NEXT_VERSION: VERSION\n\n  # note: the snapshot dir must be a relative path starting with ./\n  SNAPSHOT_DIR: ./snapshot\n  SNAPSHOT_CMD: \"{{ .GORELEASER }} release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign\"\n  BUILD_CMD:    \"{{ .GORELEASER }} build   --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --single-target\"\n  RELEASE_CMD:  \"{{ .GORELEASER }} release --clean --release-notes {{ .CHANGELOG }}\"\n  VERSION:\n    sh: git describe --dirty --always --tags\n\n  # used for acceptance tests\n  TEST_IMAGE: busybox:latest\n  DOCKER_CLI_VERSION: 28.0.0\n\nenv:\n  GNUMAKEFLAGS: '--no-print-directory'\n  DOCKER_CLI_VERSION: \"{{ .DOCKER_CLI_VERSION }}\"\n\ntasks:\n\n  ## High-level tasks #################################\n\n  default:\n    desc: Run all validation tasks\n    aliases:\n      - pr-validations\n      - validations\n    cmds:\n      - task: static-analysis\n      - task: test\n\n  static-analysis:\n    desc: Run all static analysis tasks\n    cmds:\n      - task: check-go-mod-tidy\n      - task: check-licenses\n      - task: lint\n\n  test:\n    desc: Run all levels of test\n    cmds:\n      - task: unit\n      - task: cli\n\n  ## Bootstrap tasks #################################\n\n  binny:\n    internal: true\n    # desc: Get the binny tool\n    generates:\n      - \"{{ .BINNY }}\"\n    status:\n      - \"test -f {{ .BINNY }}\"\n    cmd: \"curl -sSfL https://raw.githubusercontent.com/anchore/binny/main/install.sh | sh -s -- -b .tool\"\n    silent: true\n\n  tools:\n    desc: Install all tools needed for CI and local development\n    deps: [binny]\n    aliases:\n      - bootstrap\n    generates:\n      - \".binny.yaml\"\n      - \"{{ .TOOL_DIR }}/*\"\n    status:\n      - \"{{ .BINNY }} check -v\"\n    cmd: \"{{ .BINNY }} install -v\"\n    silent: true\n\n  update-tools:\n    desc: Update pinned versions of all tools to their latest available versions\n    deps: [binny]\n    generates:\n      - \".binny.yaml\"\n      - \"{{ .TOOL_DIR }}/*\"\n    cmd: \"{{ .BINNY }} update -v\"\n    silent: true\n\n  list-tools:\n    desc: List all tools needed for CI and local development\n    deps: [binny]\n    cmd: \"{{ .BINNY }} list\"\n    silent: true\n\n  list-tool-updates:\n    desc: List all tools that are not up to date relative to the binny config\n    deps: [binny]\n    cmd: \"{{ .BINNY }} list --updates\"\n    silent: true\n\n  tmpdir:\n    silent: true\n    generates:\n      - \"{{ .TMP_DIR }}\"\n    cmd: \"mkdir -p {{ .TMP_DIR }}\"\n\n  ## Static analysis tasks #################################\n\n  format:\n    desc: Auto-format all source code\n    deps: [tools]\n    cmds:\n      - gofmt -w -s .\n      - go mod tidy\n\n  lint-fix:\n    desc: Auto-format all source code + run golangci lint fixers\n    deps: [tools]\n    cmds:\n      - task: format\n      - \"{{ .GOLANGCI_LINT }} run --tests=false --fix\"\n\n  lint:\n    desc: Run gofmt + golangci lint checks\n    vars:\n      BAD_FMT_FILES:\n        sh: gofmt -l -s .\n      BAD_FILE_NAMES:\n        sh: \"find . | grep -e ':' || true\"\n    deps: [tools]\n    cmds:\n      # ensure there are no go fmt differences\n      - cmd: 'test -z \"{{ .BAD_FMT_FILES }}\" || (echo \"files with gofmt issues: [{{ .BAD_FMT_FILES }}]\"; exit 1)'\n        silent: true\n      # ensure there are no files with \":\" in it (a known back case in the go ecosystem)\n      - cmd: 'test -z \"{{ .BAD_FILE_NAMES }}\" || (echo \"files with bad names: [{{ .BAD_FILE_NAMES }}]\"; exit 1)'\n        silent: true\n      # run linting\n      - \"{{ .GOLANGCI_LINT }} run --tests=false\"\n\n  check-licenses:\n    # desc: Ensure transitive dependencies are compliant with the current license policy\n    deps: [tools]\n    cmd: \"{{ .BOUNCER }} check ./...\"\n\n  check-go-mod-tidy:\n    # desc: Ensure go.mod and go.sum are up to date\n    cmds:\n      - cmd: |\n          if ! go mod tidy -diff; then\n            echo \"go.mod and/or go.sum need updates. Please run 'go mod tidy'\"\n            exit 1\n          fi\n    silent: true\n\n\n  ## Testing tasks #################################\n\n  unit:\n    desc: Run unit tests\n    deps:\n      - tmpdir\n    vars:\n      TEST_PKGS:\n        sh: \"go list ./... | grep -v '^github.com/wagoodman/dive/cmd/dive/cli$'  | tr '\\n' ' '\"\n\n      # unit test coverage threshold (in % coverage)\n      COVERAGE_THRESHOLD: 25\n    cmds:\n      - \"go test -coverprofile {{ .TMP_DIR }}/unit-coverage-details.txt {{ .TEST_PKGS }}\"\n      - cmd: \".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt\"\n        silent: true\n\n  cli:\n    desc: Run CLI tests\n    cmds:\n      - \"go test github.com/wagoodman/dive/cmd/dive/cli -v\"\n\n  ## Acceptance tests #################################\n\n  ci-test-linux:\n    cmds:\n      - task: ci-test-linux-run\n      - task: ci-test-docker-image\n      - task: ci-test-deb-package-install\n      - task: ci-test-rpm-package-install\n\n  ci-test-docker-image:\n    desc: Test using the docker image\n    cmds:\n      - |\n        docker run \\\n          --rm \\\n          -t \\\n          --env CLICOLOR_FORCE=true \\\n          -v /var/run/docker.sock:/var/run/docker.sock \\\n          'docker.io/wagoodman/dive:latest' \\\n            '{{ .TEST_IMAGE }}' \\\n            --ci\n\n  ci-test-deb-package-install:\n    desc: Test debian package installation\n    cmds:\n      - |\n        docker run \\\n          --platform linux/amd64 \\\n          -v /var/run/docker.sock:/var/run/docker.sock \\\n          -v /${PWD}:/src \\\n          -w /src \\\n          --env CLICOLOR_FORCE=true \\\n          ubuntu:latest \\\n            /bin/bash -x -c \"\\\n              apt update && \\\n              apt install -y curl && \\\n              curl -L 'https://download.docker.com/linux/static/stable/x86_64/docker-{{ .DOCKER_CLI_VERSION }}.tgz' | \\\n                tar -vxzf - docker/docker --strip-component=1 && \\\n                mv docker /usr/local/bin/ &&\\\n              docker version && \\\n              apt install ./snapshot/dive_*_linux_amd64.deb -y && \\\n              dive --version && \\\n              dive '{{ .TEST_IMAGE }}' --ci \\\n            \"\n\n  ci-test-rpm-package-install:\n    desc: Test RPM package installation\n    cmds:\n      - |\n        docker run \\\n          --platform linux/amd64 \\\n          -v /var/run/docker.sock:/var/run/docker.sock \\\n          -v /${PWD}:/src \\\n          -w /src \\\n          --env CLICOLOR_FORCE=true \\\n          fedora:latest \\\n            /bin/bash -x -c \"\\\n              curl -L 'https://download.docker.com/linux/static/stable/x86_64/docker-{{ .DOCKER_CLI_VERSION }}.tgz' | \\\n                tar -vxzf - docker/docker --strip-component=1 && \\\n                mv docker /usr/local/bin/ &&\\\n              docker version && \\\n              dnf install ./snapshot/dive_*_linux_amd64.rpm -y && \\\n              dive --version && \\\n              dive '{{ .TEST_IMAGE }}' --ci \\\n            \"\n\n  generate-compressed-test-images:\n    desc: Generate compressed test images for testing\n    cmds:\n      - |\n        for alg in uncompressed gzip estargz zstd; do \\\n          for exporter in docker image; do \\\n            docker buildx build \\\n              -f .data/Dockerfile.minimal \\\n              --tag test-dive-${exporter}:${alg} \\\n              --output type=${exporter},force-compression=true,compression=${alg} . ; \\\n          done ; \\\n        done && \\\n        echo 'Exported test data!'\n\n  generate-compressed-test-data:\n    desc: Generate compressed test data for testing\n    cmds:\n      - |\n        for alg in uncompressed gzip estargz zstd; \\\n        do \\\n          docker buildx build \\\n            -f .data/Dockerfile.minimal \\\n            --output type=tar,dest=.data/test-${alg}-image.tar,force-compression=true,compression=${alg} . ; \\\n          docker buildx build \\\n            -f .data/Dockerfile.minimal \\\n            --output type=oci,dest=.data/test-oci-${alg}-image.tar,force-compression=true,compression=${alg} . ; \\\n        done && \\\n        echo 'Exported test data!'\n\n  ci-test-linux-run:\n    desc: Test Linux binary execution (CI only)\n    deps: [ci-check, generate-compressed-test-images]\n    cmds:\n      - |\n        ls -la {{ .SNAPSHOT_DIR }}\n        ls -la {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1\n        chmod 755 {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive && \\\n        {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive '{{ .TEST_IMAGE }}' --ci && \\\n        {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci\n      - |\n        for alg in uncompressed gzip estargz zstd; do \\\n          for exporter in docker image; do \\\n            {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive \"test-dive-${exporter}:${alg}\" --ci ; \\\n          done && \\\n          {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive --source docker-archive .data/test-oci-${alg}-image.tar --ci --ci-config .data/.dive-ci; \\\n        done\n\n  ci-test-mac-run:\n    desc: Test macOS binary execution (CI only)\n    deps: [ci-check]\n    cmds:\n      - |\n        chmod 755 {{ .SNAPSHOT_DIR }}/dive_darwin_amd64_v1/dive && \\\n        {{ .SNAPSHOT_DIR }}/dive_darwin_amd64_v1/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci\n\n\n  ## Build-related targets #################################\n\n  build:\n    desc: Build the project\n    deps: [tools, tmpdir]\n    generates:\n      - \"{{ .PROJECT }}\"\n    cmds:\n      - silent: true\n        cmd: |\n          echo \"dist: {{ .SNAPSHOT_DIR }}\" > {{ .TMP_DIR }}/goreleaser.yaml\n          cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml\n\n      - \"{{ .BUILD_CMD }}\"\n\n  snapshot:\n    desc: Create a snapshot release\n    aliases:\n      - build\n    deps: [tools, tmpdir]\n    sources:\n      - \"**/*.go\"\n      - \".goreleaser.yaml\"\n    method: checksum\n    cmds:\n      - silent: true\n        cmd: |\n          echo \"dist: {{ .SNAPSHOT_DIR }}\" > {{ .TMP_DIR }}/goreleaser.yaml\n          cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml\n\n      - \"{{ .SNAPSHOT_CMD }}\"\n\n  changelog:\n    desc: Generate a changelog\n    deps: [tools]\n    generates:\n      - \"{{ .CHANGELOG }}\"\n      - \"{{ .NEXT_VERSION }}\"\n    cmds:\n      - \"{{ .CHRONICLE }} -vv -n --version-file {{ .NEXT_VERSION }} > {{ .CHANGELOG }}\"\n      - \"{{ .GLOW }} -w 200 {{ .CHANGELOG }}\"\n\n\n  ## Release targets #################################\n\n  release:\n    desc: Create a release\n    interactive: true\n    deps: [tools]\n    cmds:\n      - cmd: .github/scripts/trigger-release.sh\n        silent: true\n\n\n  ## CI-only targets #################################\n\n  ci-check:\n    preconditions:\n      - sh: test -n \"$CI\"\n        msg: \"This step should ONLY be run in CI. Exiting...\"\n    cmds:\n      - echo \"Running in CI environment\"\n    silent: true\n    internal: true\n\n  ci-release:\n    # desc: \"[CI only] Create a release\"\n    deps: [ci-check, tools]\n    cmds:\n      - \"{{ .CHRONICLE }} -vvv > CHANGELOG.md\"\n      - cmd: \"cat CHANGELOG.md\"\n        silent: true\n      - \"{{ .RELEASE_CMD }}\"\n\n\n  ## Cleanup targets #################################\n\n  clean-snapshot:\n    desc: Remove any snapshot builds\n    cmds:\n      - \"rm -rf {{ .SNAPSHOT_DIR }}\"\n      - \"rm -rf {{ .TMP_DIR }}/goreleaser.yaml\"\n"
  },
  {
    "path": "cmd/dive/cli/cli.go",
    "content": "package cli\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/command\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui\"\n\t\"github.com/wagoodman/dive/internal/bus\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\nfunc Application(id clio.Identification) clio.Application {\n\tapp, _ := create(id)\n\treturn app\n}\n\nfunc Command(id clio.Identification) *cobra.Command {\n\t_, cmd := create(id)\n\treturn cmd\n}\n\nfunc create(id clio.Identification) (clio.Application, *cobra.Command) {\n\tclioCfg := clio.NewSetupConfig(id).\n\t\tWithGlobalConfigFlag().   // add persistent -c <path> for reading an application config from\n\t\tWithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config\n\t\tWithConfigInRootHelp().   // --help on the root command renders the full application config in the help text\n\t\tWithUI(ui.None()).\n\t\tWithInitializers(\n\t\t\tfunc(state *clio.State) error {\n\t\t\t\tbus.Set(state.Bus)\n\t\t\t\tlog.Set(state.Logger)\n\n\t\t\t\t//stereoscope.SetBus(state.Bus)\n\t\t\t\t//stereoscope.SetLogger(state.Logger)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\t//WithPostRuns(func(_ *clio.State, _ error) {\n\t//\tstereoscope.Cleanup()\n\t//})\n\n\tapp := clio.New(*clioCfg)\n\n\trootCmd := command.Root(app)\n\n\trootCmd.AddCommand(\n\t\tclio.VersionCommand(id),\n\t\tclio.ConfigCommand(app, nil),\n\t\tcommand.Build(app),\n\t)\n\n\treturn app, rootCmd\n}\n"
  },
  {
    "path": "cmd/dive/cli/cli_build_test.go",
    "content": "package cli\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"regexp\"\n\t\"testing\"\n)\n\nfunc Test_Build_Dockerfile(t *testing.T) {\n\tt.Setenv(\"DIVE_CONFIG\", \"./testdata/image-multi-layer-dockerfile/dive-pass.yaml\")\n\n\tt.Run(\"implicit dockerfile\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"build testdata/image-multi-layer-dockerfile\")\n\t\tstdout := Capture().WithStdout().WithSuppress().Run(t, func() {\n\t\t\trequire.NoError(t, rootCmd.Execute())\n\t\t})\n\t\tsnaps.MatchSnapshot(t, stdout)\n\t})\n\n\tt.Run(\"explicit file flag\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"build testdata/image-multi-layer-dockerfile -f testdata/image-multi-layer-dockerfile/Dockerfile\")\n\t\tstdout := Capture().WithStdout().WithSuppress().Run(t, func() {\n\t\t\trequire.NoError(t, rootCmd.Execute())\n\t\t})\n\t\tsnaps.MatchSnapshot(t, stdout)\n\t})\n}\n\nfunc Test_Build_Containerfile(t *testing.T) {\n\tt.Setenv(\"DIVE_CONFIG\", \"./testdata/image-multi-layer-containerfile/dive-pass.yaml\")\n\n\tt.Run(\"implicit containerfile\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"build testdata/image-multi-layer-containerfile\")\n\t\tstdout := Capture().WithStdout().WithSuppress().Run(t, func() {\n\t\t\trequire.NoError(t, rootCmd.Execute())\n\t\t})\n\t\tsnaps.MatchSnapshot(t, stdout)\n\t})\n\n\tt.Run(\"explicit file flag\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"build testdata/image-multi-layer-containerfile -f testdata/image-multi-layer-containerfile/Containerfile\")\n\t\tstdout := Capture().WithStdout().WithSuppress().Run(t, func() {\n\t\t\trequire.NoError(t, rootCmd.Execute())\n\t\t})\n\t\tsnaps.MatchSnapshot(t, stdout)\n\t})\n}\n\nfunc Test_Build_CI_gate_fail(t *testing.T) {\n\tt.Setenv(\"DIVE_CONFIG\", \"./testdata/image-multi-layer-dockerfile/dive-fail.yaml\")\n\n\trootCmd := getTestCommand(t, \"build testdata/image-multi-layer-dockerfile\")\n\tstdout := Capture().WithStdout().WithSuppress().Run(t, func() {\n\t\t// failing gate should result in a non-zero exit code\n\t\trequire.Error(t, rootCmd.Execute())\n\t})\n\tsnaps.MatchSnapshot(t, stdout)\n\n}\n\nfunc Test_BuildFailure(t *testing.T) {\n\n\tt.Run(\"nonexistent directory\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"build ./path/does/not/exist\")\n\t\tcombined := Capture().WithStdout().WithStderr().Run(t, func() {\n\t\t\trequire.ErrorContains(t, rootCmd.Execute(), \"could not find Containerfile or Dockerfile\")\n\t\t})\n\n\t\tassert.Contains(t, combined, \"Building image\")\n\n\t\tsnaps.MatchSnapshot(t, combined)\n\t})\n\n\tt.Run(\"invalid dockerfile\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"build ./testdata/invalid\")\n\t\tcombined := Capture().WithStdout().WithStderr().WithSuppress().Run(t, func() {\n\n\t\t\trequire.ErrorContains(t, rootCmd.Execute(), \"cannot build image: exit status 1\")\n\t\t})\n\n\t\tassert.Contains(t, combined, \"Building image\")\n\t\t// ensure we're passing through docker feedback\n\t\tassert.Contains(t, combined, \"unknown instruction: INVALID\")\n\n\t\t// replace anything starting with \"docker-desktop://\", like \"docker-desktop://dashboard/build/desktop-linux/desktop-linux/ujdmhgkwo0sqqpopsnum3xakd\"\n\t\tcombined = regexp.MustCompile(\"docker-desktop://[^ ]+\").ReplaceAllString(combined, \"docker-desktop://<redacted>\")\n\n\t\tsnaps.MatchSnapshot(t, combined)\n\t})\n}\n"
  },
  {
    "path": "cmd/dive/cli/cli_ci_test.go",
    "content": "package cli\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"testing\"\n)\n\nfunc Test_CI_DefaultCIConfig(t *testing.T) {\n\t// this lets the test harness to unset any DIVE_CONFIG env var\n\tt.Setenv(\"DIVE_CONFIG\", \"-\")\n\n\trootCmd := getTestCommand(t, repoPath(t, \".data/test-docker-image.tar\")+\" -vv\")\n\tcd(t, \"testdata/default-ci-config\")\n\tcombined := Capture().WithStdout().WithStderr().Run(t, func() {\n\t\t// failing gate should result in a non-zero exit code\n\t\trequire.Error(t, rootCmd.Execute())\n\t})\n\n\tassert.Contains(t, combined, \"lowest-efficiency: \\\"0.96\\\"\", \"missing lowest-efficiency rule\")\n\tassert.Contains(t, combined, \"highest-wasted-bytes: 19Mb\", \"missing highest-wasted-bytes rule\")\n\tassert.Contains(t, combined, \"highest-user-wasted-percent: \\\"0.6\\\"\", \"missing highest-user-wasted-percent rule\")\n\n\tsnaps.MatchSnapshot(t, combined)\n}\n\nfunc Test_CI_Fail(t *testing.T) {\n\tt.Setenv(\"DIVE_CONFIG\", \"./testdata/image-multi-layer-dockerfile/dive-fail.yaml\")\n\n\trootCmd := getTestCommand(t, \"build testdata/image-multi-layer-dockerfile\")\n\tstdout := Capture().WithStdout().WithSuppress().Run(t, func() {\n\t\t// failing gate should result in a non-zero exit code\n\t\trequire.Error(t, rootCmd.Execute())\n\t})\n\tsnaps.MatchSnapshot(t, stdout)\n\n}\n\nfunc Test_CI_LegacyRules(t *testing.T) {\n\tt.Setenv(\"DIVE_CONFIG\", \"./testdata/config/dive-ci-legacy.yaml\")\n\n\trootCmd := getTestCommand(t, \"config --load\")\n\tall := Capture().All().Run(t, func() {\n\t\trequire.NoError(t, rootCmd.Execute())\n\t})\n\n\t// this proves that we can load the legacy rules and map them to the standard rules\n\tassert.Contains(t, all, \"lowest-efficiency: '0.95'\", \"missing lowest-efficiency legacy rule\")\n\tassert.Contains(t, all, \"highest-wasted-bytes: '20MB'\", \"missing highest-wasted-bytes legacy rule\")\n\tassert.Contains(t, all, \"highest-user-wasted-percent: '0.2'\", \"missing highest-user-wasted-percent legacy rule\")\n}\n"
  },
  {
    "path": "cmd/dive/cli/cli_config_test.go",
    "content": "package cli\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"testing\"\n)\n\nfunc Test_Config(t *testing.T) {\n\tt.Setenv(\"DIVE_CONFIG\", \"./testdata/image-multi-layer-dockerfile/dive-pass.yaml\")\n\n\trootCmd := getTestCommand(t, \"config --load\")\n\tall := Capture().All().Run(t, func() {\n\t\trequire.NoError(t, rootCmd.Execute())\n\t})\n\n\tsnaps.MatchSnapshot(t, all)\n}\n"
  },
  {
    "path": "cmd/dive/cli/cli_json_test.go",
    "content": "package cli\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc Test_JsonOutput(t *testing.T) {\n\n\tt.Run(\"json output\", func(t *testing.T) {\n\t\tdest := t.TempDir()\n\t\tfile := filepath.Join(dest, \"output.json\")\n\t\trootCmd := getTestCommand(t, \"busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f --json \"+file)\n\t\tcombined := Capture().WithStdout().WithStderr().Run(t, func() {\n\t\t\trequire.NoError(t, rootCmd.Execute())\n\t\t})\n\n\t\tassert.Contains(t, combined, \"Exporting details\")\n\t\tassert.Contains(t, combined, \"file\")\n\n\t\tcontents, err := os.ReadFile(file)\n\t\trequire.NoError(t, err)\n\n\t\tsnaps.MatchJSON(t, contents)\n\t})\n}\n"
  },
  {
    "path": "cmd/dive/cli/cli_load_test.go",
    "content": "package cli\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n)\n\nfunc Test_LoadImage(t *testing.T) {\n\timage := \"busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f\"\n\tarchive := repoPath(t, \".data/test-docker-image.tar\")\n\n\tt.Run(\"from docker engine\", func(t *testing.T) {\n\t\trunWithCombinedOutput(t, fmt.Sprintf(\"docker://%s\", image))\n\t})\n\n\tt.Run(\"from docker engine (flag)\", func(t *testing.T) {\n\n\t\trunWithCombinedOutput(t, fmt.Sprintf(\"--source docker %s\", image))\n\t})\n\n\tt.Run(\"from podman engine\", func(t *testing.T) {\n\t\tif _, err := exec.LookPath(\"podman\"); err != nil {\n\t\t\tt.Skip(\"podman not installed, skipping test\")\n\t\t}\n\t\t// pull the image from podman first\n\t\trequire.NoError(t, exec.Command(\"podman\", \"pull\", image).Run())\n\n\t\trunWithCombinedOutput(t, fmt.Sprintf(\"podman://%s\", image))\n\t})\n\n\tt.Run(\"from podman engine (flag)\", func(t *testing.T) {\n\t\tif _, err := exec.LookPath(\"podman\"); err != nil {\n\t\t\tt.Skip(\"podman not installed, skipping test\")\n\t\t}\n\n\t\t// pull the image from podman first\n\t\trequire.NoError(t, exec.Command(\"podman\", \"pull\", image).Run())\n\n\t\trunWithCombinedOutput(t, fmt.Sprintf(\"--source podman %s\", image))\n\t})\n\n\tt.Run(\"from archive\", func(t *testing.T) {\n\t\trunWithCombinedOutput(t, fmt.Sprintf(\"docker-archive://%s\", archive))\n\t})\n\n\tt.Run(\"from archive (flag)\", func(t *testing.T) {\n\t\trunWithCombinedOutput(t, fmt.Sprintf(\"--source docker-archive %s\", archive))\n\t})\n}\n\nfunc runWithCombinedOutput(t testing.TB, cmd string) {\n\tt.Helper()\n\trootCmd := getTestCommand(t, cmd)\n\tcombined := Capture().WithStdout().WithStderr().Run(t, func() {\n\t\trequire.NoError(t, rootCmd.Execute())\n\t})\n\n\tassertLoadOutput(t, combined)\n}\n\nfunc assertLoadOutput(t testing.TB, combined string) {\n\tt.Helper()\n\tassert.Contains(t, combined, \"Loading image\")\n\tassert.Contains(t, combined, \"Analyzing image\")\n\tassert.Contains(t, combined, \"Evaluating image\")\n\tsnaps.MatchSnapshot(t, combined)\n}\n\nfunc Test_FetchFailure(t *testing.T) {\n\tt.Run(\"nonexistent image\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"docker:wagoodman/nonexistent/image:tag\")\n\t\tcombined := Capture().WithStdout().WithStderr().Run(t, func() {\n\t\t\trequire.ErrorContains(t, rootCmd.Execute(), \"cannot load image: Error response from daemon: invalid reference format\")\n\t\t})\n\n\t\tassert.Contains(t, combined, \"Loading image\")\n\n\t\tsnaps.MatchSnapshot(t, combined)\n\t})\n\n\tt.Run(\"invalid image name\", func(t *testing.T) {\n\t\trootCmd := getTestCommand(t, \"docker:///wagoodman/invalid:image:format\")\n\t\tcombined := Capture().WithStdout().WithStderr().Run(t, func() {\n\t\t\trequire.ErrorContains(t, rootCmd.Execute(), \"cannot load image: Error response from daemon: invalid reference format\")\n\t\t})\n\n\t\tassert.Contains(t, combined, \"Loading image\")\n\n\t\tsnaps.MatchSnapshot(t, combined)\n\t})\n}\n\nfunc cd(t testing.TB, to string) {\n\tt.Helper()\n\tfrom, err := os.Getwd()\n\trequire.NoError(t, err)\n\trequire.NoError(t, os.Chdir(to))\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, os.Chdir(from))\n\t})\n}\n"
  },
  {
    "path": "cmd/dive/cli/cli_test.go",
    "content": "package cli\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"github.com/anchore/clio\"\n\t\"github.com/charmbracelet/lipgloss\"\n\tsnapsPkg \"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/google/shlex\"\n\t\"github.com/muesli/termenv\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.uber.org/atomic\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nvar (\n\tupdateSnapshot = flag.Bool(\"update\", false, \"update any test snapshots\")\n\tsnaps          *snapsPkg.Config\n\trepoRootCache  atomic.String\n)\n\nfunc TestMain(m *testing.M) {\n\t// flags are not parsed until after test.Main is called...\n\tflag.Parse()\n\n\tos.Unsetenv(\"DIVE_CONFIG\")\n\n\t// disable colors\n\tlipgloss.SetColorProfile(termenv.Ascii)\n\n\tsnaps = snapsPkg.WithConfig(\n\t\tsnapsPkg.Update(*updateSnapshot),\n\t\tsnapsPkg.Dir(\"testdata/snapshots\"),\n\t)\n\n\tv := m.Run()\n\n\tsnapsPkg.Clean(m)\n\n\tos.Exit(v)\n}\n\nfunc TestUpdateSnapshotDisabled(t *testing.T) {\n\trequire.False(t, *updateSnapshot, \"update snapshot flag should be disabled\")\n}\n\nfunc repoPath(t testing.TB, path string) string {\n\tt.Helper()\n\troot := repoRoot(t)\n\treturn filepath.Join(root, path)\n}\n\nfunc repoRoot(t testing.TB) string {\n\tval := repoRootCache.Load()\n\tif val != \"\" {\n\t\treturn val\n\t}\n\tt.Helper()\n\t// use git to find the root of the repo\n\tout, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get repo root: %v\", err)\n\t}\n\tval = strings.TrimSpace(string(out))\n\trepoRootCache.Store(val)\n\treturn val\n}\n\nfunc getTestCommand(t testing.TB, cmd string) *cobra.Command {\n\tswitch os.Getenv(\"DIVE_CONFIG\") {\n\tcase \"\":\n\t\tt.Setenv(\"DIVE_CONFIG\", \"./testdata/dive-enable-ci.yaml\")\n\tcase \"-\":\n\t\tt.Setenv(\"DIVE_CONFIG\", \"\")\n\t}\n\n\t// need basic output to logger for testing...\n\t//l, err := logrus.New(logrus.DefaultConfig())\n\t//require.NoError(t, err)\n\t//log.Set(l)\n\n\t// get the root command\n\tc := Command(clio.Identification{\n\t\tName:    \"dive\",\n\t\tVersion: \"testing\",\n\t})\n\n\targs, err := shlex.Split(cmd)\n\trequire.NoError(t, err, \"failed to parse command line %q\", cmd)\n\n\tc.SetArgs(args)\n\n\treturn c\n}\n\ntype capturer struct {\n\tstdout   bool\n\tstderr   bool\n\tsuppress bool\n}\n\nfunc Capture() *capturer {\n\treturn &capturer{}\n}\n\nfunc (c *capturer) WithSuppress() *capturer {\n\tc.suppress = true\n\treturn c\n}\n\nfunc (c *capturer) All() *capturer {\n\tc.stdout = true\n\tc.stderr = true\n\treturn c\n}\n\nfunc (c *capturer) WithStdout() *capturer {\n\tc.stdout = true\n\treturn c\n}\n\nfunc (c *capturer) WithStderr() *capturer {\n\tc.stderr = true\n\treturn c\n}\n\nfunc (c *capturer) Run(t testing.TB, f func()) string {\n\tt.Helper()\n\n\tr, w, err := os.Pipe()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdevNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer devNull.Close()\n\n\toldStdout := os.Stdout\n\toldStderr := os.Stderr\n\n\tif c.stdout {\n\t\tos.Stdout = w\n\t} else if c.suppress {\n\t\tos.Stdout = devNull\n\t}\n\n\tif c.stderr {\n\t\tos.Stderr = w\n\t} else if c.suppress {\n\t\tos.Stderr = devNull\n\t}\n\n\tdefer func() {\n\t\tos.Stdout = oldStdout\n\t\tos.Stderr = oldStderr\n\t}()\n\n\tf()\n\trequire.NoError(t, w.Close())\n\n\tvar buf bytes.Buffer\n\t_, err = io.Copy(&buf, r)\n\trequire.NoError(t, err)\n\n\treturn buf.String()\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/adapter/analyzer.go",
    "content": "package adapter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/bus\"\n\t\"github.com/wagoodman/dive/internal/bus/event/payload\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\ntype Analyzer interface {\n\tAnalyze(ctx context.Context, img *image.Image) (*image.Analysis, error)\n}\n\ntype analysisActionObserver struct {\n\tAnalyzer func(context.Context, *image.Image) (*image.Analysis, error)\n}\n\nfunc NewAnalyzer() Analyzer {\n\treturn analysisActionObserver{\n\t\tAnalyzer: image.Analyze,\n\t}\n}\n\nfunc (a analysisActionObserver) Analyze(ctx context.Context, img *image.Image) (*image.Analysis, error) {\n\tlog.WithFields(\"image\", img.Request).Infof(\"analyzing\")\n\n\tlayers := len(img.Layers)\n\tvar files int\n\tvar fileSize uint64\n\tfor _, layer := range img.Layers {\n\t\tfiles += layer.Tree.Size\n\t\tfileSize += layer.Tree.FileSize\n\t}\n\tfileSizeStr := humanize.Bytes(fileSize)\n\tfilesStr := humanize.Comma(int64(files))\n\n\tlog.Debugf(\"├── layers: %d\", layers)\n\tlog.Debugf(\"├── files: %s\", filesStr)\n\tlog.Debugf(\"└── file size: %s\", fileSizeStr)\n\n\tmon := bus.StartTask(payload.GenericTask{\n\t\tTitle: payload.Title{\n\t\t\tDefault:      \"Analyzing image\",\n\t\t\tWhileRunning: \"Analyzing image\",\n\t\t\tOnSuccess:    \"Analyzed image\",\n\t\t},\n\t\tHideOnSuccess:      false,\n\t\tHideStageOnSuccess: false,\n\t\tID:                 img.Request,\n\t\tContext:            fmt.Sprintf(\"[layers:%d files:%s size:%s]\", layers, filesStr, fileSizeStr),\n\t})\n\n\tanalysis, err := a.Analyzer(ctx, img)\n\tif err != nil {\n\t\tmon.SetError(err)\n\t} else {\n\t\tmon.SetCompleted()\n\t}\n\n\tif err == nil && analysis == nil {\n\t\terr = fmt.Errorf(\"no results returned\")\n\t}\n\n\treturn analysis, err\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/adapter/evaluator.go",
    "content": "package adapter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/command/ci\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/bus\"\n\t\"github.com/wagoodman/dive/internal/bus/event/payload\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\ntype Evaluator interface {\n\tEvaluate(ctx context.Context, analysis *image.Analysis) ci.Evaluation\n}\n\ntype evaluationActionObserver struct {\n\tci.Evaluator\n}\n\nfunc NewEvaluator(rules []ci.Rule) Evaluator {\n\treturn evaluationActionObserver{\n\t\tEvaluator: ci.NewEvaluator(rules),\n\t}\n}\n\nfunc (c evaluationActionObserver) Evaluate(ctx context.Context, analysis *image.Analysis) ci.Evaluation {\n\tlog.WithFields(\"image\", analysis.Image).Infof(\"evaluating image\")\n\tmon := bus.StartTask(payload.GenericTask{\n\t\tTitle: payload.Title{\n\t\t\tDefault:      \"Evaluating image\",\n\t\t\tWhileRunning: \"Evaluating image\",\n\t\t\tOnSuccess:    \"Evaluated image\",\n\t\t},\n\t\tHideOnSuccess:      false,\n\t\tHideStageOnSuccess: false,\n\t\tID:                 analysis.Image,\n\t\tContext:            fmt.Sprintf(\"[rules: %d]\", len(c.Rules)),\n\t})\n\teval := c.Evaluator.Evaluate(ctx, analysis)\n\tif eval.Pass {\n\t\tmon.SetCompleted()\n\t} else {\n\t\tmon.SetError(fmt.Errorf(\"failed evaluation\"))\n\t}\n\tbus.Report(eval.Report)\n\treturn eval\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/adapter/exporter.go",
    "content": "package adapter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/command/export\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/bus\"\n\t\"github.com/wagoodman/dive/internal/bus/event/payload\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"os\"\n)\n\ntype Exporter interface {\n\tExportTo(ctx context.Context, img *image.Analysis, path string) error\n}\n\ntype jsonExporter struct {\n\tfilesystem afero.Fs\n}\n\nfunc NewExporter(fs afero.Fs) Exporter {\n\treturn &jsonExporter{\n\t\tfilesystem: fs,\n\t}\n}\n\nfunc (e *jsonExporter) ExportTo(ctx context.Context, analysis *image.Analysis, path string) error {\n\tlog.WithFields(\"path\", path).Infof(\"exporting analysis\")\n\n\tmon := bus.StartTask(payload.GenericTask{\n\t\tTitle: payload.Title{\n\t\t\tDefault:      \"Exporting details\",\n\t\t\tWhileRunning: \"Exporting details\",\n\t\t\tOnSuccess:    \"Exported details\",\n\t\t},\n\t\tHideOnSuccess:      false,\n\t\tHideStageOnSuccess: false,\n\t\tID:                 analysis.Image,\n\t\tContext:            fmt.Sprintf(\"[file: %s]\", path),\n\t})\n\n\tbytes, err := export.NewExport(analysis).Marshal()\n\tif err != nil {\n\t\tmon.SetError(err)\n\t\treturn fmt.Errorf(\"cannot marshal export payload: %w\", err)\n\t} else {\n\t\tmon.SetCompleted()\n\t}\n\n\tfile, err := e.filesystem.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot open export file: %w\", err)\n\t}\n\tdefer file.Close()\n\n\t_, err = file.Write(bytes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot write to export file: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/adapter/resolver.go",
    "content": "package adapter\n\nimport (\n\t\"context\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/bus\"\n\t\"github.com/wagoodman/dive/internal/bus/event/payload\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype imageActionObserver struct {\n\timage.Resolver\n}\n\nfunc ImageResolver(resolver image.Resolver) image.Resolver {\n\treturn imageActionObserver{\n\t\tResolver: resolver,\n\t}\n}\n\nfunc (i imageActionObserver) Build(ctx context.Context, options []string) (*image.Image, error) {\n\tlog.Info(\"building image\")\n\tlog.Debugf(\"└── %s\", strings.Join(options, \" \"))\n\n\tmon := bus.StartTask(payload.GenericTask{\n\t\tTitle: payload.Title{\n\t\t\tDefault:      \"Building image\",\n\t\t\tWhileRunning: \"Building image\",\n\t\t\tOnSuccess:    \"Built image\",\n\t\t},\n\t\tHideOnSuccess:      false,\n\t\tHideStageOnSuccess: false,\n\t\tContext:            \"... \" + strings.Join(options, \" \"),\n\t})\n\n\tctx = payload.SetGenericProgressToContext(ctx, mon)\n\n\timg, err := i.Resolver.Build(ctx, options)\n\tif err != nil {\n\t\tmon.SetError(err)\n\t} else {\n\t\tmon.SetCompleted()\n\t}\n\treturn img, err\n}\n\nfunc (i imageActionObserver) Fetch(ctx context.Context, id string) (*image.Image, error) {\n\tlog.WithFields(\"image\", id).Info(\"fetching\")\n\tlog.Debugf(\"└── resolver: %s\", i.Resolver.Name())\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tmon := bus.StartTask(payload.GenericTask{\n\t\tTitle: payload.Title{\n\t\t\tDefault:      \"Loading image\",\n\t\t\tWhileRunning: \"Loading image\",\n\t\t\tOnSuccess:    \"Fetched image\",\n\t\t},\n\t\tHideOnSuccess:      false,\n\t\tHideStageOnSuccess: false,\n\t\tID:                 id,\n\t\tContext:            id,\n\t})\n\n\tctx = payload.SetGenericProgressToContext(ctx, mon)\n\n\tgo func() {\n\t\t// in 5 seconds if the context is not cancelled, log the message\n\t\tselect { // nolint:gosimple\n\t\tcase <-time.After(3 * time.Second):\n\t\t\tif ctx.Err() == nil {\n\t\t\t\tbus.Notify(\" • this can take a while for large images...\")\n\t\t\t\tmon.AtomicStage.Set(\"(this can take a while for large images)\")\n\n\t\t\t\t// TODO: default level should be error for this to work when using the UI\n\t\t\t\t//log.Warn(\"this can take a while for large images\")\n\t\t\t}\n\t\t}\n\t}()\n\n\timg, err := i.Resolver.Fetch(ctx, id)\n\tif err != nil {\n\t\tmon.SetError(err)\n\t} else {\n\t\tmon.SetCompleted()\n\t}\n\treturn img, err\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/build.go",
    "content": "package command\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/clio\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/command/adapter\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/options\"\n\t\"github.com/wagoodman/dive/dive\"\n)\n\ntype buildOptions struct {\n\toptions.Application `yaml:\",inline\" mapstructure:\",squash\"`\n\n\t// reserved for future use of build-only flags\n}\n\nfunc Build(app clio.Application) *cobra.Command {\n\topts := &buildOptions{\n\t\tApplication: options.DefaultApplication(),\n\t}\n\treturn app.SetupCommand(&cobra.Command{\n\t\tUse:                \"build [any valid `docker build` arguments]\",\n\t\tShort:              \"Builds and analyzes a docker image from a Dockerfile (this is a thin wrapper for the `docker build` command).\",\n\t\tDisableFlagParsing: true,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := setUI(app, opts.Application); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to set UI: %w\", err)\n\t\t\t}\n\n\t\t\tresolver, err := dive.GetImageResolver(opts.Analysis.Source)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot determine image provider for build: %w\", err)\n\t\t\t}\n\n\t\t\tctx := cmd.Context()\n\n\t\t\timg, err := adapter.ImageResolver(resolver).Build(ctx, args)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot build image: %w\", err)\n\t\t\t}\n\n\t\t\treturn run(cmd.Context(), opts.Application, img, resolver)\n\t\t},\n\t}, opts)\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/ci/evaluator.go",
    "content": "package ci\n\nimport (\n\t\"fmt\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"golang.org/x/net/context\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\ntype Evaluation struct {\n\tReport string\n\tPass   bool\n}\n\ntype Evaluator struct {\n\tRules            []Rule\n\tResults          map[string]RuleResult\n\tTally            ResultTally\n\tPass             bool\n\tMisconfigured    bool\n\tInefficientFiles []ReferenceFile\n\tformat           format\n}\n\ntype format struct {\n\tTitle       lipgloss.Style\n\tSuccess     lipgloss.Style\n\tWarning     lipgloss.Style\n\tDisabled    lipgloss.Style\n\tFailure     lipgloss.Style\n\tTableHeader lipgloss.Style\n\tLabel       lipgloss.Style\n\tAux         lipgloss.Style\n\tValue       lipgloss.Style\n}\n\ntype ResultTally struct {\n\tPass  int\n\tFail  int\n\tSkip  int\n\tWarn  int\n\tTotal int\n}\n\ntype ReferenceFile struct {\n\tReferences int    `json:\"count\"`\n\tSizeBytes  uint64 `json:\"sizeBytes\"`\n\tPath       string `json:\"file\"`\n}\n\nfunc NewEvaluator(rules []Rule) Evaluator {\n\treturn Evaluator{\n\t\tRules:   rules,\n\t\tResults: make(map[string]RuleResult),\n\t\tPass:    true,\n\t\tformat: format{\n\t\t\tTitle:       lipgloss.NewStyle().Bold(true),\n\t\t\tSuccess:     lipgloss.NewStyle().Foreground(lipgloss.Color(\"2\")),\n\t\t\tWarning:     lipgloss.NewStyle().Foreground(lipgloss.Color(\"3\")),\n\t\t\tDisabled:    lipgloss.NewStyle().Faint(true),\n\t\t\tFailure:     lipgloss.NewStyle().Foreground(lipgloss.Color(\"1\")).Bold(true),\n\t\t\tTableHeader: lipgloss.NewStyle().Bold(true),\n\t\t\tLabel:       lipgloss.NewStyle().Width(18),\n\t\t\tAux:         lipgloss.NewStyle().Faint(true),\n\t\t\tValue:       lipgloss.NewStyle(),\n\t\t},\n\t}\n}\n\nfunc (e Evaluator) isRuleEnabled(rule Rule) bool {\n\treturn rule.Configuration() != \"disabled\"\n}\n\nfunc (e Evaluator) Evaluate(ctx context.Context, analysis *image.Analysis) Evaluation {\n\tfor _, rule := range e.Rules {\n\t\tif !e.isRuleEnabled(rule) {\n\t\t\te.Results[rule.Key()] = RuleResult{\n\t\t\t\tstatus:  RuleConfigured,\n\t\t\t\tmessage: \"rule disabled\",\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\te.Results[rule.Key()] = RuleResult{\n\t\t\tstatus:  RuleConfigured,\n\t\t\tmessage: \"test\",\n\t\t}\n\t}\n\n\t// capture inefficient files\n\tfor idx := 0; idx < len(analysis.Inefficiencies); idx++ {\n\t\tfileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]\n\n\t\te.InefficientFiles = append(e.InefficientFiles, ReferenceFile{\n\t\t\tReferences: len(fileData.Nodes),\n\t\t\tSizeBytes:  uint64(fileData.CumulativeSize),\n\t\t\tPath:       fileData.Path,\n\t\t})\n\t}\n\n\t// evaluate results against the configured CI rules\n\tfor _, rule := range e.Rules {\n\t\tif !e.isRuleEnabled(rule) {\n\t\t\te.Results[rule.Key()] = RuleResult{\n\t\t\t\tstatus:  RuleDisabled,\n\t\t\t\tmessage: \"disabled\",\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tstatus, message := rule.Evaluate(analysis)\n\n\t\tif value, exists := e.Results[rule.Key()]; exists && value.status != RuleConfigured && value.status != RuleMisconfigured {\n\t\t\tpanic(fmt.Errorf(\"CI rule result recorded twice: %s\", rule.Key()))\n\t\t}\n\n\t\tif status == RuleFailed {\n\t\t\te.Pass = false\n\t\t}\n\n\t\tif message == \"\" {\n\t\t\tmessage = rule.Configuration()\n\t\t}\n\n\t\te.Results[rule.Key()] = RuleResult{\n\t\t\tstatus:  status,\n\t\t\tmessage: message,\n\t\t}\n\t}\n\n\te.Tally.Total = len(e.Results)\n\tfor rule, result := range e.Results {\n\t\tswitch result.status {\n\t\tcase RulePassed:\n\t\t\te.Tally.Pass++\n\t\tcase RuleFailed:\n\t\t\te.Tally.Fail++\n\t\tcase RuleWarning:\n\t\t\te.Tally.Warn++\n\t\tcase RuleDisabled:\n\t\t\te.Tally.Skip++\n\t\tdefault:\n\t\t\tpanic(fmt.Errorf(\"unknown test status (rule='%v'): %v\", rule, result.status))\n\t\t}\n\t}\n\n\treturn Evaluation{\n\t\tReport: e.report(analysis),\n\t\tPass:   e.Pass,\n\t}\n}\n\nfunc (e Evaluator) report(analysis *image.Analysis) string {\n\tsections := []string{\n\t\te.renderAnalysisSection(analysis),\n\t\te.renderInefficientFilesSection(analysis),\n\t\te.renderEvaluationSection(),\n\t}\n\n\treturn strings.Join(sections, \"\\n\\n\")\n}\n\nfunc (e Evaluator) renderAnalysisSection(analysis *image.Analysis) string {\n\twastedByteStr := \"\"\n\tuserWastedPercent := \"0 %\"\n\n\tif analysis.WastedBytes > 0 {\n\t\twastedByteStr = fmt.Sprintf(\"(%s)\", humanize.Bytes(analysis.WastedBytes))\n\t\tuserWastedPercent = fmt.Sprintf(\"%.2f %%\", analysis.WastedUserPercent*100)\n\t}\n\n\ttitle := e.format.Title.Render(\"Analysis:\")\n\n\trows := []string{\n\t\tformatKeyValue(e.format, \"efficiency\", fmt.Sprintf(\"%.2f %%\", analysis.Efficiency*100)),\n\t\tformatKeyValue(e.format, \"wastedBytes\", fmt.Sprintf(\"%d bytes %s\", analysis.WastedBytes, wastedByteStr)),\n\t\tformatKeyValue(e.format, \"userWastedPercent\", userWastedPercent),\n\t}\n\n\treturn title + \"\\n\" + strings.Join(rows, \"\\n\")\n}\n\nfunc (e Evaluator) renderInefficientFilesSection(analysis *image.Analysis) string {\n\ttitle := e.format.Title.Render(\"Inefficient Files:\")\n\n\tif len(analysis.Inefficiencies) == 0 {\n\t\treturn title + \" (None)\"\n\t}\n\n\theader := e.format.TableHeader.Render(\n\t\tfmt.Sprintf(\"  %-5s  %-12s  %-s\", \"Count\", \"Wasted Space\", \"File Path\"),\n\t)\n\n\trows := []string{header}\n\tfor _, file := range e.InefficientFiles {\n\t\trow := fmt.Sprintf(\"  %-5s  %-12s  %-s\",\n\t\t\tstrconv.Itoa(file.References),\n\t\t\thumanize.Bytes(file.SizeBytes),\n\t\t\tfile.Path,\n\t\t)\n\t\trows = append(rows, row)\n\t}\n\n\treturn title + \"\\n\" + strings.Join(rows, \"\\n\")\n}\n\nfunc (e Evaluator) renderEvaluationSection() string {\n\ttitle := e.format.Title.Render(\"Evaluation:\")\n\n\t// sort rules by name for consistent output\n\trules := make([]string, 0, len(e.Results))\n\tfor name := range e.Results {\n\t\trules = append(rules, name)\n\t}\n\tsort.Strings(rules)\n\n\truleResults := []string{}\n\tfor _, rule := range rules {\n\t\tresult := e.Results[rule]\n\t\truleResult := e.formatRuleResult(rule, result)\n\t\truleResults = append(ruleResults, ruleResult)\n\t}\n\n\tstatus := e.renderStatusSummary()\n\n\treturn title + \"\\n\" + strings.Join(ruleResults, \"\\n\") + \"\\n\\n\" + status\n}\n\nfunc (e Evaluator) formatRuleResult(ruleName string, result RuleResult) string {\n\tvar style lipgloss.Style\n\ttextStyle := lipgloss.NewStyle()\n\tswitch result.status {\n\tcase RulePassed:\n\t\tstyle = e.format.Success\n\tcase RuleFailed:\n\t\tstyle = e.format.Failure\n\tcase RuleWarning, RuleMisconfigured:\n\t\tstyle = e.format.Warning\n\tcase RuleDisabled:\n\t\tstyle = e.format.Disabled\n\t\ttextStyle = e.format.Disabled\n\tdefault:\n\t\tstyle = lipgloss.NewStyle()\n\t}\n\n\tstatusStr := style.Render(result.status.String(e.format))\n\n\tif result.message != \"\" {\n\t\treturn fmt.Sprintf(\"  %s  %s\", statusStr, textStyle.Render(ruleName+\" (\"+result.message+\")\"))\n\t}\n\n\treturn fmt.Sprintf(\"  %s  %s\", statusStr, textStyle.Render(ruleName))\n}\n\nfunc (e Evaluator) renderStatusSummary() string {\n\tif e.Misconfigured {\n\t\treturn e.format.Failure.Render(\"CI Misconfigured\")\n\t}\n\n\tstatus := \"PASS\"\n\tif e.Tally.Fail > 0 {\n\t\tstatus = \"FAIL\"\n\t}\n\n\tparts := []string{}\n\n\ttype tallyItem struct {\n\t\tname  string\n\t\tvalue int\n\t}\n\n\titems := []tallyItem{\n\t\t//{\"total\", e.Tally.Total},\n\t\t{\"pass\", e.Tally.Pass},\n\t\t{\"fail\", e.Tally.Fail},\n\t\t{\"warn\", e.Tally.Warn},\n\t\t{\"skip\", e.Tally.Skip},\n\t}\n\n\tfor _, item := range items {\n\t\tif item.value > 0 {\n\t\t\tparts = append(parts, fmt.Sprintf(\"%s:%d\", item.name, item.value))\n\t\t}\n\t}\n\n\tauxSummary := e.format.Aux.Render(\" [\" + strings.Join(parts, \" \") + \"]\")\n\n\tvar style lipgloss.Style\n\tswitch {\n\tcase e.Pass && e.Tally.Warn == 0:\n\t\tstyle = e.format.Success\n\tcase e.Pass && e.Tally.Warn > 0:\n\t\tstyle = e.format.Warning\n\tdefault:\n\t\tstyle = e.format.Failure\n\t}\n\treturn style.Render(status) + auxSummary\n}\n\nfunc formatKeyValue(f format, key, value string) string {\n\tformattedKey := f.Label.Render(key + \":\")\n\treturn fmt.Sprintf(\"  %s %s\", formattedKey, value)\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/ci/evaluator_test.go",
    "content": "package ci\n\nimport (\n\t\"context\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.uber.org/atomic\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/wagoodman/dive/dive/image/docker\"\n)\n\nvar repoRootCache atomic.String\n\nfunc Test_Evaluator(t *testing.T) {\n\tresult := docker.TestAnalysisFromArchive(t, repoPath(t, \".data/test-docker-image.tar\"))\n\n\tvalidTests := []struct {\n\t\tname           string\n\t\tefficiency     string\n\t\twastedBytes    string\n\t\twastedPercent  string\n\t\texpectedPass   bool\n\t\texpectedResult map[string]RuleStatus\n\t}{\n\t\t{\n\t\t\tname:          \"allFail\",\n\t\t\tefficiency:    \"0.99\",\n\t\t\twastedBytes:   \"1B\",\n\t\t\twastedPercent: \"0.01\",\n\t\t\texpectedPass:  false,\n\t\t\texpectedResult: map[string]RuleStatus{\n\t\t\t\t\"lowestEfficiency\":         RuleFailed,\n\t\t\t\t\"highestWastedBytes\":       RuleFailed,\n\t\t\t\t\"highestUserWastedPercent\": RuleFailed,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"allPass\",\n\t\t\tefficiency:    \"0.9\",\n\t\t\twastedBytes:   \"50kB\",\n\t\t\twastedPercent: \"0.5\",\n\t\t\texpectedPass:  true,\n\t\t\texpectedResult: map[string]RuleStatus{\n\t\t\t\t\"lowestEfficiency\":         RulePassed,\n\t\t\t\t\"highestWastedBytes\":       RulePassed,\n\t\t\t\t\"highestUserWastedPercent\": RulePassed,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"allDisabled\",\n\t\t\tefficiency:    \"disabled\",\n\t\t\twastedBytes:   \"disabled\",\n\t\t\twastedPercent: \"disabled\",\n\t\t\texpectedPass:  true,\n\t\t\texpectedResult: map[string]RuleStatus{\n\t\t\t\t\"lowestEfficiency\":         RuleDisabled,\n\t\t\t\t\"highestWastedBytes\":       RuleDisabled,\n\t\t\t\t\"highestUserWastedPercent\": RuleDisabled,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"mixedResults\",\n\t\t\tefficiency:    \"0.9\",\n\t\t\twastedBytes:   \"1B\",\n\t\t\twastedPercent: \"0.5\",\n\t\t\texpectedPass:  false,\n\t\t\texpectedResult: map[string]RuleStatus{\n\t\t\t\t\"lowestEfficiency\":         RulePassed,\n\t\t\t\t\"highestWastedBytes\":       RuleFailed,\n\t\t\t\t\"highestUserWastedPercent\": RulePassed,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range validTests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t// Create rules - these should not error\n\t\t\trules, err := Rules(test.efficiency, test.wastedBytes, test.wastedPercent)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tevaluator := NewEvaluator(rules)\n\t\t\teval := evaluator.Evaluate(context.TODO(), result)\n\n\t\t\tif test.expectedPass != eval.Pass {\n\t\t\t\tt.Errorf(\"expected pass=%v, got %v\", test.expectedPass, eval.Pass)\n\t\t\t}\n\n\t\t\tif len(test.expectedResult) != len(evaluator.Results) {\n\t\t\t\tt.Errorf(\"expected %v results, got %v\", len(test.expectedResult), len(evaluator.Results))\n\t\t\t}\n\n\t\t\tfor rule, actualResult := range evaluator.Results {\n\t\t\t\texpectedStatus := test.expectedResult[rule]\n\t\t\t\tif expectedStatus != actualResult.status {\n\t\t\t\t\tt.Errorf(\"%v: expected %v rule status, got %v: %v\",\n\t\t\t\t\t\trule, expectedStatus, actualResult.status, actualResult)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n}\n\nfunc Test_Evaluator_Misconfigurations(t *testing.T) {\n\tinvalidTests := []struct {\n\t\tname          string\n\t\tefficiency    string\n\t\twastedBytes   string\n\t\twastedPercent string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:          \"invalid_efficiency_too_high\",\n\t\t\tefficiency:    \"1.1\", // fail!\n\t\t\twastedBytes:   \"50kB\",\n\t\t\twastedPercent: \"0.5\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid_efficiency_too_low\",\n\t\t\tefficiency:    \"-0.1\", // fail!\n\t\t\twastedBytes:   \"50kB\",\n\t\t\twastedPercent: \"0.5\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid_efficiency_format\",\n\t\t\tefficiency:    \"not_a_number\", // fail!\n\t\t\twastedBytes:   \"50kB\",\n\t\t\twastedPercent: \"0.5\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid_wasted_bytes_format\",\n\t\t\tefficiency:    \"0.9\",\n\t\t\twastedBytes:   \"not_a_size\", // fail!\n\t\t\twastedPercent: \"0.5\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid_wasted_percent_high\",\n\t\t\tefficiency:    \"0.9\",\n\t\t\twastedBytes:   \"50kB\",\n\t\t\twastedPercent: \"1.1\", // fail!\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid_wasted_percent_low\",\n\t\t\tefficiency:    \"0.9\",\n\t\t\twastedBytes:   \"50kB\",\n\t\t\twastedPercent: \"-0.1\", // fail!\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid_wasted_percent_format\",\n\t\t\tefficiency:    \"0.9\",\n\t\t\twastedBytes:   \"50kB\",\n\t\t\twastedPercent: \"not_a_number\", // fail!\n\t\t\texpectError:   true,\n\t\t},\n\t}\n\n\tfor _, test := range invalidTests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := Rules(test.efficiency, test.wastedBytes, test.wastedPercent)\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected an error for invalid configuration\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Expected no error for valid configuration\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc repoPath(t testing.TB, path string) string {\n\tt.Helper()\n\troot := repoRoot(t)\n\treturn filepath.Join(root, path)\n}\n\nfunc repoRoot(t testing.TB) string {\n\tval := repoRootCache.Load()\n\tif val != \"\" {\n\t\treturn val\n\t}\n\tt.Helper()\n\t// use git to find the root of the repo\n\tout, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get repo root: %v\", err)\n\t}\n\tval = strings.TrimSpace(string(out))\n\trepoRootCache.Store(val)\n\treturn val\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/ci/rule.go",
    "content": "package ci\n\nimport (\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\nconst (\n\tRuleUnknown = iota\n\tRulePassed\n\tRuleFailed\n\tRuleWarning\n\tRuleDisabled\n\tRuleMisconfigured\n\tRuleConfigured\n)\n\ntype Rule interface {\n\tKey() string\n\tConfiguration() string\n\tEvaluate(result *image.Analysis) (RuleStatus, string)\n}\n\ntype RuleStatus int\n\ntype RuleResult struct {\n\tstatus  RuleStatus\n\tmessage string\n}\n\nfunc (status RuleStatus) String(f format) string {\n\tswitch status {\n\tcase RulePassed:\n\t\treturn f.Success.Render(\"PASS\")\n\tcase RuleFailed:\n\t\treturn f.Failure.Render(\"FAIL\")\n\tcase RuleWarning:\n\t\treturn f.Warning.Render(\"WARN\")\n\tcase RuleDisabled:\n\t\treturn f.Disabled.Render(\"SKIP\")\n\tcase RuleMisconfigured:\n\t\treturn f.Warning.Render(\"MISCONFIGURED\")\n\tcase RuleConfigured:\n\t\treturn \"CONFIGURED   \"\n\tdefault:\n\t\treturn f.Warning.Render(\"Unknown\")\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/ci/rules.go",
    "content": "package ci\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tciKeyLowestEfficiencyThreshold = \"lowestEfficiency\"\n\tciKeyHighestWastedBytes        = \"highestWastedBytes\"\n\tciKeyHighestUserWastedPercent  = \"highestUserWastedPercent\"\n)\n\nfunc Rules(lowerEfficiency, highestWastedBytes, highestUserWastedPercent string) ([]Rule, error) {\n\tvar rules []Rule\n\tvar errs []error\n\n\tlowestEfficiencyRule, err := NewLowestEfficiencyRule(lowerEfficiency)\n\tif err != nil {\n\t\terrs = append(errs, err)\n\t}\n\trules = append(rules, lowestEfficiencyRule)\n\n\thighestWastedBytesRule, err := NewHighestWastedBytesRule(highestWastedBytes)\n\tif err != nil {\n\t\terrs = append(errs, err)\n\t}\n\trules = append(rules, highestWastedBytesRule)\n\n\thighestUserWastedPercentRule, err := NewHighestUserWastedPercentRule(highestUserWastedPercent)\n\tif err != nil {\n\t\terrs = append(errs, err)\n\t}\n\trules = append(rules, highestUserWastedPercentRule)\n\n\treturn rules, errors.Join(errs...)\n\n}\n\nfunc DisabledRule(key string) Rule {\n\treturn &BaseRule{\n\t\tkey:         key,\n\t\tconfigValue: \"disabled\",\n\t\tevaluator: func(_ *image.Analysis) (RuleStatus, string) {\n\t\t\treturn RuleDisabled, \"rule disabled\"\n\t\t},\n\t}\n}\n\ntype BaseRule struct {\n\tkey         string\n\tconfigValue string\n\tstatus      RuleStatus\n\tevaluator   func(*image.Analysis) (RuleStatus, string)\n}\n\nfunc (rule *BaseRule) Key() string {\n\treturn rule.key\n}\n\nfunc (rule *BaseRule) Configuration() string {\n\treturn rule.configValue\n}\n\nfunc (rule *BaseRule) Evaluate(result *image.Analysis) (RuleStatus, string) {\n\tif rule.status != RuleUnknown {\n\t\treturn rule.status, \"\"\n\t}\n\treturn rule.evaluator(result)\n}\n\n// LowestEfficiencyRule checks if image efficiency is above threshold\ntype LowestEfficiencyRule struct {\n\tBaseRule\n\tthreshold float64\n}\n\n// HighestWastedBytesRule checks if wasted bytes are below threshold\ntype HighestWastedBytesRule struct {\n\tBaseRule\n\tthreshold uint64\n}\n\n// HighestUserWastedPercentRule checks if percentage of wasted bytes is below threshold\ntype HighestUserWastedPercentRule struct {\n\tBaseRule\n\tthreshold float64\n}\n\nfunc NewLowestEfficiencyRule(configValue string) (Rule, error) {\n\tif isRuleDisabled(configValue) {\n\t\treturn DisabledRule(ciKeyLowestEfficiencyThreshold), nil\n\t}\n\n\tthreshold, err := strconv.ParseFloat(configValue, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid %s config value, given %q: %v\",\n\t\t\tciKeyLowestEfficiencyThreshold, configValue, err)\n\t}\n\n\tif threshold < 0 || threshold > 1 {\n\t\treturn nil, fmt.Errorf(\"%s config value is outside allowed range (0-1), given '%f'\",\n\t\t\tciKeyLowestEfficiencyThreshold, threshold)\n\t}\n\n\treturn &LowestEfficiencyRule{\n\t\tBaseRule: BaseRule{\n\t\t\tkey:         ciKeyLowestEfficiencyThreshold,\n\t\t\tconfigValue: configValue,\n\t\t},\n\t\tthreshold: threshold,\n\t}, nil\n}\n\nfunc (r *LowestEfficiencyRule) Evaluate(analysis *image.Analysis) (RuleStatus, string) {\n\tif r.threshold > analysis.Efficiency {\n\t\treturn RuleFailed, fmt.Sprintf(\n\t\t\t\"image efficiency is too low (efficiency=%2.2f < threshold=%v)\",\n\t\t\tanalysis.Efficiency, r.threshold)\n\t}\n\treturn RulePassed, \"\"\n}\n\n// NewHighestWastedBytesRule creates a new rule to check wasted bytes\nfunc NewHighestWastedBytesRule(configValue string) (Rule, error) {\n\tif isRuleDisabled(configValue) {\n\t\treturn DisabledRule(ciKeyHighestWastedBytes), nil\n\t}\n\n\tthreshold, err := humanize.ParseBytes(configValue)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid highestWastedBytes config value, given %q: %v\",\n\t\t\tconfigValue, err)\n\t}\n\n\treturn &HighestWastedBytesRule{\n\t\tBaseRule: BaseRule{\n\t\t\tkey:         ciKeyHighestWastedBytes,\n\t\t\tconfigValue: configValue,\n\t\t},\n\t\tthreshold: threshold,\n\t}, nil\n}\n\nfunc (r *HighestWastedBytesRule) Evaluate(analysis *image.Analysis) (RuleStatus, string) {\n\tif analysis.WastedBytes > r.threshold {\n\t\treturn RuleFailed, fmt.Sprintf(\n\t\t\t\"too many bytes wasted (wasted-bytes=%d > threshold=%v)\",\n\t\t\tanalysis.WastedBytes, r.threshold)\n\t}\n\treturn RulePassed, \"\"\n}\n\n// NewHighestUserWastedPercentRule creates a new rule to check percentage of wasted bytes\nfunc NewHighestUserWastedPercentRule(configValue string) (Rule, error) {\n\tif isRuleDisabled(configValue) {\n\t\treturn DisabledRule(ciKeyHighestUserWastedPercent), nil\n\t}\n\n\tthreshold, err := strconv.ParseFloat(configValue, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid highestUserWastedPercent config value, given %q: %v\",\n\t\t\tconfigValue, err)\n\t}\n\n\tif threshold < 0 || threshold > 1 {\n\t\treturn nil, fmt.Errorf(\"highestUserWastedPercent config value is outside allowed range (0-1), given '%f'\",\n\t\t\tthreshold)\n\t}\n\n\treturn &HighestUserWastedPercentRule{\n\t\tBaseRule: BaseRule{\n\t\t\tkey:         ciKeyHighestUserWastedPercent,\n\t\t\tconfigValue: configValue,\n\t\t},\n\t\tthreshold: threshold,\n\t}, nil\n}\n\nfunc (r *HighestUserWastedPercentRule) Evaluate(analysis *image.Analysis) (RuleStatus, string) {\n\tif analysis.WastedUserPercent > r.threshold {\n\t\treturn RuleFailed, fmt.Sprintf(\n\t\t\t\"too many bytes wasted, relative to the user bytes added (%%-user-wasted-bytes=%2.2f > threshold=%v)\",\n\t\t\tanalysis.WastedUserPercent, r.threshold)\n\t}\n\treturn RulePassed, \"\"\n}\n\nfunc isRuleDisabled(value string) bool {\n\tvalue = strings.TrimSpace(strings.ToLower(value))\n\treturn value == \"\" || value == \"disabled\" || value == \"off\" || value == \"false\"\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/export/export.go",
    "content": "package export\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/wagoodman/dive/dive/filetree\"\n\tdiveImage \"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\ntype Export struct {\n\tLayer []Layer `json:\"layer\"`\n\tImage Image   `json:\"image\"`\n}\n\ntype Layer struct {\n\tIndex     int                 `json:\"index\"`\n\tID        string              `json:\"id\"`\n\tDigestID  string              `json:\"digestId\"`\n\tSizeBytes uint64              `json:\"sizeBytes\"`\n\tCommand   string              `json:\"command\"`\n\tFileList  []filetree.FileInfo `json:\"fileList\"`\n}\n\ntype Image struct {\n\tSizeBytes        uint64          `json:\"sizeBytes\"`\n\tInefficientBytes uint64          `json:\"inefficientBytes\"`\n\tEfficiencyScore  float64         `json:\"efficiencyScore\"`\n\tInefficientFiles []FileReference `json:\"fileReference\"`\n}\n\ntype FileReference struct {\n\tReferences int    `json:\"count\"`\n\tSizeBytes  uint64 `json:\"sizeBytes\"`\n\tPath       string `json:\"file\"`\n}\n\n// NewExport exports the analysis to a JSON\nfunc NewExport(analysis *diveImage.Analysis) *Export {\n\tdata := Export{\n\t\tLayer: make([]Layer, len(analysis.Layers)),\n\t\tImage: Image{\n\t\t\tInefficientFiles: make([]FileReference, len(analysis.Inefficiencies)),\n\t\t\tSizeBytes:        analysis.SizeBytes,\n\t\t\tEfficiencyScore:  analysis.Efficiency,\n\t\t\tInefficientBytes: analysis.WastedBytes,\n\t\t},\n\t}\n\n\t// export layers in order\n\tfor idx, curLayer := range analysis.Layers {\n\t\tlayerFileList := make([]filetree.FileInfo, 0)\n\t\tvisitor := func(node *filetree.FileNode) error {\n\t\t\tlayerFileList = append(layerFileList, node.Data.FileInfo)\n\t\t\treturn nil\n\t\t}\n\t\terr := curLayer.Tree.VisitDepthChildFirst(visitor, nil)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"layer\", curLayer.Id, \"error\", err).Debug(\"unable to propagate layer tree\")\n\t\t}\n\t\tdata.Layer[idx] = Layer{\n\t\t\tIndex:     curLayer.Index,\n\t\t\tID:        curLayer.Id,\n\t\t\tDigestID:  curLayer.Digest,\n\t\t\tSizeBytes: curLayer.Size,\n\t\t\tCommand:   curLayer.Command,\n\t\t\tFileList:  layerFileList,\n\t\t}\n\t}\n\n\t// add file references\n\tfor idx := 0; idx < len(analysis.Inefficiencies); idx++ {\n\t\tfileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]\n\n\t\tdata.Image.InefficientFiles[idx] = FileReference{\n\t\t\tReferences: len(fileData.Nodes),\n\t\t\tSizeBytes:  uint64(fileData.CumulativeSize),\n\t\t\tPath:       fileData.Path,\n\t\t}\n\t}\n\n\treturn &data\n}\n\nfunc (exp *Export) Marshal() ([]byte, error) {\n\treturn json.MarshalIndent(&exp, \"\", \"  \")\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/export/export_test.go",
    "content": "package export\n\nimport (\n\t\"testing\"\n\n\t\"github.com/wagoodman/dive/dive/image/docker\"\n)\n\nfunc Test_Export(t *testing.T) {\n\tresult := docker.TestAnalysisFromArchive(t, repoPath(t, \".data/test-docker-image.tar\"))\n\n\texport := NewExport(result)\n\tpayload, err := export.Marshal()\n\tif err != nil {\n\t\tt.Errorf(\"Test_Export: unable to export analysis: %v\", err)\n\t}\n\n\tsnaps.MatchJSON(t, payload)\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/export/main_test.go",
    "content": "package export\n\nimport (\n\t\"flag\"\n\t\"github.com/charmbracelet/lipgloss\"\n\tsnapsPkg \"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/muesli/termenv\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.uber.org/atomic\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nvar (\n\tupdateSnapshot = flag.Bool(\"update\", false, \"update any test snapshots\")\n\tsnaps          *snapsPkg.Config\n\trepoRootCache  atomic.String\n)\n\nfunc TestMain(m *testing.M) {\n\t// flags are not parsed until after test.Main is called...\n\tflag.Parse()\n\n\tos.Unsetenv(\"DIVE_CONFIG\")\n\n\t// disable colors\n\tlipgloss.SetColorProfile(termenv.Ascii)\n\n\tsnaps = snapsPkg.WithConfig(\n\t\tsnapsPkg.Update(*updateSnapshot),\n\t\tsnapsPkg.Dir(\"testdata/snapshots\"),\n\t)\n\n\tv := m.Run()\n\n\tsnapsPkg.Clean(m)\n\n\tos.Exit(v)\n}\n\nfunc TestUpdateSnapshotDisabled(t *testing.T) {\n\trequire.False(t, *updateSnapshot, \"update snapshot flag should be disabled\")\n}\n\nfunc repoPath(t testing.TB, path string) string {\n\tt.Helper()\n\troot := repoRoot(t)\n\treturn filepath.Join(root, path)\n}\n\nfunc repoRoot(t testing.TB) string {\n\tval := repoRootCache.Load()\n\tif val != \"\" {\n\t\treturn val\n\t}\n\tt.Helper()\n\t// use git to find the root of the repo\n\tout, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get repo root: %v\", err)\n\t}\n\tval = strings.TrimSpace(string(out))\n\trepoRootCache.Store(val)\n\treturn val\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/export/testdata/snapshots/export_test.snap",
    "content": "\n[Test_Export - 1]\n{\n \"image\": {\n  \"efficiencyScore\": 0.9844212134184309,\n  \"fileReference\": [\n   {\n    \"count\": 2,\n    \"file\": \"/root/saved.txt\",\n    \"sizeBytes\": 12810\n   },\n   {\n    \"count\": 2,\n    \"file\": \"/root/example/somefile1.txt\",\n    \"sizeBytes\": 12810\n   },\n   {\n    \"count\": 2,\n    \"file\": \"/root/example/somefile3.txt\",\n    \"sizeBytes\": 6405\n   }\n  ],\n  \"inefficientBytes\": 32025,\n  \"sizeBytes\": 1220598\n },\n \"layer\": [\n  {\n   \"command\": \"#(nop) ADD file:ce026b62356eec3ad1214f92be2c9dc063fe205bd5e600be3492c4dfb17148bd in / \",\n   \"digestId\": \"sha256:23bc2b70b2014dec0ac22f27bb93e9babd08cdd6f1115d0c955b9ff22b382f5a\",\n   \"fileList\": [\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"bin/[\",\n     \"size\": 1075464,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/[[\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/acpid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/add-shell\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/addgroup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/adduser\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/adjtimex\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ar\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/arch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/arp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/arping\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ash\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/awk\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/base64\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/basename\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/beep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/blkdiscard\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/blkid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/blockdev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bootchartd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/brctl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bunzip2\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/busybox\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bzcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bzip2\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cal\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chgrp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chown\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chpasswd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chpst\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chroot\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chrt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chvt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cksum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/clear\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cmp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/comm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/conspy\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cpio\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/crond\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/crontab\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cryptpw\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cttyhack\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cut\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/date\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/deallocvt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/delgroup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/deluser\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/depmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/devmem\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/df\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dhcprelay\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/diff\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dirname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dmesg\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dnsd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dnsdomainname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dos2unix\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dpkg\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dpkg-deb\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/du\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dumpkmap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dumpleases\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/echo\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ed\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/egrep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/eject\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/env\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/envdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/envuidgid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ether-wake\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/expand\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/expr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/factor\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fakeidentd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fallocate\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/false\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fatattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fbset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fbsplash\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fdflush\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fdformat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fdisk\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fgconsole\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fgrep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/find\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/findfs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/flock\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fold\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/free\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/freeramdisk\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsck\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsck.minix\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsfreeze\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fstrim\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsync\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ftpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ftpget\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ftpput\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fuser\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"bin/getconf\",\n     \"size\": 77880,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/getopt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/getty\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/grep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/groups\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/gunzip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/gzip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/halt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hdparm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/head\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hexdump\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hexedit\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hostid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hostname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/httpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hush\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hwclock\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cdetect\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cdump\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cget\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/id\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifconfig\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifdown\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifenslave\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifplugd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/inetd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/init\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/insmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/install\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ionice\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iostat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipaddr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipcalc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipcrm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipcs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iplink\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipneigh\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iproute\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iprule\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iptunnel\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/kbd_mode\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/kill\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/killall\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/killall5\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/klogd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/last\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/less\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/link\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/linux32\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/linux64\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/linuxrc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ln\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/loadfont\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/loadkmap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/logger\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/login\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/logname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/logread\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/losetup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lpq\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lpr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ls\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsof\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lspci\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsscsi\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsusb\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lzcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lzma\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lzop\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/makedevs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/makemime\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/man\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/md5sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mdev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mesg\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/microcom\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkdosfs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mke2fs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfifo\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfs.ext2\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfs.minix\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfs.vfat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mknod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkpasswd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkswap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mktemp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/modinfo\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/modprobe\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/more\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mount\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mountpoint\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mpstat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nameif\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nanddump\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nandwrite\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nbd-client\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/netstat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nice\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nmeter\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nohup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nproc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nsenter\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nslookup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ntpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nuke\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/od\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/openvt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/partprobe\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/passwd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/paste\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/patch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pgrep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pidof\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ping\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ping6\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pipe_progress\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pivot_root\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pkill\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pmap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/popmaildir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/poweroff\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/powertop\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/printenv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/printf\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ps\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pscan\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pstree\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pwd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pwdx\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/raidautorun\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rdate\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rdev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/readahead\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/readlink\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/readprofile\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/realpath\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/reboot\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/reformime\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/remove-shell\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/renice\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/reset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/resize\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/resume\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rmdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rmmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/route\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rpm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rpm2cpio\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rtcwake\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/run-init\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/run-parts\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/runlevel\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/runsv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/runsvdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rx\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/script\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/scriptreplay\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sed\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sendmail\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/seq\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setarch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setconsole\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setfattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setfont\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setkeycodes\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setlogcons\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setpriv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setserial\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setsid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setuidgid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sh\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha1sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha256sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha3sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha512sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/showkey\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/shred\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/shuf\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/slattach\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sleep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/smemcap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/softlimit\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sort\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/split\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ssl_client\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/start-stop-daemon\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/stat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/strings\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/stty\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/su\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sulogin\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/svc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/svlogd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/svok\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/swapoff\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/swapon\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/switch_root\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sync\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sysctl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/syslogd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tac\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tail\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tar\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/taskset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tcpsvd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tee\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/telnet\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/telnetd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/test\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tftp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tftpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/time\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/timeout\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/top\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/touch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/traceroute\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/traceroute6\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/true\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/truncate\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tty\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ttysize\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tunctl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubiattach\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubidetach\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubimkvol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubirename\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubirmvol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubirsvol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubiupdatevol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/udhcpc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/udhcpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/udpsvd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uevent\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/umount\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unexpand\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uniq\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unix2dos\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unlink\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unlzma\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unshare\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unxz\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unzip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uptime\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/users\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/usleep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uudecode\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uuencode\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/vconfig\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/vi\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/vlock\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/volname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/w\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/wall\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/watch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/watchdog\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/wc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/wget\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/which\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/who\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/whoami\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/whois\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xargs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xxd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xz\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xzcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/yes\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/zcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/zcip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"bin\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"dev\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 436,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/group\",\n     \"size\": 307,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/localtime\",\n     \"size\": 127,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-down.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-post-down.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-pre-up.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-up.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/passwd\",\n     \"size\": 340,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 384,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/shadow\",\n     \"size\": 243,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 65534,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"home\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 65534\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2148532735,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"tmp\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 1,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"usr/sbin\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 1\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"usr\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 8,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var/spool/mail\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 8\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var/spool\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var/www\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"28cfe03618aa2e914e81fdd90345245c15f4478e35252c06ca52d238fd3cc694\",\n   \"index\": 0,\n   \"sizeBytes\": 1154361\n  },\n  {\n   \"command\": \"#(nop) ADD file:139c3708fb6261126453e34483abd8bf7b26ed16d952fd976994d68e72d93be2 in /somefile.txt \",\n   \"digestId\": \"sha256:a65b7d7ac139a0e4337bc3c73ce511f937d6140ef61a0108f7d4b8aab8d67274\",\n   \"fileList\": [\n    {\n     \"fileMode\": 436,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"somefile.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"1871059774abe6914075e4a919b778fa1561f577d620ae52438a9635e6241936\",\n   \"index\": 1,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"mkdir -p /root/example/really/nested\",\n   \"digestId\": \"sha256:93e208d471756ffbac88cf9c25feb442007f221d3bd73231e27b747a0a68927c\",\n   \"fileList\": [\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example/really/nested\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example/really\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"49fe2a475548bfa4d493fc796fce41f30704e3d4cbff3e45dd3e06f463236d1d\",\n   \"index\": 2,\n   \"sizeBytes\": 0\n  },\n  {\n   \"command\": \"cp /somefile.txt /root/example/somefile1.txt\",\n   \"digestId\": \"sha256:4abad3abe3cb99ad7a492a9d9f6b3d66287c1646843c74128bbbec4f7be5aa9e\",\n   \"fileList\": [\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/example/somefile1.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"80cd2ca1ffc89962b9349c80280c2bc551acbd11e09b16badb0669f8e2369020\",\n   \"index\": 3,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"chmod 444 /root/example/somefile1.txt\",\n   \"digestId\": \"sha256:14c9a6ffcb6a0f32d1035f97373b19608e2d307961d8be156321c3f1c1504cbf\",\n   \"fileList\": [\n    {\n     \"fileMode\": 292,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/example/somefile1.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"c99e2f8d3f6282668f0d30dc1db5e67a51d7a1dcd7ff6ddfa0f90760836778ec\",\n   \"index\": 4,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"cp /somefile.txt /root/example/somefile2.txt\",\n   \"digestId\": \"sha256:778fb5770ef466f314e79cc9dc418eba76bfc0a64491ce7b167b76aa52c736c4\",\n   \"fileList\": [\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/example/somefile2.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"5eca617bdc3bc06134fe957a30da4c57adb7c340a6d749c8edc4c15861c928d7\",\n   \"index\": 5,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"cp /somefile.txt /root/example/somefile3.txt\",\n   \"digestId\": \"sha256:f275b8a31a71deb521cc048e6021e2ff6fa52bedb25c9b7bbe129a0195ddca5f\",\n   \"fileList\": [\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/example/somefile3.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"f07c3eb887572395408f8e11a07af945e4da5f02b3188bb06b93fad713ca0b99\",\n   \"index\": 6,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"mv /root/example/somefile3.txt /root/saved.txt\",\n   \"digestId\": \"sha256:dd1effc5eb19894c3e9b57411c98dd1cf30fa1de4253c7fae53c9cea67267d83\",\n   \"fileList\": [\n    {\n     \"fileMode\": 0,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/example/.wh.somefile3.txt\",\n     \"size\": 0,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/example\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/saved.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"461885fc22589158dee3c5b9f01cc41c87805439f58b4399d733b51aa305cbf9\",\n   \"index\": 7,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"cp /root/saved.txt /root/.saved.txt\",\n   \"digestId\": \"sha256:8d1869a0a066cdd12e48d648222866e77b5e2814f773bb3bd8774ab4052f0f1d\",\n   \"fileList\": [\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/.saved.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"a10327f68ffed4afcba78919052809a8f774978a6b87fc117d39c53c4842f72c\",\n   \"index\": 8,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"rm -rf /root/example/\",\n   \"digestId\": \"sha256:bc2e36423fa31a97223fd421f22c35466220fa160769abf697b8eb58c896b468\",\n   \"fileList\": [\n    {\n     \"fileMode\": 0,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/.wh.example\",\n     \"size\": 0,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"f2fc54e25cb7966dc9732ec671a77a1c5c104e732bd15ad44a2dc1ac42368f84\",\n   \"index\": 9,\n   \"sizeBytes\": 0\n  },\n  {\n   \"command\": \"#(nop) ADD dir:7ec14b81316baa1a31c38c97686a8f030c98cba2035c968412749e33e0c4427e in /root/.data/ \",\n   \"digestId\": \"sha256:7f648d45ee7b6de2292162fba498b66cbaaf181da9004fcceef824c72dbae445\",\n   \"fileList\": [\n    {\n     \"fileMode\": 509,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/.data/tag.sh\",\n     \"size\": 917,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/.data/test.sh\",\n     \"size\": 1270,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/.data\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"aad36d0b05e71c7e6d4dfe0ca9ed6be89e2e0d8995dafe83438299a314e91071\",\n   \"index\": 10,\n   \"sizeBytes\": 2187\n  },\n  {\n   \"command\": \"cp /root/saved.txt /tmp/saved.again1.txt\",\n   \"digestId\": \"sha256:a4b8f95f266d5c063c9a9473c45f2f85ddc183e37941b5e6b6b9d3c00e8e0457\",\n   \"fileList\": [\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"tmp/saved.again1.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2148532735,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"tmp\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"3d4ad907517a021d86a4102d2764ad2161e4818bbd144e41d019bfc955434181\",\n   \"index\": 11,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"cp /root/saved.txt /root/.data/saved.again2.txt\",\n   \"digestId\": \"sha256:22a44d45780a541e593a8862d80f3e14cb80b6bf76aa42ce68dc207a35bf3a4a\",\n   \"fileList\": [\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/.data/saved.again2.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root/.data\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"81b1b002d4b4c1325a9cad9990b5277e7f29f79e0f24582344c0891178f95905\",\n   \"index\": 12,\n   \"sizeBytes\": 6405\n  },\n  {\n   \"command\": \"chmod +x /root/saved.txt\",\n   \"digestId\": \"sha256:ba689cac6a98c92d121fa5c9716a1bab526b8bb1fd6d43625c575b79e97300c5\",\n   \"fileList\": [\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"root/saved.txt\",\n     \"size\": 6405,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"cfb35bb5c127d848739be5ca726057e6e2c77b2849f588e7aebb642c0d3d4b7b\",\n   \"index\": 13,\n   \"sizeBytes\": 6405\n  }\n ]\n}\n---\n"
  },
  {
    "path": "cmd/dive/cli/internal/command/root.go",
    "content": "package command\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/anchore/clio\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/command/adapter\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/options\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui\"\n\t\"github.com/wagoodman/dive/dive\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/bus\"\n\t\"os\"\n)\n\ntype rootOptions struct {\n\toptions.Application `yaml:\",inline\" mapstructure:\",squash\"`\n\n\t// reserved for future use of root-only flags\n}\n\nfunc Root(app clio.Application) *cobra.Command {\n\topts := &rootOptions{\n\t\tApplication: options.DefaultApplication(),\n\t}\n\treturn app.SetupRootCommand(&cobra.Command{\n\t\tUse:   \"dive [IMAGE]\",\n\t\tShort: \"Docker Image Visualizer & Explorer\",\n\t\tLong: `This tool provides a way to discover and explore the contents of a docker image. Additionally the tool estimates\nthe amount of wasted space and identifies the offending files from the image.`,\n\t\tArgs: func(cmd *cobra.Command, args []string) error {\n\t\t\tif len(args) != 1 {\n\t\t\t\treturn fmt.Errorf(\"exactly one argument is required\")\n\t\t\t}\n\t\t\topts.Analysis.Image = args[0]\n\t\t\treturn nil\n\t\t},\n\t\tRunE: func(cmd *cobra.Command, _ []string) error {\n\t\t\tif err := setUI(app, opts.Application); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to set UI: %w\", err)\n\t\t\t}\n\n\t\t\tresolver, err := dive.GetImageResolver(opts.Analysis.Source)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot determine image provider to fetch from: %w\", err)\n\t\t\t}\n\n\t\t\tctx := cmd.Context()\n\n\t\t\timg, err := adapter.ImageResolver(resolver).Fetch(ctx, opts.Analysis.Image)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot load image: %w\", err)\n\t\t\t}\n\n\t\t\treturn run(ctx, opts.Application, img, resolver)\n\t\t},\n\t}, opts)\n}\n\nfunc setUI(app clio.Application, opts options.Application) error {\n\ttype Stater interface {\n\t\tState() *clio.State\n\t}\n\n\tstate := app.(Stater).State()\n\n\tux := ui.NewV1UI(opts.V1Preferences(), os.Stdout, state.Config.Log.Quiet, state.Config.Log.Verbosity)\n\treturn state.UI.Replace(ux)\n}\n\nfunc run(ctx context.Context, opts options.Application, img *image.Image, content image.ContentReader) error {\n\tanalysis, err := adapter.NewAnalyzer().Analyze(ctx, img)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot analyze image: %w\", err)\n\t}\n\n\tif opts.Export.JsonPath != \"\" {\n\t\tif err := adapter.NewExporter(afero.NewOsFs()).ExportTo(ctx, analysis, opts.Export.JsonPath); err != nil {\n\t\t\treturn fmt.Errorf(\"cannot export analysis: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif opts.CI.Enabled {\n\t\teval := adapter.NewEvaluator(opts.CI.Rules.List).Evaluate(ctx, analysis)\n\n\t\tif !eval.Pass {\n\t\t\treturn errors.New(\"evaluation failed\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tbus.ExploreAnalysis(*analysis, content)\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/analysis.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/clio\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/wagoodman/dive/dive\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"strings\"\n)\n\nconst defaultContainerEngine = \"docker\"\n\nvar _ interface {\n\tclio.PostLoader\n\tclio.FieldDescriber\n} = (*Analysis)(nil)\n\n// Analysis provides configuration for the image analysis behavior\ntype Analysis struct {\n\tImage                     string           `yaml:\"image\" mapstructure:\"-\"`\n\tContainerEngine           string           `yaml:\"container-engine\" mapstructure:\"container-engine\"`\n\tSource                    dive.ImageSource `yaml:\"-\" mapstructure:\"-\"`\n\tIgnoreErrors              bool             `yaml:\"ignore-errors\" mapstructure:\"ignore-errors\"`\n\tAvailableContainerEngines []string         `yaml:\"-\" mapstructure:\"-\"`\n}\n\nfunc DefaultAnalysis() Analysis {\n\treturn Analysis{\n\t\tContainerEngine:           defaultContainerEngine,\n\t\tIgnoreErrors:              false,\n\t\tAvailableContainerEngines: dive.ImageSources,\n\t}\n}\n\nfunc (c *Analysis) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&c.ContainerEngine, \"container engine to use for image analysis (supported options: 'docker' and 'podman')\")\n\tdescriptions.Add(&c.IgnoreErrors, \"continue with analysis even if there are errors parsing the image archive\")\n}\n\nfunc (c *Analysis) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&c.ContainerEngine, \"source\", \"\",\n\t\tfmt.Sprintf(\"The container engine to fetch the image from. Allowed values: %s\", strings.Join(c.AvailableContainerEngines, \", \")))\n\n\tflags.BoolVarP(&c.IgnoreErrors, \"ignore-errors\", \"i\", \"ignore image parsing errors and run the analysis anyway\")\n}\n\nfunc (c *Analysis) PostLoad() error {\n\tvalidEngines := strset.New(c.AvailableContainerEngines...)\n\tif !validEngines.Has(c.ContainerEngine) {\n\t\tlog.Warnf(\"invalid container engine: %s (valid options: %s), using default %q\", c.ContainerEngine, strings.Join(c.AvailableContainerEngines, \", \"), defaultContainerEngine)\n\t\tc.ContainerEngine = \"docker\"\n\t}\n\n\tif c.Image != \"\" {\n\t\tsourceType, imageStr := dive.DeriveImageSource(c.Image)\n\n\t\tif sourceType == dive.SourceUnknown {\n\t\t\tsourceType = dive.ParseImageSource(c.ContainerEngine)\n\t\t\tif sourceType == dive.SourceUnknown {\n\t\t\t\treturn fmt.Errorf(\"unable to determine image source from %q: %v\\n\", c.Image, c.ContainerEngine)\n\t\t\t}\n\n\t\t\t// use exactly what the user provided\n\t\t\timageStr = c.Image\n\t\t}\n\n\t\tc.Image = imageStr\n\t\tc.Source = sourceType\n\t} else {\n\t\tc.Source = dive.ParseImageSource(c.ContainerEngine)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/application.go",
    "content": "package options\n\nimport (\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n)\n\ntype Application struct {\n\tAnalysis Analysis `yaml:\",inline\" mapstructure:\",squash\"`\n\tCI       CI       `yaml:\",inline\" mapstructure:\",squash\"`\n\tExport   Export   `yaml:\",inline\" mapstructure:\",squash\"`\n\tUI       UI       `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\nfunc DefaultApplication() Application {\n\treturn Application{\n\t\tAnalysis: DefaultAnalysis(),\n\t\tCI:       DefaultCI(),\n\t\tExport:   DefaultExport(),\n\t\tUI:       DefaultUI(),\n\t}\n}\n\nfunc (c Application) V1Preferences() v1.Preferences {\n\treturn v1.Preferences{\n\t\tKeyBindings:                c.UI.Keybinding.Config,\n\t\tShowFiletreeAttributes:     c.UI.Filetree.ShowAttributes,\n\t\tShowAggregatedLayerChanges: c.UI.Layer.ShowAggregatedChanges,\n\t\tCollapseFiletreeDirectory:  c.UI.Filetree.CollapseDir,\n\t\tFiletreePaneWidth:          c.UI.Filetree.PaneWidth,\n\t\tFiletreeDiffHide:           nil,\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/ci.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/clio\"\n\t\"gopkg.in/yaml.v3\"\n\t\"os\"\n)\n\nvar _ interface {\n\tclio.PostLoader\n\tclio.FieldDescriber\n\tclio.FlagAdder\n} = (*CI)(nil)\n\nconst defaultCIConfigPath = \".dive-ci\"\n\ntype CI struct {\n\tEnabled    bool    `yaml:\"ci\" mapstructure:\"ci\"`\n\tConfigPath string  `yaml:\"ci-config\" mapstructure:\"ci-config\"`\n\tRules      CIRules `yaml:\"rules\" mapstructure:\"rules\"`\n}\n\nfunc DefaultCI() CI {\n\treturn CI{\n\t\tEnabled:    false,\n\t\tConfigPath: defaultCIConfigPath,\n\t\tRules:      DefaultCIRules(),\n\t}\n}\n\nfunc (c *CI) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&c.Enabled, \"enable CI mode\")\n\tdescriptions.Add(&c.ConfigPath, \"path to the CI config file\")\n}\n\nfunc (c *CI) AddFlags(flags clio.FlagSet) {\n\tflags.BoolVarP(&c.Enabled, \"ci\", \"\", \"skip the interactive TUI and validate against CI rules (same as env var CI=true)\")\n\tflags.StringVarP(&c.ConfigPath, \"ci-config\", \"\", \"if CI=true in the environment, use the given yaml to drive validation rules.\")\n}\n\nfunc (c *CI) PostLoad() error {\n\tenabledFromEnv := truthy(os.Getenv(\"CI\"))\n\tif !c.Enabled && enabledFromEnv {\n\t\tc.Enabled = true\n\t}\n\n\tif c.ConfigPath != \"\" {\n\t\tif fileExists(c.ConfigPath) {\n\t\t\t// if a config file is provided, load it and override any values provided in the application config.\n\t\t\t// If we're hitting this case we should pretend that only the config file was provided and applied\n\t\t\t// on top of the default config values.\n\t\t\tyamlFile, err := os.ReadFile(c.ConfigPath)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to read CI config file %s: %w\", c.ConfigPath, err)\n\t\t\t}\n\t\t\tdef := DefaultCIRules()\n\t\t\tr := legacyRuleFile{\n\t\t\t\tLowestEfficiencyThresholdString: def.LowestEfficiencyThresholdString,\n\t\t\t\tHighestWastedBytesString:        def.HighestWastedBytesString,\n\t\t\t\tHighestUserWastedPercentString:  def.HighestUserWastedPercentString,\n\t\t\t}\n\t\t\twrapper := struct {\n\t\t\t\tRules *legacyRuleFile `yaml:\"rules\"`\n\t\t\t}{\n\t\t\t\tRules: &r,\n\t\t\t}\n\t\t\tif err := yaml.Unmarshal(yamlFile, &wrapper); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmarshal CI config file %s: %w\", c.ConfigPath, err)\n\t\t\t}\n\t\t\t// TODO: should this be a deprecated use warning in the future?\n\t\t\tc.Rules = CIRules{\n\t\t\t\tLowestEfficiencyThresholdString: r.LowestEfficiencyThresholdString,\n\t\t\t\tHighestWastedBytesString:        r.HighestWastedBytesString,\n\t\t\t\tHighestUserWastedPercentString:  r.HighestUserWastedPercentString,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype legacyRuleFile struct {\n\tLowestEfficiencyThresholdString string `yaml:\"lowestEfficiency\"`\n\tHighestWastedBytesString        string `yaml:\"highestWastedBytes\"`\n\tHighestUserWastedPercentString  string `yaml:\"highestUserWastedPercent\"`\n}\n\nfunc fileExists(path string) bool {\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc truthy(value string) bool {\n\tswitch value {\n\tcase \"true\", \"1\", \"yes\":\n\t\treturn true\n\tcase \"false\", \"0\", \"no\":\n\t\treturn false\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/ci_rules.go",
    "content": "package options\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/command/ci\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\ntype CIRules struct {\n\tLowestEfficiencyThresholdString       string `yaml:\"lowest-efficiency\" mapstructure:\"lowest-efficiency\"`\n\tLegacyLowestEfficiencyThresholdString string `yaml:\"-\" mapstructure:\"lowestEfficiency\"`\n\n\tHighestWastedBytesString       string `yaml:\"highest-wasted-bytes\" mapstructure:\"highest-wasted-bytes\"`\n\tLegacyHighestWastedBytesString string `yaml:\"-\" mapstructure:\"highestWastedBytes\"`\n\n\tHighestUserWastedPercentString       string `yaml:\"highest-user-wasted-percent\" mapstructure:\"highest-user-wasted-percent\"`\n\tLegacyHighestUserWastedPercentString string `yaml:\"-\" mapstructure:\"highestUserWastedPercent\"`\n\n\tList []ci.Rule `yaml:\"-\" mapstructure:\"-\"`\n}\n\nfunc DefaultCIRules() CIRules {\n\treturn CIRules{\n\t\tLowestEfficiencyThresholdString: \"0.9\",\n\t\tHighestWastedBytesString:        \"disabled\",\n\t\tHighestUserWastedPercentString:  \"0.1\",\n\t}\n}\n\nfunc (c *CIRules) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&c.LowestEfficiencyThresholdString, \"lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail.\")\n\tdescriptions.Add(&c.HighestWastedBytesString, \"highest allowable bytes wasted, otherwise CI validation will fail.\")\n\tdescriptions.Add(&c.HighestUserWastedPercentString, \"highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail.\")\n}\n\nfunc (c *CIRules) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&c.LowestEfficiencyThresholdString, \"lowestEfficiency\", \"\", \"(only valid with --ci given) lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail.\")\n\tflags.StringVarP(&c.HighestWastedBytesString, \"highestWastedBytes\", \"\", \"(only valid with --ci given) highest allowable bytes wasted, otherwise CI validation will fail.\")\n\tflags.StringVarP(&c.HighestUserWastedPercentString, \"highestUserWastedPercent\", \"\", \"(only valid with --ci given) highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail.\")\n}\n\nfunc (c CIRules) hasLegacyOptionsInUse() bool {\n\treturn c.LegacyLowestEfficiencyThresholdString != \"\" || c.LegacyHighestWastedBytesString != \"\" || c.LegacyHighestUserWastedPercentString != \"\"\n}\n\nfunc (c *CIRules) PostLoad() error {\n\t// protect against repeated calls\n\tc.List = nil\n\n\tif c.hasLegacyOptionsInUse() {\n\t\tlog.Warnf(\"please specify ci rules in snake-case (the legacy camelCase format is deprecated)\")\n\t}\n\n\tif c.LegacyLowestEfficiencyThresholdString != \"\" {\n\t\tc.LowestEfficiencyThresholdString = c.LegacyLowestEfficiencyThresholdString\n\t}\n\n\tif c.LegacyHighestWastedBytesString != \"\" {\n\t\tc.HighestWastedBytesString = c.LegacyHighestWastedBytesString\n\t}\n\n\tif c.LegacyHighestUserWastedPercentString != \"\" {\n\t\tc.HighestUserWastedPercentString = c.LegacyHighestUserWastedPercentString\n\t}\n\n\trules, err := ci.Rules(c.LowestEfficiencyThresholdString, c.HighestWastedBytesString, c.HighestUserWastedPercentString)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.List = append(c.List, rules...)\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/export.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\n\t\"github.com/anchore/clio\"\n)\n\nvar _ interface {\n\tclio.FlagAdder\n\tclio.PostLoader\n} = (*Export)(nil)\n\n// Export provides configuration for data export functionality\ntype Export struct {\n\t// Path to export analysis results as JSON (empty string = disabled)\n\tJsonPath string `yaml:\"json-path\" json:\"json-path\" mapstructure:\"json-path\"`\n}\n\nfunc DefaultExport() Export {\n\treturn Export{}\n}\n\nfunc (o *Export) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&o.JsonPath, \"json\", \"j\", \"Skip the interactive TUI and write the layer analysis statistics to a given file.\")\n}\n\nfunc (o *Export) PostLoad() error {\n\n\tif o.JsonPath != \"\" {\n\t\tdir := path.Dir(o.JsonPath)\n\t\tif _, err := os.Stat(dir); os.IsNotExist(err) {\n\t\t\treturn fmt.Errorf(\"directory for JSON export does not exist: %s\", dir)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/ui.go",
    "content": "package options\n\n// UI combines all UI configuration elements\ntype UI struct {\n\tKeybinding UIKeybindings `yaml:\"keybinding\" mapstructure:\"keybinding\"`\n\tDiff       UIDiff        `yaml:\"diff\" mapstructure:\"diff\"`\n\tFiletree   UIFiletree    `yaml:\"filetree\" mapstructure:\"filetree\"`\n\tLayer      UILayers      `yaml:\"layer\" mapstructure:\"layer\"`\n}\n\nfunc DefaultUI() UI {\n\treturn UI{\n\t\tKeybinding: DefaultUIKeybinding(),\n\t\tDiff:       DefaultUIDiff(),\n\t\tFiletree:   DefaultUIFiletree(),\n\t\tLayer:      DefaultUILayers(),\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/ui_diff.go",
    "content": "package options\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\nvar _ interface {\n\tclio.PostLoader\n\tclio.FieldDescriber\n} = (*UIDiff)(nil)\n\n// UIDiff provides configuration for how differences are displayed\ntype UIDiff struct {\n\tHide []string `yaml:\"hide\" mapstructure:\"hide\"`\n}\n\nfunc DefaultUIDiff() UIDiff {\n\tprefs := v1.DefaultPreferences()\n\treturn UIDiff{\n\t\tHide: prefs.FiletreeDiffHide,\n\t}\n}\n\nfunc (c *UIDiff) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&c.Hide, \"types of file differences to hide (added, removed, modified, unmodified)\")\n}\n\nfunc (c *UIDiff) PostLoad() error {\n\tvalidHideValues := map[string]bool{\"added\": true, \"removed\": true, \"modified\": true, \"unmodified\": true}\n\tfor _, value := range c.Hide {\n\t\tif _, ok := validHideValues[value]; !ok {\n\t\t\tlog.Warnf(\"invalid diff hide value: %s (valid values: added, removed, modified, unmodified)\", value)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/ui_filetree.go",
    "content": "package options\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\nvar _ interface {\n\tclio.PostLoader\n\tclio.FieldDescriber\n} = (*UIFiletree)(nil)\n\n// UIFiletree provides configuration for the file tree display\ntype UIFiletree struct {\n\tCollapseDir    bool    `yaml:\"collapse-dir\" mapstructure:\"collapse-dir\"`\n\tPaneWidth      float64 `yaml:\"pane-width\" mapstructure:\"pane-width\"`\n\tShowAttributes bool    `yaml:\"show-attributes\" mapstructure:\"show-attributes\"`\n}\n\nfunc DefaultUIFiletree() UIFiletree {\n\tprefs := v1.DefaultPreferences()\n\treturn UIFiletree{\n\t\tCollapseDir:    prefs.CollapseFiletreeDirectory,\n\t\tPaneWidth:      prefs.FiletreePaneWidth,\n\t\tShowAttributes: prefs.ShowFiletreeAttributes,\n\t}\n}\n\nfunc (c *UIFiletree) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&c.CollapseDir, \"collapse directories by default in the filetree\")\n\tdescriptions.Add(&c.PaneWidth, \"percentage of screen width for the filetree pane (must be >0 and <1)\")\n\tdescriptions.Add(&c.ShowAttributes, \"show file attributes in the filetree view\")\n}\n\nfunc (c *UIFiletree) PostLoad() error {\n\t// Validate pane width is between 0 and 1\n\tif c.PaneWidth <= 0 || c.PaneWidth >= 1 {\n\t\tlog.Warnf(\"filetree pane-width must be >0 and <1, got %v, resetting to default 0.5\", c.PaneWidth)\n\t\tc.PaneWidth = 0.5\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/ui_keybindings.go",
    "content": "package options\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"reflect\"\n)\n\nvar _ interface {\n\tclio.FieldDescriber\n} = (*UIKeybindings)(nil)\n\n// UIKeybindings provides configuration for all keyboard shortcuts\ntype UIKeybindings struct {\n\tGlobal     GlobalBindings     `yaml:\",inline\" mapstructure:\",squash\"`\n\tNavigation NavigationBindings `yaml:\",inline\" mapstructure:\",squash\"`\n\tLayer      LayerBindings      `yaml:\",inline\" mapstructure:\",squash\"`\n\tFiletree   FiletreeBindings   `yaml:\",inline\" mapstructure:\",squash\"`\n\n\tConfig key.Bindings `yaml:\"-\" mapstructure:\"-\"`\n}\n\ntype GlobalBindings struct {\n\tQuit             string `yaml:\"quit\" mapstructure:\"quit\"`\n\tToggleView       string `yaml:\"toggle-view\" mapstructure:\"toggle-view\"`\n\tFilterFiles      string `yaml:\"filter-files\" mapstructure:\"filter-files\"`\n\tCloseFilterFiles string `yaml:\"close-filter-files\" mapstructure:\"close-filter-files\"`\n}\n\ntype NavigationBindings struct {\n\tUp       string `yaml:\"up\" mapstructure:\"up\"`\n\tDown     string `yaml:\"down\" mapstructure:\"down\"`\n\tLeft     string `yaml:\"left\" mapstructure:\"left\"`\n\tRight    string `yaml:\"right\" mapstructure:\"right\"`\n\tPageUp   string `yaml:\"page-up\" mapstructure:\"page-up\"`\n\tPageDown string `yaml:\"page-down\" mapstructure:\"page-down\"`\n}\n\ntype LayerBindings struct {\n\tCompareAll   string `yaml:\"compare-all\" mapstructure:\"compare-all\"`\n\tCompareLayer string `yaml:\"compare-layer\" mapstructure:\"compare-layer\"`\n}\n\ntype FiletreeBindings struct {\n\tToggleCollapseDir     string `yaml:\"toggle-collapse-dir\" mapstructure:\"toggle-collapse-dir\"`\n\tToggleCollapseAllDir  string `yaml:\"toggle-collapse-all-dir\" mapstructure:\"toggle-collapse-all-dir\"`\n\tToggleAddedFiles      string `yaml:\"toggle-added-files\" mapstructure:\"toggle-added-files\"`\n\tToggleRemovedFiles    string `yaml:\"toggle-removed-files\" mapstructure:\"toggle-removed-files\"`\n\tToggleModifiedFiles   string `yaml:\"toggle-modified-files\" mapstructure:\"toggle-modified-files\"`\n\tToggleUnmodifiedFiles string `yaml:\"toggle-unmodified-files\" mapstructure:\"toggle-unmodified-files\"`\n\tToggleTreeAttributes  string `yaml:\"toggle-filetree-attributes\" mapstructure:\"toggle-filetree-attributes\"`\n\tToggleSortOrder       string `yaml:\"toggle-sort-order\" mapstructure:\"toggle-sort-order\"`\n\tToggleWrapTree        string `yaml:\"toggle-wrap-tree\" mapstructure:\"toggle-wrap-tree\"`\n\tExtractFile           string `yaml:\"extract-file\" mapstructure:\"extract-file\"`\n}\n\nfunc DefaultUIKeybinding() UIKeybindings {\n\tvar result UIKeybindings\n\tdefaults := key.DefaultBindings()\n\n\t// converts from key.Bindings to UIKeybindings\n\tgetUIBindingValues(reflect.ValueOf(defaults), reflect.ValueOf(&result).Elem())\n\n\treturn result\n}\n\nfunc getUIBindingValues(src, dst reflect.Value) {\n\tswitch src.Kind() {\n\tcase reflect.Struct:\n\t\tfor i := 0; i < src.NumField(); i++ {\n\t\t\tsrcField := src.Field(i)\n\t\t\tsrcType := src.Type().Field(i)\n\n\t\t\tif !srcField.CanInterface() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdstField := dst.FieldByName(srcType.Name)\n\t\t\tif !dstField.IsValid() || !dstField.CanSet() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif srcType.Type.Name() == \"Config\" {\n\t\t\t\tinputField := srcField.FieldByName(\"Input\")\n\t\t\t\tif inputField.IsValid() && dstField.Kind() == reflect.String {\n\t\t\t\t\tdstField.SetString(inputField.String())\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgetUIBindingValues(srcField, dstField)\n\t\t}\n\t}\n}\n\nfunc (c *UIKeybindings) PostLoad() error {\n\tcfg := key.Bindings{}\n\n\t// convert UIKeybindings to key.Bindings\n\terr := createKeyBindings(reflect.ValueOf(c).Elem(), reflect.ValueOf(&cfg).Elem())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.Config = cfg\n\treturn nil\n}\n\nfunc createKeyBindings(src, dst reflect.Value) error {\n\tswitch dst.Kind() {\n\tcase reflect.Struct:\n\t\tfor i := 0; i < dst.NumField(); i++ {\n\t\t\tdstField := dst.Field(i)\n\t\t\tdstType := dst.Type().Field(i)\n\n\t\t\tif !dstField.CanSet() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsrcField := src.FieldByName(dstType.Name)\n\t\t\tif !srcField.IsValid() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif dstType.Type.Name() == \"Config\" {\n\t\t\t\tinputField := dstField.FieldByName(\"Input\")\n\t\t\t\tif inputField.IsValid() && inputField.CanSet() && srcField.Kind() == reflect.String {\n\t\t\t\t\tinputField.SetString(srcField.String())\n\n\t\t\t\t\t// call the Setup method if it exists\n\t\t\t\t\tsetupMethod := dstField.Addr().MethodByName(\"Setup\")\n\t\t\t\t\tif setupMethod.IsValid() {\n\t\t\t\t\t\tresult := setupMethod.Call([]reflect.Value{})\n\t\t\t\t\t\tif !result[0].IsNil() {\n\t\t\t\t\t\t\treturn result[0].Interface().(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr := createKeyBindings(srcField, dstField)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\nfunc (c *UIKeybindings) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\t// global keybindings\n\tdescriptions.Add(&c.Global.Quit, \"quit the application (global)\")\n\tdescriptions.Add(&c.Global.ToggleView, \"toggle between different views (global)\")\n\tdescriptions.Add(&c.Global.FilterFiles, \"filter files by name (global)\")\n\tdescriptions.Add(&c.Global.CloseFilterFiles, \"close file filtering (global)\")\n\n\t// navigation keybindings\n\tdescriptions.Add(&c.Navigation.Up, \"move cursor up (global)\")\n\tdescriptions.Add(&c.Navigation.Down, \"move cursor down (global)\")\n\tdescriptions.Add(&c.Navigation.Left, \"move cursor left (global)\")\n\tdescriptions.Add(&c.Navigation.Right, \"move cursor right (global)\")\n\tdescriptions.Add(&c.Navigation.PageUp, \"scroll page up (file view)\")\n\tdescriptions.Add(&c.Navigation.PageDown, \"scroll page down (file view)\")\n\n\t// layer view keybindings\n\tdescriptions.Add(&c.Layer.CompareAll, \"compare all layers (layer view)\")\n\tdescriptions.Add(&c.Layer.CompareLayer, \"compare specific layer (layer view)\")\n\n\t// file view keybindings\n\tdescriptions.Add(&c.Filetree.ToggleCollapseDir, \"toggle directory collapse (file view)\")\n\tdescriptions.Add(&c.Filetree.ToggleCollapseAllDir, \"toggle collapse all directories (file view)\")\n\tdescriptions.Add(&c.Filetree.ToggleAddedFiles, \"toggle visibility of added files (file view)\")\n\tdescriptions.Add(&c.Filetree.ToggleRemovedFiles, \"toggle visibility of removed files (file view)\")\n\tdescriptions.Add(&c.Filetree.ToggleModifiedFiles, \"toggle visibility of modified files (file view)\")\n\tdescriptions.Add(&c.Filetree.ToggleUnmodifiedFiles, \"toggle visibility of unmodified files (file view)\")\n\tdescriptions.Add(&c.Filetree.ToggleTreeAttributes, \"toggle display of file attributes (file view)\")\n\tdescriptions.Add(&c.Filetree.ToggleSortOrder, \"toggle sort order (file view)\")\n\tdescriptions.Add(&c.Filetree.ExtractFile, \"extract file contents (file view)\")\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/options/ui_layers.go",
    "content": "package options\n\nimport \"github.com/anchore/clio\"\n\nvar _ clio.FieldDescriber = (*UILayers)(nil)\n\n// UILayers provides configuration for layer display behavior\ntype UILayers struct {\n\tShowAggregatedChanges bool `yaml:\"show-aggregated-changes\" mapstructure:\"show-aggregated-changes\"`\n}\n\nfunc DefaultUILayers() UILayers {\n\treturn UILayers{\n\t\tShowAggregatedChanges: false,\n\t}\n}\n\nfunc (c *UILayers) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&c.ShowAggregatedChanges, \"show aggregated changes across all previous layers\")\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/no_ui.go",
    "content": "package ui\n\nimport (\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/clio\"\n)\n\nvar _ clio.UI = (*NoUI)(nil)\n\ntype NoUI struct {\n\tsubscription partybus.Unsubscribable\n}\n\nfunc None() *NoUI {\n\treturn &NoUI{}\n}\n\nfunc (n *NoUI) Setup(subscription partybus.Unsubscribable) error {\n\tn.subscription = subscription\n\treturn nil\n}\n\nfunc (n *NoUI) Handle(_ partybus.Event) error {\n\treturn nil\n}\n\nfunc (n NoUI) Teardown(_ bool) error {\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/app/app.go",
    "content": "package app\n\nimport (\n\t\"errors\"\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/layout\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/layout/compound\"\n\t\"golang.org/x/net/context\"\n\t\"time\"\n)\n\nconst debug = false\n\ntype app struct {\n\tgui        *gocui.Gui\n\tcontroller *controller\n\tlayout     *layout.Manager\n}\n\n// Run is the UI entrypoint.\nfunc Run(ctx context.Context, c v1.Config) error {\n\tvar err error\n\n\t// it appears there is a race condition where termbox.Init() will\n\t// block nearly indefinitely when running as the first process in\n\t// a Docker container when started within ~25ms of container startup.\n\t// I can't seem to determine the exact root cause, however, a large\n\t// enough sleep will prevent this behavior (todo: remove this hack)\n\ttime.Sleep(100 * time.Millisecond)\n\n\tg, err := gocui.NewGui(gocui.OutputNormal, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer g.Close()\n\n\t_, err = newApp(ctx, g, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tk, mod := gocui.MustParse(\"Ctrl+Z\")\n\tif err := g.SetKeybinding(\"\", k, mod, handle_ctrl_z); err != nil {\n\t\treturn err\n\t}\n\n\tif err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc newApp(ctx context.Context, gui *gocui.Gui, cfg v1.Config) (*app, error) {\n\tvar err error\n\tvar c *controller\n\tvar globalHelpKeys []*key.Binding\n\n\tc, err = newController(ctx, gui, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// note: order matters when adding elements to the layout\n\tlm := layout.NewManager()\n\tlm.Add(c.views.Status, layout.LocationFooter)\n\tlm.Add(c.views.Filter, layout.LocationFooter)\n\tlm.Add(compound.NewLayerDetailsCompoundLayout(c.views.Layer, c.views.LayerDetails, c.views.ImageDetails), layout.LocationColumn)\n\tlm.Add(c.views.Tree, layout.LocationColumn)\n\n\t// todo: access this more programmatically\n\tif debug {\n\t\tlm.Add(c.views.Debug, layout.LocationColumn)\n\t}\n\tgui.Cursor = false\n\t// g.Mouse = true\n\tgui.SetManagerFunc(lm.Layout)\n\n\ta := &app{\n\t\tgui:        gui,\n\t\tcontroller: c,\n\t\tlayout:     lm,\n\t}\n\n\tvar infos = []key.BindingInfo{\n\t\t{\n\t\t\tConfig:   cfg.Preferences.KeyBindings.Global.Quit,\n\t\t\tOnAction: a.quit,\n\t\t\tDisplay:  \"Quit\",\n\t\t},\n\t\t{\n\t\t\tConfig:   cfg.Preferences.KeyBindings.Global.ToggleView,\n\t\t\tOnAction: c.ToggleView,\n\t\t\tDisplay:  \"Switch view\",\n\t\t},\n\t\t{\n\t\t\tConfig:   cfg.Preferences.KeyBindings.Navigation.Right,\n\t\t\tOnAction: c.NextPane,\n\t\t},\n\t\t{\n\t\t\tConfig:   cfg.Preferences.KeyBindings.Navigation.Left,\n\t\t\tOnAction: c.PrevPane,\n\t\t},\n\t\t{\n\t\t\tConfig:     cfg.Preferences.KeyBindings.Global.FilterFiles,\n\t\t\tOnAction:   c.ToggleFilterView,\n\t\t\tIsSelected: c.views.Filter.IsVisible,\n\t\t\tDisplay:    \"Filter\",\n\t\t},\n\t\t{\n\t\t\tConfig:   cfg.Preferences.KeyBindings.Global.CloseFilterFiles,\n\t\t\tOnAction: c.CloseFilterView,\n\t\t},\n\t}\n\n\tglobalHelpKeys, err = key.GenerateBindings(gui, \"\", infos)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.views.Status.AddHelpKeys(globalHelpKeys...)\n\n\t// perform the first update and render now that all resources have been loaded\n\terr = c.UpdateAndRender()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn a, err\n}\n\n// quit is the gocui callback invoked when the user hits Ctrl+C\nfunc (a *app) quit() error {\n\treturn gocui.ErrQuit\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/app/controller.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/view\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel\"\n\t\"golang.org/x/net/context\"\n\t\"regexp\"\n\n\t\"github.com/awesome-gocui/gocui\"\n)\n\ntype controller struct {\n\tgui    *gocui.Gui\n\tviews  *view.Views\n\tconfig v1.Config\n\tctx    context.Context // TODO: storing context in the controller is not ideal\n}\n\nfunc newController(ctx context.Context, g *gocui.Gui, cfg v1.Config) (*controller, error) {\n\tviews, err := view.NewViews(g, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &controller{\n\t\tgui:    g,\n\t\tviews:  views,\n\t\tconfig: cfg,\n\t\tctx:    ctx,\n\t}\n\n\t// layer view cursor down event should trigger an update in the file tree\n\tc.views.Layer.AddLayerChangeListener(c.onLayerChange)\n\n\t// update the status pane when a filetree option is changed by the user\n\tc.views.Tree.AddViewOptionChangeListener(c.onFileTreeViewOptionChange)\n\n\t// update the status pane when a filetree option is changed by the user\n\tc.views.Tree.AddViewExtractListener(c.onFileTreeViewExtract)\n\n\t// update the tree view while the user types into the filter view\n\tc.views.Filter.AddFilterEditListener(c.onFilterEdit)\n\n\t// propagate initial conditions to necessary views\n\terr = c.onLayerChange(viewmodel.LayerSelection{\n\t\tLayer:           c.views.Layer.CurrentLayer(),\n\t\tBottomTreeStart: 0,\n\t\tBottomTreeStop:  0,\n\t\tTopTreeStart:    0,\n\t\tTopTreeStop:     0,\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *controller) onFileTreeViewExtract(p string) error {\n\treturn c.config.Content.Extract(c.ctx, c.config.Analysis.Image, c.views.LayerDetails.CurrentLayer.Id, p)\n}\n\nfunc (c *controller) onFileTreeViewOptionChange() error {\n\terr := c.views.Status.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.views.Status.Render()\n}\n\nfunc (c *controller) onFilterEdit(filter string) error {\n\tvar filterRegex *regexp.Regexp\n\tvar err error\n\n\tif len(filter) > 0 {\n\t\tfilterRegex, err = regexp.Compile(filter)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tc.views.Tree.SetFilterRegex(filterRegex)\n\n\terr = c.views.Tree.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.views.Tree.Render()\n}\n\nfunc (c *controller) onLayerChange(selection viewmodel.LayerSelection) error {\n\t// update the details\n\tc.views.LayerDetails.CurrentLayer = selection.Layer\n\n\t// update the filetree\n\terr := c.views.Tree.SetTree(selection.BottomTreeStart, selection.BottomTreeStop, selection.TopTreeStart, selection.TopTreeStop)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.views.Layer.CompareMode() == viewmodel.CompareAllLayers {\n\t\tc.views.Tree.SetTitle(\"Aggregated Layer Contents\")\n\t} else {\n\t\tc.views.Tree.SetTitle(\"Current Layer Contents\")\n\t}\n\n\t// update details and filetree panes\n\treturn c.UpdateAndRender()\n}\n\nfunc (c *controller) UpdateAndRender() error {\n\terr := c.Update()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"controller failed update: %w\", err)\n\t}\n\n\terr = c.Render()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"controller failed render: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Update refreshes the state objects for future rendering.\nfunc (c *controller) Update() error {\n\tfor _, v := range c.views.Renderers() {\n\t\terr := v.Update()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"controller unable to update view: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Render flushes the state objects to the screen.\nfunc (c *controller) Render() error {\n\tfor _, v := range c.views.Renderers() {\n\t\tif v.IsVisible() {\n\t\t\terr := v.Render()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n//nolint:dupl\nfunc (c *controller) NextPane() (err error) {\n\tv := c.gui.CurrentView()\n\tif v == nil {\n\t\tpanic(\"CurrentView is nil\")\n\t}\n\tif v.Name() == c.views.Layer.Name() {\n\t\t_, err = c.gui.SetCurrentView(c.views.LayerDetails.Name())\n\t\tc.views.Status.SetCurrentView(c.views.LayerDetails)\n\t} else if v.Name() == c.views.LayerDetails.Name() {\n\t\t_, err = c.gui.SetCurrentView(c.views.ImageDetails.Name())\n\t\tc.views.Status.SetCurrentView(c.views.ImageDetails)\n\t} else if v.Name() == c.views.ImageDetails.Name() {\n\t\t_, err = c.gui.SetCurrentView(c.views.Layer.Name())\n\t\tc.views.Status.SetCurrentView(c.views.Layer)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"controller unable to switch to next pane: %w\", err)\n\t}\n\n\treturn c.UpdateAndRender()\n}\n\n//nolint:dupl\nfunc (c *controller) PrevPane() (err error) {\n\tv := c.gui.CurrentView()\n\tif v == nil {\n\t\tpanic(\"Current view is nil\")\n\t}\n\tif v.Name() == c.views.Layer.Name() {\n\t\t_, err = c.gui.SetCurrentView(c.views.ImageDetails.Name())\n\t\tc.views.Status.SetCurrentView(c.views.ImageDetails)\n\t} else if v.Name() == c.views.LayerDetails.Name() {\n\t\t_, err = c.gui.SetCurrentView(c.views.Layer.Name())\n\t\tc.views.Status.SetCurrentView(c.views.Layer)\n\t} else if v.Name() == c.views.ImageDetails.Name() {\n\t\t_, err = c.gui.SetCurrentView(c.views.LayerDetails.Name())\n\t\tc.views.Status.SetCurrentView(c.views.LayerDetails)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"controller unable to switch to previous pane: %w\", err)\n\t}\n\n\treturn c.UpdateAndRender()\n}\n\n// ToggleView switches between the file view and the layer view and re-renders the screen.\nfunc (c *controller) ToggleView() (err error) {\n\tv := c.gui.CurrentView()\n\tif v == nil || v.Name() == c.views.Layer.Name() {\n\t\t_, err = c.gui.SetCurrentView(c.views.Tree.Name())\n\t\tc.views.Status.SetCurrentView(c.views.Tree)\n\t} else {\n\t\t_, err = c.gui.SetCurrentView(c.views.Layer.Name())\n\t\tc.views.Status.SetCurrentView(c.views.Layer)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"controller unable to toggle view: %w\", err)\n\t}\n\n\treturn c.UpdateAndRender()\n}\n\nfunc (c *controller) CloseFilterView() error {\n\t// filter view needs to be visible\n\tif c.views.Filter.IsVisible() {\n\t\t// toggle filter view\n\t\treturn c.ToggleFilterView()\n\t}\n\treturn nil\n}\n\nfunc (c *controller) ToggleFilterView() error {\n\t// delete all user input from the tree view\n\terr := c.views.Filter.ToggleVisible()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to toggle filter visibility: %w\", err)\n\t}\n\n\t// we have just hidden the filter view...\n\tif !c.views.Filter.IsVisible() {\n\t\t// ...remove any filter from the tree\n\t\tc.views.Tree.SetFilterRegex(nil)\n\n\t\t// ...adjust focus to a valid (visible) view\n\t\terr = c.ToggleView()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to toggle filter view (back): %w\", err)\n\t\t}\n\t}\n\n\treturn c.UpdateAndRender()\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/app/job_control_other.go",
    "content": "//go:build windows\n// +build windows\n\npackage app\n\nimport (\n\t\"github.com/awesome-gocui/gocui\"\n)\n\n// handle ctrl+z not supported on windows\nfunc handle_ctrl_z(_ *gocui.Gui, _ *gocui.View) error {\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/app/job_control_unix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage app\n\nimport (\n\t\"syscall\"\n\n\t\"github.com/awesome-gocui/gocui\"\n)\n\n// handle ctrl+z\nfunc handle_ctrl_z(g *gocui.Gui, v *gocui.View) error {\n\tgocui.Suspend()\n\tif err := syscall.Kill(syscall.Getpid(), syscall.SIGSTOP); err != nil {\n\t\treturn err\n\t}\n\treturn gocui.Resume()\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/config.go",
    "content": "package v1\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"github.com/wagoodman/dive/dive/filetree\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"golang.org/x/net/context\"\n\t\"sync\"\n)\n\ntype Config struct {\n\t// required input\n\tAnalysis    image.Analysis\n\tContent     ContentReader\n\tPreferences Preferences\n\n\tstack     filetree.Comparer\n\tstackErrs error\n\tdo        *sync.Once\n}\n\ntype Preferences struct {\n\tKeyBindings                key.Bindings\n\tIgnoreErrors               bool\n\tShowFiletreeAttributes     bool\n\tShowAggregatedLayerChanges bool\n\tCollapseFiletreeDirectory  bool\n\tFiletreePaneWidth          float64\n\tFiletreeDiffHide           []string\n}\n\nfunc DefaultPreferences() Preferences {\n\treturn Preferences{\n\t\tKeyBindings:                key.DefaultBindings(),\n\t\tShowFiletreeAttributes:     true,\n\t\tShowAggregatedLayerChanges: true,\n\t\tCollapseFiletreeDirectory:  false, // don't start with collapsed directories\n\t\tFiletreePaneWidth:          0.5,\n\t\tFiletreeDiffHide:           []string{}, // empty slice means show all\n\t}\n}\n\nfunc (c *Config) TreeComparer() (filetree.Comparer, error) {\n\tif c.do == nil {\n\t\tc.do = &sync.Once{}\n\t}\n\tc.do.Do(func() {\n\t\ttreeStack := filetree.NewComparer(c.Analysis.RefTrees)\n\t\terrs := treeStack.BuildCache()\n\t\tif errs != nil {\n\t\t\tif !c.Preferences.IgnoreErrors {\n\t\t\t\terrs = append(errs, fmt.Errorf(\"file tree has path errors (use '--ignore-errors' to attempt to continue)\"))\n\t\t\t\tc.stackErrs = errors.Join(errs...)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tc.stack = treeStack\n\t})\n\n\treturn c.stack, c.stackErrs\n}\n\ntype ContentReader interface {\n\tExtract(ctx context.Context, id string, layer string, path string) error\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/format/format.go",
    "content": "package format\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/lunixbochs/vtclean\"\n)\n\nconst (\n\t// selectedLeftBracketStr = \" \"\n\t// selectedRightBracketStr = \" \"\n\t// selectedFillStr = \" \"\n\t//\n\t//leftBracketStr = \"▏\"\n\t//rightBracketStr = \"▕\"\n\t//fillStr = \"─\"\n\n\t// selectedLeftBracketStr = \" \"\n\t// selectedRightBracketStr = \" \"\n\t// selectedFillStr = \"━\"\n\t//\n\t//leftBracketStr = \"▏\"\n\t//rightBracketStr = \"▕\"\n\t//fillStr = \"─\"\n\n\tselectedLeftBracketStr  = \"┃\"\n\tselectedRightBracketStr = \"┣\"\n\tselectedFillStr         = \"━\"\n\n\tleftBracketStr  = \"│\"\n\trightBracketStr = \"├\"\n\tfillStr         = \"─\"\n\n\tselectStr = \" ● \"\n\t// selectStr = \" \"\n)\n\nvar (\n\tHeader                func(...interface{}) string\n\tSelected              func(...interface{}) string\n\tStatusSelected        func(...interface{}) string\n\tStatusNormal          func(...interface{}) string\n\tStatusControlSelected func(...interface{}) string\n\tStatusControlNormal   func(...interface{}) string\n\tCompareTop            func(...interface{}) string\n\tCompareBottom         func(...interface{}) string\n\treset                 = color.New(color.Reset).Sprint(\"\")\n)\n\nfunc init() {\n\twrapper := func(fn func(a ...any) string) func(a ...any) string {\n\t\treturn func(a ...any) string {\n\t\t\t// for some reason not all color formatter functions are not applying RESET, we'll add it manually for now\n\t\t\treturn fn(a...) + reset\n\t\t}\n\t}\n\n\tSelected = wrapper(color.New(color.ReverseVideo, color.Bold).SprintFunc())\n\tHeader = wrapper(color.New(color.Bold).SprintFunc())\n\tStatusSelected = wrapper(color.New(color.BgMagenta, color.FgWhite).SprintFunc())\n\tStatusNormal = wrapper(color.New(color.ReverseVideo).SprintFunc())\n\tStatusControlSelected = wrapper(color.New(color.BgMagenta, color.FgWhite, color.Bold).SprintFunc())\n\tStatusControlNormal = wrapper(color.New(color.ReverseVideo, color.Bold).SprintFunc())\n\tCompareTop = wrapper(color.New(color.BgMagenta).SprintFunc())\n\tCompareBottom = wrapper(color.New(color.BgGreen).SprintFunc())\n}\n\nfunc RenderNoHeader(width int, selected bool) string {\n\tif selected {\n\t\treturn strings.Repeat(selectedFillStr, width)\n\t}\n\treturn strings.Repeat(fillStr, width)\n}\n\nfunc RenderHeader(title string, width int, selected bool) string {\n\tif selected {\n\t\tbody := Header(fmt.Sprintf(\"%s%s \", selectStr, title))\n\t\tbodyLen := len(vtclean.Clean(body, false))\n\t\trepeatCount := width - bodyLen - 2\n\t\tif repeatCount < 0 {\n\t\t\trepeatCount = 0\n\t\t}\n\t\treturn fmt.Sprintf(\"%s%s%s%s\\n\", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, repeatCount))\n\t\t// return fmt.Sprintf(\"%s%s%s%s\\n\", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), Selected(strings.Repeat(selectedFillStr, width-bodyLen-2)))\n\t\t// return fmt.Sprintf(\"%s%s%s%s\\n\", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), strings.Repeat(selectedFillStr, width-bodyLen-2))\n\t}\n\tbody := Header(fmt.Sprintf(\" %s \", title))\n\tbodyLen := len(vtclean.Clean(body, false))\n\trepeatCount := width - bodyLen - 2\n\tif repeatCount < 0 {\n\t\trepeatCount = 0\n\t}\n\treturn fmt.Sprintf(\"%s%s%s%s\\n\", leftBracketStr, body, rightBracketStr, strings.Repeat(fillStr, repeatCount))\n}\n\nfunc RenderHelpKey(control, title string, selected bool) string {\n\tif selected {\n\t\treturn StatusSelected(\"▏\") + StatusControlSelected(control) + StatusSelected(\" \"+title+\" \")\n\t} else {\n\t\treturn StatusNormal(\"▏\") + StatusControlNormal(control) + StatusNormal(\" \"+title+\" \")\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/key/binding.go",
    "content": "package key\n\nimport (\n\t\"fmt\"\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/awesome-gocui/keybinding\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n)\n\ntype BindingInfo struct {\n\tKey        gocui.Key\n\tModifier   gocui.Modifier\n\tConfig     Config\n\tOnAction   func() error\n\tIsSelected func() bool\n\tDisplay    string\n}\n\ntype Binding struct {\n\tkey         []keybinding.Key\n\tdisplayName string\n\tselectedFn  func() bool\n\tactionFn    func() error\n}\n\nfunc GenerateBindings(gui *gocui.Gui, influence string, infos []BindingInfo) ([]*Binding, error) {\n\tvar result = make([]*Binding, 0)\n\tfor _, info := range infos {\n\t\tif len(info.Config.Keys) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"no keybinding configured for '%s'\", info.Display)\n\t\t}\n\n\t\tbinding, err := newBinding(gui, influence, info.Config.Keys, info.Display, info.OnAction)\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif info.IsSelected != nil {\n\t\t\tbinding.RegisterSelectionFn(info.IsSelected)\n\t\t}\n\t\tif len(info.Display) > 0 {\n\t\t\tresult = append(result, binding)\n\t\t}\n\t}\n\treturn result, nil\n}\n\nfunc newBinding(gui *gocui.Gui, influence string, keys []keybinding.Key, displayName string, actionFn func() error) (*Binding, error) {\n\tbinding := &Binding{\n\t\tkey:         keys,\n\t\tdisplayName: displayName,\n\t\tactionFn:    actionFn,\n\t}\n\n\tfor _, key := range keys {\n\t\tif err := gui.SetKeybinding(influence, key.Value, key.Modifier, binding.onAction); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn binding, nil\n}\n\nfunc (binding *Binding) RegisterSelectionFn(selectedFn func() bool) {\n\tbinding.selectedFn = selectedFn\n}\n\nfunc (binding *Binding) onAction(*gocui.Gui, *gocui.View) error {\n\tif binding.actionFn == nil {\n\t\treturn fmt.Errorf(\"no action configured for '%+v'\", binding)\n\t}\n\treturn binding.actionFn()\n}\n\nfunc (binding *Binding) isSelected() bool {\n\tif binding.selectedFn == nil {\n\t\treturn false\n\t}\n\n\treturn binding.selectedFn()\n}\n\nfunc (binding *Binding) RenderKeyHelp() string {\n\treturn format.RenderHelpKey(binding.key[0].String(), binding.displayName, binding.isSelected())\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/key/config.go",
    "content": "package key\n\nimport (\n\t\"fmt\"\n\t\"github.com/awesome-gocui/keybinding\"\n)\n\ntype Config struct {\n\tInput string\n\tKeys  []keybinding.Key `yaml:\"-\" mapstructure:\"-\"`\n}\n\nfunc (c *Config) Setup() error {\n\tif len(c.Input) == 0 {\n\t\treturn nil\n\t}\n\n\tparsed, err := keybinding.ParseAll(c.Input)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse key %q: %w\", c.Input, err)\n\t}\n\tc.Keys = parsed\n\treturn nil\n}\n\ntype Bindings struct {\n\tGlobal     GlobalBindings     `yaml:\",inline\" mapstructure:\",squash\"`\n\tNavigation NavigationBindings `yaml:\",inline\" mapstructure:\",squash\"`\n\tLayer      LayerBindings      `yaml:\",inline\" mapstructure:\",squash\"`\n\tFiletree   FiletreeBindings   `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\ntype GlobalBindings struct {\n\tQuit             Config `yaml:\"quit\" mapstructure:\"quit\"`\n\tToggleView       Config `yaml:\"toggle-view\" mapstructure:\"toggle-view\"`\n\tFilterFiles      Config `yaml:\"filter-files\" mapstructure:\"filter-files\"`\n\tCloseFilterFiles Config `yaml:\"close-filter-files\" mapstructure:\"close-filter-files\"`\n}\n\ntype NavigationBindings struct {\n\tUp       Config `yaml:\"up\" mapstructure:\"up\"`\n\tDown     Config `yaml:\"down\" mapstructure:\"down\"`\n\tLeft     Config `yaml:\"left\" mapstructure:\"left\"`\n\tRight    Config `yaml:\"right\" mapstructure:\"right\"`\n\tPageUp   Config `yaml:\"page-up\" mapstructure:\"page-up\"`\n\tPageDown Config `yaml:\"page-down\" mapstructure:\"page-down\"`\n}\n\ntype LayerBindings struct {\n\tCompareAll   Config `yaml:\"compare-all\" mapstructure:\"compare-all\"`\n\tCompareLayer Config `yaml:\"compare-layer\" mapstructure:\"compare-layer\"`\n}\n\ntype FiletreeBindings struct {\n\tToggleCollapseDir     Config `yaml:\"toggle-collapse-dir\" mapstructure:\"toggle-collapse-dir\"`\n\tToggleCollapseAllDir  Config `yaml:\"toggle-collapse-all-dir\" mapstructure:\"toggle-collapse-all-dir\"`\n\tToggleAddedFiles      Config `yaml:\"toggle-added-files\" mapstructure:\"toggle-added-files\"`\n\tToggleRemovedFiles    Config `yaml:\"toggle-removed-files\" mapstructure:\"toggle-removed-files\"`\n\tToggleModifiedFiles   Config `yaml:\"toggle-modified-files\" mapstructure:\"toggle-modified-files\"`\n\tToggleUnmodifiedFiles Config `yaml:\"toggle-unmodified-files\" mapstructure:\"toggle-unmodified-files\"`\n\tToggleTreeAttributes  Config `yaml:\"toggle-filetree-attributes\" mapstructure:\"toggle-filetree-attributes\"`\n\tToggleSortOrder       Config `yaml:\"toggle-sort-order\" mapstructure:\"toggle-sort-order\"`\n\tToggleWrapTree        Config `yaml:\"toggle-wrap-tree\" mapstructure:\"toggle-wrap-tree\"`\n\tExtractFile           Config `yaml:\"extract-file\" mapstructure:\"extract-file\"`\n}\n\nfunc DefaultBindings() Bindings {\n\treturn Bindings{\n\t\tGlobal: GlobalBindings{\n\t\t\tQuit:             Config{Input: \"ctrl+c\"},\n\t\t\tToggleView:       Config{Input: \"tab\"},\n\t\t\tFilterFiles:      Config{Input: \"ctrl+f, ctrl+slash\"},\n\t\t\tCloseFilterFiles: Config{Input: \"esc\"},\n\t\t},\n\t\tNavigation: NavigationBindings{\n\t\t\tUp:       Config{Input: \"up,k\"},\n\t\t\tDown:     Config{Input: \"down,j\"},\n\t\t\tLeft:     Config{Input: \"left,h\"},\n\t\t\tRight:    Config{Input: \"right,l\"},\n\t\t\tPageUp:   Config{Input: \"pgup,u\"},\n\t\t\tPageDown: Config{Input: \"pgdn,d\"},\n\t\t},\n\t\tLayer: LayerBindings{\n\t\t\tCompareAll:   Config{Input: \"ctrl+a\"},\n\t\t\tCompareLayer: Config{Input: \"ctrl+l\"},\n\t\t},\n\t\tFiletree: FiletreeBindings{\n\t\t\tToggleCollapseDir:     Config{Input: \"space\"},\n\t\t\tToggleCollapseAllDir:  Config{Input: \"ctrl+space\"},\n\t\t\tToggleAddedFiles:      Config{Input: \"ctrl+a\"},\n\t\t\tToggleRemovedFiles:    Config{Input: \"ctrl+r\"},\n\t\t\tToggleModifiedFiles:   Config{Input: \"ctrl+m\"},\n\t\t\tToggleUnmodifiedFiles: Config{Input: \"ctrl+u\"},\n\t\t\tToggleTreeAttributes:  Config{Input: \"ctrl+b\"},\n\t\t\tToggleWrapTree:        Config{Input: \"ctrl+p\"},\n\t\t\tToggleSortOrder:       Config{Input: \"ctrl+o\"},\n\t\t\tExtractFile:           Config{Input: \"ctrl+e\"},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/layout/area.go",
    "content": "package layout\n\ntype Area struct {\n\tminX, minY, maxX, maxY int\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/layout/compound/layer_details_column.go",
    "content": "package compound\n\nimport (\n\t\"fmt\"\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/view\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/dive/internal/utils\"\n)\n\ntype LayerDetailsCompoundLayout struct {\n\tlayer               *view.Layer\n\tlayerDetails        *view.LayerDetails\n\timageDetails        *view.ImageDetails\n\tconstrainRealEstate bool\n}\n\nfunc NewLayerDetailsCompoundLayout(layer *view.Layer, layerDetails *view.LayerDetails, imageDetails *view.ImageDetails) *LayerDetailsCompoundLayout {\n\treturn &LayerDetailsCompoundLayout{\n\t\tlayer:        layer,\n\t\tlayerDetails: layerDetails,\n\t\timageDetails: imageDetails,\n\t}\n}\n\nfunc (cl *LayerDetailsCompoundLayout) Name() string {\n\treturn \"layer-details-compound-column\"\n}\n\n// OnLayoutChange is called whenever the screen dimensions are changed\nfunc (cl *LayerDetailsCompoundLayout) OnLayoutChange() error {\n\terr := cl.layer.OnLayoutChange()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to setup layer controller onLayoutChange: %w\", err)\n\t}\n\n\terr = cl.layerDetails.OnLayoutChange()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to setup layer details controller onLayoutChange: %w\", err)\n\t}\n\n\terr = cl.imageDetails.OnLayoutChange()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to setup image details controller onLayoutChange: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (cl *LayerDetailsCompoundLayout) layoutRow(g *gocui.Gui, minX, minY, maxX, maxY int, viewName string, setup func(*gocui.View, *gocui.View) error) error {\n\tlog.WithFields(\"ui\", cl.Name()).Tracef(\"layoutRow(g, minX: %d, minY: %d, maxX: %d, maxY: %d, viewName: %s, <setup func>)\", minX, minY, maxX, maxY, viewName)\n\t// header + border\n\theaderHeight := 2\n\n\t// TODO: investigate overlap\n\t// note: maxY needs to account for the (invisible) border, thus a +1\n\theaderView, headerErr := g.SetView(viewName+\"Header\", minX, minY, maxX, minY+headerHeight+1, 0)\n\n\t// we are going to overlap the view over the (invisible) border (so minY will be one less than expected)\n\tbodyView, bodyErr := g.SetView(viewName, minX, minY+headerHeight, maxX, maxY, 0)\n\n\tif utils.IsNewView(bodyErr, headerErr) {\n\t\terr := setup(bodyView, headerView)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to setup row layout for %s: %w\", viewName, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cl *LayerDetailsCompoundLayout) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {\n\tlog.WithFields(\"ui\", cl.Name()).Tracef(\"layout(minX: %d, minY: %d, maxX: %d, maxY: %d)\", minX, minY, maxX, maxY)\n\n\tlayouts := []view.View{\n\t\tcl.layer,\n\t\tcl.layerDetails,\n\t\tcl.imageDetails,\n\t}\n\n\trowHeight := maxY / 3\n\tfor i := 0; i < 3; i++ {\n\t\tif err := cl.layoutRow(g, minX, i*rowHeight, maxX, (i+1)*rowHeight, layouts[i].Name(), layouts[i].Setup); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to layout %q: %w\", layouts[i].Name(), err)\n\t\t}\n\t}\n\n\tif g.CurrentView() == nil {\n\t\tif _, err := g.SetCurrentView(cl.layer.Name()); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to set view to layer %q: %w\", cl.layer.Name(), err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cl *LayerDetailsCompoundLayout) RequestedSize(available int) *int {\n\t// \"available\" is the entire screen real estate, so we can guess when its a bit too small and take action.\n\t// This isn't perfect, but it gets the job done for now without complicated layout constraint solvers\n\tif available < 90 {\n\t\tcl.layer.ConstrainLayout()\n\t\tcl.constrainRealEstate = true\n\t\tsize := 8\n\t\treturn &size\n\t}\n\tcl.layer.ExpandLayout()\n\tcl.constrainRealEstate = false\n\treturn nil\n}\n\n// todo: make this variable based on the nested views\nfunc (cl *LayerDetailsCompoundLayout) IsVisible() bool {\n\treturn true\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/layout/layout.go",
    "content": "package layout\n\nimport \"github.com/awesome-gocui/gocui\"\n\ntype Layout interface {\n\tName() string\n\tLayout(g *gocui.Gui, minX, minY, maxX, maxY int) error\n\tRequestedSize(available int) *int\n\tIsVisible() bool\n\tOnLayoutChange() error\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/layout/location.go",
    "content": "package layout\n\nconst (\n\tLocationFooter Location = iota\n\tLocationHeader\n\tLocationColumn\n)\n\ntype Location int\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/layout/manager.go",
    "content": "package layout\n\nimport (\n\t\"fmt\"\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\ntype Constraint func(int) int\n\ntype Manager struct {\n\tlastX, lastY                                   int\n\tlastHeaderArea, lastFooterArea, lastColumnArea Area\n\telements                                       map[Location][]Layout\n}\n\nfunc NewManager() *Manager {\n\treturn &Manager{\n\t\telements: make(map[Location][]Layout),\n\t}\n}\n\nfunc (lm *Manager) Add(element Layout, location Location) {\n\tif _, exists := lm.elements[location]; !exists {\n\t\tlm.elements[location] = make([]Layout, 0)\n\t}\n\tlm.elements[location] = append(lm.elements[location], element)\n}\n\nfunc (lm *Manager) planAndLayoutHeaders(g *gocui.Gui, area Area) (Area, error) {\n\t// layout headers top down\n\tif elements, exists := lm.elements[LocationHeader]; exists {\n\t\tfor _, element := range elements {\n\t\t\t// a visible header cannot take up the whole screen, default to 1.\n\t\t\t// this eliminates the need to discover a default size based on all element requests\n\t\t\theight := 0\n\t\t\tif element.IsVisible() {\n\t\t\t\trequestedHeight := element.RequestedSize(area.maxY)\n\t\t\t\tif requestedHeight != nil {\n\t\t\t\t\theight = *requestedHeight\n\t\t\t\t} else {\n\t\t\t\t\theight = 1\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// layout the header within the allocated space\n\t\t\terr := element.Layout(g, area.minX, area.minY, area.maxX, area.minY+height)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(\"element\", element.Name(), \"error\", err).Error(\"failed to layout header\")\n\t\t\t\treturn area, err\n\t\t\t}\n\n\t\t\t// restrict the available screen real estate\n\t\t\tarea.minY += height\n\t\t}\n\t}\n\treturn area, nil\n}\n\nfunc (lm *Manager) planFooters(g *gocui.Gui, area Area) (Area, []int) {\n\tvar footerHeights = make([]int, 0)\n\t// we need to layout the footers last, but account for them when drawing the columns. This block is for planning\n\t// out the real estate needed for the footers now (but not laying out yet)\n\tif elements, exists := lm.elements[LocationFooter]; exists {\n\t\tfooterHeights = make([]int, len(elements))\n\t\tfor idx := range footerHeights {\n\t\t\tfooterHeights[idx] = 1\n\t\t}\n\n\t\tfor idx, element := range elements {\n\t\t\t// a visible footer cannot take up the whole screen, default to 1.\n\t\t\t// this eliminates the need to discover a default size based on all element requests\n\t\t\theight := 0\n\t\t\tif element.IsVisible() {\n\t\t\t\trequestedHeight := element.RequestedSize(area.maxY)\n\t\t\t\tif requestedHeight != nil {\n\t\t\t\t\theight = *requestedHeight\n\t\t\t\t} else {\n\t\t\t\t\theight = 1\n\t\t\t\t}\n\t\t\t}\n\t\t\tfooterHeights[idx] = height\n\t\t}\n\t\t// restrict the available screen real estate\n\t\tfor _, height := range footerHeights {\n\t\t\tarea.maxY -= height\n\t\t}\n\t}\n\treturn area, footerHeights\n}\n\nfunc (lm *Manager) planAndLayoutColumns(g *gocui.Gui, area Area) (Area, error) {\n\t// layout columns left to right\n\tif elements, exists := lm.elements[LocationColumn]; exists {\n\t\twidths := make([]int, len(elements))\n\t\tfor idx := range widths {\n\t\t\twidths[idx] = -1\n\t\t}\n\t\tvariableColumns := len(elements)\n\t\tavailableWidth := area.maxX + 1\n\n\t\t// first pass: planout the column sizes based on the given requests\n\t\tfor idx, element := range elements {\n\t\t\tif !element.IsVisible() {\n\t\t\t\twidths[idx] = 0\n\t\t\t\tvariableColumns--\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trequestedWidth := element.RequestedSize(availableWidth)\n\t\t\tif requestedWidth != nil {\n\t\t\t\twidths[idx] = *requestedWidth\n\t\t\t\tvariableColumns--\n\t\t\t\tavailableWidth -= widths[idx]\n\t\t\t}\n\t\t}\n\n\t\t// at least one column must have a variable width, force the last column to be variable if there are no\n\t\t// variable columns\n\t\tif variableColumns == 0 {\n\t\t\tvariableColumns = 1\n\t\t\twidths[len(widths)-1] = -1\n\t\t}\n\n\t\tdefaultWidth := availableWidth / variableColumns\n\n\t\t// second pass: layout columns left to right (based off predetermined widths)\n\t\tfor idx, element := range elements {\n\t\t\t// use the requested or default width\n\t\t\twidth := widths[idx]\n\t\t\tif width == -1 {\n\t\t\t\twidth = defaultWidth\n\t\t\t}\n\n\t\t\t// layout the column within the allocated space\n\t\t\terr := element.Layout(g, area.minX, area.minY, area.minX+width, area.maxY)\n\t\t\tif err != nil {\n\t\t\t\treturn area, fmt.Errorf(\"failed to layout '%s' column: %w\", element.Name(), err)\n\t\t\t}\n\n\t\t\t// move left to right, scratching off real estate as it is taken\n\t\t\tarea.minX += width\n\t\t}\n\t}\n\treturn area, nil\n}\n\nfunc (lm *Manager) layoutFooters(g *gocui.Gui, area Area, footerHeights []int) error {\n\t// layout footers top down (which is why the list is reversed). Top down is needed due to border overlap.\n\tif elements, exists := lm.elements[LocationFooter]; exists {\n\t\tfor idx := len(elements) - 1; idx >= 0; idx-- {\n\t\t\telement := elements[idx]\n\t\t\theight := footerHeights[idx]\n\t\t\tvar topY, bottomY, bottomPadding int\n\t\t\tfor oIdx := 0; oIdx <= idx; oIdx++ {\n\t\t\t\tbottomPadding += footerHeights[oIdx]\n\t\t\t}\n\t\t\ttopY = area.maxY - bottomPadding - height\n\t\t\t// +1 for border\n\t\t\tbottomY = topY + height + 1\n\n\t\t\t// layout the footer within the allocated space\n\t\t\t// note: since the headers and rows are inclusive counting from -1 (to account for a border) we must\n\t\t\t// do the same vertically, thus a -1 is needed for a starting Y\n\t\t\terr := element.Layout(g, area.minX, topY, area.maxX, bottomY)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to layout %q footer: %w\", element.Name(), err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (lm *Manager) notifyLayoutChange() error {\n\tfor _, elements := range lm.elements {\n\t\tfor _, element := range elements {\n\t\t\terr := element.OnLayoutChange()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (lm *Manager) Layout(g *gocui.Gui) error {\n\tcurMaxX, curMaxY := g.Size()\n\treturn lm.layout(g, curMaxX, curMaxY)\n}\n\n// layout defines the definition of the window pane size and placement relations to one another. This\n// is invoked at application start and whenever the screen dimensions change.\n// A few things to note:\n//  1. gocui has borders around all views (even if Frame=false). This means there are a lot of +1/-1 magic numbers\n//     needed (but there are comments!).\n//  2. since there are borders, in order for it to appear as if there aren't any spaces for borders, the views must\n//     overlap. To prevent screen artifacts, all elements must be layedout from the top of the screen to the bottom.\nfunc (lm *Manager) layout(g *gocui.Gui, curMaxX, curMaxY int) error {\n\tvar headerAreaChanged, footerAreaChanged, columnAreaChanged bool\n\n\t// compare current screen size with the last known size at time of layout\n\tarea := Area{\n\t\tminX: -1,\n\t\tminY: -1,\n\t\tmaxX: curMaxX,\n\t\tmaxY: curMaxY,\n\t}\n\n\tvar hasResized bool\n\tif curMaxX != lm.lastX || curMaxY != lm.lastY {\n\t\thasResized = true\n\t}\n\tlm.lastX, lm.lastY = curMaxX, curMaxY\n\n\t// pass 1: plan and layout elements\n\n\t// headers...\n\tarea, err := lm.planAndLayoutHeaders(g, area)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif area != lm.lastHeaderArea {\n\t\theaderAreaChanged = true\n\t}\n\tlm.lastHeaderArea = area\n\n\t// plan footers... don't layout until all columns have been layedout. This is necessary since we must layout from\n\t// top to bottom, but we need the real estate planned for the footers to determine the bottom of the columns.\n\tvar footerArea = area\n\tarea, footerHeights := lm.planFooters(g, area)\n\n\tif area != lm.lastFooterArea {\n\t\tfooterAreaChanged = true\n\t}\n\tlm.lastFooterArea = area\n\n\t// columns...\n\tarea, err = lm.planAndLayoutColumns(g, area)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif area != lm.lastColumnArea {\n\t\tcolumnAreaChanged = true\n\t}\n\tlm.lastColumnArea = area\n\n\t// footers... layout according to the original available area and planned heights\n\terr = lm.layoutFooters(g, footerArea, footerHeights)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\t// pass 2: notify everyone of a layout change (allow to update and render)\n\t// note: this may mean that each element will update and rerender, which may cause a secondary layout call.\n\t// the conditions which we notify elements of layout changes must be very selective!\n\tif hasResized || headerAreaChanged || footerAreaChanged || columnAreaChanged {\n\t\treturn lm.notifyLayoutChange()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/layout/manager_test.go",
    "content": "package layout\n\nimport (\n\t\"testing\"\n\n\t\"github.com/awesome-gocui/gocui\"\n)\n\ntype testElement struct {\n\tt          *testing.T\n\tsize       int\n\tlayoutArea Area\n\tlocation   Location\n}\n\nfunc newTestElement(t *testing.T, size int, layoutArea Area, location Location) *testElement {\n\treturn &testElement{\n\t\tt:          t,\n\t\tsize:       size,\n\t\tlayoutArea: layoutArea,\n\t\tlocation:   location,\n\t}\n}\n\nfunc (te *testElement) Name() string {\n\treturn \"dont care\"\n}\nfunc (te *testElement) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {\n\tactualLayoutArea := Area{\n\t\tminX: minX,\n\t\tminY: minY,\n\t\tmaxX: maxX,\n\t\tmaxY: maxY,\n\t}\n\n\tif te.layoutArea != actualLayoutArea {\n\t\tte.t.Errorf(\"expected layout area '%+v', got '%+v'\", te.layoutArea, actualLayoutArea)\n\t}\n\treturn nil\n}\nfunc (te *testElement) RequestedSize(available int) *int {\n\tif te.size == -1 {\n\t\treturn nil\n\t}\n\treturn &te.size\n}\nfunc (te *testElement) IsVisible() bool {\n\treturn true\n}\nfunc (te *testElement) OnLayoutChange() error {\n\treturn nil\n}\n\ntype layoutReturn struct {\n\tarea Area\n\terr  error\n}\n\nfunc Test_planAndLayoutHeaders(t *testing.T) {\n\n\ttable := map[string]struct {\n\t\theaders  []*testElement\n\t\texpected layoutReturn\n\t}{\n\t\t\"single header\": {\n\t\t\theaders: []*testElement{newTestElement(t, 1, Area{\n\t\t\t\tminX: -1,\n\t\t\t\tminY: -1,\n\t\t\t\tmaxX: 120,\n\t\t\t\tmaxY: 0,\n\t\t\t}, LocationHeader)},\n\t\t\texpected: layoutReturn{\n\t\t\t\tarea: Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: 0,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t},\n\t\t},\n\t\t\"two headers\": {\n\t\t\theaders: []*testElement{\n\t\t\t\tnewTestElement(t, 1, Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 0,\n\t\t\t\t}, LocationHeader),\n\t\t\t\tnewTestElement(t, 1, Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: 0,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 1,\n\t\t\t\t}, LocationHeader),\n\t\t\t},\n\t\t\texpected: layoutReturn{\n\t\t\t\tarea: Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: 1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t},\n\t\t},\n\t\t\"two odd-sized headers\": {\n\t\t\theaders: []*testElement{\n\t\t\t\tnewTestElement(t, 2, Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 1,\n\t\t\t\t}, LocationHeader),\n\t\t\t\tnewTestElement(t, 3, Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: 1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 4,\n\t\t\t\t}, LocationHeader),\n\t\t\t},\n\t\t\texpected: layoutReturn{\n\t\t\t\tarea: Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: 4,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, test := range table {\n\t\tt.Log(\"case: \", name, \" ---\")\n\t\tlm := NewManager()\n\t\tfor _, element := range test.headers {\n\t\t\tlm.Add(element, element.location)\n\t\t}\n\n\t\tarea, err := lm.planAndLayoutHeaders(nil, Area{\n\t\t\tminX: -1,\n\t\t\tminY: -1,\n\t\t\tmaxX: 120,\n\t\t\tmaxY: 80,\n\t\t})\n\n\t\tif err != test.expected.err {\n\t\t\tt.Errorf(\"%s: expected err '%+v', got error '%+v'\", name, test.expected.err, err)\n\t\t}\n\n\t\tif area != test.expected.area {\n\t\t\tt.Errorf(\"%s: expected returned area '%+v', got area '%+v'\", name, test.expected.area, area)\n\t\t}\n\n\t}\n}\n\nfunc Test_planAndLayoutColumns(t *testing.T) {\n\n\ttable := map[string]struct {\n\t\tcolumns  []*testElement\n\t\texpected layoutReturn\n\t}{\n\t\t\"single column\": {\n\t\t\tcolumns: []*testElement{newTestElement(t, -1, Area{\n\t\t\t\tminX: -1,\n\t\t\t\tminY: -1,\n\t\t\t\tmaxX: 120,\n\t\t\t\tmaxY: 80,\n\t\t\t}, LocationColumn)},\n\t\t\texpected: layoutReturn{\n\t\t\t\tarea: Area{\n\t\t\t\t\tminX: 120,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t},\n\t\t},\n\t\t\"two equal columns\": {\n\t\t\tcolumns: []*testElement{\n\t\t\t\tnewTestElement(t, -1, Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 59,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t}, LocationColumn),\n\t\t\t\tnewTestElement(t, -1, Area{\n\t\t\t\t\tminX: 59,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 119,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t}, LocationColumn),\n\t\t\t},\n\t\t\texpected: layoutReturn{\n\t\t\t\tarea: Area{\n\t\t\t\t\tminX: 119,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t},\n\t\t},\n\t\t\"two odd-sized columns\": {\n\t\t\tcolumns: []*testElement{\n\t\t\t\tnewTestElement(t, 30, Area{\n\t\t\t\t\tminX: -1,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 29,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t}, LocationColumn),\n\t\t\t\tnewTestElement(t, -1, Area{\n\t\t\t\t\tminX: 29,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t}, LocationColumn),\n\t\t\t},\n\t\t\texpected: layoutReturn{\n\t\t\t\tarea: Area{\n\t\t\t\t\tminX: 120,\n\t\t\t\t\tminY: -1,\n\t\t\t\t\tmaxX: 120,\n\t\t\t\t\tmaxY: 80,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, test := range table {\n\t\tt.Log(\"case: \", name, \" ---\")\n\t\tlm := NewManager()\n\t\tfor _, element := range test.columns {\n\t\t\tlm.Add(element, element.location)\n\t\t}\n\n\t\tarea, err := lm.planAndLayoutColumns(nil, Area{\n\t\t\tminX: -1,\n\t\t\tminY: -1,\n\t\t\tmaxX: 120,\n\t\t\tmaxY: 80,\n\t\t})\n\n\t\tif err != test.expected.err {\n\t\t\tt.Errorf(\"%s: expected err '%+v', got error '%+v'\", name, test.expected.err, err)\n\t\t}\n\n\t\tif area != test.expected.area {\n\t\t\tt.Errorf(\"%s: expected returned area '%+v', got area '%+v'\", name, test.expected.area, area)\n\t\t}\n\n\t}\n}\n\nfunc Test_layout(t *testing.T) {\n\n\ttable := map[string]struct {\n\t\telements []*testElement\n\t}{\n\t\t\"1 header + 1 footer + 1 column\": {\n\t\t\telements: []*testElement{\n\t\t\t\tnewTestElement(t, 1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: -1,\n\t\t\t\t\t\tmaxX: 120,\n\t\t\t\t\t\tmaxY: 0,\n\t\t\t\t\t}, LocationHeader),\n\t\t\t\tnewTestElement(t, 1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: 78,\n\t\t\t\t\t\tmaxX: 120,\n\t\t\t\t\t\tmaxY: 80,\n\t\t\t\t\t}, LocationFooter),\n\t\t\t\tnewTestElement(t, -1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: 0,\n\t\t\t\t\t\tmaxX: 120,\n\t\t\t\t\t\tmaxY: 79,\n\t\t\t\t\t}, LocationColumn),\n\t\t\t},\n\t\t},\n\t\t\"1 header + 1 footer + 3 column\": {\n\t\t\telements: []*testElement{\n\t\t\t\tnewTestElement(t, 1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: -1,\n\t\t\t\t\t\tmaxX: 120,\n\t\t\t\t\t\tmaxY: 0,\n\t\t\t\t\t}, LocationHeader),\n\t\t\t\tnewTestElement(t, 1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: 78,\n\t\t\t\t\t\tmaxX: 120,\n\t\t\t\t\t\tmaxY: 80,\n\t\t\t\t\t}, LocationFooter),\n\t\t\t\tnewTestElement(t, -1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: 0,\n\t\t\t\t\t\tmaxX: 39,\n\t\t\t\t\t\tmaxY: 79,\n\t\t\t\t\t}, LocationColumn),\n\t\t\t\tnewTestElement(t, -1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: 39,\n\t\t\t\t\t\tminY: 0,\n\t\t\t\t\t\tmaxX: 79,\n\t\t\t\t\t\tmaxY: 79,\n\t\t\t\t\t}, LocationColumn),\n\t\t\t\tnewTestElement(t, -1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: 79,\n\t\t\t\t\t\tminY: 0,\n\t\t\t\t\t\tmaxX: 119,\n\t\t\t\t\t\tmaxY: 79,\n\t\t\t\t\t}, LocationColumn),\n\t\t\t},\n\t\t},\n\t\t\"1 header + 1 footer + 2 equal columns + 1 sized column\": {\n\t\t\telements: []*testElement{\n\t\t\t\tnewTestElement(t, 1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: -1,\n\t\t\t\t\t\tmaxX: 120,\n\t\t\t\t\t\tmaxY: 0,\n\t\t\t\t\t}, LocationHeader),\n\t\t\t\tnewTestElement(t, 1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: 78,\n\t\t\t\t\t\tmaxX: 120,\n\t\t\t\t\t\tmaxY: 80,\n\t\t\t\t\t}, LocationFooter),\n\t\t\t\tnewTestElement(t, -1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: -1,\n\t\t\t\t\t\tminY: 0,\n\t\t\t\t\t\tmaxX: 19,\n\t\t\t\t\t\tmaxY: 79,\n\t\t\t\t\t}, LocationColumn),\n\t\t\t\tnewTestElement(t, 80,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: 19,\n\t\t\t\t\t\tminY: 0,\n\t\t\t\t\t\tmaxX: 99,\n\t\t\t\t\t\tmaxY: 79,\n\t\t\t\t\t}, LocationColumn),\n\t\t\t\tnewTestElement(t, -1,\n\t\t\t\t\tArea{\n\t\t\t\t\t\tminX: 99,\n\t\t\t\t\t\tminY: 0,\n\t\t\t\t\t\tmaxX: 119,\n\t\t\t\t\t\tmaxY: 79,\n\t\t\t\t\t}, LocationColumn),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, test := range table {\n\t\tt.Log(\"case: \", name, \" ---\")\n\t\tlm := NewManager()\n\t\tfor _, element := range test.elements {\n\t\t\tlm.Add(element, element.location)\n\t\t}\n\n\t\terr := lm.layout(nil, 120, 80)\n\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%s: unexpected error: %+v\", name, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/cursor.go",
    "content": "package view\n\nimport (\n\t\"errors\"\n\n\t\"github.com/awesome-gocui/gocui\"\n)\n\n// CursorDown moves the cursor down in the currently selected gocui pane, scrolling the screen as needed.\nfunc CursorDown(g *gocui.Gui, v *gocui.View) error {\n\treturn CursorStep(g, v, 1)\n}\n\n// CursorUp moves the cursor up in the currently selected gocui pane, scrolling the screen as needed.\nfunc CursorUp(g *gocui.Gui, v *gocui.View) error {\n\treturn CursorStep(g, v, -1)\n}\n\n// Moves the cursor the given step distance, setting the origin to the new cursor line\nfunc CursorStep(g *gocui.Gui, v *gocui.View, step int) error {\n\tcx, cy := v.Cursor()\n\n\t// if there isn't a next line\n\tline, err := v.Line(cy + step)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(line) == 0 {\n\t\treturn errors.New(\"unable to move the cursor, empty line\")\n\t}\n\tif err := v.SetCursor(cx, cy+step); err != nil {\n\t\tox, oy := v.Origin()\n\t\tif err := v.SetOrigin(ox, oy+step); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/debug.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/dive/internal/utils\"\n)\n\n// Debug is just for me :)\ntype Debug struct {\n\tname   string\n\tgui    *gocui.Gui\n\tview   *gocui.View\n\theader *gocui.View\n\tlogger logger.Logger\n\n\tselectedView Helper\n}\n\n// newDebugView creates a new view object attached the global [gocui] screen object.\nfunc newDebugView(gui *gocui.Gui) *Debug {\n\tc := new(Debug)\n\n\t// populate main fields\n\tc.name = \"debug\"\n\tc.gui = gui\n\tc.logger = log.Nested(\"ui\", \"debug\")\n\n\treturn c\n}\n\nfunc (v *Debug) SetCurrentView(r Helper) {\n\tv.selectedView = r\n}\n\nfunc (v *Debug) Name() string {\n\treturn v.name\n}\n\n// Setup initializes the UI concerns within the context of a global [gocui] view object.\nfunc (v *Debug) Setup(view *gocui.View, header *gocui.View) error {\n\tv.logger.Trace(\"setup()\")\n\n\t// set controller options\n\tv.view = view\n\tv.view.Editable = false\n\tv.view.Wrap = false\n\tv.view.Frame = false\n\n\tv.header = header\n\tv.header.Editable = false\n\tv.header.Wrap = false\n\tv.header.Frame = false\n\n\treturn v.Render()\n}\n\n// IsVisible indicates if the status view pane is currently initialized.\nfunc (v *Debug) IsVisible() bool {\n\treturn v != nil\n}\n\n// Update refreshes the state objects for future rendering (currently does nothing).\nfunc (v *Debug) Update() error {\n\treturn nil\n}\n\n// OnLayoutChange is called whenever the screen dimensions are changed\nfunc (v *Debug) OnLayoutChange() error {\n\terr := v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// Render flushes the state objects to the screen.\nfunc (v *Debug) Render() error {\n\tv.logger.Trace(\"render()\")\n\n\tv.gui.Update(func(g *gocui.Gui) error {\n\t\t// update header...\n\t\tv.header.Clear()\n\t\twidth, _ := g.Size()\n\t\theaderStr := format.RenderHeader(\"Debug\", width, false)\n\t\t_, _ = fmt.Fprintln(v.header, headerStr)\n\n\t\t// update view...\n\t\tv.view.Clear()\n\t\t_, err := fmt.Fprintln(v.view, \"blerg\")\n\t\tif err != nil {\n\t\t\tv.logger.WithFields(\"error\", err).Debug(\"unable to write to buffer\")\n\t\t}\n\n\t\treturn nil\n\t})\n\treturn nil\n}\n\nfunc (v *Debug) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {\n\tv.logger.Tracef(\"layout(minX: %d, minY: %d, maxX: %d, maxY: %d)\", minX, minY, maxX, maxY)\n\n\t// header\n\theaderSize := 1\n\t// note: maxY needs to account for the (invisible) border, thus a +1\n\theader, headerErr := g.SetView(v.Name()+\"header\", minX, minY, maxX, minY+headerSize+1, 0)\n\t// we are going to overlap the view over the (invisible) border (so minY will be one less than expected).\n\t// additionally, maxY will be bumped by one to include the border\n\tview, viewErr := g.SetView(v.Name(), minX, minY+headerSize, maxX, maxY+1, 0)\n\tif utils.IsNewView(viewErr, headerErr) {\n\t\terr := v.Setup(view, header)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to setup debug controller: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (v *Debug) RequestedSize(available int) *int {\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/filetree.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/dive/internal/utils\"\n\t\"regexp\"\n\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/dive/filetree\"\n)\n\ntype ViewOptionChangeListener func() error\n\ntype ViewExtractListener func(string) error\n\n// FileTree holds the UI objects and data models for populating the right pane. Specifically, the pane that\n// shows selected layer or aggregate file ASCII tree.\ntype FileTree struct {\n\tname   string\n\tgui    *gocui.Gui\n\tview   *gocui.View\n\theader *gocui.View\n\tvm     *viewmodel.FileTreeViewModel\n\ttitle  string\n\tkb     key.Bindings\n\tlogger logger.Logger\n\n\tfilterRegex         *regexp.Regexp\n\tlisteners           []ViewOptionChangeListener\n\textractListeners    []ViewExtractListener\n\thelpKeys            []*key.Binding\n\trequestedWidthRatio float64\n}\n\n// newFileTreeView creates a new view object attached the global [gocui] screen object.\nfunc newFileTreeView(gui *gocui.Gui, cfg v1.Config, initial int) (v *FileTree, err error) {\n\tv = new(FileTree)\n\tv.logger = log.Nested(\"ui\", \"filetree\")\n\tv.listeners = make([]ViewOptionChangeListener, 0)\n\n\t// populate main fields\n\tv.name = \"filetree\"\n\tv.gui = gui\n\tv.kb = cfg.Preferences.KeyBindings\n\tv.vm, err = viewmodel.NewFileTreeViewModel(cfg, initial)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequestedWidthRatio := cfg.Preferences.FiletreePaneWidth\n\tif requestedWidthRatio >= 1 || requestedWidthRatio <= 0 {\n\t\tv.logger.Warnf(\"invalid config value: 'filetree.pane-width' should be 0 < value < 1, given '%v'\", requestedWidthRatio)\n\n\t\trequestedWidthRatio = 0.5\n\t}\n\tv.requestedWidthRatio = requestedWidthRatio\n\n\treturn v, err\n}\n\nfunc (v *FileTree) AddViewOptionChangeListener(listener ...ViewOptionChangeListener) {\n\tv.listeners = append(v.listeners, listener...)\n}\n\nfunc (v *FileTree) AddViewExtractListener(listener ...ViewExtractListener) {\n\tv.extractListeners = append(v.extractListeners, listener...)\n}\n\nfunc (v *FileTree) SetTitle(title string) {\n\tv.title = title\n}\n\nfunc (v *FileTree) SetFilterRegex(filterRegex *regexp.Regexp) {\n\tv.filterRegex = filterRegex\n}\n\nfunc (v *FileTree) Name() string {\n\treturn v.name\n}\n\n// Setup initializes the UI concerns within the context of a global [gocui] view object.\nfunc (v *FileTree) Setup(view, header *gocui.View) error {\n\tlog.Trace(\"setup()\")\n\n\t// set controller options\n\tv.view = view\n\tv.view.Editable = false\n\tv.view.Wrap = false\n\tv.view.Frame = false\n\n\tv.header = header\n\tv.header.Editable = false\n\tv.header.Wrap = false\n\tv.header.Frame = false\n\n\tvar infos = []key.BindingInfo{\n\t\t{\n\t\t\tConfig:   v.kb.Filetree.ToggleCollapseDir,\n\t\t\tOnAction: v.toggleCollapse,\n\t\t\tDisplay:  \"Collapse dir\",\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Filetree.ToggleCollapseAllDir,\n\t\t\tOnAction: v.toggleCollapseAll,\n\t\t\tDisplay:  \"Collapse all dir\",\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Filetree.ToggleSortOrder,\n\t\t\tOnAction: v.toggleSortOrder,\n\t\t\tDisplay:  \"Toggle sort order\",\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Filetree.ExtractFile,\n\t\t\tOnAction: v.extractFile,\n\t\t\tDisplay:  \"Extract File\",\n\t\t},\n\t\t{\n\t\t\tConfig:     v.kb.Filetree.ToggleAddedFiles,\n\t\t\tOnAction:   func() error { return v.toggleShowDiffType(filetree.Added) },\n\t\t\tIsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Added] },\n\t\t\tDisplay:    \"Added\",\n\t\t},\n\t\t{\n\t\t\tConfig:     v.kb.Filetree.ToggleRemovedFiles,\n\t\t\tOnAction:   func() error { return v.toggleShowDiffType(filetree.Removed) },\n\t\t\tIsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Removed] },\n\t\t\tDisplay:    \"Removed\",\n\t\t},\n\t\t{\n\t\t\tConfig:     v.kb.Filetree.ToggleModifiedFiles,\n\t\t\tOnAction:   func() error { return v.toggleShowDiffType(filetree.Modified) },\n\t\t\tIsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Modified] },\n\t\t\tDisplay:    \"Modified\",\n\t\t},\n\t\t{\n\t\t\tConfig:     v.kb.Filetree.ToggleUnmodifiedFiles,\n\t\t\tOnAction:   func() error { return v.toggleShowDiffType(filetree.Unmodified) },\n\t\t\tIsSelected: func() bool { return !v.vm.HiddenDiffTypes[filetree.Unmodified] },\n\t\t\tDisplay:    \"Unmodified\",\n\t\t},\n\t\t{\n\t\t\tConfig:     v.kb.Filetree.ToggleTreeAttributes,\n\t\t\tOnAction:   v.toggleAttributes,\n\t\t\tIsSelected: func() bool { return v.vm.ShowAttributes },\n\t\t\tDisplay:    \"Attributes\",\n\t\t},\n\t\t{\n\t\t\tConfig:     v.kb.Filetree.ToggleWrapTree,\n\t\t\tOnAction:   v.toggleWrapTree,\n\t\t\tIsSelected: func() bool { return v.view.Wrap },\n\t\t\tDisplay:    \"Wrap\",\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.PageUp,\n\t\t\tOnAction: v.PageUp,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.PageDown,\n\t\t\tOnAction: v.PageDown,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Down,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorDown,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Up,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorUp,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Left,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorLeft,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Right,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorRight,\n\t\t},\n\t}\n\n\thelpKeys, err := key.GenerateBindings(v.gui, v.name, infos)\n\tif err != nil {\n\t\treturn err\n\t}\n\tv.helpKeys = helpKeys\n\n\t_, height := v.view.Size()\n\tv.vm.Setup(0, height)\n\t_ = v.Update()\n\t_ = v.Render()\n\n\treturn nil\n}\n\n// IsVisible indicates if the file tree view pane is currently initialized\nfunc (v *FileTree) IsVisible() bool {\n\treturn v != nil\n}\n\n// ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.\nfunc (v *FileTree) resetCursor() {\n\t_ = v.view.SetCursor(0, 0)\n\tv.vm.ResetCursor()\n}\n\n// SetTreeByLayer populates the view model by stacking the indicated image layer file trees.\nfunc (v *FileTree) SetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {\n\terr := v.vm.SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_ = v.Update()\n\treturn v.Render()\n}\n\n// CursorDown moves the cursor down and renders the view.\n// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.\n// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing\n// this range into the view buffer. This is much faster when tree sizes are large.\nfunc (v *FileTree) CursorDown() error {\n\tif v.vm.CursorDown() {\n\t\treturn v.Render()\n\t}\n\treturn nil\n}\n\n// CursorUp moves the cursor up and renders the view.\n// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.\n// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing\n// this range into the view buffer. This is much faster when tree sizes are large.\nfunc (v *FileTree) CursorUp() error {\n\tif v.vm.CursorUp() {\n\t\treturn v.Render()\n\t}\n\treturn nil\n}\n\n// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree\nfunc (v *FileTree) CursorLeft() error {\n\terr := v.vm.CursorLeft(v.filterRegex)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_ = v.Update()\n\treturn v.Render()\n}\n\n// CursorRight descends into directory expanding it if needed\nfunc (v *FileTree) CursorRight() error {\n\terr := v.vm.CursorRight(v.filterRegex)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_ = v.Update()\n\treturn v.Render()\n}\n\n// PageDown moves to next page putting the cursor on top\nfunc (v *FileTree) PageDown() error {\n\terr := v.vm.PageDown()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// PageUp moves to previous page putting the cursor on top\nfunc (v *FileTree) PageUp() error {\n\terr := v.vm.PageUp()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.\n// func (controller *FileTree) getAbsPositionNode() (node *filetree.FileNode) {\n// \treturn controller.vm.getAbsPositionNode(filterRegex())\n// }\n\n// ToggleCollapse will collapse/expand the selected FileNode.\nfunc (v *FileTree) toggleCollapse() error {\n\terr := v.vm.ToggleCollapse(v.filterRegex)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_ = v.Update()\n\treturn v.Render()\n}\n\n// ToggleCollapseAll will collapse/expand the all directories.\nfunc (v *FileTree) toggleCollapseAll() error {\n\terr := v.vm.ToggleCollapseAll()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif v.vm.CollapseAll {\n\t\tv.resetCursor()\n\t}\n\t_ = v.Update()\n\treturn v.Render()\n}\n\nfunc (v *FileTree) toggleSortOrder() error {\n\terr := v.vm.ToggleSortOrder()\n\tif err != nil {\n\t\treturn err\n\t}\n\tv.resetCursor()\n\t_ = v.Update()\n\treturn v.Render()\n}\n\nfunc (v *FileTree) extractFile() error {\n\tnode := v.vm.CurrentNode(v.filterRegex)\n\tfor _, listener := range v.extractListeners {\n\t\terr := listener(node.Path())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (v *FileTree) toggleWrapTree() error {\n\tv.view.Wrap = !v.view.Wrap\n\n\terr := v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = v.Render()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// we need to render the changes to the status pane as well (not just this contoller/view)\n\treturn v.notifyOnViewOptionChangeListeners()\n}\n\nfunc (v *FileTree) notifyOnViewOptionChangeListeners() error {\n\tfor _, listener := range v.listeners {\n\t\terr := listener()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"notifyOnViewOptionChangeListeners error: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// ToggleAttributes will show/hide file attributes\nfunc (v *FileTree) toggleAttributes() error {\n\terr := v.vm.ToggleAttributes()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = v.Render()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// we need to render the changes to the status pane as well (not just this controller/view)\n\treturn v.notifyOnViewOptionChangeListeners()\n}\n\n// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane.\nfunc (v *FileTree) toggleShowDiffType(diffType filetree.DiffType) error {\n\tv.vm.ToggleShowDiffType(diffType)\n\n\terr := v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = v.Render()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// we need to render the changes to the status pane as well (not just this controller/view)\n\treturn v.notifyOnViewOptionChangeListeners()\n}\n\n// OnLayoutChange is called by the UI framework to inform the view-model of the new screen dimensions\nfunc (v *FileTree) OnLayoutChange() error {\n\terr := v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// Update refreshes the state objects for future rendering.\nfunc (v *FileTree) Update() error {\n\tvar width, height int\n\n\tif v.view != nil {\n\t\twidth, height = v.view.Size()\n\t} else {\n\t\t// before the TUI is setup there may not be a controller to reference. Use the entire screen as reference.\n\t\twidth, height = v.gui.Size()\n\t}\n\t// height should account for the header\n\treturn v.vm.Update(v.filterRegex, width, height-1)\n}\n\n// Render flushes the state objects (file tree) to the pane.\nfunc (v *FileTree) Render() error {\n\tv.logger.Trace(\"render()\")\n\n\ttitle := v.title\n\tisSelected := v.gui.CurrentView() == v.view\n\n\tv.gui.Update(func(g *gocui.Gui) error {\n\t\t// update the header\n\t\tv.header.Clear()\n\t\twidth, _ := g.Size()\n\t\theaderStr := format.RenderHeader(title, width, isSelected)\n\t\tif v.vm.ShowAttributes {\n\t\t\theaderStr += fmt.Sprintf(filetree.AttributeFormat+\" %s\", \"P\", \"ermission\", \"UID:GID\", \"Size\", \"Filetree\")\n\t\t}\n\t\t_, _ = fmt.Fprintln(v.header, headerStr)\n\n\t\t// update the contents\n\t\tv.view.Clear()\n\t\terr := v.vm.Render()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = fmt.Fprint(v.view, v.vm.Buffer.String())\n\n\t\treturn err\n\t})\n\treturn nil\n}\n\n// KeyHelp indicates all the possible actions a user can take while the current pane is selected.\nfunc (v *FileTree) KeyHelp() string {\n\tvar help string\n\tfor _, binding := range v.helpKeys {\n\t\thelp += binding.RenderKeyHelp()\n\t}\n\treturn help\n}\n\nfunc (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {\n\tv.logger.Tracef(\"layout(minX: %d, minY: %d, maxX: %d, maxY: %d)\", minX, minY, maxX, maxY)\n\tattributeRowSize := 0\n\n\t// make the layout responsive to the available realestate. Make more room for the main content by hiding auxiliary\n\t// content when there is not enough room\n\tif maxX-minX < 60 {\n\t\tv.vm.ConstrainLayout()\n\t} else {\n\t\tv.vm.ExpandLayout()\n\t}\n\n\tif v.vm.ShowAttributes {\n\t\tattributeRowSize = 1\n\t}\n\n\t// header + attribute header\n\theaderSize := 1 + attributeRowSize\n\t// note: maxY needs to account for the (invisible) border, thus a +1\n\theader, headerErr := g.SetView(v.Name()+\"header\", minX, minY, maxX, minY+headerSize+1, 0)\n\t// we are going to overlap the view over the (invisible) border (so minY will be one less than expected).\n\t// additionally, maxY will be bumped by one to include the border\n\tview, viewErr := g.SetView(v.Name(), minX, minY+headerSize, maxX, maxY+1, 0)\n\tif utils.IsNewView(viewErr, headerErr) {\n\t\terr := v.Setup(view, header)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to setup tree controller: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (v *FileTree) RequestedSize(available int) *int {\n\t// var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio))\n\t// return &requestedWidth\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/filter.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/dive/internal/utils\"\n\t\"strings\"\n\n\t\"github.com/awesome-gocui/gocui\"\n)\n\ntype FilterEditListener func(string) error\n\n// Filter holds the UI objects and data models for populating the bottom row. Specifically the pane that\n// allows the user to filter the file tree by path.\ntype Filter struct {\n\tgui    *gocui.Gui\n\tview   *gocui.View\n\theader *gocui.View\n\tlogger logger.Logger\n\n\tlabelStr        string\n\tmaxLength       int\n\thidden          bool\n\trequestedHeight int\n\n\tfilterEditListeners []FilterEditListener\n}\n\n// newFilterView creates a new view object attached the global [gocui] screen object.\nfunc newFilterView(gui *gocui.Gui) *Filter {\n\tc := new(Filter)\n\tc.logger = log.Nested(\"ui\", \"filter\")\n\n\tc.filterEditListeners = make([]FilterEditListener, 0)\n\n\t// populate main fields\n\tc.gui = gui\n\tc.labelStr = \"Path Filter: \"\n\tc.hidden = true\n\n\tc.requestedHeight = 1\n\n\treturn c\n}\n\nfunc (v *Filter) AddFilterEditListener(listener ...FilterEditListener) {\n\tv.filterEditListeners = append(v.filterEditListeners, listener...)\n}\n\nfunc (v *Filter) Name() string {\n\treturn \"filter\"\n}\n\n// Setup initializes the UI concerns within the context of a global [gocui] view object.\nfunc (v *Filter) Setup(view, header *gocui.View) error {\n\tlog.Trace(\"Setup()\")\n\n\t// set controller options\n\tv.view = view\n\tv.maxLength = 200\n\tv.view.Frame = false\n\tv.view.BgColor = gocui.AttrReverse\n\tv.view.Editable = true\n\tv.view.Editor = v\n\n\tv.header = header\n\tv.header.BgColor = gocui.AttrReverse\n\tv.header.Editable = false\n\tv.header.Wrap = false\n\tv.header.Frame = false\n\n\treturn v.Render()\n}\n\n// ToggleFilterView shows/hides the file tree filter pane.\nfunc (v *Filter) ToggleVisible() error {\n\t// delete all user input from the tree view\n\tv.view.Clear()\n\n\t// toggle hiding\n\tv.hidden = !v.hidden\n\n\tif !v.hidden {\n\t\t_, err := v.gui.SetCurrentView(v.Name())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to toggle filter view: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// reset the cursor for the next time it is visible\n\t// Note: there is a subtle gocui behavior here where this cannot be called when the view\n\t// is newly visible. Is this a problem with dive or gocui?\n\treturn v.view.SetCursor(0, 0)\n}\n\n// IsVisible indicates if the filter view pane is currently initialized\nfunc (v *Filter) IsVisible() bool {\n\tif v == nil {\n\t\treturn false\n\t}\n\treturn !v.hidden\n}\n\n// Edit intercepts the key press events in the filer view to update the file view in real time.\nfunc (v *Filter) Edit(view *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {\n\tif !v.IsVisible() {\n\t\treturn\n\t}\n\n\tcx, _ := view.Cursor()\n\tox, _ := view.Origin()\n\tlimit := ox+cx+1 > v.maxLength\n\tswitch {\n\tcase ch != 0 && mod == 0 && !limit:\n\t\tview.EditWrite(ch)\n\tcase key == gocui.KeySpace && !limit:\n\t\tview.EditWrite(' ')\n\tcase key == gocui.KeyBackspace || key == gocui.KeyBackspace2:\n\t\tview.EditDelete(true)\n\t}\n\n\t// notify listeners\n\tv.notifyFilterEditListeners()\n}\n\nfunc (v *Filter) notifyFilterEditListeners() {\n\tcurrentValue := strings.TrimSpace(v.view.Buffer())\n\tfor _, listener := range v.filterEditListeners {\n\t\terr := listener(currentValue)\n\t\tif err != nil {\n\t\t\t// note: cannot propagate error from here since this is from the main gogui thread\n\t\t\tv.logger.WithFields(\"error\", err).Debug(\"unable to notify filter edit listeners\")\n\t\t}\n\t}\n}\n\n// Update refreshes the state objects for future rendering (currently does nothing).\nfunc (v *Filter) Update() error {\n\treturn nil\n}\n\n// Render flushes the state objects to the screen. Currently this is the users path filter input.\nfunc (v *Filter) Render() error {\n\tv.logger.Trace(\"render()\")\n\n\tv.gui.Update(func(g *gocui.Gui) error {\n\t\t_, err := fmt.Fprintln(v.header, format.Header(v.labelStr))\n\t\treturn err\n\t})\n\treturn nil\n}\n\n// KeyHelp indicates all the possible actions a user can take while the current pane is selected.\nfunc (v *Filter) KeyHelp() string {\n\treturn format.StatusControlNormal(\"▏Type to filter the file tree \")\n}\n\n// OnLayoutChange is called whenever the screen dimensions are changed\nfunc (v *Filter) OnLayoutChange() error {\n\terr := v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\nfunc (v *Filter) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {\n\tv.logger.Tracef(\"layout(minX: %d, minY: %d, maxX: %d, maxY: %d)\", minX, minY, maxX, maxY)\n\n\tlabel, labelErr := g.SetView(v.Name()+\"label\", minX, minY, len(v.labelStr), maxY, 0)\n\tview, viewErr := g.SetView(v.Name(), minX+(len(v.labelStr)-1), minY, maxX, maxY, 0)\n\n\tif utils.IsNewView(viewErr, labelErr) {\n\t\terr := v.Setup(view, label)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to setup filter controller: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (v *Filter) RequestedSize(available int) *int {\n\treturn &v.requestedHeight\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/image_details.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/wagoodman/dive/dive/filetree\"\n)\n\ntype ImageDetails struct {\n\tgui    *gocui.Gui\n\tbody   *gocui.View\n\theader *gocui.View\n\tlogger logger.Logger\n\n\timageName      string\n\timageSize      uint64\n\tefficiency     float64\n\tinefficiencies filetree.EfficiencySlice\n\tkb             key.Bindings\n}\n\nfunc (v *ImageDetails) Name() string {\n\treturn \"imageDetails\"\n}\n\nfunc (v *ImageDetails) Setup(body, header *gocui.View) error {\n\tv.logger = log.Nested(\"ui\", \"imageDetails\")\n\tv.logger.Trace(\"Setup()\")\n\n\tv.body = body\n\tv.body.Editable = false\n\tv.body.Wrap = true\n\tv.body.Highlight = true\n\tv.body.Frame = false\n\n\tv.header = header\n\tv.header.Editable = false\n\tv.header.Wrap = true\n\tv.header.Highlight = false\n\tv.header.Frame = false\n\n\tvar infos = []key.BindingInfo{\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Down,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorDown,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Up,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorUp,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.PageUp,\n\t\t\tOnAction: v.PageUp,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.PageDown,\n\t\t\tOnAction: v.PageDown,\n\t\t},\n\t}\n\n\t_, err := key.GenerateBindings(v.gui, v.Name(), infos)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// Render flushes the state objects to the screen. The details pane reports:\n// 1. the image efficiency score\n// 2. the estimated wasted image space\n// 3. a list of inefficient file allocations\nfunc (v *ImageDetails) Render() error {\n\tanalysisTemplate := \"%5s  %12s  %-s\\n\"\n\tinefficiencyReport := fmt.Sprintf(format.Header(analysisTemplate), \"Count\", \"Total Space\", \"Path\")\n\n\tvar wastedSpace int64\n\tfor idx := 0; idx < len(v.inefficiencies); idx++ {\n\t\tdata := v.inefficiencies[len(v.inefficiencies)-1-idx]\n\t\twastedSpace += data.CumulativeSize\n\n\t\tinefficiencyReport += fmt.Sprintf(analysisTemplate, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)\n\t}\n\n\timageNameStr := fmt.Sprintf(\"%s %s\", format.Header(\"Image name:\"), v.imageName)\n\timageSizeStr := fmt.Sprintf(\"%s %s\", format.Header(\"Total Image size:\"), humanize.Bytes(v.imageSize))\n\tefficiencyStr := fmt.Sprintf(\"%s %d %%\", format.Header(\"Image efficiency score:\"), int(100.0*v.efficiency))\n\twastedSpaceStr := fmt.Sprintf(\"%s %s\", format.Header(\"Potential wasted space:\"), humanize.Bytes(uint64(wastedSpace)))\n\n\tv.gui.Update(func(g *gocui.Gui) error {\n\t\twidth, _ := v.body.Size()\n\n\t\timageHeaderStr := format.RenderHeader(\"Image Details\", width, v.gui.CurrentView() == v.body)\n\n\t\tv.header.Clear()\n\t\t_, err := fmt.Fprintln(v.header, imageHeaderStr)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Debug(\"unable to write to buffer\")\n\t\t}\n\n\t\tvar lines = []string{\n\t\t\timageNameStr,\n\t\t\timageSizeStr,\n\t\t\twastedSpaceStr,\n\t\t\tefficiencyStr,\n\t\t\t\" \", // to avoid an empty line so CursorDown can work as expected\n\t\t\tinefficiencyReport,\n\t\t}\n\n\t\tv.body.Clear()\n\t\t_, err = fmt.Fprintln(v.body, strings.Join(lines, \"\\n\"))\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Debug(\"unable to write to buffer\")\n\t\t}\n\t\treturn err\n\t})\n\n\treturn nil\n}\n\nfunc (v *ImageDetails) OnLayoutChange() error {\n\tif err := v.Update(); err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// IsVisible indicates if the details view pane is currently initialized.\nfunc (v *ImageDetails) IsVisible() bool {\n\treturn v.body != nil\n}\n\nfunc (v *ImageDetails) PageUp() error {\n\t_, height := v.body.Size()\n\tif err := CursorStep(v.gui, v.body, -height); err != nil {\n\t\tv.logger.WithFields(\"error\", err).Debugf(\"couldn't move the cursor up by %d steps\", height)\n\t}\n\treturn nil\n}\n\nfunc (v *ImageDetails) PageDown() error {\n\t_, height := v.body.Size()\n\tif err := CursorStep(v.gui, v.body, height); err != nil {\n\t\tv.logger.WithFields(\"error\", err).Debugf(\"couldn't move the cursor down by %d steps\", height)\n\t}\n\treturn nil\n}\n\nfunc (v *ImageDetails) CursorUp() error {\n\tif err := CursorUp(v.gui, v.body); err != nil {\n\t\tv.logger.WithFields(\"error\", err).Debug(\"couldn't move the cursor up\")\n\t}\n\treturn nil\n}\n\nfunc (v *ImageDetails) CursorDown() error {\n\tif err := CursorDown(v.gui, v.body); err != nil {\n\t\tv.logger.WithFields(\"error\", err).Debug(\"couldn't move the cursor down\")\n\t}\n\treturn nil\n}\n\n// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).\nfunc (v *ImageDetails) KeyHelp() string {\n\treturn \"\"\n}\n\n// Update refreshes the state objects for future rendering.\nfunc (v *ImageDetails) Update() error {\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/layer.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\n// Layer holds the UI objects and data models for populating the lower-left pane.\n// Specifically the pane that shows the image layers and layer selector.\ntype Layer struct {\n\tname                  string\n\tgui                   *gocui.Gui\n\tbody                  *gocui.View\n\theader                *gocui.View\n\tvm                    *viewmodel.LayerSetState\n\tkb                    key.Bindings\n\tlogger                logger.Logger\n\tconstrainedRealEstate bool\n\n\tlisteners []LayerChangeListener\n\n\thelpKeys []*key.Binding\n}\n\n// newLayerView creates a new view object attached the global [gocui] screen object.\nfunc newLayerView(gui *gocui.Gui, cfg v1.Config) (c *Layer, err error) {\n\tc = new(Layer)\n\n\tc.logger = log.Nested(\"ui\", \"layer\")\n\tc.listeners = make([]LayerChangeListener, 0)\n\n\t// populate main fields\n\tc.name = \"layer\"\n\tc.gui = gui\n\tc.kb = cfg.Preferences.KeyBindings\n\n\tvar compareMode viewmodel.LayerCompareMode\n\n\tswitch mode := cfg.Preferences.ShowAggregatedLayerChanges; mode {\n\tcase true:\n\t\tcompareMode = viewmodel.CompareAllLayers\n\tcase false:\n\t\tcompareMode = viewmodel.CompareSingleLayer\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown layer.show-aggregated-changes value: %v\", mode)\n\t}\n\n\tc.vm = viewmodel.NewLayerSetState(cfg.Analysis.Layers, compareMode)\n\n\treturn c, err\n}\n\nfunc (v *Layer) AddLayerChangeListener(listener ...LayerChangeListener) {\n\tv.listeners = append(v.listeners, listener...)\n}\n\nfunc (v *Layer) notifyLayerChangeListeners() error {\n\tbottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.vm.GetCompareIndexes()\n\tselection := viewmodel.LayerSelection{\n\t\tLayer:           v.CurrentLayer(),\n\t\tBottomTreeStart: bottomTreeStart,\n\t\tBottomTreeStop:  bottomTreeStop,\n\t\tTopTreeStart:    topTreeStart,\n\t\tTopTreeStop:     topTreeStop,\n\t}\n\tfor _, listener := range v.listeners {\n\t\terr := listener(selection)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error notifying layer change listeners: %w\", err)\n\t\t}\n\t}\n\t// this is hacky, and I do not like it\n\tif layerDetails, err := v.gui.View(\"layerDetails\"); err == nil {\n\t\tif err := layerDetails.SetCursor(0, 0); err != nil {\n\t\t\tv.logger.Debug(\"Couldn't set cursor to 0,0 for layerDetails\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (v *Layer) Name() string {\n\treturn v.name\n}\n\n// Setup initializes the UI concerns within the context of a global [gocui] view object.\nfunc (v *Layer) Setup(body *gocui.View, header *gocui.View) error {\n\tv.logger.Trace(\"Setup()\")\n\n\t// set controller options\n\tv.body = body\n\tv.body.Editable = false\n\tv.body.Wrap = false\n\tv.body.Frame = false\n\n\tv.header = header\n\tv.header.Editable = false\n\tv.header.Wrap = false\n\tv.header.Frame = false\n\n\tvar infos = []key.BindingInfo{\n\t\t{\n\t\t\tConfig:     v.kb.Layer.CompareLayer,\n\t\t\tOnAction:   func() error { return v.setCompareMode(viewmodel.CompareSingleLayer) },\n\t\t\tIsSelected: func() bool { return v.vm.CompareMode == viewmodel.CompareSingleLayer },\n\t\t\tDisplay:    \"Show layer changes\",\n\t\t},\n\t\t{\n\t\t\tConfig:     v.kb.Layer.CompareAll,\n\t\t\tOnAction:   func() error { return v.setCompareMode(viewmodel.CompareAllLayers) },\n\t\t\tIsSelected: func() bool { return v.vm.CompareMode == viewmodel.CompareAllLayers },\n\t\t\tDisplay:    \"Show aggregated changes\",\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Down,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorDown,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Up,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorUp,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.PageUp,\n\t\t\tOnAction: v.PageUp,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.PageDown,\n\t\t\tOnAction: v.PageDown,\n\t\t},\n\t}\n\n\thelpKeys, err := key.GenerateBindings(v.gui, v.name, infos)\n\tif err != nil {\n\t\treturn err\n\t}\n\tv.helpKeys = helpKeys\n\n\treturn v.Render()\n}\n\n// height obtains the height of the current pane (taking into account the lost space due to the header).\nfunc (v *Layer) height() uint {\n\t_, height := v.body.Size()\n\treturn uint(height - 1)\n}\n\nfunc (v *Layer) CompareMode() viewmodel.LayerCompareMode {\n\treturn v.vm.CompareMode\n}\n\n// IsVisible indicates if the layer view pane is currently initialized.\nfunc (v *Layer) IsVisible() bool {\n\treturn v != nil\n}\n\n// PageDown moves to next page putting the cursor on top\nfunc (v *Layer) PageDown() error {\n\tstep := int(v.height()) + 1\n\ttargetLayerIndex := v.vm.LayerIndex + step\n\n\tif targetLayerIndex > len(v.vm.Layers) {\n\t\tstep -= targetLayerIndex - (len(v.vm.Layers) - 1)\n\t}\n\n\tif step > 0 {\n\t\t// err := CursorStep(v.gui, v.body, step)\n\t\terr := error(nil)\n\t\tif err == nil {\n\t\t\treturn v.SetCursor(v.vm.LayerIndex + step)\n\t\t}\n\t}\n\treturn nil\n}\n\n// PageUp moves to previous page putting the cursor on top\nfunc (v *Layer) PageUp() error {\n\tstep := int(v.height()) + 1\n\ttargetLayerIndex := v.vm.LayerIndex - step\n\n\tif targetLayerIndex < 0 {\n\t\tstep += targetLayerIndex\n\t}\n\n\tif step > 0 {\n\t\t// err := CursorStep(v.gui, v.body, -step)\n\t\terr := error(nil)\n\t\tif err == nil {\n\t\t\treturn v.SetCursor(v.vm.LayerIndex - step)\n\t\t}\n\t}\n\treturn nil\n}\n\n// CursorDown moves the cursor down in the layer pane (selecting a higher layer).\nfunc (v *Layer) CursorDown() error {\n\tif v.vm.LayerIndex < len(v.vm.Layers)-1 {\n\t\t// err := CursorDown(v.gui, v.body)\n\t\terr := error(nil)\n\t\tif err == nil {\n\t\t\treturn v.SetCursor(v.vm.LayerIndex + 1)\n\t\t}\n\t}\n\treturn nil\n}\n\n// CursorUp moves the cursor up in the layer pane (selecting a lower layer).\nfunc (v *Layer) CursorUp() error {\n\tif v.vm.LayerIndex > 0 {\n\t\t// err := CursorUp(v.gui, v.body)\n\t\terr := error(nil)\n\t\tif err == nil {\n\t\t\treturn v.SetCursor(v.vm.LayerIndex - 1)\n\t\t}\n\t}\n\treturn nil\n}\n\n// SetOrigin updates the origin of the layer view pane.\nfunc (v *Layer) SetOrigin(x, y int) error {\n\tif err := v.body.SetOrigin(x, y); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// SetCursor resets the cursor and orients the file tree view based on the given layer index.\nfunc (v *Layer) SetCursor(layer int) error {\n\tv.vm.LayerIndex = layer\n\terr := v.notifyLayerChangeListeners()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn v.Render()\n}\n\n// CurrentLayer returns the Layer object currently selected.\nfunc (v *Layer) CurrentLayer() *image.Layer {\n\treturn v.vm.Layers[v.vm.LayerIndex]\n}\n\n// setCompareMode switches the layer comparison between a single-layer comparison to an aggregated comparison.\nfunc (v *Layer) setCompareMode(compareMode viewmodel.LayerCompareMode) error {\n\tv.vm.CompareMode = compareMode\n\treturn v.notifyLayerChangeListeners()\n}\n\n// renderCompareBar returns the formatted string for the given layer.\nfunc (v *Layer) renderCompareBar(layerIdx int) string {\n\tbottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := v.vm.GetCompareIndexes()\n\tresult := \"  \"\n\n\tif layerIdx >= bottomTreeStart && layerIdx <= bottomTreeStop {\n\t\tresult = format.CompareBottom(\"  \")\n\t}\n\tif layerIdx >= topTreeStart && layerIdx <= topTreeStop {\n\t\tresult = format.CompareTop(\"  \")\n\t}\n\n\treturn result\n}\n\nfunc (v *Layer) ConstrainLayout() {\n\tif !v.constrainedRealEstate {\n\t\tv.logger.Debug(\"constraining layout\")\n\t\tv.constrainedRealEstate = true\n\t}\n}\n\nfunc (v *Layer) ExpandLayout() {\n\tif v.constrainedRealEstate {\n\t\tv.logger.Debug(\"expanding layout\")\n\t\tv.constrainedRealEstate = false\n\t}\n}\n\n// OnLayoutChange is called whenever the screen dimensions are changed\nfunc (v *Layer) OnLayoutChange() error {\n\terr := v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// Update refreshes the state objects for future rendering (currently does nothing).\nfunc (v *Layer) Update() error {\n\treturn nil\n}\n\n// Render flushes the state objects to the screen. The layers pane reports:\n// 1. the layers of the image + metadata\n// 2. the current selected image\nfunc (v *Layer) Render() error {\n\tv.logger.Trace(\"render()\")\n\n\t// indicate when selected\n\ttitle := \"Layers\"\n\tisSelected := v.gui.CurrentView() == v.body\n\n\tv.gui.Update(func(g *gocui.Gui) error {\n\t\tvar err error\n\t\t// update header\n\t\tv.header.Clear()\n\t\twidth, _ := g.Size()\n\t\tif v.constrainedRealEstate {\n\t\t\theaderStr := format.RenderNoHeader(width, isSelected)\n\t\t\theaderStr += \"\\nLayer\"\n\t\t\t_, err := fmt.Fprintln(v.header, headerStr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\theaderStr := format.RenderHeader(title, width, isSelected)\n\t\t\theaderStr += fmt.Sprintf(\"Cmp\"+image.LayerFormat, \"Size\", \"Command\")\n\t\t\t_, err := fmt.Fprintln(v.header, headerStr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// update contents\n\t\tv.body.Clear()\n\t\tfor idx, layer := range v.vm.Layers {\n\t\t\tvar layerStr string\n\t\t\tif v.constrainedRealEstate {\n\t\t\t\tlayerStr = fmt.Sprintf(\"%-4d\", layer.Index)\n\t\t\t} else {\n\t\t\t\tlayerStr = layer.String()\n\t\t\t}\n\n\t\t\tcompareBar := v.renderCompareBar(idx)\n\n\t\t\tif idx == v.vm.LayerIndex {\n\t\t\t\t_, err = fmt.Fprintln(v.body, compareBar+\" \"+format.Selected(layerStr))\n\t\t\t} else {\n\t\t\t\t_, err = fmt.Fprintln(v.body, compareBar+\" \"+layerStr)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// Adjust origin, if necessary\n\t\tmaxBodyDisplayHeight := int(v.height())\n\t\tif v.vm.LayerIndex > maxBodyDisplayHeight {\n\t\t\tif err := v.SetOrigin(0, v.vm.LayerIndex-maxBodyDisplayHeight); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\treturn nil\n}\n\nfunc (v *Layer) LayerCount() int {\n\treturn len(v.vm.Layers)\n}\n\n// KeyHelp indicates all the possible actions a user can take while the current pane is selected.\nfunc (v *Layer) KeyHelp() string {\n\tvar help string\n\tfor _, binding := range v.helpKeys {\n\t\thelp += binding.RenderKeyHelp()\n\t}\n\treturn help\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/layer_change_listener.go",
    "content": "package view\n\nimport (\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel\"\n)\n\ntype LayerChangeListener func(viewmodel.LayerSelection) error\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/layer_details.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"strings\"\n\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\ntype LayerDetails struct {\n\tgui          *gocui.Gui\n\theader       *gocui.View\n\tbody         *gocui.View\n\tCurrentLayer *image.Layer\n\tkb           key.Bindings\n\tlogger       logger.Logger\n}\n\nfunc (v *LayerDetails) Name() string {\n\treturn \"layerDetails\"\n}\n\nfunc (v *LayerDetails) Setup(body, header *gocui.View) error {\n\tv.logger = log.Nested(\"ui\", \"layerDetails\")\n\tv.logger.Trace(\"setup()\")\n\n\tv.body = body\n\tv.body.Editable = false\n\tv.body.Wrap = true\n\tv.body.Highlight = true\n\tv.body.Frame = false\n\n\tv.header = header\n\tv.header.Editable = false\n\tv.header.Wrap = true\n\tv.header.Highlight = false\n\tv.header.Frame = false\n\n\tvar infos = []key.BindingInfo{\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Down,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorDown,\n\t\t},\n\t\t{\n\t\t\tConfig:   v.kb.Navigation.Up,\n\t\t\tModifier: gocui.ModNone,\n\t\t\tOnAction: v.CursorUp,\n\t\t},\n\t}\n\n\t_, err := key.GenerateBindings(v.gui, v.Name(), infos)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// Render flushes the state objects to the screen.\n// The details pane reports the currently selected layer's:\n// 1. tags\n// 2. ID\n// 3. digest\n// 4. command\nfunc (v *LayerDetails) Render() error {\n\tv.gui.Update(func(g *gocui.Gui) error {\n\t\tv.header.Clear()\n\t\twidth, _ := v.body.Size()\n\n\t\tlayerHeaderStr := format.RenderHeader(\"Layer Details\", width, v.gui.CurrentView() == v.body)\n\n\t\t_, err := fmt.Fprintln(v.header, layerHeaderStr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// this is for layer details\n\t\tvar lines = make([]string, 0)\n\n\t\ttags := \"(none)\"\n\t\tif len(v.CurrentLayer.Names) > 0 {\n\t\t\ttags = strings.Join(v.CurrentLayer.Names, \", \")\n\t\t}\n\n\t\tlines = append(lines, []string{\n\t\t\tformat.Header(\"Tags:   \") + tags,\n\t\t\tformat.Header(\"Id:     \") + v.CurrentLayer.Id,\n\t\t\tformat.Header(\"Size:   \") + humanize.Bytes(v.CurrentLayer.Size),\n\t\t\tformat.Header(\"Digest: \") + v.CurrentLayer.Digest,\n\t\t\tformat.Header(\"Command:\"),\n\t\t\tv.CurrentLayer.Command,\n\t\t}...)\n\n\t\tv.body.Clear()\n\t\tif _, err = fmt.Fprintln(v.body, strings.Join(lines, \"\\n\")); err != nil {\n\t\t\tlog.WithFields(\"layer\", v.CurrentLayer.Id, \"error\", err).Debug(\"unable to write to buffer\")\n\t\t}\n\t\treturn nil\n\t})\n\treturn nil\n}\n\nfunc (v *LayerDetails) OnLayoutChange() error {\n\tif err := v.Update(); err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// IsVisible indicates if the details view pane is currently initialized.\nfunc (v *LayerDetails) IsVisible() bool {\n\treturn v.body != nil\n}\n\n// CursorUp moves the cursor up in the details pane\nfunc (v *LayerDetails) CursorUp() error {\n\tif err := CursorUp(v.gui, v.body); err != nil {\n\t\tv.logger.WithFields(\"error\", err).Debug(\"couldn't move the cursor up\")\n\t}\n\treturn nil\n}\n\n// CursorDown moves the cursor up in the details pane\nfunc (v *LayerDetails) CursorDown() error {\n\tif err := CursorDown(v.gui, v.body); err != nil {\n\t\tv.logger.WithFields(\"error\", err).Debug(\"couldn't move the cursor down\")\n\t}\n\treturn nil\n}\n\n// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).\nfunc (v *LayerDetails) KeyHelp() string {\n\treturn \"\"\n}\n\n// Update refreshes the state objects for future rendering.\nfunc (v *LayerDetails) Update() error {\n\treturn nil\n}\n\nfunc (v *LayerDetails) SetCursor(x, y int) error {\n\treturn v.body.SetCursor(x, y)\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/renderer.go",
    "content": "package view\n\n// Controller defines the a renderable terminal screen pane.\ntype Renderer interface {\n\tUpdate() error\n\tRender() error\n\tIsVisible() bool\n}\n\ntype Helper interface {\n\tKeyHelp() string\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/status.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/key\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/dive/internal/utils\"\n\t\"strings\"\n\n\t\"github.com/awesome-gocui/gocui\"\n)\n\n// Status holds the UI objects and data models for populating the bottom-most pane. Specifically the panel\n// shows the user a set of possible actions to take in the window and currently selected pane.\ntype Status struct {\n\tname   string\n\tgui    *gocui.Gui\n\tview   *gocui.View\n\tlogger logger.Logger\n\n\tselectedView    Helper\n\trequestedHeight int\n\n\thelpKeys []*key.Binding\n}\n\n// newStatusView creates a new view object attached the global [gocui] screen object.\nfunc newStatusView(gui *gocui.Gui) *Status {\n\tc := new(Status)\n\n\t// populate main fields\n\tc.name = \"status\"\n\tc.gui = gui\n\tc.helpKeys = make([]*key.Binding, 0)\n\tc.requestedHeight = 1\n\tc.logger = log.Nested(\"ui\", \"status\")\n\n\treturn c\n}\n\nfunc (v *Status) SetCurrentView(r Helper) {\n\tv.selectedView = r\n}\n\nfunc (v *Status) Name() string {\n\treturn v.name\n}\n\nfunc (v *Status) AddHelpKeys(keys ...*key.Binding) {\n\tv.helpKeys = append(v.helpKeys, keys...)\n}\n\n// Setup initializes the UI concerns within the context of a global [gocui] view object.\nfunc (v *Status) Setup(view *gocui.View) error {\n\tv.logger.Trace(\"setup()\")\n\n\t// set controller options\n\tv.view = view\n\tv.view.Frame = false\n\n\treturn v.Render()\n}\n\n// IsVisible indicates if the status view pane is currently initialized.\nfunc (v *Status) IsVisible() bool {\n\treturn v != nil\n}\n\n// Update refreshes the state objects for future rendering (currently does nothing).\nfunc (v *Status) Update() error {\n\treturn nil\n}\n\n// OnLayoutChange is called whenever the screen dimensions are changed\nfunc (v *Status) OnLayoutChange() error {\n\terr := v.Update()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn v.Render()\n}\n\n// Render flushes the state objects to the screen.\nfunc (v *Status) Render() error {\n\tv.logger.Trace(\"render()\")\n\n\tv.gui.Update(func(g *gocui.Gui) error {\n\t\tv.view.Clear()\n\n\t\tvar selectedHelp string\n\t\tif v.selectedView != nil {\n\t\t\tselectedHelp = v.selectedView.KeyHelp()\n\t\t}\n\n\t\t_, err := fmt.Fprintln(v.view, v.KeyHelp()+selectedHelp+format.StatusNormal(\"▏\"+strings.Repeat(\" \", 1000)))\n\t\tif err != nil {\n\t\t\tv.logger.WithFields(\"error\", err).Debug(\"unable to write to buffer\")\n\t\t}\n\n\t\treturn err\n\t})\n\treturn nil\n}\n\n// KeyHelp indicates all the possible global actions a user can take when any pane is selected.\nfunc (v *Status) KeyHelp() string {\n\tvar help string\n\tfor _, binding := range v.helpKeys {\n\t\thelp += binding.RenderKeyHelp()\n\t}\n\treturn help\n}\n\nfunc (v *Status) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {\n\tv.logger.Tracef(\"layout(minX: %d, minY: %d, maxX: %d, maxY: %d)\", minX, minY, maxX, maxY)\n\n\tview, viewErr := g.SetView(v.Name(), minX, minY, maxX, maxY, 0)\n\tif utils.IsNewView(viewErr) {\n\t\terr := v.Setup(view)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to setup status controller: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (v *Status) RequestedSize(available int) *int {\n\treturn &v.requestedHeight\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/view/views.go",
    "content": "package view\n\nimport (\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n)\n\ntype View interface {\n\tSetup(*gocui.View, *gocui.View) error\n\tName() string\n\tIsVisible() bool\n}\n\ntype Views struct {\n\tTree         *FileTree\n\tLayer        *Layer\n\tStatus       *Status\n\tFilter       *Filter\n\tLayerDetails *LayerDetails\n\tImageDetails *ImageDetails\n\tDebug        *Debug\n}\n\nfunc NewViews(g *gocui.Gui, cfg v1.Config) (*Views, error) {\n\tlayer, err := newLayerView(g, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttree, err := newFileTreeView(g, cfg, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstatus := newStatusView(g)\n\n\t// set the layer view as the first selected view\n\tstatus.SetCurrentView(layer)\n\n\treturn &Views{\n\t\tTree:   tree,\n\t\tLayer:  layer,\n\t\tStatus: status,\n\t\tFilter: newFilterView(g),\n\t\tImageDetails: &ImageDetails{\n\t\t\tgui:            g,\n\t\t\timageName:      cfg.Analysis.Image,\n\t\t\timageSize:      cfg.Analysis.SizeBytes,\n\t\t\tefficiency:     cfg.Analysis.Efficiency,\n\t\t\tinefficiencies: cfg.Analysis.Inefficiencies,\n\t\t\tkb:             cfg.Preferences.KeyBindings,\n\t\t},\n\t\tLayerDetails: &LayerDetails{gui: g, kb: cfg.Preferences.KeyBindings},\n\t\tDebug:        newDebugView(g),\n\t}, nil\n}\n\nfunc (views *Views) Renderers() []Renderer {\n\treturn []Renderer{\n\t\tviews.Tree,\n\t\tviews.Layer,\n\t\tviews.Status,\n\t\tviews.Filter,\n\t\tviews.LayerDetails,\n\t\tviews.ImageDetails,\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/config.go",
    "content": "package viewmodel\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/filetree.go",
    "content": "package viewmodel\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/format\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/lunixbochs/vtclean\"\n\t\"github.com/wagoodman/dive/dive/filetree\"\n)\n\n// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically, the pane that\n// shows selected layer or aggregate file ASCII tree.\ntype FileTreeViewModel struct {\n\tModelTree *filetree.FileTree\n\tViewTree  *filetree.FileTree\n\tRefTrees  []*filetree.FileTree\n\tcomparer  filetree.Comparer\n\n\tconstrainedRealEstate bool\n\n\tCollapseAll                 bool\n\tShowAttributes              bool\n\tunconstrainedShowAttributes bool\n\tHiddenDiffTypes             []bool\n\tTreeIndex                   int\n\tbufferIndex                 int\n\tbufferIndexLowerBound       int\n\n\trefHeight int\n\trefWidth  int\n\n\tBuffer bytes.Buffer\n}\n\n// NewFileTreeViewModel creates a new view object attached the global [gocui] screen object.\nfunc NewFileTreeViewModel(cfg v1.Config, initialLayer int) (treeViewModel *FileTreeViewModel, err error) {\n\ttreeViewModel = new(FileTreeViewModel)\n\n\tcomparer, err := cfg.TreeComparer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// populate main fields\n\ttreeViewModel.ShowAttributes = cfg.Preferences.ShowFiletreeAttributes\n\ttreeViewModel.unconstrainedShowAttributes = treeViewModel.ShowAttributes\n\ttreeViewModel.CollapseAll = cfg.Preferences.CollapseFiletreeDirectory\n\ttreeViewModel.ModelTree = cfg.Analysis.RefTrees[initialLayer]\n\ttreeViewModel.RefTrees = cfg.Analysis.RefTrees\n\ttreeViewModel.comparer = comparer\n\ttreeViewModel.HiddenDiffTypes = make([]bool, 4)\n\n\thiddenTypes := cfg.Preferences.FiletreeDiffHide\n\tfor _, hType := range hiddenTypes {\n\t\tswitch t := strings.ToLower(hType); t {\n\t\tcase \"added\":\n\t\t\ttreeViewModel.HiddenDiffTypes[filetree.Added] = true\n\t\tcase \"removed\":\n\t\t\ttreeViewModel.HiddenDiffTypes[filetree.Removed] = true\n\t\tcase \"modified\":\n\t\t\ttreeViewModel.HiddenDiffTypes[filetree.Modified] = true\n\t\tcase \"unmodified\":\n\t\t\ttreeViewModel.HiddenDiffTypes[filetree.Unmodified] = true\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unknown diff.hide value: %s\", t)\n\t\t}\n\t}\n\n\treturn treeViewModel, treeViewModel.SetTreeByLayer(0, 0, initialLayer, initialLayer)\n}\n\n// Setup initializes the UI concerns within the context of a global [gocui] view object.\nfunc (vm *FileTreeViewModel) Setup(lowerBound, height int) {\n\tvm.bufferIndexLowerBound = lowerBound\n\tvm.refHeight = height\n}\n\n// height returns the current height and considers the header\nfunc (vm *FileTreeViewModel) height() int {\n\tif vm.ShowAttributes {\n\t\treturn vm.refHeight - 1\n\t}\n\treturn vm.refHeight\n}\n\n// bufferIndexUpperBound returns the current upper bounds for the view\nfunc (vm *FileTreeViewModel) bufferIndexUpperBound() int {\n\treturn vm.bufferIndexLowerBound + vm.height()\n}\n\n// IsVisible indicates if the file tree view pane is currently initialized\nfunc (vm *FileTreeViewModel) IsVisible() bool {\n\treturn vm != nil\n}\n\n// ResetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.\nfunc (vm *FileTreeViewModel) ResetCursor() {\n\tvm.TreeIndex = 0\n\tvm.bufferIndex = 0\n\tvm.bufferIndexLowerBound = 0\n}\n\n// SetTreeByLayer populates the view model by stacking the indicated image layer file trees.\nfunc (vm *FileTreeViewModel) SetTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {\n\tif topTreeStop > len(vm.RefTrees)-1 {\n\t\treturn fmt.Errorf(\"invalid layer index given: %d of %d\", topTreeStop, len(vm.RefTrees)-1)\n\t}\n\tnewTree, err := vm.comparer.GetTree(filetree.NewTreeIndexKey(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to fetch layer tree from cache: %w\", err)\n\t}\n\n\t// preserve vm state on copy\n\tvisitor := func(node *filetree.FileNode) error {\n\t\tnewNode, err := newTree.GetNode(node.Path())\n\t\tif err == nil {\n\t\t\tnewNode.Data.ViewInfo = node.Data.ViewInfo\n\t\t}\n\t\treturn nil\n\t}\n\terr = vm.ModelTree.VisitDepthChildFirst(visitor, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to propagate layer tree: %w\", err)\n\t}\n\n\tvm.ModelTree = newTree\n\treturn nil\n}\n\n// CursorUp performs the internal view's buffer adjustments on cursor up. Note: this is independent of the gocui buffer.\nfunc (vm *FileTreeViewModel) CursorUp() bool {\n\tif vm.TreeIndex <= 0 {\n\t\treturn false\n\t}\n\tvm.TreeIndex--\n\tif vm.TreeIndex < vm.bufferIndexLowerBound {\n\t\tvm.bufferIndexLowerBound--\n\t}\n\tif vm.bufferIndex > 0 {\n\t\tvm.bufferIndex--\n\t}\n\treturn true\n}\n\n// CursorDown performs the internal view's buffer adjustments on cursor down. Note: this is independent of the gocui buffer.\nfunc (vm *FileTreeViewModel) CursorDown() bool {\n\tif vm.TreeIndex >= vm.ModelTree.VisibleSize() {\n\t\treturn false\n\t}\n\tvm.TreeIndex++\n\tif vm.TreeIndex > vm.bufferIndexUpperBound() {\n\t\tvm.bufferIndexLowerBound++\n\t}\n\tvm.bufferIndex++\n\tif vm.bufferIndex > vm.height() {\n\t\tvm.bufferIndex = vm.height()\n\t}\n\treturn true\n}\n\nfunc (vm *FileTreeViewModel) CurrentNode(filterRegex *regexp.Regexp) *filetree.FileNode {\n\treturn vm.getAbsPositionNode(filterRegex)\n}\n\n// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree\nfunc (vm *FileTreeViewModel) CursorLeft(filterRegex *regexp.Regexp) error {\n\tvar visitor func(*filetree.FileNode) error\n\tvar evaluator func(*filetree.FileNode) bool\n\tvar dfsCounter, newIndex int\n\toldIndex := vm.TreeIndex\n\tcurrentNode := vm.getAbsPositionNode(filterRegex)\n\n\tif currentNode == nil {\n\t\treturn nil\n\t}\n\tparentPath := currentNode.Parent.Path()\n\n\tvisitor = func(curNode *filetree.FileNode) error {\n\t\tif strings.Compare(parentPath, curNode.Path()) == 0 {\n\t\t\tnewIndex = dfsCounter\n\t\t}\n\t\tdfsCounter++\n\t\treturn nil\n\t}\n\n\tevaluator = func(curNode *filetree.FileNode) bool {\n\t\tregexMatch := true\n\t\tif filterRegex != nil {\n\t\t\tmatch := filterRegex.Find([]byte(curNode.Path()))\n\t\t\tregexMatch = match != nil\n\t\t}\n\t\treturn !curNode.Parent.Data.ViewInfo.Collapsed && !curNode.Data.ViewInfo.Hidden && regexMatch\n\t}\n\n\terr := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to propagate tree on cursorLeft: %w\", err)\n\t}\n\n\tvm.TreeIndex = newIndex\n\tmoveIndex := oldIndex - newIndex\n\tif newIndex < vm.bufferIndexLowerBound {\n\t\tvm.bufferIndexLowerBound = vm.TreeIndex\n\t}\n\n\tif vm.bufferIndex > moveIndex {\n\t\tvm.bufferIndex -= moveIndex\n\t} else {\n\t\tvm.bufferIndex = 0\n\t}\n\n\treturn nil\n}\n\n// CursorRight descends into directory expanding it if needed\nfunc (vm *FileTreeViewModel) CursorRight(filterRegex *regexp.Regexp) error {\n\tnode := vm.getAbsPositionNode(filterRegex)\n\tif node == nil {\n\t\treturn nil\n\t}\n\n\tif !node.Data.FileInfo.IsDir {\n\t\treturn nil\n\t}\n\n\tif len(node.Children) == 0 {\n\t\treturn nil\n\t}\n\n\tif node.Data.ViewInfo.Collapsed {\n\t\tnode.Data.ViewInfo.Collapsed = false\n\t}\n\n\tvm.TreeIndex++\n\tif vm.TreeIndex > vm.bufferIndexUpperBound() {\n\t\tvm.bufferIndexLowerBound++\n\t}\n\n\tvm.bufferIndex++\n\tif vm.bufferIndex > vm.height() {\n\t\tvm.bufferIndex = vm.height()\n\t}\n\n\treturn nil\n}\n\n// PageDown moves to next page putting the cursor on top\nfunc (vm *FileTreeViewModel) PageDown() error {\n\tnextBufferIndexLowerBound := vm.bufferIndexLowerBound + vm.height()\n\tnextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()\n\n\t// todo: this work should be saved or passed to render...\n\ttreeString := vm.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, vm.ShowAttributes)\n\tlines := strings.Split(treeString, \"\\n\")\n\n\tnewLines := len(lines) - 1\n\tif vm.height() >= newLines {\n\t\tnextBufferIndexLowerBound = vm.bufferIndexLowerBound + newLines\n\t}\n\n\tvm.bufferIndexLowerBound = nextBufferIndexLowerBound\n\n\tif vm.TreeIndex < nextBufferIndexLowerBound {\n\t\tvm.bufferIndex = 0\n\t\tvm.TreeIndex = nextBufferIndexLowerBound\n\t} else {\n\t\tvm.bufferIndex -= newLines\n\t}\n\n\treturn nil\n}\n\n// PageUp moves to previous page putting the cursor on top\nfunc (vm *FileTreeViewModel) PageUp() error {\n\tnextBufferIndexLowerBound := vm.bufferIndexLowerBound - vm.height()\n\tnextBufferIndexUpperBound := nextBufferIndexLowerBound + vm.height()\n\n\t// todo: this work should be saved or passed to render...\n\ttreeString := vm.ViewTree.StringBetween(nextBufferIndexLowerBound, nextBufferIndexUpperBound, vm.ShowAttributes)\n\tlines := strings.Split(treeString, \"\\n\")\n\n\tnewLines := len(lines) - 2\n\tif vm.height() >= newLines {\n\t\tnextBufferIndexLowerBound = vm.bufferIndexLowerBound - newLines\n\t}\n\n\tvm.bufferIndexLowerBound = nextBufferIndexLowerBound\n\n\tif vm.TreeIndex > (nextBufferIndexUpperBound - 1) {\n\t\tvm.bufferIndex = 0\n\t\tvm.TreeIndex = nextBufferIndexLowerBound\n\t} else {\n\t\tvm.bufferIndex += newLines\n\t}\n\treturn nil\n}\n\n// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.\nfunc (vm *FileTreeViewModel) getAbsPositionNode(filterRegex *regexp.Regexp) (node *filetree.FileNode) {\n\tvar visitor func(*filetree.FileNode) error\n\tvar evaluator func(*filetree.FileNode) bool\n\tvar dfsCounter int\n\n\tvisitor = func(curNode *filetree.FileNode) error {\n\t\tif dfsCounter == vm.TreeIndex {\n\t\t\tnode = curNode\n\t\t}\n\t\tdfsCounter++\n\t\treturn nil\n\t}\n\n\tevaluator = func(curNode *filetree.FileNode) bool {\n\t\tregexMatch := true\n\t\tif filterRegex != nil {\n\t\t\tmatch := filterRegex.Find([]byte(curNode.Path()))\n\t\t\tregexMatch = match != nil\n\t\t}\n\t\tparentCollapsed := false\n\t\tif curNode.Parent != nil {\n\t\t\tparentCollapsed = curNode.Parent.Data.ViewInfo.Collapsed\n\t\t}\n\t\treturn !parentCollapsed && !curNode.Data.ViewInfo.Hidden && regexMatch\n\t}\n\n\terr := vm.ModelTree.VisitDepthParentFirst(visitor, evaluator)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Debug(\"unable to propagate tree on getAbsPositionNode\")\n\t}\n\n\treturn node\n}\n\n// ToggleCollapse will collapse/expand the selected FileNode.\nfunc (vm *FileTreeViewModel) ToggleCollapse(filterRegex *regexp.Regexp) error {\n\tnode := vm.getAbsPositionNode(filterRegex)\n\tif node != nil && node.Data.FileInfo.IsDir {\n\t\tnode.Data.ViewInfo.Collapsed = !node.Data.ViewInfo.Collapsed\n\t}\n\treturn nil\n}\n\n// ToggleCollapseAll will collapse/expand the all directories.\nfunc (vm *FileTreeViewModel) ToggleCollapseAll() error {\n\tvm.CollapseAll = !vm.CollapseAll\n\n\tvisitor := func(curNode *filetree.FileNode) error {\n\t\tcurNode.Data.ViewInfo.Collapsed = vm.CollapseAll\n\t\treturn nil\n\t}\n\n\tevaluator := func(curNode *filetree.FileNode) bool {\n\t\treturn curNode.Data.FileInfo.IsDir\n\t}\n\n\terr := vm.ModelTree.VisitDepthChildFirst(visitor, evaluator)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Debug(\"unable to propagate tree on ToggleCollapseAll\")\n\t}\n\n\treturn nil\n}\n\n// ToggleSortOrder will toggle the sort order in which files are displayed\nfunc (vm *FileTreeViewModel) ToggleSortOrder() error {\n\tvm.ModelTree.SortOrder = (vm.ModelTree.SortOrder + 1) % filetree.NumSortOrderConventions\n\n\treturn nil\n}\n\nfunc (vm *FileTreeViewModel) ConstrainLayout() {\n\tif !vm.constrainedRealEstate {\n\t\tvm.constrainedRealEstate = true\n\t\tvm.unconstrainedShowAttributes = vm.ShowAttributes\n\t\tvm.ShowAttributes = false\n\t}\n}\n\nfunc (vm *FileTreeViewModel) ExpandLayout() {\n\tif vm.constrainedRealEstate {\n\t\tvm.ShowAttributes = vm.unconstrainedShowAttributes\n\t\tvm.constrainedRealEstate = false\n\t}\n}\n\n// ToggleAttributes will hi\nfunc (vm *FileTreeViewModel) ToggleAttributes() error {\n\t// ignore any attempt to show the attributes when the layout is constrained\n\tif vm.constrainedRealEstate {\n\t\treturn nil\n\t}\n\tvm.ShowAttributes = !vm.ShowAttributes\n\treturn nil\n}\n\n// ToggleShowDiffType will show/hide the selected DiffType in the filetree pane.\nfunc (vm *FileTreeViewModel) ToggleShowDiffType(diffType filetree.DiffType) {\n\tvm.HiddenDiffTypes[diffType] = !vm.HiddenDiffTypes[diffType]\n}\n\n// Update refreshes the state objects for future rendering.\nfunc (vm *FileTreeViewModel) Update(filterRegex *regexp.Regexp, width, height int) error {\n\tvm.refWidth = width\n\tvm.refHeight = height\n\n\t// keep the vm selection in parity with the current DiffType selection\n\terr := vm.ModelTree.VisitDepthChildFirst(func(node *filetree.FileNode) error {\n\t\tnode.Data.ViewInfo.Hidden = vm.HiddenDiffTypes[node.Data.DiffType]\n\t\tvisibleChild := false\n\t\tfor _, child := range node.Children {\n\t\t\tif !child.Data.ViewInfo.Hidden {\n\t\t\t\tvisibleChild = true\n\t\t\t\tnode.Data.ViewInfo.Hidden = false\n\t\t\t}\n\t\t}\n\t\t// hide nodes that do not match the current file filter regex (also don't unhide nodes that are already hidden)\n\t\tif filterRegex != nil && !visibleChild && !node.Data.ViewInfo.Hidden {\n\t\t\tmatch := filterRegex.FindString(node.Path())\n\t\t\tnode.Data.ViewInfo.Hidden = len(match) == 0\n\t\t}\n\t\treturn nil\n\t}, nil)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to propagate vm model tree: %w\", err)\n\t}\n\n\t// make a new tree with only visible nodes\n\tvm.ViewTree = vm.ModelTree.Copy()\n\terr = vm.ViewTree.VisitDepthParentFirst(func(node *filetree.FileNode) error {\n\t\tif node.Data.ViewInfo.Hidden {\n\t\t\terr1 := vm.ViewTree.RemovePath(node.Path())\n\t\t\tif err1 != nil {\n\t\t\t\treturn err1\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}, nil)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to propagate vm view tree: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Render flushes the state objects (file tree) to the pane.\nfunc (vm *FileTreeViewModel) Render() error {\n\ttreeString := vm.ViewTree.StringBetween(vm.bufferIndexLowerBound, vm.bufferIndexUpperBound(), vm.ShowAttributes)\n\tlines := strings.Split(treeString, \"\\n\")\n\n\t// update the contents\n\tvm.Buffer.Reset()\n\tfor idx, line := range lines {\n\t\tif idx == vm.bufferIndex {\n\t\t\t_, err := fmt.Fprintln(&vm.Buffer, format.Selected(vtclean.Clean(line, false)))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t_, err := fmt.Fprintln(&vm.Buffer, line)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/filetree_test.go",
    "content": "package viewmodel\n\nimport (\n\t\"flag\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"go.uber.org/atomic\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/wagoodman/dive/dive/filetree\"\n\t\"github.com/wagoodman/dive/dive/image/docker\"\n)\n\nvar repoRootCache atomic.String\n\nvar updateSnapshot = flag.Bool(\"update\", false, \"update any test snapshots\")\n\nfunc TestUpdateSnapshotDisabled(t *testing.T) {\n\trequire.False(t, *updateSnapshot, \"update snapshot flag should be disabled\")\n}\n\nfunc fileExists(filename string) bool {\n\tinfo, err := os.Stat(filename)\n\tif os.IsNotExist(err) {\n\t\treturn false\n\t}\n\treturn !info.IsDir()\n}\n\nfunc testCaseDataFilePath(name string) string {\n\treturn filepath.Join(\"testdata\", name+\".txt\")\n}\n\nfunc helperLoadBytes(t *testing.T) []byte {\n\tt.Helper()\n\tpath := testCaseDataFilePath(t.Name())\n\ttheBytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatalf(\"unable to load test data ('%s'): %+v\", t.Name(), err)\n\t}\n\treturn theBytes\n}\n\nfunc helperCaptureBytes(t *testing.T, data []byte) {\n\t// TODO: switch to https://github.com/gkampitakis/go-snaps\n\tt.Helper()\n\tif *updateSnapshot {\n\t\tt.Fatalf(\"cannot capture data in test mode: %s\", t.Name())\n\t}\n\n\tpath := testCaseDataFilePath(t.Name())\n\terr := os.WriteFile(path, data, 0644)\n\n\tif err != nil {\n\t\tt.Fatalf(\"unable to save test data ('%s'): %+v\", t.Name(), err)\n\t}\n}\n\nfunc initializeTestViewModel(t *testing.T) *FileTreeViewModel {\n\tt.Helper()\n\n\tresult := docker.TestAnalysisFromArchive(t, repoPath(t, \".data/test-docker-image.tar\"))\n\trequire.NotNil(t, result, \"unable to load test data\")\n\n\tvm, err := NewFileTreeViewModel(v1.Config{\n\t\tAnalysis:    *result,\n\t\tPreferences: v1.DefaultPreferences(),\n\t}, 0)\n\n\trequire.NoError(t, err, \"unable to create viewmodel\")\n\treturn vm\n}\n\nfunc runTestCase(t *testing.T, vm *FileTreeViewModel, width, height int, filterRegex *regexp.Regexp) {\n\tt.Helper()\n\terr := vm.Update(filterRegex, width, height)\n\tif err != nil {\n\t\tt.Errorf(\"failed to update viewmodel: %v\", err)\n\t}\n\n\terr = vm.Render()\n\tif err != nil {\n\t\tt.Errorf(\"failed to render viewmodel: %v\", err)\n\t}\n\n\tactualBytes := vm.Buffer.Bytes()\n\tpath := testCaseDataFilePath(t.Name())\n\tif !fileExists(path) {\n\t\tif *updateSnapshot {\n\t\t\thelperCaptureBytes(t, actualBytes)\n\t\t} else {\n\t\t\tt.Fatalf(\"missing test data: %s\", path)\n\t\t}\n\t}\n\texpectedBytes := helperLoadBytes(t)\n\tif d := cmp.Diff(string(expectedBytes), string(actualBytes)); d != \"\" {\n\t\tt.Errorf(\"bytes mismatch (-want +got):\\n%s\", d)\n\t}\n}\n\nfunc checkError(t *testing.T, err error, message string) {\n\tif err != nil {\n\t\tt.Errorf(message+\": %+v\", err)\n\t}\n}\n\nfunc TestFileTreeGoCase(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 1000\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeNoAttributes(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 1000\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = false\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeRestrictedHeight(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 20\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = false\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeDirCollapse(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\tassertPath(t, vm, \"/bin\", \"before toggle of bin\")\n\n\t// collapse /bin\n\terr := vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /bin\")\n\tassertPath(t, vm, \"/bin\", \"after toggle of bin\")\n\n\tmoved := vm.CursorDown() // select /dev\n\trequire.True(t, moved, \"unable to cursor down\")\n\tassertPath(t, vm, \"/dev\", \"down to dev\")\n\n\tmoved = vm.CursorDown() // select /etc\n\trequire.True(t, moved, \"unable to cursor down\")\n\tassertPath(t, vm, \"/etc\", \"down to etc\")\n\n\t// collapse /etc\n\terr = vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /etc\")\n\tassertPath(t, vm, \"/etc\", \"after toggle of etc\")\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc assertPath(t *testing.T, vm *FileTreeViewModel, expected string, msg string) {\n\tt.Helper()\n\tn := vm.CurrentNode(nil)\n\trequire.NotNil(t, n, \"unable to get current node\")\n\tassert.Equal(t, expected, n.Path(), msg)\n}\n\nfunc TestFileTreeDirCollapseAll(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\terr := vm.ToggleCollapseAll()\n\tcheckError(t, err, \"unable to collapse all dir\")\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeSelectLayer(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\t// collapse /bin\n\terr := vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /bin\")\n\n\t// select the next layer, compareMode = layer\n\terr = vm.SetTreeByLayer(0, 0, 1, 1)\n\tif err != nil {\n\t\tt.Errorf(\"unable to SetTreeByLayer: %v\", err)\n\t}\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileShowAggregateChanges(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\t// collapse /bin\n\terr := vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /bin\")\n\n\t// select the next layer, compareMode = layer\n\terr = vm.SetTreeByLayer(0, 0, 1, 13)\n\tcheckError(t, err, \"unable to SetTreeByLayer\")\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreePageDown(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 10\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\terr := vm.Update(nil, width, height)\n\tcheckError(t, err, \"unable to update\")\n\n\terr = vm.PageDown()\n\tcheckError(t, err, \"unable to page down\")\n\n\terr = vm.PageDown()\n\tcheckError(t, err, \"unable to page down\")\n\n\terr = vm.PageDown()\n\tcheckError(t, err, \"unable to page down\")\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreePageUp(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 10\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\t// these operations have a render step for intermediate results, which require at least one update to be done first\n\terr := vm.Update(nil, width, height)\n\tcheckError(t, err, \"unable to update\")\n\n\terr = vm.PageDown()\n\tcheckError(t, err, \"unable to page down\")\n\n\terr = vm.PageDown()\n\tcheckError(t, err, \"unable to page down\")\n\n\terr = vm.PageUp()\n\tcheckError(t, err, \"unable to page up\")\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeDirCursorRight(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\t// collapse /bin\n\terr := vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /bin\")\n\n\tmoved := vm.CursorDown()\n\tif !moved {\n\t\tt.Error(\"unable to cursor down\")\n\t}\n\n\tmoved = vm.CursorDown()\n\tif !moved {\n\t\tt.Error(\"unable to cursor down\")\n\t}\n\n\t// collapse /etc\n\terr = vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /etc\")\n\n\t// expand /etc\n\terr = vm.CursorRight(nil)\n\tcheckError(t, err, \"unable to cursor right\")\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeFilterTree(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 1000\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\tregex, err := regexp.Compile(\"network\")\n\tif err != nil {\n\t\tt.Errorf(\"could not create filter regex: %+v\", err)\n\t}\n\n\trunTestCase(t, vm, width, height, regex)\n}\n\nfunc TestFileTreeHideAddedRemovedModified(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\t// collapse /bin\n\terr := vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /bin\")\n\n\t// select the 7th layer, compareMode = layer\n\terr = vm.SetTreeByLayer(0, 0, 1, 7)\n\tif err != nil {\n\t\tt.Errorf(\"unable to SetTreeByLayer: %v\", err)\n\t}\n\n\t// hide added files\n\tvm.ToggleShowDiffType(filetree.Added)\n\n\t// hide modified files\n\tvm.ToggleShowDiffType(filetree.Modified)\n\n\t// hide removed files\n\tvm.ToggleShowDiffType(filetree.Removed)\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeHideUnmodified(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\t// collapse /bin\n\terr := vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /bin\")\n\n\t// select the 7th layer, compareMode = layer\n\terr = vm.SetTreeByLayer(0, 0, 1, 7)\n\tif err != nil {\n\t\tt.Errorf(\"unable to SetTreeByLayer: %v\", err)\n\t}\n\n\t// hide unmodified files\n\tvm.ToggleShowDiffType(filetree.Unmodified)\n\n\trunTestCase(t, vm, width, height, nil)\n}\n\nfunc TestFileTreeHideTypeWithFilter(t *testing.T) {\n\tvm := initializeTestViewModel(t)\n\n\twidth, height := 100, 100\n\tvm.Setup(0, height)\n\tvm.ShowAttributes = true\n\n\t// collapse /bin\n\terr := vm.ToggleCollapse(nil)\n\tcheckError(t, err, \"unable to collapse /bin\")\n\n\t// select the 7th layer, compareMode = layer\n\terr = vm.SetTreeByLayer(0, 0, 1, 7)\n\tif err != nil {\n\t\tt.Errorf(\"unable to SetTreeByLayer: %v\", err)\n\t}\n\n\t// hide added files\n\tvm.ToggleShowDiffType(filetree.Added)\n\n\tregex, err := regexp.Compile(\"saved\")\n\tif err != nil {\n\t\tt.Errorf(\"could not create filter regex: %+v\", err)\n\t}\n\n\trunTestCase(t, vm, width, height, regex)\n}\n\nfunc repoPath(t testing.TB, path string) string {\n\tt.Helper()\n\troot := repoRoot(t)\n\treturn filepath.Join(root, path)\n}\n\nfunc repoRoot(t testing.TB) string {\n\tval := repoRootCache.Load()\n\tif val != \"\" {\n\t\treturn val\n\t}\n\tt.Helper()\n\t// use git to find the root of the repo\n\tout, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get repo root: %v\", err)\n\t}\n\tval = strings.TrimSpace(string(out))\n\trepoRootCache.Store(val)\n\treturn val\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/layer_compare.go",
    "content": "package viewmodel\n\nconst (\n\tCompareSingleLayer LayerCompareMode = iota\n\tCompareAllLayers\n)\n\ntype LayerCompareMode int\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/layer_selection.go",
    "content": "package viewmodel\n\nimport (\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\ntype LayerSelection struct {\n\tLayer                                                      *image.Layer\n\tBottomTreeStart, BottomTreeStop, TopTreeStart, TopTreeStop int\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/layer_set_state.go",
    "content": "package viewmodel\n\nimport \"github.com/wagoodman/dive/dive/image\"\n\ntype LayerSetState struct {\n\tLayerIndex        int\n\tLayers            []*image.Layer\n\tCompareMode       LayerCompareMode\n\tCompareStartIndex int\n}\n\nfunc NewLayerSetState(layers []*image.Layer, compareMode LayerCompareMode) *LayerSetState {\n\treturn &LayerSetState{\n\t\tLayers:      layers,\n\t\tCompareMode: compareMode,\n\t}\n}\n\n// getCompareIndexes determines the layer boundaries to use for comparison (based on the current compare mode)\nfunc (state *LayerSetState) GetCompareIndexes() (bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) {\n\tbottomTreeStart = state.CompareStartIndex\n\ttopTreeStop = state.LayerIndex\n\n\tif state.LayerIndex == state.CompareStartIndex {\n\t\tbottomTreeStop = state.LayerIndex\n\t\ttopTreeStart = state.LayerIndex\n\t} else if state.CompareMode == CompareSingleLayer {\n\t\tbottomTreeStop = state.LayerIndex - 1\n\t\ttopTreeStart = state.LayerIndex\n\t} else {\n\t\tbottomTreeStop = state.CompareStartIndex\n\t\ttopTreeStart = state.CompareStartIndex + 1\n\t}\n\n\treturn bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/layer_set_state_test.go",
    "content": "package viewmodel\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGetCompareIndexes(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tlayerIndex        int\n\t\tcompareMode       LayerCompareMode\n\t\tcompareStartIndex int\n\t\texpected          [4]int\n\t}{\n\t\t{\n\t\t\tname:              \"LayerIndex equals CompareStartIndex\",\n\t\t\tlayerIndex:        2,\n\t\t\tcompareMode:       CompareSingleLayer,\n\t\t\tcompareStartIndex: 2,\n\t\t\texpected:          [4]int{2, 2, 2, 2},\n\t\t},\n\t\t{\n\t\t\tname:              \"CompareMode is CompareSingleLayer\",\n\t\t\tlayerIndex:        3,\n\t\t\tcompareMode:       CompareSingleLayer,\n\t\t\tcompareStartIndex: 1,\n\t\t\texpected:          [4]int{1, 2, 3, 3},\n\t\t},\n\t\t{\n\t\t\tname:              \"Default CompareMode\",\n\t\t\tlayerIndex:        4,\n\t\t\tcompareMode:       CompareAllLayers,\n\t\t\tcompareStartIndex: 1,\n\t\t\texpected:          [4]int{1, 1, 2, 4},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstate := &LayerSetState{\n\t\t\t\tLayerIndex:        tt.layerIndex,\n\t\t\t\tCompareMode:       tt.compareMode,\n\t\t\t\tCompareStartIndex: tt.compareStartIndex,\n\t\t\t}\n\t\t\tbottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop := state.GetCompareIndexes()\n\t\t\tactual := [4]int{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}\n\t\t\tif actual != tt.expected {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", tt.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileShowAggregateChanges.txt",
    "content": "drwxr-xr-x         0:0     1.2 MB  ├─⊕ bin\ndrwxr-xr-x         0:0        0 B  ├── dev\ndrwxr-xr-x         0:0     1.0 kB  ├── etc\n-rw-rw-r--         0:0      307 B  │   ├── group\n-rw-r--r--         0:0      127 B  │   ├── localtime\ndrwxr-xr-x         0:0        0 B  │   ├── network\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-post-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-pre-up.d\ndrwxr-xr-x         0:0        0 B  │   │   └── if-up.d\n-rw-r--r--         0:0      340 B  │   ├── passwd\n-rw-------         0:0      243 B  │   └── shadow\ndrwxr-xr-x 65534:65534        0 B  ├── home\ndrwx------         0:0      21 kB  ├── root\ndrwxr-xr-x         0:0     8.6 kB  │   ├── .data\n-rw-r--r--         0:0     6.4 kB  │   │   ├── saved.again2.txt\n-rwxrwxr-x         0:0      917 B  │   │   ├── tag.sh\n-rwxr-xr-x         0:0     1.3 kB  │   │   └── test.sh\n-rw-r--r--         0:0     6.4 kB  │   ├── .saved.txt\ndrwxr-xr-x         0:0      19 kB  │   ├── example\ndrwxr-xr-x         0:0        0 B  │   │   ├── really\ndrwxr-xr-x         0:0        0 B  │   │   │   └── nested\n-r--r--r--         0:0     6.4 kB  │   │   ├── somefile1.txt\n-rw-r--r--         0:0     6.4 kB  │   │   ├── somefile2.txt\n-rw-r--r--         0:0     6.4 kB  │   │   └── somefile3.txt\n-rwxr-xr-x         0:0     6.4 kB  │   └── saved.txt\n-rw-rw-r--         0:0     6.4 kB  ├── somefile.txt\ndrwxrwxrwt         0:0     6.4 kB  ├── tmp\n-rw-r--r--         0:0     6.4 kB  │   └── saved.again1.txt\ndrwxr-xr-x         0:0        0 B  ├── usr\ndrwxr-xr-x         1:1        0 B  │   └── sbin\ndrwxr-xr-x         0:0        0 B  └── var\ndrwxr-xr-x         0:0        0 B      ├── spool\ndrwxr-xr-x         8:8        0 B      │   └── mail\ndrwxr-xr-x         0:0        0 B      └── www\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCollapse.txt",
    "content": "drwxr-xr-x         0:0     1.2 MB  ├─⊕ bin\ndrwxr-xr-x         0:0        0 B  ├── dev\ndrwxr-xr-x         0:0     1.0 kB  ├─⊕ etc\ndrwxr-xr-x 65534:65534        0 B  ├── home\ndrwx------         0:0        0 B  ├── root\ndrwxrwxrwt         0:0        0 B  ├── tmp\ndrwxr-xr-x         0:0        0 B  ├── usr\ndrwxr-xr-x         1:1        0 B  │   └── sbin\ndrwxr-xr-x         0:0        0 B  └── var\ndrwxr-xr-x         0:0        0 B      ├── spool\ndrwxr-xr-x         8:8        0 B      │   └── mail\ndrwxr-xr-x         0:0        0 B      └── www\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCollapseAll.txt",
    "content": "drwxr-xr-x         0:0     1.2 MB  ├─⊕ bin\ndrwxr-xr-x         0:0        0 B  ├── dev\ndrwxr-xr-x         0:0     1.0 kB  ├─⊕ etc\ndrwxr-xr-x 65534:65534        0 B  ├── home\ndrwx------         0:0        0 B  ├── root\ndrwxrwxrwt         0:0        0 B  ├── tmp\ndrwxr-xr-x         0:0        0 B  ├─⊕ usr\ndrwxr-xr-x         0:0        0 B  └─⊕ var\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeDirCursorRight.txt",
    "content": "drwxr-xr-x         0:0     1.2 MB  ├─⊕ bin\ndrwxr-xr-x         0:0        0 B  ├── dev\ndrwxr-xr-x         0:0     1.0 kB  ├── etc\n-rw-rw-r--         0:0      307 B  │   ├── group\n-rw-r--r--         0:0      127 B  │   ├── localtime\ndrwxr-xr-x         0:0        0 B  │   ├── network\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-post-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-pre-up.d\ndrwxr-xr-x         0:0        0 B  │   │   └── if-up.d\n-rw-r--r--         0:0      340 B  │   ├── passwd\n-rw-------         0:0      243 B  │   └── shadow\ndrwxr-xr-x 65534:65534        0 B  ├── home\ndrwx------         0:0        0 B  ├── root\ndrwxrwxrwt         0:0        0 B  ├── tmp\ndrwxr-xr-x         0:0        0 B  ├── usr\ndrwxr-xr-x         1:1        0 B  │   └── sbin\ndrwxr-xr-x         0:0        0 B  └── var\ndrwxr-xr-x         0:0        0 B      ├── spool\ndrwxr-xr-x         8:8        0 B      │   └── mail\ndrwxr-xr-x         0:0        0 B      └── www\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeFilterTree.txt",
    "content": "drwxr-xr-x         0:0        0 B  └── etc\ndrwxr-xr-x         0:0        0 B      └── network\ndrwxr-xr-x         0:0        0 B          ├── if-down.d\ndrwxr-xr-x         0:0        0 B          ├── if-post-down.d\ndrwxr-xr-x         0:0        0 B          ├── if-pre-up.d\ndrwxr-xr-x         0:0        0 B          └── if-up.d\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeGoCase.txt",
    "content": "drwxr-xr-x         0:0     1.2 MB  ├── bin\n-rwxr-xr-x         0:0     1.1 MB  │   ├── [\n-rwxr-xr-x         0:0        0 B  │   ├── [[ → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── acpid → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── add-shell → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── addgroup → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── adduser → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── adjtimex → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ar → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── arch → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── arp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── arping → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ash → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── awk → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── base64 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── basename → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── beep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── blkdiscard → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── blkid → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── blockdev → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── bootchartd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── brctl → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── bunzip2 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── busybox → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── bzcat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── bzip2 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cal → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chattr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chgrp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chmod → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chown → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chpasswd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chpst → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chroot → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chrt → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chvt → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cksum → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── clear → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cmp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── comm → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── conspy → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cpio → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── crond → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── crontab → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cryptpw → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cttyhack → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── cut → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── date → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── deallocvt → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── delgroup → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── deluser → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── depmod → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── devmem → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── df → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dhcprelay → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── diff → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dirname → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dmesg → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dnsd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dnsdomainname → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dos2unix → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dpkg → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dpkg-deb → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── du → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dumpkmap → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── dumpleases → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── echo → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ed → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── egrep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── eject → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── env → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── envdir → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── envuidgid → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ether-wake → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── expand → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── expr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── factor → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fakeidentd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fallocate → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── false → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fatattr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fbset → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fbsplash → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fdflush → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fdformat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fdisk → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fgconsole → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fgrep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── find → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── findfs → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── flock → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fold → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── free → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── freeramdisk → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fsck → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fsck.minix → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fsfreeze → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fstrim → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fsync → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ftpd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ftpget → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ftpput → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── fuser → bin/[\n-rwxr-xr-x         0:0      78 kB  │   ├── getconf\n-rwxr-xr-x         0:0        0 B  │   ├── getopt → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── getty → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── grep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── groups → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── gunzip → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── gzip → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── halt → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hdparm → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── head → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hexdump → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hexedit → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hostid → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hostname → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── httpd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hush → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── hwclock → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── i2cdetect → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── i2cdump → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── i2cget → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── i2cset → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── id → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ifconfig → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ifdown → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ifenslave → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ifplugd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ifup → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── inetd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── init → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── insmod → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── install → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ionice → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── iostat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ip → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ipaddr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ipcalc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ipcrm → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ipcs → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── iplink → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ipneigh → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── iproute → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── iprule → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── iptunnel → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── kbd_mode → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── kill → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── killall → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── killall5 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── klogd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── last → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── less → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── link → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── linux32 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── linux64 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── linuxrc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ln → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── loadfont → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── loadkmap → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── logger → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── login → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── logname → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── logread → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── losetup → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lpd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lpq → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lpr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ls → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lsattr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lsmod → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lsof → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lspci → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lsscsi → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lsusb → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lzcat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lzma → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── lzop → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── makedevs → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── makemime → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── man → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── md5sum → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mdev → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mesg → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── microcom → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkdir → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkdosfs → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mke2fs → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkfifo → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkfs.ext2 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkfs.minix → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkfs.vfat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mknod → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkpasswd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mkswap → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mktemp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── modinfo → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── modprobe → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── more → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mount → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mountpoint → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mpstat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mt → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── mv → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nameif → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nanddump → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nandwrite → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nbd-client → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── netstat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nice → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nl → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nmeter → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nohup → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nproc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nsenter → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nslookup → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ntpd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── nuke → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── od → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── openvt → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── partprobe → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── passwd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── paste → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── patch → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pgrep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pidof → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ping → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ping6 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pipe_progress → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pivot_root → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pkill → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pmap → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── popmaildir → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── poweroff → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── powertop → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── printenv → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── printf → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ps → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pscan → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pstree → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pwd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── pwdx → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── raidautorun → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rdate → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rdev → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── readahead → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── readlink → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── readprofile → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── realpath → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── reboot → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── reformime → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── remove-shell → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── renice → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── reset → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── resize → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── resume → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rev → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rm → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rmdir → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rmmod → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── route → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rpm → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rpm2cpio → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rtcwake → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── run-init → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── run-parts → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── runlevel → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── runsv → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── runsvdir → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── rx → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── script → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── scriptreplay → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sed → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sendmail → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── seq → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setarch → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setconsole → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setfattr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setfont → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setkeycodes → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setlogcons → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setpriv → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setserial → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setsid → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── setuidgid → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sh → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sha1sum → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sha256sum → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sha3sum → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sha512sum → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── showkey → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── shred → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── shuf → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── slattach → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sleep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── smemcap → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── softlimit → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sort → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── split → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ssl_client → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── start-stop-daemon → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── stat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── strings → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── stty → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── su → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sulogin → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sum → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sv → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── svc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── svlogd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── svok → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── swapoff → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── swapon → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── switch_root → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sync → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── sysctl → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── syslogd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tac → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tail → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tar → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── taskset → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tcpsvd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tee → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── telnet → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── telnetd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── test → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tftp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tftpd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── time → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── timeout → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── top → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── touch → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── traceroute → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── traceroute6 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── true → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── truncate → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tty → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ttysize → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── tunctl → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ubiattach → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ubidetach → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ubimkvol → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ubirename → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ubirmvol → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ubirsvol → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ubiupdatevol → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── udhcpc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── udhcpd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── udpsvd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── uevent → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── umount → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── uname → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── unexpand → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── uniq → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── unix2dos → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── unlink → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── unlzma → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── unshare → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── unxz → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── unzip → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── uptime → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── users → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── usleep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── uudecode → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── uuencode → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── vconfig → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── vi → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── vlock → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── volname → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── w → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── wall → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── watch → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── watchdog → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── wc → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── wget → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── which → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── who → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── whoami → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── whois → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── xargs → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── xxd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── xz → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── xzcat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── yes → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── zcat → bin/[\n-rwxr-xr-x         0:0        0 B  │   └── zcip → bin/[\ndrwxr-xr-x         0:0        0 B  ├── dev\ndrwxr-xr-x         0:0     1.0 kB  ├── etc\n-rw-rw-r--         0:0      307 B  │   ├── group\n-rw-r--r--         0:0      127 B  │   ├── localtime\ndrwxr-xr-x         0:0        0 B  │   ├── network\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-post-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-pre-up.d\ndrwxr-xr-x         0:0        0 B  │   │   └── if-up.d\n-rw-r--r--         0:0      340 B  │   ├── passwd\n-rw-------         0:0      243 B  │   └── shadow\ndrwxr-xr-x 65534:65534        0 B  ├── home\ndrwx------         0:0        0 B  ├── root\ndrwxrwxrwt         0:0        0 B  ├── tmp\ndrwxr-xr-x         0:0        0 B  ├── usr\ndrwxr-xr-x         1:1        0 B  │   └── sbin\ndrwxr-xr-x         0:0        0 B  └── var\ndrwxr-xr-x         0:0        0 B      ├── spool\ndrwxr-xr-x         8:8        0 B      │   └── mail\ndrwxr-xr-x         0:0        0 B      └── www\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideAddedRemovedModified.txt",
    "content": "drwxr-xr-x         0:0     1.2 MB  ├─⊕ bin\ndrwxr-xr-x         0:0        0 B  ├── dev\ndrwxr-xr-x         0:0     1.0 kB  ├── etc\n-rw-rw-r--         0:0      307 B  │   ├── group\n-rw-r--r--         0:0      127 B  │   ├── localtime\ndrwxr-xr-x         0:0        0 B  │   ├── network\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-post-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-pre-up.d\ndrwxr-xr-x         0:0        0 B  │   │   └── if-up.d\n-rw-r--r--         0:0      340 B  │   ├── passwd\n-rw-------         0:0      243 B  │   └── shadow\ndrwxr-xr-x 65534:65534        0 B  ├── home\ndrwxrwxrwt         0:0        0 B  ├── tmp\ndrwxr-xr-x         0:0        0 B  ├── usr\ndrwxr-xr-x         1:1        0 B  │   └── sbin\ndrwxr-xr-x         0:0        0 B  └── var\ndrwxr-xr-x         0:0        0 B      ├── spool\ndrwxr-xr-x         8:8        0 B      │   └── mail\ndrwxr-xr-x         0:0        0 B      └── www\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideTypeWithFilter.txt",
    "content": "\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeHideUnmodified.txt",
    "content": "drwx------         0:0      19 kB  ├── root\ndrwxr-xr-x         0:0      13 kB  │   ├── example\ndrwxr-xr-x         0:0        0 B  │   │   ├── really\ndrwxr-xr-x         0:0        0 B  │   │   │   └── nested\n-r--r--r--         0:0     6.4 kB  │   │   ├── somefile1.txt\n-rw-r--r--         0:0     6.4 kB  │   │   ├── somefile2.txt\n-rw-r--r--         0:0     6.4 kB  │   │   └── somefile3.txt\n-rw-r--r--         0:0     6.4 kB  │   └── saved.txt\n-rw-rw-r--         0:0     6.4 kB  └── somefile.txt\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeNoAttributes.txt",
    "content": "├── bin\n│   ├── [\n│   ├── [[ → bin/[\n│   ├── acpid → bin/[\n│   ├── add-shell → bin/[\n│   ├── addgroup → bin/[\n│   ├── adduser → bin/[\n│   ├── adjtimex → bin/[\n│   ├── ar → bin/[\n│   ├── arch → bin/[\n│   ├── arp → bin/[\n│   ├── arping → bin/[\n│   ├── ash → bin/[\n│   ├── awk → bin/[\n│   ├── base64 → bin/[\n│   ├── basename → bin/[\n│   ├── beep → bin/[\n│   ├── blkdiscard → bin/[\n│   ├── blkid → bin/[\n│   ├── blockdev → bin/[\n│   ├── bootchartd → bin/[\n│   ├── brctl → bin/[\n│   ├── bunzip2 → bin/[\n│   ├── busybox → bin/[\n│   ├── bzcat → bin/[\n│   ├── bzip2 → bin/[\n│   ├── cal → bin/[\n│   ├── cat → bin/[\n│   ├── chat → bin/[\n│   ├── chattr → bin/[\n│   ├── chgrp → bin/[\n│   ├── chmod → bin/[\n│   ├── chown → bin/[\n│   ├── chpasswd → bin/[\n│   ├── chpst → bin/[\n│   ├── chroot → bin/[\n│   ├── chrt → bin/[\n│   ├── chvt → bin/[\n│   ├── cksum → bin/[\n│   ├── clear → bin/[\n│   ├── cmp → bin/[\n│   ├── comm → bin/[\n│   ├── conspy → bin/[\n│   ├── cp → bin/[\n│   ├── cpio → bin/[\n│   ├── crond → bin/[\n│   ├── crontab → bin/[\n│   ├── cryptpw → bin/[\n│   ├── cttyhack → bin/[\n│   ├── cut → bin/[\n│   ├── date → bin/[\n│   ├── dc → bin/[\n│   ├── dd → bin/[\n│   ├── deallocvt → bin/[\n│   ├── delgroup → bin/[\n│   ├── deluser → bin/[\n│   ├── depmod → bin/[\n│   ├── devmem → bin/[\n│   ├── df → bin/[\n│   ├── dhcprelay → bin/[\n│   ├── diff → bin/[\n│   ├── dirname → bin/[\n│   ├── dmesg → bin/[\n│   ├── dnsd → bin/[\n│   ├── dnsdomainname → bin/[\n│   ├── dos2unix → bin/[\n│   ├── dpkg → bin/[\n│   ├── dpkg-deb → bin/[\n│   ├── du → bin/[\n│   ├── dumpkmap → bin/[\n│   ├── dumpleases → bin/[\n│   ├── echo → bin/[\n│   ├── ed → bin/[\n│   ├── egrep → bin/[\n│   ├── eject → bin/[\n│   ├── env → bin/[\n│   ├── envdir → bin/[\n│   ├── envuidgid → bin/[\n│   ├── ether-wake → bin/[\n│   ├── expand → bin/[\n│   ├── expr → bin/[\n│   ├── factor → bin/[\n│   ├── fakeidentd → bin/[\n│   ├── fallocate → bin/[\n│   ├── false → bin/[\n│   ├── fatattr → bin/[\n│   ├── fbset → bin/[\n│   ├── fbsplash → bin/[\n│   ├── fdflush → bin/[\n│   ├── fdformat → bin/[\n│   ├── fdisk → bin/[\n│   ├── fgconsole → bin/[\n│   ├── fgrep → bin/[\n│   ├── find → bin/[\n│   ├── findfs → bin/[\n│   ├── flock → bin/[\n│   ├── fold → bin/[\n│   ├── free → bin/[\n│   ├── freeramdisk → bin/[\n│   ├── fsck → bin/[\n│   ├── fsck.minix → bin/[\n│   ├── fsfreeze → bin/[\n│   ├── fstrim → bin/[\n│   ├── fsync → bin/[\n│   ├── ftpd → bin/[\n│   ├── ftpget → bin/[\n│   ├── ftpput → bin/[\n│   ├── fuser → bin/[\n│   ├── getconf\n│   ├── getopt → bin/[\n│   ├── getty → bin/[\n│   ├── grep → bin/[\n│   ├── groups → bin/[\n│   ├── gunzip → bin/[\n│   ├── gzip → bin/[\n│   ├── halt → bin/[\n│   ├── hd → bin/[\n│   ├── hdparm → bin/[\n│   ├── head → bin/[\n│   ├── hexdump → bin/[\n│   ├── hexedit → bin/[\n│   ├── hostid → bin/[\n│   ├── hostname → bin/[\n│   ├── httpd → bin/[\n│   ├── hush → bin/[\n│   ├── hwclock → bin/[\n│   ├── i2cdetect → bin/[\n│   ├── i2cdump → bin/[\n│   ├── i2cget → bin/[\n│   ├── i2cset → bin/[\n│   ├── id → bin/[\n│   ├── ifconfig → bin/[\n│   ├── ifdown → bin/[\n│   ├── ifenslave → bin/[\n│   ├── ifplugd → bin/[\n│   ├── ifup → bin/[\n│   ├── inetd → bin/[\n│   ├── init → bin/[\n│   ├── insmod → bin/[\n│   ├── install → bin/[\n│   ├── ionice → bin/[\n│   ├── iostat → bin/[\n│   ├── ip → bin/[\n│   ├── ipaddr → bin/[\n│   ├── ipcalc → bin/[\n│   ├── ipcrm → bin/[\n│   ├── ipcs → bin/[\n│   ├── iplink → bin/[\n│   ├── ipneigh → bin/[\n│   ├── iproute → bin/[\n│   ├── iprule → bin/[\n│   ├── iptunnel → bin/[\n│   ├── kbd_mode → bin/[\n│   ├── kill → bin/[\n│   ├── killall → bin/[\n│   ├── killall5 → bin/[\n│   ├── klogd → bin/[\n│   ├── last → bin/[\n│   ├── less → bin/[\n│   ├── link → bin/[\n│   ├── linux32 → bin/[\n│   ├── linux64 → bin/[\n│   ├── linuxrc → bin/[\n│   ├── ln → bin/[\n│   ├── loadfont → bin/[\n│   ├── loadkmap → bin/[\n│   ├── logger → bin/[\n│   ├── login → bin/[\n│   ├── logname → bin/[\n│   ├── logread → bin/[\n│   ├── losetup → bin/[\n│   ├── lpd → bin/[\n│   ├── lpq → bin/[\n│   ├── lpr → bin/[\n│   ├── ls → bin/[\n│   ├── lsattr → bin/[\n│   ├── lsmod → bin/[\n│   ├── lsof → bin/[\n│   ├── lspci → bin/[\n│   ├── lsscsi → bin/[\n│   ├── lsusb → bin/[\n│   ├── lzcat → bin/[\n│   ├── lzma → bin/[\n│   ├── lzop → bin/[\n│   ├── makedevs → bin/[\n│   ├── makemime → bin/[\n│   ├── man → bin/[\n│   ├── md5sum → bin/[\n│   ├── mdev → bin/[\n│   ├── mesg → bin/[\n│   ├── microcom → bin/[\n│   ├── mkdir → bin/[\n│   ├── mkdosfs → bin/[\n│   ├── mke2fs → bin/[\n│   ├── mkfifo → bin/[\n│   ├── mkfs.ext2 → bin/[\n│   ├── mkfs.minix → bin/[\n│   ├── mkfs.vfat → bin/[\n│   ├── mknod → bin/[\n│   ├── mkpasswd → bin/[\n│   ├── mkswap → bin/[\n│   ├── mktemp → bin/[\n│   ├── modinfo → bin/[\n│   ├── modprobe → bin/[\n│   ├── more → bin/[\n│   ├── mount → bin/[\n│   ├── mountpoint → bin/[\n│   ├── mpstat → bin/[\n│   ├── mt → bin/[\n│   ├── mv → bin/[\n│   ├── nameif → bin/[\n│   ├── nanddump → bin/[\n│   ├── nandwrite → bin/[\n│   ├── nbd-client → bin/[\n│   ├── nc → bin/[\n│   ├── netstat → bin/[\n│   ├── nice → bin/[\n│   ├── nl → bin/[\n│   ├── nmeter → bin/[\n│   ├── nohup → bin/[\n│   ├── nproc → bin/[\n│   ├── nsenter → bin/[\n│   ├── nslookup → bin/[\n│   ├── ntpd → bin/[\n│   ├── nuke → bin/[\n│   ├── od → bin/[\n│   ├── openvt → bin/[\n│   ├── partprobe → bin/[\n│   ├── passwd → bin/[\n│   ├── paste → bin/[\n│   ├── patch → bin/[\n│   ├── pgrep → bin/[\n│   ├── pidof → bin/[\n│   ├── ping → bin/[\n│   ├── ping6 → bin/[\n│   ├── pipe_progress → bin/[\n│   ├── pivot_root → bin/[\n│   ├── pkill → bin/[\n│   ├── pmap → bin/[\n│   ├── popmaildir → bin/[\n│   ├── poweroff → bin/[\n│   ├── powertop → bin/[\n│   ├── printenv → bin/[\n│   ├── printf → bin/[\n│   ├── ps → bin/[\n│   ├── pscan → bin/[\n│   ├── pstree → bin/[\n│   ├── pwd → bin/[\n│   ├── pwdx → bin/[\n│   ├── raidautorun → bin/[\n│   ├── rdate → bin/[\n│   ├── rdev → bin/[\n│   ├── readahead → bin/[\n│   ├── readlink → bin/[\n│   ├── readprofile → bin/[\n│   ├── realpath → bin/[\n│   ├── reboot → bin/[\n│   ├── reformime → bin/[\n│   ├── remove-shell → bin/[\n│   ├── renice → bin/[\n│   ├── reset → bin/[\n│   ├── resize → bin/[\n│   ├── resume → bin/[\n│   ├── rev → bin/[\n│   ├── rm → bin/[\n│   ├── rmdir → bin/[\n│   ├── rmmod → bin/[\n│   ├── route → bin/[\n│   ├── rpm → bin/[\n│   ├── rpm2cpio → bin/[\n│   ├── rtcwake → bin/[\n│   ├── run-init → bin/[\n│   ├── run-parts → bin/[\n│   ├── runlevel → bin/[\n│   ├── runsv → bin/[\n│   ├── runsvdir → bin/[\n│   ├── rx → bin/[\n│   ├── script → bin/[\n│   ├── scriptreplay → bin/[\n│   ├── sed → bin/[\n│   ├── sendmail → bin/[\n│   ├── seq → bin/[\n│   ├── setarch → bin/[\n│   ├── setconsole → bin/[\n│   ├── setfattr → bin/[\n│   ├── setfont → bin/[\n│   ├── setkeycodes → bin/[\n│   ├── setlogcons → bin/[\n│   ├── setpriv → bin/[\n│   ├── setserial → bin/[\n│   ├── setsid → bin/[\n│   ├── setuidgid → bin/[\n│   ├── sh → bin/[\n│   ├── sha1sum → bin/[\n│   ├── sha256sum → bin/[\n│   ├── sha3sum → bin/[\n│   ├── sha512sum → bin/[\n│   ├── showkey → bin/[\n│   ├── shred → bin/[\n│   ├── shuf → bin/[\n│   ├── slattach → bin/[\n│   ├── sleep → bin/[\n│   ├── smemcap → bin/[\n│   ├── softlimit → bin/[\n│   ├── sort → bin/[\n│   ├── split → bin/[\n│   ├── ssl_client → bin/[\n│   ├── start-stop-daemon → bin/[\n│   ├── stat → bin/[\n│   ├── strings → bin/[\n│   ├── stty → bin/[\n│   ├── su → bin/[\n│   ├── sulogin → bin/[\n│   ├── sum → bin/[\n│   ├── sv → bin/[\n│   ├── svc → bin/[\n│   ├── svlogd → bin/[\n│   ├── svok → bin/[\n│   ├── swapoff → bin/[\n│   ├── swapon → bin/[\n│   ├── switch_root → bin/[\n│   ├── sync → bin/[\n│   ├── sysctl → bin/[\n│   ├── syslogd → bin/[\n│   ├── tac → bin/[\n│   ├── tail → bin/[\n│   ├── tar → bin/[\n│   ├── taskset → bin/[\n│   ├── tc → bin/[\n│   ├── tcpsvd → bin/[\n│   ├── tee → bin/[\n│   ├── telnet → bin/[\n│   ├── telnetd → bin/[\n│   ├── test → bin/[\n│   ├── tftp → bin/[\n│   ├── tftpd → bin/[\n│   ├── time → bin/[\n│   ├── timeout → bin/[\n│   ├── top → bin/[\n│   ├── touch → bin/[\n│   ├── tr → bin/[\n│   ├── traceroute → bin/[\n│   ├── traceroute6 → bin/[\n│   ├── true → bin/[\n│   ├── truncate → bin/[\n│   ├── tty → bin/[\n│   ├── ttysize → bin/[\n│   ├── tunctl → bin/[\n│   ├── ubiattach → bin/[\n│   ├── ubidetach → bin/[\n│   ├── ubimkvol → bin/[\n│   ├── ubirename → bin/[\n│   ├── ubirmvol → bin/[\n│   ├── ubirsvol → bin/[\n│   ├── ubiupdatevol → bin/[\n│   ├── udhcpc → bin/[\n│   ├── udhcpd → bin/[\n│   ├── udpsvd → bin/[\n│   ├── uevent → bin/[\n│   ├── umount → bin/[\n│   ├── uname → bin/[\n│   ├── unexpand → bin/[\n│   ├── uniq → bin/[\n│   ├── unix2dos → bin/[\n│   ├── unlink → bin/[\n│   ├── unlzma → bin/[\n│   ├── unshare → bin/[\n│   ├── unxz → bin/[\n│   ├── unzip → bin/[\n│   ├── uptime → bin/[\n│   ├── users → bin/[\n│   ├── usleep → bin/[\n│   ├── uudecode → bin/[\n│   ├── uuencode → bin/[\n│   ├── vconfig → bin/[\n│   ├── vi → bin/[\n│   ├── vlock → bin/[\n│   ├── volname → bin/[\n│   ├── w → bin/[\n│   ├── wall → bin/[\n│   ├── watch → bin/[\n│   ├── watchdog → bin/[\n│   ├── wc → bin/[\n│   ├── wget → bin/[\n│   ├── which → bin/[\n│   ├── who → bin/[\n│   ├── whoami → bin/[\n│   ├── whois → bin/[\n│   ├── xargs → bin/[\n│   ├── xxd → bin/[\n│   ├── xz → bin/[\n│   ├── xzcat → bin/[\n│   ├── yes → bin/[\n│   ├── zcat → bin/[\n│   └── zcip → bin/[\n├── dev\n├── etc\n│   ├── group\n│   ├── localtime\n│   ├── network\n│   │   ├── if-down.d\n│   │   ├── if-post-down.d\n│   │   ├── if-pre-up.d\n│   │   └── if-up.d\n│   ├── passwd\n│   └── shadow\n├── home\n├── root\n├── tmp\n├── usr\n│   └── sbin\n└── var\n    ├── spool\n    │   └── mail\n    └── www\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreePageDown.txt",
    "content": "-rwxr-xr-x         0:0        0 B  │   ├── cat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chat → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chattr → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chgrp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chmod → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chown → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chpasswd → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chpst → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chroot → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── chrt → bin/[\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreePageUp.txt",
    "content": "-rwxr-xr-x         0:0        0 B  │   ├── arch → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── arp → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── arping → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── ash → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── awk → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── base64 → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── basename → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── beep → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── blkdiscard → bin/[\n-rwxr-xr-x         0:0        0 B  │   ├── blkid → bin/[\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeRestrictedHeight.txt",
    "content": "├── bin\n│   ├── [\n│   ├── [[ → bin/[\n│   ├── acpid → bin/[\n│   ├── add-shell → bin/[\n│   ├── addgroup → bin/[\n│   ├── adduser → bin/[\n│   ├── adjtimex → bin/[\n│   ├── ar → bin/[\n│   ├── arch → bin/[\n│   ├── arp → bin/[\n│   ├── arping → bin/[\n│   ├── ash → bin/[\n│   ├── awk → bin/[\n│   ├── base64 → bin/[\n│   ├── basename → bin/[\n│   ├── beep → bin/[\n│   ├── blkdiscard → bin/[\n│   ├── blkid → bin/[\n│   ├── blockdev → bin/[\n│   ├── bootchartd → bin/[\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1/viewmodel/testdata/TestFileTreeSelectLayer.txt",
    "content": "drwxr-xr-x         0:0     1.2 MB  ├─⊕ bin\ndrwxr-xr-x         0:0        0 B  ├── dev\ndrwxr-xr-x         0:0     1.0 kB  ├── etc\n-rw-rw-r--         0:0      307 B  │   ├── group\n-rw-r--r--         0:0      127 B  │   ├── localtime\ndrwxr-xr-x         0:0        0 B  │   ├── network\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-post-down.d\ndrwxr-xr-x         0:0        0 B  │   │   ├── if-pre-up.d\ndrwxr-xr-x         0:0        0 B  │   │   └── if-up.d\n-rw-r--r--         0:0      340 B  │   ├── passwd\n-rw-------         0:0      243 B  │   └── shadow\ndrwxr-xr-x 65534:65534        0 B  ├── home\ndrwx------         0:0        0 B  ├── root\n-rw-rw-r--         0:0     6.4 kB  ├── somefile.txt\ndrwxrwxrwt         0:0        0 B  ├── tmp\ndrwxr-xr-x         0:0        0 B  ├── usr\ndrwxr-xr-x         1:1        0 B  │   └── sbin\ndrwxr-xr-x         0:0        0 B  └── var\ndrwxr-xr-x         0:0        0 B      ├── spool\ndrwxr-xr-x         8:8        0 B      │   └── mail\ndrwxr-xr-x         0:0        0 B      └── www\n\n"
  },
  {
    "path": "cmd/dive/cli/internal/ui/v1.go",
    "content": "package ui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/go-logger/adapter/discard\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/muesli/termenv\"\n\tv1 \"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/app\"\n\t\"github.com/wagoodman/dive/internal/bus/event\"\n\t\"github.com/wagoodman/dive/internal/bus/event/parser\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n)\n\nvar _ clio.UI = (*V1UI)(nil)\n\ntype V1UI struct {\n\tcfg v1.Preferences\n\tout io.Writer\n\terr io.Writer\n\n\tsubscription partybus.Unsubscribable\n\tquiet        bool\n\tverbosity    int\n\tformat       format\n}\n\ntype format struct {\n\tTitle        lipgloss.Style\n\tAux          lipgloss.Style\n\tLine         lipgloss.Style\n\tNotification lipgloss.Style\n}\n\nfunc NewV1UI(cfg v1.Preferences, out io.Writer, quiet bool, verbosity int) *V1UI {\n\treturn &V1UI{\n\t\tcfg:       cfg,\n\t\tout:       out,\n\t\terr:       os.Stderr,\n\t\tquiet:     quiet,\n\t\tverbosity: verbosity,\n\t\tformat: format{\n\t\t\tTitle:        lipgloss.NewStyle().Bold(true).Width(30),\n\t\t\tAux:          lipgloss.NewStyle().Faint(true),\n\t\t\tNotification: lipgloss.NewStyle().Foreground(lipgloss.Color(\"#A77BCA\")),\n\t\t},\n\t}\n}\n\nfunc (n *V1UI) Setup(subscription partybus.Unsubscribable) error {\n\tif n.verbosity == 0 || n.quiet {\n\t\t// we still use the UI, but we want to suppress responding to events that would print out what is already\n\t\t// being logged.\n\t\tlog.Set(discard.New())\n\t}\n\n\t// remove CI var from consideration when determining if we should use the UI\n\tlipgloss.SetDefaultRenderer(lipgloss.NewRenderer(n.out, termenv.WithEnvironment(environWithoutCI{})))\n\n\tn.subscription = subscription\n\treturn nil\n}\n\nvar _ termenv.Environ = (*environWithoutCI)(nil)\n\ntype environWithoutCI struct {\n}\n\nfunc (e environWithoutCI) Environ() []string {\n\tvar out []string\n\tfor _, s := range os.Environ() {\n\t\tif strings.HasPrefix(s, \"CI=\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, s)\n\t}\n\treturn out\n}\n\nfunc (e environWithoutCI) Getenv(s string) string {\n\tif s == \"CI\" {\n\t\treturn \"\"\n\t}\n\treturn os.Getenv(s)\n}\n\nfunc (n *V1UI) Handle(e partybus.Event) error {\n\tswitch e.Type {\n\tcase event.TaskStarted:\n\t\tif n.quiet {\n\t\t\treturn nil\n\t\t}\n\t\tprog, task, err := parser.ParseTaskStarted(e)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"event\", fmt.Sprintf(\"%#v\", e)).Warn(\"failed to parse event\")\n\t\t}\n\n\t\tvar aux string\n\t\tstage := prog.Stage()\n\t\tswitch {\n\t\tcase task.Context != \"\":\n\t\t\taux = task.Context\n\t\tcase stage != \"\":\n\t\t\taux = stage\n\t\t}\n\n\t\tif aux != \"\" {\n\t\t\taux = n.format.Aux.Render(aux)\n\t\t}\n\n\t\tn.writeToStderr(n.format.Title.Render(task.Title.Default) + aux)\n\tcase event.Notification:\n\t\tif n.quiet {\n\t\t\treturn nil\n\t\t}\n\t\t_, text, err := parser.ParseNotification(e)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"event\", fmt.Sprintf(\"%#v\", e)).Warn(\"failed to parse event\")\n\t\t}\n\n\t\tn.writeToStderr(n.format.Notification.Render(text))\n\tcase event.Report:\n\t\tif n.quiet {\n\t\t\treturn nil\n\t\t}\n\t\t_, text, err := parser.ParseReport(e)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"event\", fmt.Sprintf(\"%#v\", e)).Warn(\"failed to parse event\")\n\t\t}\n\n\t\tn.writeToStderr(\"\")\n\t\tn.writeToStdout(text)\n\tcase event.ExploreAnalysis:\n\t\tanalysis, content, err := parser.ParseExploreAnalysis(e)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"event\", fmt.Sprintf(\"%#v\", e)).Warn(\"failed to parse event\")\n\t\t}\n\n\t\t// ensure the logger will not interfere with the UI\n\t\tlog.Set(discard.New())\n\n\t\treturn app.Run(\n\t\t\t// TODO: this is not plumbed through from the command object...\n\t\t\tcontext.Background(),\n\t\t\tv1.Config{\n\t\t\t\tContent:     content,\n\t\t\t\tAnalysis:    analysis,\n\t\t\t\tPreferences: n.cfg,\n\t\t\t},\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc (n *V1UI) writeToStdout(s string) {\n\tfmt.Fprintln(n.out, s)\n}\n\nfunc (n *V1UI) writeToStderr(s string) {\n\tif n.quiet || n.verbosity > 0 {\n\t\t// we've been told to not report anything or that we're in verbose mode thus the logger should report all info.\n\t\t// This only applies to status like info on stderr, not to primary reports on stdout.\n\t\treturn\n\t}\n\tfmt.Fprintln(n.err, s)\n}\n\nfunc (n V1UI) Teardown(_ bool) error {\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/dive/cli/testdata/config/dive-ci-legacy.yaml",
    "content": "rules:\n  lowestEfficiency: 0.95\n  highestWastedBytes: 20MB\n  highestUserWastedPercent: 0.20\n"
  },
  {
    "path": "cmd/dive/cli/testdata/default-ci-config/.dive-ci",
    "content": "rules:\n  # If the efficiency is measured below X%, mark as failed. (expressed as a percentage between 0-1)\n  lowestEfficiency: 0.96\n\n  # If the amount of wasted space is at least X or larger than X, mark as failed. (expressed in B, KB, MB, and GB)\n  highestWastedBytes: 19Mb\n\n  # If the amount of wasted space makes up for X% of the image, mark as failed. (fail if the threshold is met or crossed; expressed as a percentage between 0-1)\n  highestUserWastedPercent: 0.6\n"
  },
  {
    "path": "cmd/dive/cli/testdata/dive-enable-ci.yaml",
    "content": "ci: true\nrules:\n  # lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_LOWEST_EFFICIENCY_THRESHOLD)\n  lowest-efficiency-threshold: '0.10'\n\n  # highest allowable bytes wasted, otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_WASTED_BYTES)\n  highest-wasted-bytes: '20MB'\n\n  # highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_USER_WASTED_PERCENT)\n  highest-user-wasted-percent: '0.90'\n"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-containerfile/Containerfile",
    "content": "FROM busybox:1.37.0@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f\nADD example.md /somefile.txt\nRUN mkdir -p /root/example/really/nested\nRUN cp /somefile.txt /root/example/somefile1.txt\nRUN chmod 444 /root/example/somefile1.txt\nRUN cp /somefile.txt /root/example/somefile2.txt\nRUN cp /somefile.txt /root/example/somefile3.txt\nRUN mv /root/example/somefile3.txt /root/saved.txt\nRUN cp /root/saved.txt /root/.saved.txt\nRUN chmod +x /root/saved.txt\nRUN chmod 421 /root\nRUN rm -rf /root/example/\nADD overwrite.md /root/saved.txt\n"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-containerfile/dive-pass.yaml",
    "content": "ci: true\nrules:\n  # lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_LOWEST_EFFICIENCY_THRESHOLD)\n  lowest-efficiency-threshold: '0.10'\n\n  # highest allowable bytes wasted, otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_WASTED_BYTES)\n  highest-wasted-bytes: '20MB'\n\n  # highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_USER_WASTED_PERCENT)\n  highest-user-wasted-percent: '0.90'\n"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-containerfile/example.md",
    "content": "# exmaple!\n\nwoot!"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-containerfile/overwrite.md",
    "content": "# evil!\n\nthis will overwrite the other file..."
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-dockerfile/Dockerfile",
    "content": "FROM busybox:1.37.0@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f\nADD example.md /somefile.txt\nRUN mkdir -p /root/example/really/nested\nRUN cp /somefile.txt /root/example/somefile1.txt\nRUN chmod 444 /root/example/somefile1.txt\nRUN cp /somefile.txt /root/example/somefile2.txt\nRUN cp /somefile.txt /root/example/somefile3.txt\nRUN mv /root/example/somefile3.txt /root/saved.txt\nRUN cp /root/saved.txt /root/.saved.txt\nRUN chmod +x /root/saved.txt\nRUN chmod 421 /root\nRUN rm -rf /root/example/\nADD overwrite.md /root/saved.txt\n"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-fail.yaml",
    "content": "ci: true\nrules:\n  lowest-efficiency-threshold: '0.9'\n"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-dockerfile/dive-pass.yaml",
    "content": "ci: true\nrules:\n  lowest-efficiency-threshold: '0.10'\n  highest-wasted-bytes: '20MB'\n  highest-user-wasted-percent: '0.90'\n"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-dockerfile/example.md",
    "content": "# exmaple!\n\nwoot!"
  },
  {
    "path": "cmd/dive/cli/testdata/image-multi-layer-dockerfile/overwrite.md",
    "content": "# evil!\n\nthis will overwrite the other file..."
  },
  {
    "path": "cmd/dive/cli/testdata/invalid/Dockerfile",
    "content": "FROM scratch\nINVALID woops"
  },
  {
    "path": "cmd/dive/cli/testdata/snapshots/cli_build_test.snap",
    "content": "\n[Test_Build_Dockerfile/implicit_dockerfile - 1]\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       131 bytes (131 B)\n  userWastedPercent: 71.98 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  3      80 B          /root/saved.txt\n  2      34 B          /root/example/somefile1.txt\n  2      17 B          /root/example/somefile3.txt\n  2      0 B           /root\n  10     0 B           /etc\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_Build_Dockerfile/explicit_file_flag - 1]\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       131 bytes (131 B)\n  userWastedPercent: 71.98 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  3      80 B          /root/saved.txt\n  2      34 B          /root/example/somefile1.txt\n  2      17 B          /root/example/somefile3.txt\n  2      0 B           /root\n  10     0 B           /etc\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_Build_Containerfile/implicit_containerfile - 1]\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       131 bytes (131 B)\n  userWastedPercent: 71.98 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  3      80 B          /root/saved.txt\n  2      34 B          /root/example/somefile1.txt\n  2      17 B          /root/example/somefile3.txt\n  2      0 B           /root\n  10     0 B           /etc\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_Build_Containerfile/explicit_file_flag - 1]\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       131 bytes (131 B)\n  userWastedPercent: 71.98 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  3      80 B          /root/saved.txt\n  2      34 B          /root/example/somefile1.txt\n  2      17 B          /root/example/somefile3.txt\n  2      0 B           /root\n  10     0 B           /etc\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_BuildFailure/nonexistent_directory - 1]\nBuilding image                ... ./path/does/not/exist\n\n---\n\n[Test_BuildFailure/invalid_dockerfile - 1]\nBuilding image                ... ./testdata/invalid\n#0 building with \"desktop-linux\" instance using docker driver\n\n#1 [internal] load build definition from Dockerfile\n#1 transferring dockerfile: 100B done\n#1 DONE 0.0s\nDockerfile:2\n--------------------\n   1 |     FROM scratch\n   2 | >>> INVALID woops\n--------------------\nERROR: failed to solve: dockerfile parse error on line 2: unknown instruction: INVALID\n\nView build details: docker-desktop://<redacted>\n---\n\n[Test_Build_CI_gate_fail - 1]\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       131 bytes (131 B)\n  userWastedPercent: 71.98 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  3      80 B          /root/saved.txt\n  2      34 B          /root/example/somefile1.txt\n  2      17 B          /root/example/somefile3.txt\n  2      0 B           /root\n  10     0 B           /etc\n\nEvaluation:\n  FAIL  highestUserWastedPercent (too many bytes wasted, relative to the user bytes added (%-user-wasted-bytes=0.72 > threshold=0.1))\n  SKIP  highestWastedBytes (disabled)\n  PASS  lowestEfficiency (0.9)\n\nFAIL [pass:1 fail:1 skip:1]\n\n---\n"
  },
  {
    "path": "cmd/dive/cli/testdata/snapshots/cli_ci_test.snap",
    "content": "\n[Test_CI_Fail - 1]\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       131 bytes (131 B)\n  userWastedPercent: 71.98 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  3      80 B          /root/saved.txt\n  2      34 B          /root/example/somefile1.txt\n  2      17 B          /root/example/somefile3.txt\n  2      0 B           /root\n  10     0 B           /etc\n\nEvaluation:\n  FAIL  highestUserWastedPercent (too many bytes wasted, relative to the user bytes added (%-user-wasted-bytes=0.72 > threshold=0.1))\n  SKIP  highestWastedBytes (disabled)\n  PASS  lowestEfficiency (0.9)\n\nFAIL [pass:1 fail:1 skip:1]\n\n---\n\n[Test_CI_DefaultCIConfig - 1]\n[0001]  INFO dive version: testing\n[0001] DEBUG config:\n\u001b[35m  log:\n      quiet: false\n      level: debug\n      file: \"\"\n  dev:\n      profile: none\n  image: /Users/wagoodman/code/dive/.data/test-docker-image.tar\n  container-engine: docker\n  ignore-errors: false\n  ci: false\n  ci-config: .dive-ci\n  rules:\n      lowest-efficiency: \"0.96\"\n      highest-wasted-bytes: 19Mb\n      highest-user-wasted-percent: \"0.6\"\n  json-path: \"\"\n  keybinding:\n      quit: ctrl+c\n      toggle-view: tab\n      filter-files: ctrl+f, ctrl+slash\n      close-filter-files: esc\n      up: up,k\n      down: down,j\n      left: left,h\n      right: right,l\n      page-up: pgup,u\n      page-down: pgdn,d\n      compare-all: ctrl+a\n      compare-layer: ctrl+l\n      toggle-collapse-dir: space\n      toggle-collapse-all-dir: ctrl+space\n      toggle-added-files: ctrl+a\n      toggle-removed-files: ctrl+r\n      toggle-modified-files: ctrl+m\n      toggle-unmodified-files: ctrl+u\n      toggle-filetree-attributes: ctrl+b\n      toggle-sort-order: ctrl+o\n      toggle-wrap-tree: ctrl+p\n      extract-file: ctrl+e\n  diff:\n      hide: []\n  filetree:\n      collapse-dir: false\n      pane-width: 0.5\n      show-attributes: true\n  layer:\n      show-aggregated-changes: false\u001b[0m\n[0001]  INFO fetching image=/Users/wagoodman/code/dive/.data/test-docker-image.tar\n[0001] DEBUG └── resolver: docker-engine\n\n---\n"
  },
  {
    "path": "cmd/dive/cli/testdata/snapshots/cli_config_test.snap",
    "content": "\n[Test_Config - 1]\nlog:\n  # suppress all logging output (env: DIVE_LOG_QUIET)\n  quiet: false\n\n  # explicitly set the logging level (available: [error warn info debug trace]) (env: DIVE_LOG_LEVEL)\n  level: 'warn'\n\n  # file path to write logs to (env: DIVE_LOG_FILE)\n  file: ''\n\ndev:\n  # capture resource profiling data (available: [cpu, mem]) (env: DIVE_DEV_PROFILE)\n  profile: 'none'\n\n# container engine to use for image analysis (supported options: 'docker' and 'podman') (env: DIVE_CONTAINER_ENGINE)\ncontainer-engine: 'docker'\n\n# continue with analysis even if there are errors parsing the image archive (env: DIVE_IGNORE_ERRORS)\nignore-errors: false\n\n# enable CI mode (env: DIVE_CI)\nci: true\n\n# path to the CI config file (env: DIVE_CI_CONFIG)\nci-config: '.dive-ci'\n\nrules:\n  # lowest allowable image efficiency (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_LOWEST_EFFICIENCY)\n  lowest-efficiency: '0.9'\n\n  # highest allowable bytes wasted, otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_WASTED_BYTES)\n  highest-wasted-bytes: '20MB'\n\n  # highest allowable percentage of bytes wasted (as a ratio between 0-1), otherwise CI validation will fail. (env: DIVE_RULES_HIGHEST_USER_WASTED_PERCENT)\n  highest-user-wasted-percent: '0.90'\n\n# Skip the interactive TUI and write the layer analysis statistics to a given file. (env: DIVE_JSON_PATH)\njson-path: ''\n\nkeybinding:\n  # quit the application (global) (env: DIVE_KEYBINDING_QUIT)\n  quit: 'ctrl+c'\n\n  # toggle between different views (global) (env: DIVE_KEYBINDING_TOGGLE_VIEW)\n  toggle-view: 'tab'\n\n  # filter files by name (global) (env: DIVE_KEYBINDING_FILTER_FILES)\n  filter-files: 'ctrl+f, ctrl+slash'\n\n  # close file filtering (global) (env: DIVE_KEYBINDING_CLOSE_FILTER_FILES)\n  close-filter-files: 'esc'\n\n  # move cursor up (global) (env: DIVE_KEYBINDING_UP)\n  up: 'up,k'\n\n  # move cursor down (global) (env: DIVE_KEYBINDING_DOWN)\n  down: 'down,j'\n\n  # move cursor left (global) (env: DIVE_KEYBINDING_LEFT)\n  left: 'left,h'\n\n  # move cursor right (global) (env: DIVE_KEYBINDING_RIGHT)\n  right: 'right,l'\n\n  # scroll page up (file view) (env: DIVE_KEYBINDING_PAGE_UP)\n  page-up: 'pgup,u'\n\n  # scroll page down (file view) (env: DIVE_KEYBINDING_PAGE_DOWN)\n  page-down: 'pgdn,d'\n\n  # compare all layers (layer view) (env: DIVE_KEYBINDING_COMPARE_ALL)\n  compare-all: 'ctrl+a'\n\n  # compare specific layer (layer view) (env: DIVE_KEYBINDING_COMPARE_LAYER)\n  compare-layer: 'ctrl+l'\n\n  # toggle directory collapse (file view) (env: DIVE_KEYBINDING_TOGGLE_COLLAPSE_DIR)\n  toggle-collapse-dir: 'space'\n\n  # toggle collapse all directories (file view) (env: DIVE_KEYBINDING_TOGGLE_COLLAPSE_ALL_DIR)\n  toggle-collapse-all-dir: 'ctrl+space'\n\n  # toggle visibility of added files (file view) (env: DIVE_KEYBINDING_TOGGLE_ADDED_FILES)\n  toggle-added-files: 'ctrl+a'\n\n  # toggle visibility of removed files (file view) (env: DIVE_KEYBINDING_TOGGLE_REMOVED_FILES)\n  toggle-removed-files: 'ctrl+r'\n\n  # toggle visibility of modified files (file view) (env: DIVE_KEYBINDING_TOGGLE_MODIFIED_FILES)\n  toggle-modified-files: 'ctrl+m'\n\n  # toggle visibility of unmodified files (file view) (env: DIVE_KEYBINDING_TOGGLE_UNMODIFIED_FILES)\n  toggle-unmodified-files: 'ctrl+u'\n\n  # toggle display of file attributes (file view) (env: DIVE_KEYBINDING_TOGGLE_FILETREE_ATTRIBUTES)\n  toggle-filetree-attributes: 'ctrl+b'\n\n  # toggle sort order (file view) (env: DIVE_KEYBINDING_TOGGLE_SORT_ORDER)\n  toggle-sort-order: 'ctrl+o'\n\n  # (env: DIVE_KEYBINDING_TOGGLE_WRAP_TREE)\n  toggle-wrap-tree: 'ctrl+p'\n\n  # extract file contents (file view) (env: DIVE_KEYBINDING_EXTRACT_FILE)\n  extract-file: 'ctrl+e'\n\ndiff:\n  # types of file differences to hide (added, removed, modified, unmodified) (env: DIVE_DIFF_HIDE)\n  hide: []\n\nfiletree:\n  # collapse directories by default in the filetree (env: DIVE_FILETREE_COLLAPSE_DIR)\n  collapse-dir: false\n\n  # percentage of screen width for the filetree pane (must be >0 and <1) (env: DIVE_FILETREE_PANE_WIDTH)\n  pane-width: 0.5\n\n  # show file attributes in the filetree view (env: DIVE_FILETREE_SHOW_ATTRIBUTES)\n  show-attributes: true\n\nlayer:\n  # show aggregated changes across all previous layers (env: DIVE_LAYER_SHOW_AGGREGATED_CHANGES)\n  show-aggregated-changes: false\n\n---\n"
  },
  {
    "path": "cmd/dive/cli/testdata/snapshots/cli_json_test.snap",
    "content": "\n[Test_JsonOutput/json_output - 1]\n{\n \"image\": {\n  \"efficiencyScore\": 1,\n  \"fileReference\": [],\n  \"inefficientBytes\": 0,\n  \"sizeBytes\": 4277894\n },\n \"layer\": [\n  {\n   \"command\": \"BusyBox 1.37.0 (glibc), Debian 12\",\n   \"digestId\": \"sha256:068f50152bbc6e10c9d223150c9fbd30d11bcfd7789c432152aa0a99703bd03a\",\n   \"fileList\": [\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"bin/[\",\n     \"size\": 1029688,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/[[\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/acpid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/add-shell\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/addgroup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/adduser\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/adjtimex\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ar\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/arch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/arp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/arping\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ascii\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ash\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/awk\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/base32\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/base64\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/basename\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/beep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/blkdiscard\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/blkid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/blockdev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bootchartd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/brctl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bunzip2\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/busybox\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bzcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/bzip2\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cal\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chgrp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chown\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chpasswd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chpst\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chroot\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chrt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/chvt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cksum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/clear\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cmp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/comm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/conspy\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cpio\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/crc32\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/crond\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/crontab\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cryptpw\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cttyhack\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/cut\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/date\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/deallocvt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/delgroup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/deluser\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/depmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/devmem\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/df\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dhcprelay\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/diff\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dirname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dmesg\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dnsd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dnsdomainname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dos2unix\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dpkg\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dpkg-deb\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/du\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dumpkmap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/dumpleases\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/echo\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ed\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/egrep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/eject\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/env\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/envdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/envuidgid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ether-wake\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/expand\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/expr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/factor\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fakeidentd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fallocate\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/false\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fatattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fbset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fbsplash\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fdflush\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fdformat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fdisk\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fgconsole\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fgrep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/find\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/findfs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/flock\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fold\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/free\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/freeramdisk\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsck\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsck.minix\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsfreeze\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fstrim\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fsync\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ftpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ftpget\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ftpput\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/fuser\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"bin/getconf\",\n     \"size\": 27136,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/getfattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/getopt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/getty\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/grep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/groups\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/gunzip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/gzip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/halt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hdparm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/head\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hexdump\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hexedit\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hostid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hostname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/httpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hush\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/hwclock\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cdetect\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cdump\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cget\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2cset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/i2ctransfer\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/id\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifconfig\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifdown\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifenslave\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifplugd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ifup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/inetd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/init\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/insmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/install\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ionice\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iostat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipaddr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipcalc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipcrm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipcs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iplink\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ipneigh\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iproute\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iprule\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/iptunnel\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/kbd_mode\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/kill\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/killall\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/killall5\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/klogd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/last\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/less\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/link\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/linux32\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/linux64\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/linuxrc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ln\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/loadfont\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/loadkmap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/logger\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/login\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/logname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/logread\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/losetup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lpq\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lpr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ls\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsof\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lspci\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsscsi\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lsusb\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lzcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lzma\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/lzop\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/makedevs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/makemime\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/man\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/md5sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mdev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mesg\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/microcom\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mim\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkdosfs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mke2fs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfifo\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfs.ext2\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfs.minix\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkfs.vfat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mknod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkpasswd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mkswap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mktemp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/modinfo\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/modprobe\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/more\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mount\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mountpoint\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mpstat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/mv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nameif\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nanddump\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nandwrite\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nbd-client\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/netstat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nice\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nmeter\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nohup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nologin\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nproc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nsenter\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/nslookup\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ntpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/od\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/openvt\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/partprobe\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/passwd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/paste\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/patch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pgrep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pidof\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ping\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ping6\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pipe_progress\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pivot_root\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pkill\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pmap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/popmaildir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/poweroff\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/powertop\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/printenv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/printf\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ps\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pscan\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pstree\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pwd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/pwdx\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/raidautorun\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rdate\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rdev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/readahead\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/readlink\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/readprofile\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/realpath\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/reboot\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/reformime\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/remove-shell\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/renice\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/reset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/resize\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/resume\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rev\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rmdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rmmod\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/route\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rpm\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rpm2cpio\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rtcwake\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/run-init\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/run-parts\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/runlevel\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/runsv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/runsvdir\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/rx\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/script\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/scriptreplay\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sed\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/seedrng\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sendmail\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/seq\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setarch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setconsole\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setfattr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setfont\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setkeycodes\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setlogcons\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setpriv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setserial\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setsid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/setuidgid\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sh\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha1sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha256sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha3sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sha512sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/showkey\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/shred\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/shuf\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/slattach\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sleep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/smemcap\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/softlimit\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sort\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/split\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ssl_client\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/start-stop-daemon\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/stat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/strings\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/stty\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/su\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sulogin\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sum\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sv\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/svc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/svlogd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/svok\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/swapoff\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/swapon\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/switch_root\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sync\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/sysctl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/syslogd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tac\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tail\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tar\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/taskset\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tcpsvd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tee\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/telnet\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/telnetd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/test\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tftp\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tftpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/time\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/timeout\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/top\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/touch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tr\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/traceroute\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/traceroute6\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tree\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/true\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/truncate\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ts\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tsort\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tty\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ttysize\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/tunctl\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubiattach\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubidetach\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubimkvol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubirename\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubirmvol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubirsvol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/ubiupdatevol\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/udhcpc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/udhcpc6\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/udhcpd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/udpsvd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uevent\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/umount\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unexpand\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uniq\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unix2dos\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unlink\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unlzma\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unshare\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unxz\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/unzip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uptime\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/users\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/usleep\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uudecode\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/uuencode\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/vconfig\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/vi\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/vlock\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/volname\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/w\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/wall\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/watch\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/watchdog\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/wc\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/wget\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/which\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/who\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/whoami\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/whois\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xargs\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xxd\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xz\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/xzcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/yes\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/zcat\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"bin/[\",\n     \"path\": \"bin/zcip\",\n     \"size\": 0,\n     \"typeFlag\": 49,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"bin\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"dev\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/group\",\n     \"size\": 306,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/localtime\",\n     \"size\": 114,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-down.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-post-down.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-pre-up.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network/if-up.d\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc/network\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/nsswitch.conf\",\n     \"size\": 494,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/passwd\",\n     \"size\": 340,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 384,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"etc/shadow\",\n     \"size\": 136,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"etc\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 65534,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"home\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 65534\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/ld-linux-x86-64.so.2\",\n     \"size\": 215000,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 493,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libc.so.6\",\n     \"size\": 1922136,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libm.so.6\",\n     \"size\": 911904,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libnss_compat.so.2\",\n     \"size\": 39896,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libnss_dns.so.2\",\n     \"size\": 14400,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libnss_files.so.2\",\n     \"size\": 14400,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libnss_hesiod.so.2\",\n     \"size\": 27136,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libpthread.so.0\",\n     \"size\": 14480,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 420,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"\",\n     \"path\": \"lib/libresolv.so.2\",\n     \"size\": 60328,\n     \"typeFlag\": 48,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"lib\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 134218239,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"lib\",\n     \"path\": \"lib64\",\n     \"size\": 0,\n     \"typeFlag\": 50,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484096,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"root\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2148532735,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"tmp\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 134218239,\n     \"gid\": 0,\n     \"isDir\": false,\n     \"linkName\": \"../../bin/env\",\n     \"path\": \"usr/bin/env\",\n     \"size\": 0,\n     \"typeFlag\": 50,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"usr/bin\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 1,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"usr/sbin\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 1\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"usr\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 8,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var/spool/mail\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 8\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var/spool\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var/www\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    },\n    {\n     \"fileMode\": 2147484141,\n     \"gid\": 0,\n     \"isDir\": true,\n     \"linkName\": \"\",\n     \"path\": \"var\",\n     \"size\": 0,\n     \"typeFlag\": 53,\n     \"uid\": 0\n    }\n   ],\n   \"id\": \"blobs\",\n   \"index\": 0,\n   \"sizeBytes\": 4277894\n  }\n ]\n}\n---\n"
  },
  {
    "path": "cmd/dive/cli/testdata/snapshots/cli_load_test.snap",
    "content": "\n[Test_LoadImage/from_docker_engine - 1]\nLoading image                 busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f\nAnalyzing image               [layers:1 files:441 size:4.3 MB]\nEvaluating image              [rules: 3]\n\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       0 bytes \n  userWastedPercent: 0 %\n\nInefficient Files: (None)\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_LoadImage/from_docker_engine_(flag) - 1]\nLoading image                 busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f\nAnalyzing image               [layers:1 files:441 size:4.3 MB]\nEvaluating image              [rules: 3]\n\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       0 bytes \n  userWastedPercent: 0 %\n\nInefficient Files: (None)\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_LoadImage/from_podman_engine - 1]\nLoading image                 busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f\nAnalyzing image               [layers:1 files:441 size:4.3 MB]\nEvaluating image              [rules: 3]\n\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       0 bytes \n  userWastedPercent: 0 %\n\nInefficient Files: (None)\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_LoadImage/from_podman_engine_(flag) - 1]\nLoading image                 busybox:1.37.0@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f\nAnalyzing image               [layers:1 files:441 size:4.3 MB]\nEvaluating image              [rules: 3]\n\nAnalysis:\n  efficiency:        100.00 %\n  wastedBytes:       0 bytes \n  userWastedPercent: 0 %\n\nInefficient Files: (None)\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_LoadImage/from_archive - 1]\nLoading image                 /Users/wagoodman/code/dive/.data/test-docker-image.tar\nAnalyzing image               [layers:14 files:451 size:1.2 MB]\nEvaluating image              [rules: 3]\n\nAnalysis:\n  efficiency:        98.44 %\n  wastedBytes:       32025 bytes (32 kB)\n  userWastedPercent: 48.35 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  2      13 kB         /root/saved.txt\n  2      13 kB         /root/example/somefile1.txt\n  2      6.4 kB        /root/example/somefile3.txt\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_LoadImage/from_archive_(flag) - 1]\nLoading image                 /Users/wagoodman/code/dive/.data/test-docker-image.tar\nAnalyzing image               [layers:14 files:451 size:1.2 MB]\nEvaluating image              [rules: 3]\n\nAnalysis:\n  efficiency:        98.44 %\n  wastedBytes:       32025 bytes (32 kB)\n  userWastedPercent: 48.35 %\n\nInefficient Files:\n  Count  Wasted Space  File Path\n  2      13 kB         /root/saved.txt\n  2      13 kB         /root/example/somefile1.txt\n  2      6.4 kB        /root/example/somefile3.txt\n\nEvaluation:\n  PASS  highestUserWastedPercent (0.90)\n  PASS  highestWastedBytes (20MB)\n  PASS  lowestEfficiency (0.9)\n\nPASS [pass:3]\n\n---\n\n[Test_FetchFailure/nonexistent_image - 1]\nLoading image                 docker:wagoodman/nonexistent/image:tag\n\n---\n\n[Test_FetchFailure/invalid_image_name - 1]\nLoading image                 /wagoodman/invalid:image:format\n\n---\n"
  },
  {
    "path": "cmd/dive/main.go",
    "content": "package main\n\n// Copyright © 2018 Alex Goodman\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n// THE SOFTWARE.\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/wagoodman/dive/cmd/dive/cli\"\n)\n\n// applicationName is the non-capitalized name of the application (do not change this)\nconst (\n\tapplicationName = \"dive\"\n\tnotProvided     = \"[not provided]\"\n)\n\n// TODO: these need to be wired up to the build flags\n// all variables here are provided as build-time arguments, with clear default values\nvar (\n\tversion        = notProvided\n\tbuildDate      = notProvided\n\tgitCommit      = notProvided\n\tgitDescription = notProvided\n)\n\nfunc main() {\n\tapp := cli.Application(\n\t\tclio.Identification{\n\t\t\tName:           applicationName,\n\t\t\tVersion:        version,\n\t\t\tBuildDate:      buildDate,\n\t\t\tGitCommit:      gitCommit,\n\t\t\tGitDescription: gitDescription,\n\t\t},\n\t)\n\n\tapp.Run()\n}\n"
  },
  {
    "path": "dive/filetree/comparer.go",
    "content": "package filetree\n\nimport (\n\t\"fmt\"\n)\n\ntype TreeIndexKey struct {\n\tbottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int\n}\n\nfunc NewTreeIndexKey(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) TreeIndexKey {\n\treturn TreeIndexKey{\n\t\tbottomTreeStart: bottomTreeStart,\n\t\tbottomTreeStop:  bottomTreeStop,\n\t\ttopTreeStart:    topTreeStart,\n\t\ttopTreeStop:     topTreeStop,\n\t}\n}\n\nfunc (index TreeIndexKey) String() string {\n\tif index.bottomTreeStart == index.bottomTreeStop && index.topTreeStart == index.topTreeStop {\n\t\treturn fmt.Sprintf(\"Index(%d:%d)\", index.bottomTreeStart, index.topTreeStart)\n\t} else if index.bottomTreeStart == index.bottomTreeStop {\n\t\treturn fmt.Sprintf(\"Index(%d:%d-%d)\", index.bottomTreeStart, index.topTreeStart, index.topTreeStop)\n\t} else if index.topTreeStart == index.topTreeStop {\n\t\treturn fmt.Sprintf(\"Index(%d-%d:%d)\", index.bottomTreeStart, index.bottomTreeStop, index.topTreeStart)\n\t}\n\treturn fmt.Sprintf(\"Index(%d-%d:%d-%d)\", index.bottomTreeStart, index.bottomTreeStop, index.topTreeStart, index.topTreeStop)\n}\n\ntype Comparer struct {\n\trefTrees   []*FileTree\n\ttrees      map[TreeIndexKey]*FileTree\n\tpathErrors map[TreeIndexKey][]PathError\n}\n\nfunc NewComparer(refTrees []*FileTree) Comparer {\n\treturn Comparer{\n\t\trefTrees:   refTrees,\n\t\ttrees:      make(map[TreeIndexKey]*FileTree),\n\t\tpathErrors: make(map[TreeIndexKey][]PathError),\n\t}\n}\n\nfunc (cmp *Comparer) GetPathErrors(key TreeIndexKey) ([]PathError, error) {\n\t_, pathErrors, err := cmp.get(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn pathErrors, nil\n}\n\nfunc (cmp *Comparer) GetTree(key TreeIndexKey) (*FileTree, error) {\n\t// func (cmp *Comparer) GetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) (*FileTree, []PathError, error) {\n\t// key := TreeIndexKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}\n\n\tif value, exists := cmp.trees[key]; exists {\n\t\treturn value, nil\n\t}\n\n\tvalue, pathErrors, err := cmp.get(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcmp.trees[key] = value\n\tcmp.pathErrors[key] = pathErrors\n\treturn value, nil\n}\n\nfunc (cmp *Comparer) get(key TreeIndexKey) (*FileTree, []PathError, error) {\n\tnewTree, pathErrors, err := StackTreeRange(cmp.refTrees, key.bottomTreeStart, key.bottomTreeStop)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tfor idx := key.topTreeStart; idx <= key.topTreeStop; idx++ {\n\t\tmarkPathErrors, err := newTree.CompareAndMark(cmp.refTrees[idx])\n\t\tpathErrors = append(pathErrors, markPathErrors...)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to build tree: %w\", err)\n\t\t}\n\t}\n\treturn newTree, pathErrors, nil\n}\n\n// case 1: layer compare (top tree SIZE is fixed (BUT floats forward), Bottom tree SIZE changes)\nfunc (cmp *Comparer) NaturalIndexes() <-chan TreeIndexKey {\n\tindexes := make(chan TreeIndexKey)\n\n\tgo func() {\n\t\tdefer close(indexes)\n\n\t\tvar bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int\n\n\t\tfor selectIdx := 0; selectIdx < len(cmp.refTrees); selectIdx++ {\n\t\t\tbottomTreeStart = 0\n\t\t\ttopTreeStop = selectIdx\n\n\t\t\tif selectIdx == 0 {\n\t\t\t\tbottomTreeStop = selectIdx\n\t\t\t\ttopTreeStart = selectIdx\n\t\t\t} else {\n\t\t\t\tbottomTreeStop = selectIdx - 1\n\t\t\t\ttopTreeStart = selectIdx\n\t\t\t}\n\n\t\t\tindexes <- TreeIndexKey{\n\t\t\t\tbottomTreeStart: bottomTreeStart,\n\t\t\t\tbottomTreeStop:  bottomTreeStop,\n\t\t\t\ttopTreeStart:    topTreeStart,\n\t\t\t\ttopTreeStop:     topTreeStop,\n\t\t\t}\n\t\t}\n\t}()\n\treturn indexes\n}\n\n// case 2: aggregated compare (bottom tree is ENTIRELY fixed, top tree SIZE changes)\nfunc (cmp *Comparer) AggregatedIndexes() <-chan TreeIndexKey {\n\tindexes := make(chan TreeIndexKey)\n\n\tgo func() {\n\t\tdefer close(indexes)\n\n\t\tvar bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int\n\n\t\tfor selectIdx := 0; selectIdx < len(cmp.refTrees); selectIdx++ {\n\t\t\tbottomTreeStart = 0\n\t\t\ttopTreeStop = selectIdx\n\t\t\tif selectIdx == 0 {\n\t\t\t\tbottomTreeStop = selectIdx\n\t\t\t\ttopTreeStart = selectIdx\n\t\t\t} else {\n\t\t\t\tbottomTreeStop = 0\n\t\t\t\ttopTreeStart = 1\n\t\t\t}\n\n\t\t\tindexes <- TreeIndexKey{\n\t\t\t\tbottomTreeStart: bottomTreeStart,\n\t\t\t\tbottomTreeStop:  bottomTreeStop,\n\t\t\t\ttopTreeStart:    topTreeStart,\n\t\t\t\ttopTreeStop:     topTreeStop,\n\t\t\t}\n\t\t}\n\t}()\n\treturn indexes\n}\n\nfunc (cmp *Comparer) BuildCache() (errors []error) {\n\tfor index := range cmp.NaturalIndexes() {\n\t\tpathError, _ := cmp.GetPathErrors(index)\n\t\tif len(pathError) > 0 {\n\t\t\tfor _, path := range pathError {\n\t\t\t\terrors = append(errors, fmt.Errorf(\"path error at layer index %s: %s\", index, path))\n\t\t\t}\n\t\t}\n\t\t_, err := cmp.GetTree(index)\n\t\tif err != nil {\n\t\t\terrors = append(errors, err)\n\t\t\treturn errors\n\t\t}\n\t}\n\n\tfor index := range cmp.AggregatedIndexes() {\n\t\t_, err := cmp.GetTree(index)\n\t\tif err != nil {\n\t\t\terrors = append(errors, err)\n\t\t\treturn errors\n\t\t}\n\t}\n\treturn errors\n}\n"
  },
  {
    "path": "dive/filetree/diff.go",
    "content": "package filetree\n\nimport (\n\t\"fmt\"\n)\n\nconst (\n\tUnmodified DiffType = iota\n\tModified\n\tAdded\n\tRemoved\n)\n\n// DiffType defines the comparison result between two FileNodes\ntype DiffType int\n\n// String of a DiffType\nfunc (diff DiffType) String() string {\n\tswitch diff {\n\tcase Unmodified:\n\t\treturn \"Unmodified\"\n\tcase Modified:\n\t\treturn \"Modified\"\n\tcase Added:\n\t\treturn \"Added\"\n\tcase Removed:\n\t\treturn \"Removed\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"%d\", int(diff))\n\t}\n}\n\n// merge two DiffTypes into a single result. Essentially, return the given value unless they two values differ,\n// in which case we can only determine that there is \"a change\".\nfunc (diff DiffType) merge(other DiffType) DiffType {\n\tif diff == other {\n\t\treturn diff\n\t}\n\treturn Modified\n}\n"
  },
  {
    "path": "dive/filetree/efficiency.go",
    "content": "package filetree\n\nimport (\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"sort\"\n)\n\n// EfficiencyData represents the storage and reference statistics for a given file tree path.\ntype EfficiencyData struct {\n\tPath              string\n\tNodes             []*FileNode\n\tCumulativeSize    int64\n\tminDiscoveredSize int64\n}\n\n// EfficiencySlice represents an ordered set of EfficiencyData data structures.\ntype EfficiencySlice []*EfficiencyData\n\n// Len is required for sorting.\nfunc (efs EfficiencySlice) Len() int {\n\treturn len(efs)\n}\n\n// Swap operation is required for sorting.\nfunc (efs EfficiencySlice) Swap(i, j int) {\n\tefs[i], efs[j] = efs[j], efs[i]\n}\n\n// Less comparison is required for sorting.\nfunc (efs EfficiencySlice) Less(i, j int) bool {\n\treturn efs[i].CumulativeSize < efs[j].CumulativeSize\n}\n\n// Efficiency returns the score and file set of the given set of FileTrees (layers). This is loosely based on:\n// 1. Files that are duplicated across layers discounts your score, weighted by file size\n// 2. Files that are removed discounts your score, weighted by the original file size\nfunc Efficiency(trees []*FileTree) (float64, EfficiencySlice) {\n\tefficiencyMap := make(map[string]*EfficiencyData)\n\tinefficientMatches := make(EfficiencySlice, 0)\n\tcurrentTree := 0\n\n\tvisitor := func(node *FileNode) error {\n\t\tpath := node.Path()\n\t\tif _, ok := efficiencyMap[path]; !ok {\n\t\t\tefficiencyMap[path] = &EfficiencyData{\n\t\t\t\tPath:              path,\n\t\t\t\tNodes:             make([]*FileNode, 0),\n\t\t\t\tminDiscoveredSize: -1,\n\t\t\t}\n\t\t}\n\t\tdata := efficiencyMap[path]\n\n\t\t// this node may have had children that were deleted, however, we won't explicitly list out every child, only\n\t\t// the top-most parent with the cumulative size. These operations will need to be done on the full (stacked)\n\t\t// tree.\n\t\t// Note: whiteout files may also represent directories, so we need to find out if this was previously a file or dir.\n\t\tvar sizeBytes int64\n\n\t\tif node.IsWhiteout() {\n\t\t\tsizer := func(curNode *FileNode) error {\n\t\t\t\tsizeBytes += curNode.Data.FileInfo.Size\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tstackedTree, failedPaths, err := StackTreeRange(trees, 0, currentTree-1)\n\t\t\tif len(failedPaths) > 0 {\n\t\t\t\tfor _, path := range failedPaths {\n\t\t\t\t\tlog.WithFields(\"path\", path.String()).Debug(\"unable to include path in stacked tree\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to stack tree range: %w\", err)\n\t\t\t}\n\n\t\t\tpreviousTreeNode, err := stackedTree.GetNode(node.Path())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif previousTreeNode.Data.FileInfo.IsDir {\n\t\t\t\terr = previousTreeNode.VisitDepthChildFirst(sizer, nil, nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"unable to propagate whiteout dir: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tsizeBytes = node.Data.FileInfo.Size\n\t\t}\n\n\t\tdata.CumulativeSize += sizeBytes\n\t\tif data.minDiscoveredSize < 0 || sizeBytes < data.minDiscoveredSize {\n\t\t\tdata.minDiscoveredSize = sizeBytes\n\t\t}\n\t\tdata.Nodes = append(data.Nodes, node)\n\n\t\tif len(data.Nodes) == 2 {\n\t\t\tinefficientMatches = append(inefficientMatches, data)\n\t\t}\n\n\t\treturn nil\n\t}\n\tvisitEvaluator := func(node *FileNode) bool {\n\t\treturn node.IsLeaf()\n\t}\n\tfor idx, tree := range trees {\n\t\tcurrentTree = idx\n\t\terr := tree.VisitDepthChildFirst(visitor, visitEvaluator)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"layer\", tree.Id, \"error\", err).Debug(\"unable to propagate layer tree\")\n\t\t}\n\t}\n\n\t// calculate the score\n\tvar minimumPathSizes int64\n\tvar discoveredPathSizes int64\n\n\tfor _, value := range efficiencyMap {\n\t\tminimumPathSizes += value.minDiscoveredSize\n\t\tdiscoveredPathSizes += value.CumulativeSize\n\t}\n\tvar score float64\n\tif discoveredPathSizes == 0 {\n\t\tscore = 1.0\n\t} else {\n\t\tscore = float64(minimumPathSizes) / float64(discoveredPathSizes)\n\t}\n\n\tsort.Sort(inefficientMatches)\n\n\treturn score, inefficientMatches\n}\n"
  },
  {
    "path": "dive/filetree/efficiency_test.go",
    "content": "package filetree\n\nimport (\n\t\"testing\"\n)\n\nfunc checkError(t *testing.T, err error, message string) {\n\tif err != nil {\n\t\tt.Errorf(message+\": %+v\", err)\n\t}\n}\n\nfunc TestEfficiency(t *testing.T) {\n\ttrees := make([]*FileTree, 3)\n\tfor idx := range trees {\n\t\ttrees[idx] = NewFileTree()\n\t}\n\n\t_, _, err := trees[0].AddPath(\"/etc/nginx/nginx.conf\", FileInfo{Size: 2000})\n\tcheckError(t, err, \"could not setup test\")\n\n\t_, _, err = trees[0].AddPath(\"/etc/nginx/public\", FileInfo{Size: 3000})\n\tcheckError(t, err, \"could not setup test\")\n\n\t_, _, err = trees[1].AddPath(\"/etc/nginx/nginx.conf\", FileInfo{Size: 5000})\n\tcheckError(t, err, \"could not setup test\")\n\t_, _, err = trees[1].AddPath(\"/etc/athing\", FileInfo{Size: 10000})\n\tcheckError(t, err, \"could not setup test\")\n\n\t_, _, err = trees[2].AddPath(\"/etc/.wh.nginx\", *BlankFileChangeInfo(\"/etc/.wh.nginx\"))\n\tcheckError(t, err, \"could not setup test\")\n\n\tvar expectedScore = 0.75\n\tvar expectedMatches = EfficiencySlice{\n\t\t&EfficiencyData{Path: \"/etc/nginx/nginx.conf\", CumulativeSize: 7000},\n\t}\n\tactualScore, actualMatches := Efficiency(trees)\n\n\tif expectedScore != actualScore {\n\t\tt.Errorf(\"Expected score of %v but go %v\", expectedScore, actualScore)\n\t}\n\n\tif len(actualMatches) != len(expectedMatches) {\n\t\tfor _, match := range actualMatches {\n\t\t\tt.Logf(\"   match: %+v\", match)\n\t\t}\n\t\tt.Fatalf(\"Expected to find %d inefficient paths, but found %d\", len(expectedMatches), len(actualMatches))\n\t}\n\n\tif expectedMatches[0].Path != actualMatches[0].Path {\n\t\tt.Errorf(\"Expected path of %s but go %s\", expectedMatches[0].Path, actualMatches[0].Path)\n\t}\n\n\tif expectedMatches[0].CumulativeSize != actualMatches[0].CumulativeSize {\n\t\tt.Errorf(\"Expected cumulative size of %v but go %v\", expectedMatches[0].CumulativeSize, actualMatches[0].CumulativeSize)\n\t}\n}\n\nfunc TestEfficiency_ScratchImage(t *testing.T) {\n\ttrees := make([]*FileTree, 3)\n\tfor idx := range trees {\n\t\ttrees[idx] = NewFileTree()\n\t}\n\n\t_, _, err := trees[0].AddPath(\"/nothing\", FileInfo{Size: 0})\n\tcheckError(t, err, \"could not setup test\")\n\n\tvar expectedScore = 1.0\n\tvar expectedMatches = EfficiencySlice{}\n\tactualScore, actualMatches := Efficiency(trees)\n\n\tif expectedScore != actualScore {\n\t\tt.Errorf(\"Expected score of %v but go %v\", expectedScore, actualScore)\n\t}\n\n\tif len(actualMatches) > 0 {\n\t\tt.Fatalf(\"Expected to find %d inefficient paths, but found %d\", len(expectedMatches), len(actualMatches))\n\t}\n\n}\n"
  },
  {
    "path": "dive/filetree/file_info.go",
    "content": "package filetree\n\nimport (\n\t\"archive/tar\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/cespare/xxhash/v2\"\n)\n\n// FileInfo contains tar metadata for a specific FileNode\ntype FileInfo struct {\n\tPath     string      `json:\"path\"`\n\tTypeFlag byte        `json:\"typeFlag\"`\n\tLinkname string      `json:\"linkName\"`\n\thash     uint64      //`json:\"hash\"`\n\tSize     int64       `json:\"size\"`\n\tMode     os.FileMode `json:\"fileMode\"`\n\tUid      int         `json:\"uid\"`\n\tGid      int         `json:\"gid\"`\n\tIsDir    bool        `json:\"isDir\"`\n}\n\n// NewFileInfoFromTarHeader extracts the metadata from a tar header and file contents and generates a new FileInfo object.\nfunc NewFileInfoFromTarHeader(reader *tar.Reader, header *tar.Header, path string) FileInfo {\n\tvar hash uint64\n\tif header.Typeflag != tar.TypeDir {\n\t\thash = getHashFromReader(reader)\n\t}\n\n\treturn FileInfo{\n\t\tPath:     path,\n\t\tTypeFlag: header.Typeflag,\n\t\tLinkname: header.Linkname,\n\t\thash:     hash,\n\t\tSize:     header.FileInfo().Size(),\n\t\tMode:     header.FileInfo().Mode(),\n\t\tUid:      header.Uid,\n\t\tGid:      header.Gid,\n\t\tIsDir:    header.FileInfo().IsDir(),\n\t}\n}\n\nfunc NewFileInfo(realPath, path string, info os.FileInfo) FileInfo {\n\tvar err error\n\n\t// todo: don't use tar types here, create our own...\n\tvar fileType byte\n\tvar linkName string\n\tvar size int64\n\n\tif info.Mode()&os.ModeSymlink != 0 {\n\t\tfileType = tar.TypeSymlink\n\n\t\tlinkName, err = os.Readlink(realPath)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"unable to read symlink %q: %s\", realPath, err))\n\t\t}\n\t} else if info.IsDir() {\n\t\tfileType = tar.TypeDir\n\t} else {\n\t\tfileType = tar.TypeReg\n\n\t\tsize = info.Size()\n\t}\n\n\tvar hash uint64\n\tif fileType != tar.TypeDir {\n\t\tfile, err := os.Open(realPath)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"unable to open file %q: %s\", realPath, err))\n\t\t}\n\t\tdefer file.Close()\n\t\thash = getHashFromReader(file)\n\t}\n\n\treturn FileInfo{\n\t\tPath:     path,\n\t\tTypeFlag: fileType,\n\t\tLinkname: linkName,\n\t\thash:     hash,\n\t\tSize:     size,\n\t\tMode:     info.Mode(),\n\t\t// todo: support UID/GID\n\t\tUid:   -1,\n\t\tGid:   -1,\n\t\tIsDir: info.IsDir(),\n\t}\n}\n\n// Copy duplicates a FileInfo\nfunc (data *FileInfo) Copy() *FileInfo {\n\tif data == nil {\n\t\treturn nil\n\t}\n\treturn &FileInfo{\n\t\tPath:     data.Path,\n\t\tTypeFlag: data.TypeFlag,\n\t\tLinkname: data.Linkname,\n\t\thash:     data.hash,\n\t\tSize:     data.Size,\n\t\tMode:     data.Mode,\n\t\tUid:      data.Uid,\n\t\tGid:      data.Gid,\n\t\tIsDir:    data.IsDir,\n\t}\n}\n\n// Compare determines the DiffType between two FileInfos based on the type and contents of each given FileInfo\nfunc (data *FileInfo) Compare(other FileInfo) DiffType {\n\tif data.TypeFlag == other.TypeFlag {\n\t\tif data.hash == other.hash &&\n\t\t\tdata.Mode == other.Mode &&\n\t\t\tdata.Uid == other.Uid &&\n\t\t\tdata.Gid == other.Gid {\n\t\t\treturn Unmodified\n\t\t}\n\t}\n\treturn Modified\n}\n\nfunc getHashFromReader(reader io.Reader) uint64 {\n\th := xxhash.New()\n\n\tbuf := make([]byte, 1024)\n\tfor {\n\t\tn, err := reader.Read(buf)\n\t\tif err != nil && err != io.EOF {\n\t\t\tpanic(fmt.Errorf(\"unable to read file: %w\", err))\n\t\t}\n\t\tif n == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t_, err = h.Write(buf[:n])\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"unable to write to hash: %w\", err))\n\t\t}\n\t}\n\n\treturn h.Sum64()\n}\n"
  },
  {
    "path": "dive/filetree/file_node.go",
    "content": "package filetree\n\nimport (\n\t\"archive/tar\"\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"strings\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/fatih/color\"\n\t\"github.com/phayes/permbits\"\n)\n\nconst (\n\tAttributeFormat = \"%s%s %11s %10s \"\n)\n\nvar diffTypeColor = map[DiffType]*color.Color{\n\tAdded:      color.New(color.FgGreen),\n\tRemoved:    color.New(color.FgRed),\n\tModified:   color.New(color.FgYellow),\n\tUnmodified: color.New(color.Reset),\n}\n\n// FileNode represents a single file, its relation to files beneath it, the tree it exists in, and the metadata of the given file.\ntype FileNode struct {\n\tTree     *FileTree\n\tParent   *FileNode\n\tSize     int64 // memoized total size of file or directory\n\tName     string\n\tData     NodeData\n\tChildren map[string]*FileNode\n\tpath     string\n}\n\n// NewNode creates a new FileNode relative to the given parent node with a payload.\nfunc NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {\n\tnode = new(FileNode)\n\tnode.Name = name\n\tnode.Data = *NewNodeData()\n\tnode.Data.FileInfo = *data.Copy()\n\tnode.Size = -1 // signal lazy load later\n\n\tnode.Children = make(map[string]*FileNode)\n\tnode.Parent = parent\n\tif parent != nil {\n\t\tnode.Tree = parent.Tree\n\t}\n\n\treturn node\n}\n\n// renderTreeLine returns a string representing this FileNode in the context of a greater ASCII tree.\nfunc (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string {\n\tvar otherBranches string\n\tfor _, space := range spaces {\n\t\tif space {\n\t\t\totherBranches += noBranchSpace\n\t\t} else {\n\t\t\totherBranches += branchSpace\n\t\t}\n\t}\n\n\tthisBranch := middleItem\n\tif last {\n\t\tthisBranch = lastItem\n\t}\n\n\tcollapsedIndicator := uncollapsedItem\n\tif collapsed {\n\t\tcollapsedIndicator = collapsedItem\n\t}\n\n\treturn otherBranches + thisBranch + collapsedIndicator + node.String() + newLine\n}\n\n// Copy duplicates the existing node relative to a new parent node.\nfunc (node *FileNode) Copy(parent *FileNode) *FileNode {\n\tnewNode := NewNode(parent, node.Name, node.Data.FileInfo)\n\tnewNode.Data.ViewInfo = node.Data.ViewInfo\n\tnewNode.Data.DiffType = node.Data.DiffType\n\tfor name, child := range node.Children {\n\t\tnewNode.Children[name] = child.Copy(newNode)\n\t\tchild.Parent = newNode\n\t}\n\treturn newNode\n}\n\n// AddChild creates a new node relative to the current FileNode.\nfunc (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) {\n\t// never allow processing of purely whiteout flag files (for now)\n\tif strings.HasPrefix(name, doubleWhiteoutPrefix) {\n\t\treturn nil\n\t}\n\n\tchild = NewNode(node, name, data)\n\tif node.Children[name] != nil {\n\t\t// tree node already exists, replace the payload, keep the children\n\t\tnode.Children[name].Data.FileInfo = *data.Copy()\n\t} else {\n\t\tnode.Children[name] = child\n\t\tnode.Tree.Size++\n\t}\n\n\treturn child\n}\n\n// Remove deletes the current FileNode from it's parent FileNode's relations.\nfunc (node *FileNode) Remove() error {\n\tif node == node.Tree.Root {\n\t\treturn fmt.Errorf(\"cannot remove the tree root\")\n\t}\n\tfor _, child := range node.Children {\n\t\terr := child.Remove()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tdelete(node.Parent.Children, node.Name)\n\tnode.Tree.Size--\n\treturn nil\n}\n\n// String shows the filename formatted into the proper color (by DiffType), additionally indicating if it is a symlink.\nfunc (node *FileNode) String() string {\n\tvar display string\n\tif node == nil {\n\t\treturn \"\"\n\t}\n\n\tdisplay = node.Name\n\tif node.Data.FileInfo.TypeFlag == tar.TypeSymlink || node.Data.FileInfo.TypeFlag == tar.TypeLink {\n\t\tdisplay += \" → \" + node.Data.FileInfo.Linkname\n\t}\n\treturn diffTypeColor[node.Data.DiffType].Sprint(display)\n}\n\n// MetadatString returns the FileNode metadata in a columnar string.\nfunc (node *FileNode) MetadataString() string {\n\tif node == nil {\n\t\treturn \"\"\n\t}\n\n\tdir := \"-\"\n\tif node.Data.FileInfo.IsDir {\n\t\tdir = \"d\"\n\t}\n\n\tfm := permbits.FileMode(node.Data.FileInfo.Mode)\n\tvar fileMode strings.Builder\n\tfileMode.Grow(9)\n\tcond := func(c bool, x, y byte) byte {\n\t\tif c {\n\t\t\treturn x\n\t\t} else {\n\t\t\treturn y\n\t\t}\n\t}\n\tfileMode.WriteByte(cond(fm.UserRead(), 'r', '-'))\n\tfileMode.WriteByte(cond(fm.UserWrite(), 'w', '-'))\n\tfileMode.WriteByte(cond(fm.UserExecute(), cond(fm.Setuid(), 's', 'x'), cond(fm.Setuid(), 'S', '-')))\n\n\tfileMode.WriteByte(cond(fm.GroupRead(), 'r', '-'))\n\tfileMode.WriteByte(cond(fm.GroupWrite(), 'w', '-'))\n\tfileMode.WriteByte(cond(fm.GroupExecute(), cond(fm.Setgid(), 's', 'x'), cond(fm.Setgid(), 'S', '-')))\n\n\tfileMode.WriteByte(cond(fm.OtherRead(), 'r', '-'))\n\tfileMode.WriteByte(cond(fm.OtherWrite(), 'w', '-'))\n\tfileMode.WriteByte(cond(fm.OtherExecute(), cond(fm.Sticky(), 't', 'x'), cond(fm.Sticky(), 'T', '-')))\n\n\tuser := node.Data.FileInfo.Uid\n\tgroup := node.Data.FileInfo.Gid\n\tuserGroup := fmt.Sprintf(\"%d:%d\", user, group)\n\n\t// don't include file sizes of children that have been removed (unless the node in question is a removed dir,\n\t// then show the accumulated size of removed files)\n\tsizeBytes := node.GetSize()\n\n\tsize := humanize.Bytes(uint64(sizeBytes))\n\n\treturn diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode.String(), userGroup, size))\n}\n\nfunc (node *FileNode) GetSize() int64 {\n\tif 0 <= node.Size {\n\t\treturn node.Size\n\t}\n\tvar sizeBytes int64\n\n\tif node.IsLeaf() {\n\t\tsizeBytes = node.Data.FileInfo.Size\n\t} else {\n\t\tsizer := func(curNode *FileNode) error {\n\n\t\t\tif curNode.Data.DiffType != Removed || node.Data.DiffType == Removed {\n\t\t\t\tsizeBytes += curNode.Data.FileInfo.Size\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\terr := node.VisitDepthChildFirst(sizer, nil, nil)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Debug(\"unable to propagate tree to get file size\")\n\t\t}\n\t}\n\tnode.Size = sizeBytes\n\treturn node.Size\n}\n\n// VisitDepthChildFirst iterates a tree depth-first (starting at this FileNode), evaluating the deepest depths first (visit on bubble up)\nfunc (node *FileNode) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator, sorter OrderStrategy) error {\n\tif sorter == nil {\n\t\tsorter = GetSortOrderStrategy(ByName)\n\t}\n\tkeys := sorter.orderKeys(node.Children)\n\tfor _, name := range keys {\n\t\tchild := node.Children[name]\n\t\terr := child.VisitDepthChildFirst(visitor, evaluator, sorter)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// never visit the root node\n\tif node == node.Tree.Root {\n\t\treturn nil\n\t} else if evaluator != nil && evaluator(node) || evaluator == nil {\n\t\treturn visitor(node)\n\t}\n\n\treturn nil\n}\n\n// VisitDepthParentFirst iterates a tree depth-first (starting at this FileNode), evaluating the shallowest depths first (visit while sinking down)\nfunc (node *FileNode) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator, sorter OrderStrategy) error {\n\tvar err error\n\n\tdoVisit := evaluator != nil && evaluator(node) || evaluator == nil\n\n\tif !doVisit {\n\t\treturn nil\n\t}\n\n\t// never visit the root node\n\tif node != node.Tree.Root {\n\t\terr = visitor(node)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif sorter == nil {\n\t\tsorter = GetSortOrderStrategy(ByName)\n\t}\n\tkeys := sorter.orderKeys(node.Children)\n\tfor _, name := range keys {\n\t\tchild := node.Children[name]\n\t\terr = child.VisitDepthParentFirst(visitor, evaluator, sorter)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\n// IsWhiteout returns an indication if this file may be a overlay-whiteout file.\nfunc (node *FileNode) IsWhiteout() bool {\n\treturn strings.HasPrefix(node.Name, whiteoutPrefix)\n}\n\n// IsLeaf returns true is the current node has no child nodes.\nfunc (node *FileNode) IsLeaf() bool {\n\treturn len(node.Children) == 0\n}\n\n// Path returns a slash-delimited string from the root of the greater tree to the current node (e.g. /a/path/to/here)\nfunc (node *FileNode) Path() string {\n\tif node.path == \"\" {\n\t\tvar path []string\n\t\tcurNode := node\n\t\tfor {\n\t\t\tif curNode.Parent == nil {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tname := curNode.Name\n\t\t\tif curNode == node {\n\t\t\t\t// white out prefixes are fictitious on leaf nodes\n\t\t\t\tname = strings.TrimPrefix(name, whiteoutPrefix)\n\t\t\t}\n\n\t\t\tpath = append([]string{name}, path...)\n\t\t\tcurNode = curNode.Parent\n\t\t}\n\t\tnode.path = \"/\" + strings.Join(path, \"/\")\n\t}\n\treturn strings.Replace(node.path, \"//\", \"/\", -1)\n}\n\n// deriveDiffType determines a DiffType to the current FileNode. Note: the DiffType of a node is always the DiffType of\n// its attributes and its contents. The contents are the bytes of the file of the children of a directory.\nfunc (node *FileNode) deriveDiffType(diffType DiffType) error {\n\tif node.IsLeaf() {\n\t\treturn node.AssignDiffType(diffType)\n\t}\n\n\tmyDiffType := diffType\n\tfor _, v := range node.Children {\n\t\tmyDiffType = myDiffType.merge(v.Data.DiffType)\n\t}\n\n\treturn node.AssignDiffType(myDiffType)\n}\n\n// AssignDiffType will assign the given DiffType to this node, possibly affecting child nodes.\nfunc (node *FileNode) AssignDiffType(diffType DiffType) error {\n\tvar err error\n\n\tnode.Data.DiffType = diffType\n\n\tif diffType == Removed {\n\t\t// if we've removed this node, then all children have been removed as well\n\t\tfor _, child := range node.Children {\n\t\t\terr = child.AssignDiffType(diffType)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// compare the current node against the given node, returning a definitive DiffType.\nfunc (node *FileNode) compare(other *FileNode) DiffType {\n\tif node == nil && other == nil {\n\t\treturn Unmodified\n\t}\n\n\tif node == nil && other != nil {\n\t\treturn Added\n\t}\n\n\tif node != nil && other == nil {\n\t\treturn Removed\n\t}\n\n\tif other.IsWhiteout() {\n\t\treturn Removed\n\t}\n\tif node.Name != other.Name {\n\t\tpanic(\"comparing mismatched nodes\")\n\t}\n\n\treturn node.Data.FileInfo.Compare(other.Data.FileInfo)\n}\n"
  },
  {
    "path": "dive/filetree/file_node_test.go",
    "content": "package filetree\n\nimport (\n\t\"testing\"\n)\n\nfunc TestAddChild(t *testing.T) {\n\tvar expected, actual int\n\ttree := NewFileTree()\n\n\tpayload := FileInfo{\n\t\tPath: \"stufffffs\",\n\t}\n\n\tone := tree.Root.AddChild(\"first node!\", payload)\n\n\ttwo := tree.Root.AddChild(\"nil node!\", FileInfo{})\n\n\ttree.Root.AddChild(\"third node!\", FileInfo{})\n\ttwo.AddChild(\"forth, one level down...\", FileInfo{})\n\ttwo.AddChild(\"fifth, one level down...\", FileInfo{})\n\ttwo.AddChild(\"fifth, one level down...\", FileInfo{})\n\n\texpected, actual = 5, tree.Size\n\tif expected != actual {\n\t\tt.Errorf(\"Expected a tree size of %d got %d.\", expected, actual)\n\t}\n\n\texpected, actual = 2, len(two.Children)\n\tif expected != actual {\n\t\tt.Errorf(\"Expected 'twos' number of children to be %d got %d.\", expected, actual)\n\t}\n\n\texpected, actual = 3, len(tree.Root.Children)\n\tif expected != actual {\n\t\tt.Errorf(\"Expected 'twos' number of children to be %d got %d.\", expected, actual)\n\t}\n\n\texpectedFC := FileInfo{\n\t\tPath: \"stufffffs\",\n\t}\n\tactualFC := one.Data.FileInfo\n\tif expectedFC.Path != actualFC.Path {\n\t\tt.Errorf(\"Expected 'ones' payload to be %+v got %+v.\", expectedFC, actualFC)\n\t}\n\n}\n\nfunc TestRemoveChild(t *testing.T) {\n\tvar expected, actual int\n\n\ttree := NewFileTree()\n\ttree.Root.AddChild(\"first\", FileInfo{})\n\ttwo := tree.Root.AddChild(\"nil\", FileInfo{})\n\ttree.Root.AddChild(\"third\", FileInfo{})\n\tforth := two.AddChild(\"forth\", FileInfo{})\n\ttwo.AddChild(\"fifth\", FileInfo{})\n\n\terr := forth.Remove()\n\tcheckError(t, err, \"unable to setup test\")\n\n\texpected, actual = 4, tree.Size\n\tif expected != actual {\n\t\tt.Errorf(\"Expected a tree size of %d got %d.\", expected, actual)\n\t}\n\n\tif tree.Root.Children[\"forth\"] != nil {\n\t\tt.Errorf(\"Expected 'forth' node to be deleted.\")\n\t}\n\n\terr = two.Remove()\n\tcheckError(t, err, \"unable to setup test\")\n\n\texpected, actual = 2, tree.Size\n\tif expected != actual {\n\t\tt.Errorf(\"Expected a tree size of %d got %d.\", expected, actual)\n\t}\n\n\tif tree.Root.Children[\"nil\"] != nil {\n\t\tt.Errorf(\"Expected 'nil' node to be deleted.\")\n\t}\n\n}\n\nfunc TestPath(t *testing.T) {\n\texpected := \"/etc/nginx/nginx.conf\"\n\ttree := NewFileTree()\n\tnode, _, _ := tree.AddPath(expected, FileInfo{})\n\n\tactual := node.Path()\n\tif expected != actual {\n\t\tt.Errorf(\"Expected path '%s' got '%s'\", expected, actual)\n\t}\n}\n\nfunc TestIsWhiteout(t *testing.T) {\n\ttree1 := NewFileTree()\n\tp1, _, _ := tree1.AddPath(\"/etc/nginx/public1\", FileInfo{})\n\tp2, _, _ := tree1.AddPath(\"/etc/nginx/.wh.public2\", FileInfo{})\n\tp3, _, _ := tree1.AddPath(\"/etc/nginx/public3/.wh..wh..opq\", FileInfo{})\n\n\tif p1.IsWhiteout() != false {\n\t\tt.Errorf(\"Expected path '%s' to **not** be a whiteout file\", p1.Name)\n\t}\n\n\tif p2.IsWhiteout() != true {\n\t\tt.Errorf(\"Expected path '%s' to be a whiteout file\", p2.Name)\n\t}\n\n\tif p3 != nil {\n\t\tt.Errorf(\"Expected to not be able to add path '%s'\", p2.Name)\n\t}\n}\n\nfunc TestDiffTypeFromAddedChildren(t *testing.T) {\n\ttree := NewFileTree()\n\tnode, _, _ := tree.AddPath(\"/usr\", *BlankFileChangeInfo(\"/usr\"))\n\tnode.Data.DiffType = Unmodified\n\n\tnode, _, _ = tree.AddPath(\"/usr/bin\", *BlankFileChangeInfo(\"/usr/bin\"))\n\tnode.Data.DiffType = Added\n\n\tnode, _, _ = tree.AddPath(\"/usr/bin2\", *BlankFileChangeInfo(\"/usr/bin2\"))\n\tnode.Data.DiffType = Removed\n\n\terr := tree.Root.Children[\"usr\"].deriveDiffType(Unmodified)\n\tcheckError(t, err, \"unable to setup test\")\n\n\tif tree.Root.Children[\"usr\"].Data.DiffType != Modified {\n\t\tt.Errorf(\"Expected Modified but got %v\", tree.Root.Children[\"usr\"].Data.DiffType)\n\t}\n}\nfunc TestDiffTypeFromRemovedChildren(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, _ = tree.AddPath(\"/usr\", *BlankFileChangeInfo(\"/usr\"))\n\n\tinfo1 := BlankFileChangeInfo(\"/usr/.wh.bin\")\n\tnode, _, _ := tree.AddPath(\"/usr/.wh.bin\", *info1)\n\tnode.Data.DiffType = Removed\n\n\tinfo2 := BlankFileChangeInfo(\"/usr/.wh.bin2\")\n\tnode, _, _ = tree.AddPath(\"/usr/.wh.bin2\", *info2)\n\tnode.Data.DiffType = Removed\n\n\terr := tree.Root.Children[\"usr\"].deriveDiffType(Unmodified)\n\tcheckError(t, err, \"unable to setup test\")\n\n\tif tree.Root.Children[\"usr\"].Data.DiffType != Modified {\n\t\tt.Errorf(\"Expected Modified but got %v\", tree.Root.Children[\"usr\"].Data.DiffType)\n\t}\n\n}\n\nfunc TestDirSize(t *testing.T) {\n\ttree1 := NewFileTree()\n\t_, _, err := tree1.AddPath(\"/etc/nginx/public1\", FileInfo{Size: 100})\n\tcheckError(t, err, \"unable to setup test\")\n\t_, _, err = tree1.AddPath(\"/etc/nginx/thing1\", FileInfo{Size: 200})\n\tcheckError(t, err, \"unable to setup test\")\n\t_, _, err = tree1.AddPath(\"/etc/nginx/public3/thing2\", FileInfo{Size: 300})\n\tcheckError(t, err, \"unable to setup test\")\n\n\tnode, _ := tree1.GetNode(\"/etc/nginx\")\n\texpected, actual := \"----------         0:0      600 B \", node.MetadataString()\n\tif expected != actual {\n\t\tt.Errorf(\"Expected metadata '%s' got '%s'\", expected, actual)\n\t}\n}\n"
  },
  {
    "path": "dive/filetree/file_tree.go",
    "content": "package filetree\n\nimport (\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n)\n\nconst (\n\tnewLine              = \"\\n\"\n\tnoBranchSpace        = \"    \"\n\tbranchSpace          = \"│   \"\n\tmiddleItem           = \"├─\"\n\tlastItem             = \"└─\"\n\twhiteoutPrefix       = \".wh.\"\n\tdoubleWhiteoutPrefix = \".wh..wh..\"\n\tuncollapsedItem      = \"─ \"\n\tcollapsedItem        = \"⊕ \"\n)\n\n// FileTree represents a set of files, directories, and their relations.\ntype FileTree struct {\n\tRoot      *FileNode\n\tSize      int\n\tFileSize  uint64\n\tName      string\n\tId        uuid.UUID\n\tSortOrder SortOrder\n}\n\n// NewFileTree creates an empty FileTree\nfunc NewFileTree() (tree *FileTree) {\n\ttree = new(FileTree)\n\ttree.Size = 0\n\ttree.Root = new(FileNode)\n\ttree.Root.Tree = tree\n\ttree.Root.Children = make(map[string]*FileNode)\n\ttree.Id = uuid.New()\n\ttree.SortOrder = ByName\n\treturn tree\n}\n\n// renderParams is a representation of a FileNode in the context of the greater tree. All\n// data stored is necessary for rendering a single line in a tree format.\ntype renderParams struct {\n\tnode          *FileNode\n\tspaces        []bool\n\tchildSpaces   []bool\n\tshowCollapsed bool\n\tisLast        bool\n}\n\n// renderStringTreeBetween returns a string representing the given tree between the given rows. Since each node\n// is rendered on its own line, the returned string shows the visible nodes not affected by a collapsed parent.\nfunc (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttributes bool) string {\n\t// generate a list of nodes to render\n\tvar params = make([]renderParams, 0)\n\tvar result string\n\n\t// visit from the front of the list\n\tvar paramsToVisit = []renderParams{{node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false}}\n\tfor currentRow := 0; len(paramsToVisit) > 0 && currentRow <= stopRow; currentRow++ {\n\t\t// pop the first node\n\t\tvar currentParams renderParams\n\t\tcurrentParams, paramsToVisit = paramsToVisit[0], paramsToVisit[1:]\n\n\t\t// take note of the next nodes to visit later\n\t\tsorter := GetSortOrderStrategy(tree.SortOrder)\n\t\tkeys := sorter.orderKeys(currentParams.node.Children)\n\n\t\tvar childParams = make([]renderParams, 0)\n\t\tfor idx, name := range keys {\n\t\t\tchild := currentParams.node.Children[name]\n\t\t\t// don't visit this node...\n\t\t\tif child.Data.ViewInfo.Hidden || currentParams.node.Data.ViewInfo.Collapsed {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// visit this node...\n\t\t\tisLast := idx == (len(currentParams.node.Children) - 1)\n\t\t\tshowCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0\n\n\t\t\t// completely copy the reference slice\n\t\t\tchildSpaces := make([]bool, len(currentParams.childSpaces))\n\t\t\tcopy(childSpaces, currentParams.childSpaces)\n\n\t\t\tif len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed {\n\t\t\t\tchildSpaces = append(childSpaces, isLast)\n\t\t\t}\n\n\t\t\tchildParams = append(childParams, renderParams{\n\t\t\t\tnode:          child,\n\t\t\t\tspaces:        currentParams.childSpaces,\n\t\t\t\tchildSpaces:   childSpaces,\n\t\t\t\tshowCollapsed: showCollapsed,\n\t\t\t\tisLast:        isLast,\n\t\t\t})\n\t\t}\n\t\t// keep the child nodes to visit later\n\t\tparamsToVisit = append(childParams, paramsToVisit...)\n\n\t\t// never process the root node\n\t\tif currentParams.node == tree.Root {\n\t\t\tcurrentRow--\n\t\t\tcontinue\n\t\t}\n\n\t\t// process the current node\n\t\tif currentRow >= startRow && currentRow <= stopRow {\n\t\t\tparams = append(params, currentParams)\n\t\t}\n\t}\n\n\t// render the result\n\tfor idx := range params {\n\t\tcurrentParams := params[idx]\n\n\t\tif showAttributes {\n\t\t\tresult += currentParams.node.MetadataString() + \" \"\n\t\t}\n\t\tresult += currentParams.node.renderTreeLine(currentParams.spaces, currentParams.isLast, currentParams.showCollapsed)\n\t}\n\n\treturn result\n}\n\nfunc (tree *FileTree) VisibleSize() int {\n\tvar size int\n\n\tvisitor := func(node *FileNode) error {\n\t\tsize++\n\t\treturn nil\n\t}\n\tvisitEvaluator := func(node *FileNode) bool {\n\t\tif node.Data.FileInfo.IsDir {\n\t\t\t// we won't visit a collapsed dir, but we need to count it\n\t\t\tif node.Data.ViewInfo.Collapsed {\n\t\t\t\tsize++\n\t\t\t}\n\t\t\treturn !node.Data.ViewInfo.Collapsed && !node.Data.ViewInfo.Hidden\n\t\t}\n\t\treturn !node.Data.ViewInfo.Hidden\n\t}\n\terr := tree.VisitDepthParentFirst(visitor, visitEvaluator)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Debug(\"unable to determine visible tree size\")\n\t}\n\n\t// don't include root\n\tsize--\n\n\treturn size\n}\n\n// String returns the entire tree in an ASCII representation.\nfunc (tree *FileTree) String(showAttributes bool) string {\n\treturn tree.renderStringTreeBetween(0, tree.Size, showAttributes)\n}\n\n// StringBetween returns a partial tree in an ASCII representation.\nfunc (tree *FileTree) StringBetween(start, stop int, showAttributes bool) string {\n\treturn tree.renderStringTreeBetween(start, stop, showAttributes)\n}\n\n// Copy returns a copy of the given FileTree\nfunc (tree *FileTree) Copy() *FileTree {\n\tnewTree := NewFileTree()\n\tnewTree.Size = tree.Size\n\tnewTree.FileSize = tree.FileSize\n\tnewTree.Root = tree.Root.Copy(newTree.Root)\n\tnewTree.SortOrder = tree.SortOrder\n\n\t// update the tree pointers\n\terr := newTree.VisitDepthChildFirst(func(node *FileNode) error {\n\t\tnode.Tree = newTree\n\t\treturn nil\n\t}, nil)\n\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Debug(\"unable to propagate tree on copy\")\n\t}\n\n\treturn newTree\n}\n\n// Visitor is a function that processes, observes, or otherwise transforms the given node\ntype Visitor func(*FileNode) error\n\n// VisitEvaluator is a function that indicates whether the given node should be visited by a Visitor.\ntype VisitEvaluator func(*FileNode) bool\n\n// VisitDepthChildFirst iterates the given tree depth-first, evaluating the deepest depths first (visit on bubble up)\nfunc (tree *FileTree) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {\n\tsorter := GetSortOrderStrategy(tree.SortOrder)\n\treturn tree.Root.VisitDepthChildFirst(visitor, evaluator, sorter)\n}\n\n// VisitDepthParentFirst iterates the given tree depth-first, evaluating the shallowest depths first (visit while sinking down)\nfunc (tree *FileTree) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {\n\tsorter := GetSortOrderStrategy(tree.SortOrder)\n\treturn tree.Root.VisitDepthParentFirst(visitor, evaluator, sorter)\n}\n\n// Stack takes two trees and combines them together. This is done by \"stacking\" the given tree on top of the owning tree.\nfunc (tree *FileTree) Stack(upper *FileTree) (failed []PathError, stackErr error) {\n\tgraft := func(node *FileNode) error {\n\t\tif node.IsWhiteout() {\n\t\t\terr := tree.RemovePath(node.Path())\n\t\t\tif err != nil {\n\t\t\t\tfailed = append(failed, NewPathError(node.Path(), ActionAdd, err))\n\t\t\t}\n\t\t} else {\n\t\t\t_, _, err := tree.AddPath(node.Path(), node.Data.FileInfo)\n\t\t\tif err != nil {\n\t\t\t\tfailed = append(failed, NewPathError(node.Path(), ActionRemove, err))\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\tstackErr = upper.VisitDepthChildFirst(graft, nil)\n\treturn failed, stackErr\n}\n\n// GetNode fetches a single node when given a slash-delimited string from root ('/') to the desired node (e.g. '/a/node/path')\nfunc (tree *FileTree) GetNode(path string) (*FileNode, error) {\n\tnodeNames := strings.Split(strings.Trim(path, \"/\"), \"/\")\n\tnode := tree.Root\n\tfor _, name := range nodeNames {\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif node.Children[name] == nil {\n\t\t\treturn nil, fmt.Errorf(\"path does not exist: %s\", path)\n\t\t}\n\t\tnode = node.Children[name]\n\t}\n\treturn node, nil\n}\n\n// AddPath adds a new node to the tree with the given payload\nfunc (tree *FileTree) AddPath(filepath string, data FileInfo) (*FileNode, []*FileNode, error) {\n\tfilepath = path.Clean(filepath)\n\tif filepath == \".\" {\n\t\treturn nil, nil, fmt.Errorf(\"cannot add relative path '%s'\", filepath)\n\t}\n\tnodeNames := strings.Split(strings.Trim(filepath, \"/\"), \"/\")\n\tnode := tree.Root\n\taddedNodes := make([]*FileNode, 0)\n\tfor idx, name := range nodeNames {\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// find or create node\n\t\tif node.Children[name] != nil {\n\t\t\tnode = node.Children[name]\n\t\t} else {\n\t\t\t// don't add paths that should be deleted\n\t\t\tif strings.HasPrefix(name, doubleWhiteoutPrefix) {\n\t\t\t\treturn nil, addedNodes, nil\n\t\t\t}\n\n\t\t\t// don't attach the payload. The payload is destined for the\n\t\t\t// Path's end node, not any intermediary node.\n\t\t\tnode = node.AddChild(name, FileInfo{})\n\t\t\taddedNodes = append(addedNodes, node)\n\n\t\t\tif node == nil {\n\t\t\t\t// the child could not be added\n\t\t\t\treturn node, addedNodes, fmt.Errorf(\"could not add child node: '%s' (path:'%s')\", name, filepath)\n\t\t\t}\n\t\t}\n\n\t\t// attach payload to the last specified node\n\t\tif idx == len(nodeNames)-1 {\n\t\t\tnode.Data.FileInfo = data\n\t\t}\n\t}\n\treturn node, addedNodes, nil\n}\n\n// RemovePath removes a node from the tree given its path.\nfunc (tree *FileTree) RemovePath(path string) error {\n\tnode, err := tree.GetNode(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn node.Remove()\n}\n\ntype compareMark struct {\n\tlowerNode *FileNode\n\tupperNode *FileNode\n\ttentative DiffType\n\tfinal     DiffType\n}\n\n// CompareAndMark marks the FileNodes in the owning (lower) tree with DiffType annotations when compared to the given (upper) tree.\nfunc (tree *FileTree) CompareAndMark(upper *FileTree) ([]PathError, error) {\n\t// always compare relative to the original, unaltered tree.\n\toriginalTree := tree\n\n\tmodifications := make([]compareMark, 0)\n\tfailed := make([]PathError, 0)\n\n\tgraft := func(upperNode *FileNode) error {\n\t\tif upperNode.IsWhiteout() {\n\t\t\terr := tree.markRemoved(upperNode.Path())\n\t\t\tif err != nil {\n\t\t\t\tfailed = append(failed, NewPathError(upperNode.Path(), ActionRemove, err))\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\t// note: since we are not comparing against the original tree (copying the tree is expensive) we may mark the parent\n\t\t// of an added node incorrectly as modified. This will be corrected later.\n\t\toriginalLowerNode, _ := originalTree.GetNode(upperNode.Path())\n\n\t\tif originalLowerNode == nil {\n\t\t\t_, newNodes, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)\n\t\t\tif err != nil {\n\t\t\t\tfailed = append(failed, NewPathError(upperNode.Path(), ActionAdd, err))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tfor idx := len(newNodes) - 1; idx >= 0; idx-- {\n\t\t\t\tnewNode := newNodes[idx]\n\t\t\t\tmodifications = append(modifications, compareMark{lowerNode: newNode, upperNode: upperNode, tentative: -1, final: Added})\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\t// the file exists in the lower layer\n\t\tlowerNode, _ := tree.GetNode(upperNode.Path())\n\t\tdiffType := lowerNode.compare(upperNode)\n\t\tmodifications = append(modifications, compareMark{lowerNode: lowerNode, upperNode: upperNode, tentative: diffType, final: -1})\n\n\t\treturn nil\n\t}\n\t// we must visit from the leaves upwards to ensure that diff types can be derived from and assigned to children\n\terr := upper.VisitDepthChildFirst(graft, nil)\n\tif err != nil {\n\t\treturn failed, err\n\t}\n\n\t// take note of the comparison results on each note in the owning tree.\n\tfor _, pair := range modifications {\n\t\tif pair.final > 0 {\n\t\t\terr = pair.lowerNode.AssignDiffType(pair.final)\n\t\t\tif err != nil {\n\t\t\t\treturn failed, err\n\t\t\t}\n\t\t} else if pair.lowerNode.Data.DiffType == Unmodified {\n\t\t\terr = pair.lowerNode.deriveDiffType(pair.tentative)\n\t\t\tif err != nil {\n\t\t\t\treturn failed, err\n\t\t\t}\n\t\t}\n\n\t\t// persist the upper's payload on the owning tree\n\t\tpair.lowerNode.Data.FileInfo = *pair.upperNode.Data.FileInfo.Copy()\n\t}\n\treturn failed, nil\n}\n\n// markRemoved annotates the FileNode at the given path as Removed.\nfunc (tree *FileTree) markRemoved(path string) error {\n\tnode, err := tree.GetNode(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn node.AssignDiffType(Removed)\n}\n\n// StackTreeRange combines an array of trees into a single tree\nfunc StackTreeRange(trees []*FileTree, start, stop int) (*FileTree, []PathError, error) {\n\terrors := make([]PathError, 0)\n\ttree := trees[0].Copy()\n\tfor idx := start; idx <= stop; idx++ {\n\t\tfailedPaths, err := tree.Stack(trees[idx])\n\t\tif len(failedPaths) > 0 {\n\t\t\terrors = append(errors, failedPaths...)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"could not stack tree range: %w\", err)\n\t\t}\n\t}\n\treturn tree, errors, nil\n}\n"
  },
  {
    "path": "dive/filetree/file_tree_test.go",
    "content": "package filetree\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\"\n)\n\nfunc stringInSlice(a string, list []string) bool {\n\tfor _, b := range list {\n\t\tif b == a {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc AssertDiffType(node *FileNode, expectedDiffType DiffType) error {\n\tif node.Data.DiffType != expectedDiffType {\n\t\treturn fmt.Errorf(\"Expecting node at %s to have DiffType %v, but had %v\", node.Path(), expectedDiffType, node.Data.DiffType)\n\t}\n\treturn nil\n}\n\nfunc TestStringCollapsed(t *testing.T) {\n\ttree := NewFileTree()\n\ttree.Root.AddChild(\"1 node!\", FileInfo{})\n\ttwo := tree.Root.AddChild(\"2 node!\", FileInfo{})\n\tsubTwo := two.AddChild(\"2 child!\", FileInfo{})\n\tsubTwo.AddChild(\"2 grandchild!\", FileInfo{})\n\tsubTwo.Data.ViewInfo.Collapsed = true\n\tthree := tree.Root.AddChild(\"3 node!\", FileInfo{})\n\tsubThree := three.AddChild(\"3 child!\", FileInfo{})\n\tthree.AddChild(\"3 nested child 1!\", FileInfo{})\n\tthreeGc1 := subThree.AddChild(\"3 grandchild 1!\", FileInfo{})\n\tthreeGc1.AddChild(\"3 greatgrandchild 1!\", FileInfo{})\n\tsubThree.AddChild(\"3 grandchild 2!\", FileInfo{})\n\tfour := tree.Root.AddChild(\"4 node!\", FileInfo{})\n\tfour.Data.ViewInfo.Collapsed = true\n\ttree.Root.AddChild(\"5 node!\", FileInfo{})\n\tfour.AddChild(\"6, one level down...\", FileInfo{})\n\n\texpected :=\n\t\t`├── 1 node!\n├── 2 node!\n│   └─⊕ 2 child!\n├── 3 node!\n│   ├── 3 child!\n│   │   ├── 3 grandchild 1!\n│   │   │   └── 3 greatgrandchild 1!\n│   │   └── 3 grandchild 2!\n│   └── 3 nested child 1!\n├─⊕ 4 node!\n└── 5 node!\n`\n\tactual := tree.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestString(t *testing.T) {\n\ttree := NewFileTree()\n\ttree.Root.AddChild(\"1 node!\", FileInfo{})\n\ttree.Root.AddChild(\"2 node!\", FileInfo{})\n\ttree.Root.AddChild(\"3 node!\", FileInfo{})\n\tfour := tree.Root.AddChild(\"4 node!\", FileInfo{})\n\ttree.Root.AddChild(\"5 node!\", FileInfo{})\n\tfour.AddChild(\"6, one level down...\", FileInfo{})\n\n\texpected :=\n\t\t`├── 1 node!\n├── 2 node!\n├── 3 node!\n├── 4 node!\n│   └── 6, one level down...\n└── 5 node!\n`\n\tactual := tree.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestStringBetween(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, err := tree.AddPath(\"/etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/etc/nginx/public\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/systemd\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/bashful\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp/nonsense\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\texpected :=\n\t\t`│       └── public\n├── tmp\n│   └── nonsense\n`\n\tactual := tree.StringBetween(3, 5, false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestRejectPurelyRelativePath(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, err := tree.AddPath(\"./etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"./\", FileInfo{})\n\n\tif err == nil {\n\t\tt.Errorf(\"expected to reject relative path, but did not\")\n\t}\n\n}\n\nfunc TestAddRelativePath(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, err := tree.AddPath(\"./etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\texpected :=\n\t\t`└── etc\n    └── nginx\n        └── nginx.conf\n`\n\tactual := tree.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestAddPath(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, err := tree.AddPath(\"/etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/etc/nginx/public\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/systemd\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/bashful\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp/nonsense\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\texpected :=\n\t\t`├── etc\n│   └── nginx\n│       ├── nginx.conf\n│       └── public\n├── tmp\n│   └── nonsense\n└── var\n    └── run\n        ├── bashful\n        └── systemd\n`\n\tactual := tree.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestAddWhiteoutPath(t *testing.T) {\n\ttree := NewFileTree()\n\tnode, _, err := tree.AddPath(\"usr/local/lib/python3.7/site-packages/pip/.wh..wh..opq\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"expected no error but got: %v\", err)\n\t}\n\tif node != nil {\n\t\tt.Errorf(\"expected node to be nil, but got: %v\", node)\n\t}\n\texpected :=\n\t\t`└── usr\n    └── local\n        └── lib\n            └── python3.7\n                └── site-packages\n                    └── pip\n`\n\tactual := tree.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n}\n\nfunc TestRemovePath(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, err := tree.AddPath(\"/etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/etc/nginx/public\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/systemd\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/bashful\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp/nonsense\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\terr = tree.RemovePath(\"/var/run/bashful\")\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\terr = tree.RemovePath(\"/tmp\")\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\texpected :=\n\t\t`├── etc\n│   └── nginx\n│       ├── nginx.conf\n│       └── public\n└── var\n    └── run\n        └── systemd\n`\n\tactual := tree.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestStack(t *testing.T) {\n\tpayloadKey := \"/var/run/systemd\"\n\tpayloadValue := FileInfo{\n\t\tPath: \"yup\",\n\t}\n\n\ttree1 := NewFileTree()\n\n\t_, _, err := tree1.AddPath(\"/etc/nginx/public\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree1.AddPath(payloadKey, FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree1.AddPath(\"/var/run/bashful\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree1.AddPath(\"/tmp\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree1.AddPath(\"/tmp/nonsense\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\ttree2 := NewFileTree()\n\t// add new files\n\t_, _, err = tree2.AddPath(\"/etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t// modify current files\n\t_, _, err = tree2.AddPath(payloadKey, payloadValue)\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t// whiteout the following files\n\t_, _, err = tree2.AddPath(\"/var/run/.wh.bashful\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree2.AddPath(\"/.wh.tmp\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t// ignore opaque whiteout files entirely\n\tnode, _, err := tree2.AddPath(\"/.wh..wh..opq\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"expected no error on whiteout file add, but got %v\", err)\n\t}\n\tif node != nil {\n\t\tt.Errorf(\"expected no node on whiteout file add, but got %v\", node)\n\t}\n\n\tfailedPaths, err := tree1.Stack(tree2)\n\n\tif err != nil {\n\t\tt.Errorf(\"Could not stack refTrees: %v\", err)\n\t}\n\n\tif len(failedPaths) > 0 {\n\t\tt.Errorf(\"expected no filepath errors, got %d\", len(failedPaths))\n\t}\n\n\texpected :=\n\t\t`├── etc\n│   └── nginx\n│       ├── nginx.conf\n│       └── public\n└── var\n    └── run\n        └── systemd\n`\n\n\tnode, err = tree1.GetNode(payloadKey)\n\tif err != nil {\n\t\tt.Errorf(\"Expected '%s' to still exist, but it doesn't\", payloadKey)\n\t}\n\n\tif node == nil || node.Data.FileInfo.Path != payloadValue.Path {\n\t\tt.Errorf(\"Expected '%s' value to be %+v but got %+v\", payloadKey, payloadValue, node.Data.FileInfo)\n\t}\n\n\tactual := tree1.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestCopy(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, err := tree.AddPath(\"/etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/etc/nginx/public\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/systemd\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/bashful\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp/nonsense\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\terr = tree.RemovePath(\"/var/run/bashful\")\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\terr = tree.RemovePath(\"/tmp\")\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\texpected :=\n\t\t`├── etc\n│   └── nginx\n│       ├── nginx.conf\n│       └── public\n└── var\n    └── run\n        └── systemd\n`\n\n\tNewFileTree := tree.Copy()\n\tactual := NewFileTree.String(false)\n\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n\nfunc TestCompareWithNoChanges(t *testing.T) {\n\tlowerTree := NewFileTree()\n\tupperTree := NewFileTree()\n\tpaths := [...]string{\"/etc\", \"/etc/sudoers\", \"/etc/hosts\", \"/usr/bin\", \"/usr/bin/bash\", \"/usr\"}\n\n\tfor _, value := range paths {\n\t\tfakeData := FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t}\n\t\t_, _, err := lowerTree.AddPath(value, fakeData)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t\t_, _, err = upperTree.AddPath(value, fakeData)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\tfailedPaths, err := lowerTree.CompareAndMark(upperTree)\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\tif len(failedPaths) > 0 {\n\t\tt.Errorf(\"expected no filepath errors, got %d\", len(failedPaths))\n\t}\n\tasserter := func(n *FileNode) error {\n\t\tif n.Path() == \"/\" {\n\t\t\treturn nil\n\t\t}\n\t\tif (n.Data.DiffType) != Unmodified {\n\t\t\tt.Errorf(\"Expecting node at %s to have DiffType unchanged, but had %v\", n.Path(), n.Data.DiffType)\n\t\t}\n\t\treturn nil\n\t}\n\terr = lowerTree.VisitDepthChildFirst(asserter, nil)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestCompareWithAdds(t *testing.T) {\n\tlowerTree := NewFileTree()\n\tupperTree := NewFileTree()\n\tlowerPaths := [...]string{\"/etc\", \"/etc/sudoers\", \"/usr\", \"/etc/hosts\", \"/usr/bin\"}\n\tupperPaths := [...]string{\"/etc\", \"/etc/sudoers\", \"/usr\", \"/etc/hosts\", \"/usr/bin\", \"/usr/bin/bash\", \"/a/new/path\"}\n\n\tfor _, value := range lowerPaths {\n\t\t_, _, err := lowerTree.AddPath(value, FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\n\tfor _, value := range upperPaths {\n\t\t_, _, err := upperTree.AddPath(value, FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\n\tfailedAssertions := []error{}\n\tfailedPaths, err := lowerTree.CompareAndMark(upperTree)\n\tif err != nil {\n\t\tt.Errorf(\"Expected tree compare to have no errors, got: %v\", err)\n\t}\n\tif len(failedPaths) > 0 {\n\t\tt.Errorf(\"expected no filepath errors, got %d\", len(failedPaths))\n\t}\n\tasserter := func(n *FileNode) error {\n\n\t\tp := n.Path()\n\t\tif p == \"/\" {\n\t\t\treturn nil\n\t\t} else if stringInSlice(p, []string{\"/usr/bin/bash\", \"/a\", \"/a/new\", \"/a/new/path\"}) {\n\t\t\tif err := AssertDiffType(n, Added); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t} else if stringInSlice(p, []string{\"/usr/bin\", \"/usr\"}) {\n\t\t\tif err := AssertDiffType(n, Modified); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := AssertDiffType(n, Unmodified); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\terr = lowerTree.VisitDepthChildFirst(asserter, nil)\n\tif err != nil {\n\t\tt.Errorf(\"Expected no errors when visiting nodes, got: %+v\", err)\n\t}\n\n\tif len(failedAssertions) > 0 {\n\t\tstr := \"\\n\"\n\t\tfor _, value := range failedAssertions {\n\t\t\tstr += fmt.Sprintf(\"  - %s\\n\", value.Error())\n\t\t}\n\t\tt.Errorf(\"Expected no errors when evaluating nodes, got: %s\", str)\n\t}\n}\n\nfunc TestCompareWithChanges(t *testing.T) {\n\tlowerTree := NewFileTree()\n\tupperTree := NewFileTree()\n\tchangedPaths := []string{\"/etc\", \"/usr\", \"/etc/hosts\", \"/etc/sudoers\", \"/usr/bin\"}\n\n\tfor _, value := range changedPaths {\n\t\t_, _, err := lowerTree.AddPath(value, FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t\t_, _, err = upperTree.AddPath(value, FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     456,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\n\tchmodPath := \"/etc/non-data-change\"\n\n\t_, _, err := lowerTree.AddPath(chmodPath, FileInfo{\n\t\tPath:     chmodPath,\n\t\tTypeFlag: 1,\n\t\thash:     123,\n\t\tMode:     0,\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\t_, _, err = upperTree.AddPath(chmodPath, FileInfo{\n\t\tPath:     chmodPath,\n\t\tTypeFlag: 1,\n\t\thash:     123,\n\t\tMode:     1,\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\tchangedPaths = append(changedPaths, chmodPath)\n\n\tchownPath := \"/etc/non-data-change-2\"\n\n\t_, _, err = lowerTree.AddPath(chmodPath, FileInfo{\n\t\tPath:     chownPath,\n\t\tTypeFlag: 1,\n\t\thash:     123,\n\t\tMode:     1,\n\t\tGid:      0,\n\t\tUid:      0,\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\t_, _, err = upperTree.AddPath(chmodPath, FileInfo{\n\t\tPath:     chownPath,\n\t\tTypeFlag: 1,\n\t\thash:     123,\n\t\tMode:     1,\n\t\tGid:      12,\n\t\tUid:      12,\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\tchangedPaths = append(changedPaths, chownPath)\n\n\tfailedPaths, err := lowerTree.CompareAndMark(upperTree)\n\tif err != nil {\n\t\tt.Errorf(\"unable to compare and mark: %+v\", err)\n\t}\n\tif len(failedPaths) > 0 {\n\t\tt.Errorf(\"expected no filepath errors, got %d\", len(failedPaths))\n\t}\n\tfailedAssertions := []error{}\n\tasserter := func(n *FileNode) error {\n\t\tp := n.Path()\n\t\tif p == \"/\" {\n\t\t\treturn nil\n\t\t} else if stringInSlice(p, changedPaths) {\n\t\t\tif err := AssertDiffType(n, Modified); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := AssertDiffType(n, Unmodified); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\terr = lowerTree.VisitDepthChildFirst(asserter, nil)\n\tif err != nil {\n\t\tt.Errorf(\"Expected no errors when visiting nodes, got: %+v\", err)\n\t}\n\n\tif len(failedAssertions) > 0 {\n\t\tstr := \"\\n\"\n\t\tfor _, value := range failedAssertions {\n\t\t\tstr += fmt.Sprintf(\"  - %s\\n\", value.Error())\n\t\t}\n\t\tt.Errorf(\"Expected no errors when evaluating nodes, got: %s\", str)\n\t}\n}\n\nfunc TestCompareWithRemoves(t *testing.T) {\n\tlowerTree := NewFileTree()\n\tupperTree := NewFileTree()\n\tlowerPaths := [...]string{\"/etc\", \"/usr\", \"/etc/hosts\", \"/etc/sudoers\", \"/usr/bin\", \"/root\", \"/root/example\", \"/root/example/some1\", \"/root/example/some2\"}\n\tupperPaths := [...]string{\"/.wh.etc\", \"/usr\", \"/usr/.wh.bin\", \"/root/.wh.example\"}\n\n\tfor _, value := range lowerPaths {\n\t\tfakeData := FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t}\n\t\t_, _, err := lowerTree.AddPath(value, fakeData)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\n\tfor _, value := range upperPaths {\n\t\tfakeData := FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t}\n\t\t_, _, err := upperTree.AddPath(value, fakeData)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\n\tfailedPaths, err := lowerTree.CompareAndMark(upperTree)\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\tif len(failedPaths) > 0 {\n\t\tt.Errorf(\"expected no filepath errors, got %d\", len(failedPaths))\n\t}\n\tfailedAssertions := []error{}\n\tasserter := func(n *FileNode) error {\n\t\tp := n.Path()\n\t\tif p == \"/\" {\n\t\t\treturn nil\n\t\t} else if stringInSlice(p, []string{\"/etc\", \"/usr/bin\", \"/etc/hosts\", \"/etc/sudoers\", \"/root/example/some1\", \"/root/example/some2\", \"/root/example\"}) {\n\t\t\tif err := AssertDiffType(n, Removed); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t} else if stringInSlice(p, []string{\"/usr\", \"/root\"}) {\n\t\t\tif err := AssertDiffType(n, Modified); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := AssertDiffType(n, Unmodified); err != nil {\n\t\t\t\tfailedAssertions = append(failedAssertions, err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\terr = lowerTree.VisitDepthChildFirst(asserter, nil)\n\tif err != nil {\n\t\tt.Errorf(\"Expected no errors when visiting nodes, got: %+v\", err)\n\t}\n\n\tif len(failedAssertions) > 0 {\n\t\tstr := \"\\n\"\n\t\tfor _, value := range failedAssertions {\n\t\t\tstr += fmt.Sprintf(\"  - %s\\n\", value.Error())\n\t\t}\n\t\tt.Errorf(\"Expected no errors when evaluating nodes, got: %s\", str)\n\t}\n}\n\nfunc TestStackRange(t *testing.T) {\n\ttree := NewFileTree()\n\t_, _, err := tree.AddPath(\"/etc/nginx/nginx.conf\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/etc/nginx/public\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/systemd\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/var/run/bashful\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\t_, _, err = tree.AddPath(\"/tmp/nonsense\", FileInfo{})\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\terr = tree.RemovePath(\"/var/run/bashful\")\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\terr = tree.RemovePath(\"/tmp\")\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\tlowerTree := NewFileTree()\n\tupperTree := NewFileTree()\n\tlowerPaths := [...]string{\"/etc\", \"/usr\", \"/etc/hosts\", \"/etc/sudoers\", \"/usr/bin\"}\n\tupperPaths := [...]string{\"/etc\", \"/usr\", \"/etc/hosts\", \"/etc/sudoers\", \"/usr/bin\"}\n\n\tfor _, value := range lowerPaths {\n\t\tfakeData := FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t}\n\t\t_, _, err = lowerTree.AddPath(value, fakeData)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\n\tfor _, value := range upperPaths {\n\t\tfakeData := FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     456,\n\t\t}\n\t\t_, _, err = upperTree.AddPath(value, fakeData)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t}\n\t}\n\ttrees := []*FileTree{lowerTree, upperTree, tree}\n\t_, failedPaths, err := StackTreeRange(trees, 0, 2)\n\tif len(failedPaths) > 0 {\n\t\tt.Errorf(\"expected no filepath errors, got %d\", len(failedPaths))\n\t}\n\tassert.NoError(t, err)\n}\n\nfunc TestRemoveOnIterate(t *testing.T) {\n\n\ttree := NewFileTree()\n\tpaths := [...]string{\"/etc\", \"/usr\", \"/etc/hosts\", \"/etc/sudoers\", \"/usr/bin\", \"/usr/something\"}\n\n\tfor _, value := range paths {\n\t\tfakeData := FileInfo{\n\t\t\tPath:     value,\n\t\t\tTypeFlag: 1,\n\t\t\thash:     123,\n\t\t}\n\t\tnode, _, err := tree.AddPath(value, fakeData)\n\t\tif err == nil && stringInSlice(node.Path(), []string{\"/etc\"}) {\n\t\t\tnode.Data.ViewInfo.Hidden = true\n\t\t}\n\t}\n\n\terr := tree.VisitDepthChildFirst(func(node *FileNode) error {\n\t\tif node.Data.ViewInfo.Hidden {\n\t\t\terr := tree.RemovePath(node.Path())\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"could not setup test: %v\", err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}, nil)\n\tif err != nil {\n\t\tt.Errorf(\"could not setup test: %v\", err)\n\t}\n\n\texpected :=\n\t\t`└── usr\n    ├── bin\n    └── something\n`\n\tactual := tree.String(false)\n\tif expected != actual {\n\t\tt.Errorf(\"Expected tree string:\\n--->%s<---\\nGot:\\n--->%s<---\", expected, actual)\n\t}\n\n}\n"
  },
  {
    "path": "dive/filetree/node_data.go",
    "content": "package filetree\n\nvar GlobalFileTreeCollapse bool\n\n// NodeData is the payload for a FileNode\ntype NodeData struct {\n\tViewInfo ViewInfo\n\tFileInfo FileInfo `json:\"fileInfo\"`\n\tDiffType DiffType\n}\n\n// NewNodeData creates an empty NodeData struct for a FileNode\nfunc NewNodeData() *NodeData {\n\treturn &NodeData{\n\t\tViewInfo: *NewViewInfo(),\n\t\tFileInfo: FileInfo{},\n\t\tDiffType: Unmodified,\n\t}\n}\n\n// Copy duplicates a NodeData\nfunc (data *NodeData) Copy() *NodeData {\n\treturn &NodeData{\n\t\tViewInfo: *data.ViewInfo.Copy(),\n\t\tFileInfo: *data.FileInfo.Copy(),\n\t\tDiffType: data.DiffType,\n\t}\n}\n"
  },
  {
    "path": "dive/filetree/node_data_test.go",
    "content": "package filetree\n\nimport (\n\t\"testing\"\n)\n\nfunc TestAssignDiffType(t *testing.T) {\n\ttree := NewFileTree()\n\tnode, _, err := tree.AddPath(\"/usr\", *BlankFileChangeInfo(\"/usr\"))\n\tif err != nil {\n\t\tt.Errorf(\"Expected no error from fetching path. got: %v\", err)\n\t}\n\tnode.Data.DiffType = Modified\n\tif tree.Root.Children[\"usr\"].Data.DiffType != Modified {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestMergeDiffTypes(t *testing.T) {\n\ta := Unmodified\n\tb := Unmodified\n\tmerged := a.merge(b)\n\tif merged != Unmodified {\n\t\tt.Errorf(\"Expected Unchanged (0) but got %v\", merged)\n\t}\n\ta = Modified\n\tb = Unmodified\n\tmerged = a.merge(b)\n\tif merged != Modified {\n\t\tt.Errorf(\"Expected Unchanged (0) but got %v\", merged)\n\t}\n}\n\nfunc BlankFileChangeInfo(path string) (f *FileInfo) {\n\tresult := FileInfo{\n\t\tPath:     path,\n\t\tTypeFlag: 1,\n\t\thash:     123,\n\t}\n\treturn &result\n}\n"
  },
  {
    "path": "dive/filetree/order_strategy.go",
    "content": "package filetree\n\nimport (\n\t\"sort\"\n)\n\ntype SortOrder int\n\nconst (\n\tByName = iota\n\tBySizeDesc\n\n\tNumSortOrderConventions\n)\n\ntype OrderStrategy interface {\n\torderKeys(files map[string]*FileNode) []string\n}\n\nfunc GetSortOrderStrategy(sortOrder SortOrder) OrderStrategy {\n\tswitch sortOrder {\n\tcase ByName:\n\t\treturn orderByNameStrategy{}\n\tcase BySizeDesc:\n\t\treturn orderBySizeDescStrategy{}\n\t}\n\treturn orderByNameStrategy{}\n}\n\ntype orderByNameStrategy struct{}\n\nfunc (orderByNameStrategy) orderKeys(files map[string]*FileNode) []string {\n\tvar keys []string\n\tfor key := range files {\n\t\tkeys = append(keys, key)\n\t}\n\n\tsort.Strings(keys)\n\n\treturn keys\n}\n\ntype orderBySizeDescStrategy struct{}\n\nfunc (orderBySizeDescStrategy) orderKeys(files map[string]*FileNode) []string {\n\tvar keys []string\n\tfor key := range files {\n\t\tkeys = append(keys, key)\n\t}\n\n\tsort.Slice(keys, func(i, j int) bool {\n\t\tki, kj := keys[i], keys[j]\n\t\tni, nj := files[ki], files[kj]\n\t\tif ni.GetSize() == nj.GetSize() {\n\t\t\treturn ki < kj\n\t\t}\n\t\treturn ni.GetSize() > nj.GetSize()\n\t})\n\n\treturn keys\n}\n"
  },
  {
    "path": "dive/filetree/path_error.go",
    "content": "package filetree\n\nimport \"fmt\"\n\nconst (\n\tActionAdd FileAction = iota\n\tActionRemove\n)\n\ntype FileAction int\n\nfunc (fa FileAction) String() string {\n\tswitch fa {\n\tcase ActionAdd:\n\t\treturn \"add\"\n\tcase ActionRemove:\n\t\treturn \"remove\"\n\tdefault:\n\t\treturn \"<unknown file action>\"\n\t}\n}\n\ntype PathError struct {\n\tPath   string\n\tAction FileAction\n\tErr    error\n}\n\nfunc NewPathError(path string, action FileAction, err error) PathError {\n\treturn PathError{\n\t\tPath:   path,\n\t\tAction: action,\n\t\tErr:    err,\n\t}\n}\n\nfunc (pe PathError) String() string {\n\treturn fmt.Sprintf(\"unable to %s '%s': %+v\", pe.Action.String(), pe.Path, pe.Err)\n}\n"
  },
  {
    "path": "dive/filetree/view_info.go",
    "content": "package filetree\n\n// ViewInfo contains UI specific detail for a specific FileNode\ntype ViewInfo struct {\n\tCollapsed bool\n\tHidden    bool\n}\n\n// NewViewInfo creates a default ViewInfo\nfunc NewViewInfo() (view *ViewInfo) {\n\treturn &ViewInfo{\n\t\tCollapsed: GlobalFileTreeCollapse,\n\t\tHidden:    false,\n\t}\n}\n\n// Copy duplicates a ViewInfo\nfunc (view *ViewInfo) Copy() (newView *ViewInfo) {\n\tnewView = NewViewInfo()\n\t*newView = *view\n\treturn newView\n}\n"
  },
  {
    "path": "dive/get_image_resolver.go",
    "content": "package dive\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/dive/image/docker\"\n\t\"github.com/wagoodman/dive/dive/image/podman\"\n)\n\nconst (\n\tSourceUnknown ImageSource = iota\n\tSourceDockerEngine\n\tSourcePodmanEngine\n\tSourceDockerArchive\n)\n\ntype ImageSource int\n\nvar ImageSources = []string{SourceDockerEngine.String(), SourcePodmanEngine.String(), SourceDockerArchive.String()}\n\nfunc (r ImageSource) String() string {\n\treturn [...]string{\"unknown\", \"docker\", \"podman\", \"docker-archive\"}[r]\n}\n\nfunc ParseImageSource(r string) ImageSource {\n\tswitch r {\n\tcase SourceDockerEngine.String():\n\t\treturn SourceDockerEngine\n\tcase SourcePodmanEngine.String():\n\t\treturn SourcePodmanEngine\n\tcase SourceDockerArchive.String():\n\t\treturn SourceDockerArchive\n\tcase \"docker-tar\":\n\t\treturn SourceDockerArchive\n\tdefault:\n\t\treturn SourceUnknown\n\t}\n}\n\nfunc DeriveImageSource(image string) (ImageSource, string) {\n\ts := strings.SplitN(image, \"://\", 2)\n\tif len(s) < 2 {\n\t\treturn SourceUnknown, \"\"\n\t}\n\tscheme, imageSource := s[0], s[1]\n\n\tswitch scheme {\n\tcase SourceDockerEngine.String():\n\t\treturn SourceDockerEngine, imageSource\n\tcase SourcePodmanEngine.String():\n\t\treturn SourcePodmanEngine, imageSource\n\tcase SourceDockerArchive.String():\n\t\treturn SourceDockerArchive, imageSource\n\tcase \"docker-tar\":\n\t\treturn SourceDockerArchive, imageSource\n\t}\n\treturn SourceUnknown, \"\"\n}\n\nfunc GetImageResolver(r ImageSource) (image.Resolver, error) {\n\tswitch r {\n\tcase SourceDockerEngine:\n\t\treturn docker.NewResolverFromEngine(), nil\n\tcase SourcePodmanEngine:\n\t\treturn podman.NewResolverFromEngine(), nil\n\tcase SourceDockerArchive:\n\t\treturn docker.NewResolverFromArchive(), nil\n\t}\n\n\treturn nil, fmt.Errorf(\"unable to determine image resolver\")\n}\n"
  },
  {
    "path": "dive/image/analysis.go",
    "content": "package image\n\nimport (\n\t\"context\"\n\t\"github.com/wagoodman/dive/dive/filetree\"\n)\n\ntype Analysis struct {\n\tImage             string\n\tLayers            []*Layer\n\tRefTrees          []*filetree.FileTree\n\tEfficiency        float64\n\tSizeBytes         uint64\n\tUserSizeByes      uint64  // this is all bytes except for the base image\n\tWastedUserPercent float64 // = wasted-bytes/user-size-bytes\n\tWastedBytes       uint64\n\tInefficiencies    filetree.EfficiencySlice\n}\n\nfunc Analyze(ctx context.Context, img *Image) (*Analysis, error) {\n\tefficiency, inefficiencies := filetree.Efficiency(img.Trees)\n\tvar sizeBytes, userSizeBytes uint64\n\n\tfor i, v := range img.Layers {\n\t\tsizeBytes += v.Size\n\t\tif i != 0 {\n\t\t\tuserSizeBytes += v.Size\n\t\t}\n\t}\n\n\tvar wastedBytes uint64\n\tfor _, file := range inefficiencies {\n\t\twastedBytes += uint64(file.CumulativeSize)\n\t}\n\n\treturn &Analysis{\n\t\tImage:             img.Request,\n\t\tLayers:            img.Layers,\n\t\tRefTrees:          img.Trees,\n\t\tEfficiency:        efficiency,\n\t\tUserSizeByes:      userSizeBytes,\n\t\tSizeBytes:         sizeBytes,\n\t\tWastedBytes:       wastedBytes,\n\t\tWastedUserPercent: float64(wastedBytes) / float64(userSizeBytes),\n\t\tInefficiencies:    inefficiencies,\n\t}, nil\n}\n"
  },
  {
    "path": "dive/image/docker/archive_resolver.go",
    "content": "package docker\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\ntype archiveResolver struct{}\n\nfunc NewResolverFromArchive() *archiveResolver {\n\treturn &archiveResolver{}\n}\n\n// Name returns the name of the resolver to display to the user.\nfunc (r *archiveResolver) Name() string {\n\treturn \"docker-archive\"\n}\n\nfunc (r *archiveResolver) Fetch(ctx context.Context, path string) (*image.Image, error) {\n\treader, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer reader.Close()\n\n\timg, err := NewImageArchive(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn img.ToImage(path)\n}\n\nfunc (r *archiveResolver) Build(ctx context.Context, args []string) (*image.Image, error) {\n\treturn nil, fmt.Errorf(\"build option not supported for docker archive resolver\")\n}\n\nfunc (r *archiveResolver) Extract(ctx context.Context, id string, l string, p string) error {\n\treturn fmt.Errorf(\"not implemented\")\n}\n"
  },
  {
    "path": "dive/image/docker/build.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/spf13/afero\"\n)\n\nconst (\n\tdefaultDockerfileName    = \"Dockerfile\"\n\tdefaultContainerfileName = \"Containerfile\"\n)\n\nfunc buildImageFromCli(fs afero.Fs, buildArgs []string) (string, error) {\n\tiidfile, err := afero.TempFile(fs, \"\", \"dive.*.iid\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer fs.Remove(iidfile.Name()) // nolint:errcheck\n\tdefer iidfile.Close()\n\n\tvar allArgs []string\n\tif isFileFlagsAreSet(buildArgs, \"-f\", \"--file\") {\n\t\tallArgs = append([]string{\"--iidfile\", iidfile.Name()}, buildArgs...)\n\t} else {\n\t\tcontainerFilePath, err := tryFindContainerfile(fs, buildArgs)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tallArgs = append([]string{\"--iidfile\", iidfile.Name(), \"-f\", containerFilePath}, buildArgs...)\n\t}\n\n\terr = runDockerCmd(\"build\", allArgs...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\timageId, err := afero.ReadFile(fs, iidfile.Name())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(imageId), nil\n}\n\n// isFileFlagsAreSet Checks if specified flags are present in the argument list.\nfunc isFileFlagsAreSet(args []string, flags ...string) bool {\n\tflagSet := strset.New(flags...)\n\tfor i, arg := range args {\n\t\tif flagSet.Has(arg) && i+1 < len(args) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// tryFindContainerfile loops through provided build arguments and tries to find a Containerfile or a Dockerfile.\nfunc tryFindContainerfile(fs afero.Fs, buildArgs []string) (string, error) {\n\t// Look for a build context within the provided build arguments.\n\t// Test build arguments one by one to find a valid path containing default names of `Containerfile` or a `Dockerfile` (in that order).\n\tcandidates := []string{\n\t\tdefaultContainerfileName,                  // Containerfile\n\t\tstrings.ToLower(defaultContainerfileName), // containerfile\n\t\tdefaultDockerfileName,                     // Dockerfile\n\t\tstrings.ToLower(defaultDockerfileName),    // dockerfile\n\t}\n\n\tfor _, arg := range buildArgs {\n\t\tfileInfo, err := fs.Stat(arg)\n\t\tif err == nil && fileInfo.IsDir() {\n\t\t\tfor _, candidate := range candidates {\n\t\t\t\tfilePath := filepath.Join(arg, candidate)\n\t\t\t\tif exists, _ := afero.Exists(fs, filePath); exists {\n\t\t\t\t\treturn filePath, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not find Containerfile or Dockerfile\\n\")\n}\n"
  },
  {
    "path": "dive/image/docker/build_test.go",
    "content": "package docker\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsFileFlagsAreSet(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\targs     []string\n\t\tflags    []string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"flag present in the middle with value\",\n\t\t\targs:     []string{\"arg1\", \"-f\", \"dockerfile\", \"arg2\"},\n\t\t\tflags:    []string{\"-f\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"flag present at the beginning with value\",\n\t\t\targs:     []string{\"-f\", \"dockerfile\", \"arg1\", \"arg2\"},\n\t\t\tflags:    []string{\"-f\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"flag present at the end with no value\",\n\t\t\targs:     []string{\"arg1\", \"arg2\", \"-f\"},\n\t\t\tflags:    []string{\"-f\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"flag not present\",\n\t\t\targs:     []string{\"arg1\", \"arg2\", \"arg3\"},\n\t\t\tflags:    []string{\"-f\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"one of multiple flags present\",\n\t\t\targs:     []string{\"arg1\", \"--file\", \"dockerfile\", \"arg2\"},\n\t\t\tflags:    []string{\"-f\", \"--file\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"none of multiple flags present\",\n\t\t\targs:     []string{\"arg1\", \"-x\", \"value\", \"arg2\"},\n\t\t\tflags:    []string{\"-f\", \"--file\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty args\",\n\t\t\targs:     []string{},\n\t\t\tflags:    []string{\"-f\", \"--file\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty flags\",\n\t\t\targs:     []string{\"arg1\", \"-f\", \"value\"},\n\t\t\tflags:    []string{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"flag with multiple values\",\n\t\t\targs:     []string{\"arg1\", \"-f\", \"value1\", \"value2\"},\n\t\t\tflags:    []string{\"-f\"},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isFileFlagsAreSet(tt.args, tt.flags...)\n\t\t\tassert.Equal(t, tt.expected, result, \"isFileFlagsAreSet() = %v, want %v\", result, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestTryFindContainerfile(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tbuildArgs      []string\n\t\tsetupFs        func(t testing.TB, fs afero.Fs)\n\t\texpectedPath   string\n\t\texpectedErrMsg string\n\t}{\n\t\t{\n\t\t\tname:      \"find Containerfile (uppercase)\",\n\t\t\tbuildArgs: []string{\"testdir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testdir/Containerfile\", \"FROM alpine\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"testdir\", \"Containerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"find containerfile (lowercase)\",\n\t\t\tbuildArgs: []string{\"testdir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testdir/containerfile\", \"FROM alpine\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"testdir\", \"containerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"find Dockerfile when no Containerfile exists\",\n\t\t\tbuildArgs: []string{\"testdir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testdir/Dockerfile\", \"FROM alpine\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"testdir\", \"Dockerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"find dockerfile (lowercase)\",\n\t\t\tbuildArgs: []string{\"testdir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testdir/dockerfile\", \"FROM alpine\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"testdir\", \"dockerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"prefer Containerfile over Dockerfile\",\n\t\t\tbuildArgs: []string{\"testdir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testdir/Containerfile\", \"FROM alpine\")\n\t\t\t\tcreate(t, fs, \"testdir/Dockerfile\", \"FROM ubuntu\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"testdir\", \"Containerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"non-existent directory\",\n\t\t\tbuildArgs:      []string{\"nonexistentdir\"},\n\t\t\tsetupFs:        func(t testing.TB, fs afero.Fs) {},\n\t\t\texpectedPath:   \"\",\n\t\t\texpectedErrMsg: \"could not find Containerfile or Dockerfile\",\n\t\t},\n\t\t{\n\t\t\tname:           \"empty build args\",\n\t\t\tbuildArgs:      []string{},\n\t\t\tsetupFs:        func(t testing.TB, fs afero.Fs) {},\n\t\t\texpectedPath:   \"\",\n\t\t\texpectedErrMsg: \"could not find Containerfile or Dockerfile\",\n\t\t},\n\t\t{\n\t\t\tname:      \"directory exists but no container files\",\n\t\t\tbuildArgs: []string{\"testdir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testdir/somefile.txt\", \"content\")\n\t\t\t},\n\t\t\texpectedPath:   \"\",\n\t\t\texpectedErrMsg: \"could not find Containerfile or Dockerfile\",\n\t\t},\n\t\t{\n\t\t\tname:      \"find in second directory\",\n\t\t\tbuildArgs: []string{\"firstdir\", \"seconddir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\terr := fs.MkdirAll(\"firstdir\", 0755)\n\t\t\t\trequire.NoError(t, err, \"Failed to create directory: firstdir\")\n\t\t\t\tcreate(t, fs, \"seconddir/Dockerfile\", \"FROM alpine\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"seconddir\", \"Dockerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"find in first directory when both have files\",\n\t\t\tbuildArgs: []string{\"firstdir\", \"seconddir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"firstdir/Containerfile\", \"FROM alpine\")\n\t\t\t\tcreate(t, fs, \"seconddir/Dockerfile\", \"FROM ubuntu\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"firstdir\", \"Containerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"file argument not a directory\",\n\t\t\tbuildArgs: []string{\"testfile.txt\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testfile.txt\", \"content\")\n\t\t\t},\n\t\t\texpectedPath:   \"\",\n\t\t\texpectedErrMsg: \"could not find Containerfile or Dockerfile\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mixed args with valid directory\",\n\t\t\tbuildArgs: []string{\"testfile.txt\", \"testdir\"},\n\t\t\tsetupFs: func(t testing.TB, fs afero.Fs) {\n\t\t\t\tcreate(t, fs, \"testfile.txt\", \"content\")\n\t\t\t\tcreate(t, fs, \"testdir/Dockerfile\", \"FROM alpine\")\n\t\t\t},\n\t\t\texpectedPath:   filepath.Join(\"testdir\", \"Dockerfile\"),\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\ttt.setupFs(t, fs)\n\n\t\t\tresult, err := tryFindContainerfile(fs, tt.buildArgs)\n\n\t\t\tif tt.expectedErrMsg != \"\" {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.expectedErrMsg)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expectedPath, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc create(t testing.TB, fs afero.Fs, path, contents string) {\n\tt.Helper()\n\n\tdir := filepath.Dir(path)\n\tif dir != \".\" {\n\t\terr := fs.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err, \"Failed to create directory: %s\", dir)\n\t}\n\n\terr := afero.WriteFile(fs, path, []byte(contents), 0644)\n\trequire.NoError(t, err, \"Failed to write file: %s\", path)\n}\n"
  },
  {
    "path": "dive/image/docker/cli.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/dive/internal/utils\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\n// runDockerCmd runs a given Docker command in the current tty\nfunc runDockerCmd(cmdStr string, args ...string) error {\n\n\tif !isDockerClientBinaryAvailable() {\n\t\treturn fmt.Errorf(\"cannot find docker client executable\")\n\t}\n\n\tallArgs := utils.CleanArgs(append([]string{cmdStr}, args...))\n\n\tfullCmd := strings.Join(append([]string{\"docker\"}, allArgs...), \" \")\n\tlog.WithFields(\"cmd\", fullCmd).Trace(\"executing\")\n\n\tcmd := exec.Command(\"docker\", allArgs...)\n\tcmd.Env = os.Environ()\n\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdin = os.Stdin\n\n\treturn cmd.Run()\n}\n\nfunc isDockerClientBinaryAvailable() bool {\n\t_, err := exec.LookPath(\"docker\")\n\treturn err == nil\n}\n"
  },
  {
    "path": "dive/image/docker/config.go",
    "content": "package docker\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype config struct {\n\tHistory []historyEntry `json:\"history\"`\n\tRootFs  rootFs         `json:\"rootfs\"`\n}\n\ntype rootFs struct {\n\tType    string   `json:\"type\"`\n\tDiffIds []string `json:\"diff_ids\"`\n}\n\ntype historyEntry struct {\n\tID         string\n\tSize       uint64\n\tCreated    string `json:\"created\"`\n\tAuthor     string `json:\"author\"`\n\tCreatedBy  string `json:\"created_by\"`\n\tEmptyLayer bool   `json:\"empty_layer\"`\n}\n\nfunc newConfig(configBytes []byte) config {\n\tvar imageConfig config\n\terr := json.Unmarshal(configBytes, &imageConfig)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to unmarshal docker config: %w\", err))\n\t}\n\n\tlayerIdx := 0\n\tfor idx := range imageConfig.History {\n\t\tif imageConfig.History[idx].EmptyLayer {\n\t\t\timageConfig.History[idx].ID = \"<missing>\"\n\t\t} else {\n\t\t\timageConfig.History[idx].ID = imageConfig.RootFs.DiffIds[layerIdx]\n\t\t\tlayerIdx++\n\t\t}\n\t}\n\n\treturn imageConfig\n}\n\nfunc isConfig(configBytes []byte) bool {\n\tvar imageConfig config\n\terr := json.Unmarshal(configBytes, &imageConfig)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn imageConfig.RootFs.Type == \"layers\"\n}\n"
  },
  {
    "path": "dive/image/docker/docker_host_unix.go",
    "content": "//go:build !windows\n\npackage docker\n\nconst (\n\tdefaultDockerHost = \"unix:///var/run/docker.sock\"\n)\n"
  },
  {
    "path": "dive/image/docker/docker_host_windows.go",
    "content": "package docker\n\nconst (\n\tdefaultDockerHost = \"npipe:////.pipe/docker_engine\"\n)\n"
  },
  {
    "path": "dive/image/docker/engine_resolver.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/wagoodman/dive/internal/bus/event/payload\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcliconfig \"github.com/docker/cli/cli/config\"\n\t\"github.com/docker/cli/cli/connhelper\"\n\tddocker \"github.com/docker/cli/cli/context/docker\"\n\tctxstore \"github.com/docker/cli/cli/context/store\"\n\t\"github.com/docker/docker/client\"\n\t\"golang.org/x/net/context\"\n\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\ntype engineResolver struct{}\n\nfunc NewResolverFromEngine() *engineResolver {\n\treturn &engineResolver{}\n}\n\n// Name returns the name of the resolver to display to the user.\nfunc (r *engineResolver) Name() string {\n\treturn \"docker-engine\"\n}\n\nfunc (r *engineResolver) Fetch(ctx context.Context, id string) (*image.Image, error) {\n\treader, err := r.fetchArchive(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer reader.Close()\n\n\timg, err := NewImageArchive(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn img.ToImage(id)\n}\n\nfunc (r *engineResolver) Build(ctx context.Context, args []string) (*image.Image, error) {\n\tid, err := buildImageFromCli(afero.NewOsFs(), args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn r.Fetch(ctx, id)\n}\n\nfunc (r *engineResolver) Extract(ctx context.Context, id string, l string, p string) error {\n\treader, err := r.fetchArchive(ctx, id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := ExtractFromImage(io.NopCloser(reader), l, p); err == nil {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"unable to extract from image '%s': %+v\", id, err)\n}\n\nfunc (r *engineResolver) fetchArchive(ctx context.Context, id string) (io.ReadCloser, error) {\n\tvar err error\n\tvar dockerClient *client.Client\n\n\thost, err := determineDockerHost()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not determine docker host: %v\", err)\n\t}\n\tclientOpts := []client.Opt{client.FromEnv}\n\tclientOpts = append(clientOpts, client.WithHost(host))\n\n\tswitch strings.Split(host, \":\")[0] {\n\tcase \"ssh\":\n\t\thelper, err := connhelper.GetConnectionHelper(host)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get docker connection helper: %w\", err)\n\t\t}\n\t\tclientOpts = append(clientOpts, func(c *client.Client) error {\n\t\t\thttpClient := &http.Client{\n\t\t\t\tTransport: &http.Transport{\n\t\t\t\t\tDialContext: helper.Dialer,\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn client.WithHTTPClient(httpClient)(c)\n\t\t})\n\n\t\tclientOpts = append(clientOpts, client.WithHost(host))\n\t\tclientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))\n\n\tdefault:\n\n\t\tif os.Getenv(\"DOCKER_TLS_VERIFY\") != \"\" && os.Getenv(\"DOCKER_CERT_PATH\") == \"\" {\n\t\t\tos.Setenv(\"DOCKER_CERT_PATH\", \"~/.docker\")\n\t\t}\n\t}\n\n\tclientOpts = append(clientOpts, client.WithAPIVersionNegotiation())\n\tdockerClient, err = client.NewClientWithOpts(clientOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, err = dockerClient.ImageInspect(ctx, id)\n\tif err != nil {\n\t\t// check if the error is due to the image not existing locally\n\t\tif client.IsErrNotFound(err) {\n\t\t\tmon := payload.GetGenericProgressFromContext(ctx)\n\t\t\tif mon != nil {\n\t\t\t\tmon.AtomicStage.Set(\"attempting to pull\")\n\t\t\t\tlog.Debugf(\"the image is not available locally, pulling %q\", id)\n\t\t\t} else {\n\t\t\t\tlog.Infof(\"the image is not available locally, pulling %q\", id)\n\t\t\t}\n\t\t\terr = runDockerCmd(\"pull\", id)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\t// Some other error occurred, return it\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treadCloser, err := dockerClient.ImageSave(ctx, []string{id})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn readCloser, nil\n}\n\n// determineDockerHost tries to the determine the docker host that we should connect to\n// in the following order of decreasing precedence:\n//   - value of \"DOCKER_HOST\" environment variable\n//   - host retrieved from the current context (specified via DOCKER_CONTEXT)\n//   - \"default docker host\" for the host operating system, otherwise\nfunc determineDockerHost() (string, error) {\n\t// If the docker host is explicitly set via the \"DOCKER_HOST\" environment variable,\n\t// then its a no-brainer :shrug:\n\tif os.Getenv(\"DOCKER_HOST\") != \"\" {\n\t\treturn os.Getenv(\"DOCKER_HOST\"), nil\n\t}\n\n\tcurrentContext := os.Getenv(\"DOCKER_CONTEXT\")\n\tif currentContext == \"\" {\n\t\tcf, err := cliconfig.Load(cliconfig.Dir())\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tcurrentContext = cf.CurrentContext\n\t}\n\n\tif currentContext == \"\" {\n\t\t// If a docker context is neither specified via the \"DOCKER_CONTEXT\" environment variable nor via the\n\t\t// $HOME/.docker/config file, then we fall back to connecting to the \"default docker host\" meant for\n\t\t// the host operating system.\n\t\treturn defaultDockerHost, nil\n\t}\n\n\tstoreConfig := ctxstore.NewConfig(\n\t\tfunc() interface{} { return &ddocker.EndpointMeta{} },\n\t\tctxstore.EndpointTypeGetter(ddocker.DockerEndpoint, func() interface{} { return &ddocker.EndpointMeta{} }),\n\t)\n\n\tst := ctxstore.New(cliconfig.ContextStoreDir(), storeConfig)\n\tmd, err := st.GetMetadata(currentContext)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdockerEP, ok := md.Endpoints[ddocker.DockerEndpoint]\n\tif !ok {\n\t\treturn \"\", err\n\t}\n\tdockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"expected docker.EndpointMeta, got %T\", dockerEP)\n\t}\n\n\tif dockerEPMeta.Host != \"\" {\n\t\treturn dockerEPMeta.Host, nil\n\t}\n\n\t// We might end up here, if the context was created with the `host` set to an empty value (i.e. '').\n\t// For example:\n\t// ```sh\n\t// docker context create foo --docker \"host=\"\n\t// ```\n\t// In such scenario, we mimic the `docker` cli and try to connect to the \"default docker host\".\n\treturn defaultDockerHost, nil\n}\n"
  },
  {
    "path": "dive/image/docker/image_archive.go",
    "content": "package docker\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/klauspost/compress/zstd\"\n\n\t\"github.com/wagoodman/dive/dive/filetree\"\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\ntype ImageArchive struct {\n\tmanifest manifest\n\tconfig   config\n\tlayerMap map[string]*filetree.FileTree\n}\n\nfunc NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {\n\timg := &ImageArchive{\n\t\tlayerMap: make(map[string]*filetree.FileTree),\n\t}\n\n\ttarReader := tar.NewReader(tarFile)\n\n\t// store discovered json files in a map so we can read the image in one pass\n\tjsonFiles := make(map[string][]byte)\n\n\tvar currentLayer uint\n\tfor {\n\t\theader, err := tarReader.Next()\n\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tname := header.Name\n\n\t\t// some layer tars can be relative layer symlinks to other layer tars\n\t\tif header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeReg {\n\t\t\t// For the Docker image format, use file name conventions\n\t\t\tif strings.HasSuffix(name, \".tar\") {\n\t\t\t\tcurrentLayer++\n\t\t\t\tlayerReader := tar.NewReader(tarReader)\n\t\t\t\ttree, err := processLayerTar(name, layerReader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn img, err\n\t\t\t\t}\n\n\t\t\t\t// add the layer to the image\n\t\t\t\timg.layerMap[tree.Name] = tree\n\t\t\t} else if strings.HasSuffix(name, \".tar.gz\") || strings.HasSuffix(name, \"tgz\") {\n\t\t\t\tcurrentLayer++\n\n\t\t\t\t// Add gzip reader\n\t\t\t\tgz, err := gzip.NewReader(tarReader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn img, err\n\t\t\t\t}\n\n\t\t\t\t// Add tar reader\n\t\t\t\tlayerReader := tar.NewReader(gz)\n\n\t\t\t\t// Process layer\n\t\t\t\ttree, err := processLayerTar(name, layerReader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn img, err\n\t\t\t\t}\n\n\t\t\t\t// add the layer to the image\n\t\t\t\timg.layerMap[tree.Name] = tree\n\t\t\t} else if strings.HasSuffix(name, \".json\") || strings.HasPrefix(name, \"sha256:\") {\n\t\t\t\tfileBuffer, err := io.ReadAll(tarReader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn img, err\n\t\t\t\t}\n\t\t\t\tjsonFiles[name] = fileBuffer\n\t\t\t} else if strings.HasPrefix(name, \"blobs/\") {\n\t\t\t\t// For the OCI-compatible image format (used since Docker 25), use mime sniffing\n\t\t\t\t// but limit this to only the blobs/ (containing the config, and the layers)\n\n\t\t\t\t// The idea here is that we try various formats in turn, and those tries should\n\t\t\t\t// never consume more bytes than this buffer contains so we can start again.\n\n\t\t\t\t// 512 bytes ought to be enough (as that's the size of a TAR entry header),\n\t\t\t\t// but play it safe with 1024 bytes. This should also include very small layers.\n\t\t\t\tbuffer := make([]byte, 1024)\n\t\t\t\tn, err := io.ReadFull(tarReader, buffer)\n\t\t\t\tif err != nil && err != io.ErrUnexpectedEOF {\n\t\t\t\t\treturn img, err\n\t\t\t\t}\n\n\t\t\t\toriginalReader := func() io.Reader {\n\t\t\t\t\treturn io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)\n\t\t\t\t}\n\n\t\t\t\t// Try reading a gzip/estargz compressed layer\n\t\t\t\tgzipReader, err := gzip.NewReader(originalReader())\n\t\t\t\tif err == nil {\n\t\t\t\t\tlayerReader := tar.NewReader(gzipReader)\n\t\t\t\t\ttree, err := processLayerTar(name, layerReader)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tcurrentLayer++\n\t\t\t\t\t\t// add the layer to the image\n\t\t\t\t\t\timg.layerMap[tree.Name] = tree\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Try reading a zstd compressed layer\n\t\t\t\tzstdReader, err := zstd.NewReader(originalReader())\n\t\t\t\tif err == nil {\n\t\t\t\t\tlayerReader := tar.NewReader(zstdReader)\n\t\t\t\t\ttree, err := processLayerTar(name, layerReader)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tcurrentLayer++\n\t\t\t\t\t\t// add the layer to the image\n\t\t\t\t\t\timg.layerMap[tree.Name] = tree\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Try reading a plain tar layer\n\t\t\t\tlayerReader := tar.NewReader(originalReader())\n\t\t\t\ttree, err := processLayerTar(name, layerReader)\n\t\t\t\tif err == nil {\n\t\t\t\t\tcurrentLayer++\n\t\t\t\t\t// add the layer to the image\n\t\t\t\t\timg.layerMap[tree.Name] = tree\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Not a TAR/GZIP/ZSTD, might be a JSON file\n\t\t\t\tdecoder := json.NewDecoder(bytes.NewReader(buffer[:n]))\n\t\t\t\ttoken, err := decoder.Token()\n\t\t\t\tif _, ok := token.(json.Delim); err == nil && ok {\n\t\t\t\t\t// Looks like a JSON object (or array)\n\t\t\t\t\t// XXX: should we add a header.Size check too?\n\t\t\t\t\tfileBuffer, err := io.ReadAll(originalReader())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn img, err\n\t\t\t\t\t}\n\t\t\t\t\tjsonFiles[name] = fileBuffer\n\t\t\t\t}\n\t\t\t\t// Ignore every other unknown file type\n\t\t\t}\n\t\t}\n\t}\n\n\tmanifestContent, exists := jsonFiles[\"manifest.json\"]\n\tif exists {\n\t\timg.manifest = newManifest(manifestContent)\n\t} else {\n\t\t// manifest.json is not part of the OCI spec, docker includes it for compatibility\n\t\t// Provide compatibility by finding the config and using our layerMap\n\t\tvar configPath string\n\t\tfor path, content := range jsonFiles {\n\t\t\tif isConfig(content) {\n\t\t\t\tconfigPath = path\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(configPath) == 0 {\n\t\t\treturn img, fmt.Errorf(\"could not find image manifest\")\n\t\t}\n\n\t\tvar layerPaths []string\n\t\tfor k := range img.layerMap {\n\t\t\tlayerPaths = append(layerPaths, k)\n\t\t}\n\t\timg.manifest = manifest{\n\t\t\tConfigPath:    configPath,\n\t\t\tLayerTarPaths: layerPaths,\n\t\t}\n\t}\n\n\tconfigContent, exists := jsonFiles[img.manifest.ConfigPath]\n\tif !exists {\n\t\treturn img, fmt.Errorf(\"could not find image config\")\n\t}\n\n\timg.config = newConfig(configContent)\n\n\treturn img, nil\n}\n\nfunc processLayerTar(name string, reader *tar.Reader) (*filetree.FileTree, error) {\n\ttree := filetree.NewFileTree()\n\ttree.Name = name\n\n\tfileInfos, err := getFileList(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, element := range fileInfos {\n\t\ttree.FileSize += uint64(element.Size)\n\n\t\t_, _, err := tree.AddPath(element.Path, element)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn tree, nil\n}\n\nfunc getFileList(tarReader *tar.Reader) ([]filetree.FileInfo, error) {\n\tvar files []filetree.FileInfo\n\n\tfor {\n\t\theader, err := tarReader.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t} else if err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// always ensure relative path notations are not parsed as part of the filename\n\t\tname := path.Clean(header.Name)\n\t\tif name == \".\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch header.Typeflag {\n\t\tcase tar.TypeXGlobalHeader:\n\t\t\treturn nil, fmt.Errorf(\"unexpected tar file: (XGlobalHeader): type=%v name=%s\", header.Typeflag, name)\n\t\tcase tar.TypeXHeader:\n\t\t\treturn nil, fmt.Errorf(\"unexpected tar file (XHeader): type=%v name=%s\", header.Typeflag, name)\n\t\tdefault:\n\t\t\tfiles = append(files, filetree.NewFileInfoFromTarHeader(tarReader, header, name))\n\t\t}\n\t}\n\treturn files, nil\n}\n\nfunc (img *ImageArchive) ToImage(id string) (*image.Image, error) {\n\ttrees := make([]*filetree.FileTree, 0)\n\n\t// build the content tree\n\tfor _, treeName := range img.manifest.LayerTarPaths {\n\t\ttr, exists := img.layerMap[treeName]\n\t\tif exists {\n\t\t\ttrees = append(trees, tr)\n\t\t\tcontinue\n\t\t}\n\t\treturn nil, fmt.Errorf(\"could not find '%s' in parsed layers\", treeName)\n\t}\n\n\t// build the layers array\n\tlayers := make([]*image.Layer, 0)\n\n\t// note that the engineResolver config stores images in reverse chronological order, so iterate backwards through layers\n\t// as you iterate chronologically through history (ignoring history items that have no layer contents)\n\t// Note: history is not required metadata in a docker image!\n\thistIdx := 0\n\tfor idx, tree := range trees {\n\t\t// ignore empty layers, we are only observing layers with content\n\t\thistoryObj := historyEntry{\n\t\t\tCreatedBy: \"(missing)\",\n\t\t}\n\t\tfor nextHistIdx := histIdx; nextHistIdx < len(img.config.History); nextHistIdx++ {\n\t\t\tif !img.config.History[nextHistIdx].EmptyLayer {\n\t\t\t\thistIdx = nextHistIdx\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif histIdx < len(img.config.History) && !img.config.History[histIdx].EmptyLayer {\n\t\t\thistoryObj = img.config.History[histIdx]\n\t\t\thistIdx++\n\t\t}\n\n\t\thistoryObj.Size = tree.FileSize\n\n\t\tdockerLayer := layer{\n\t\t\thistory: historyObj,\n\t\t\tindex:   idx,\n\t\t\ttree:    tree,\n\t\t}\n\t\tlayers = append(layers, dockerLayer.ToLayer())\n\t}\n\n\treturn &image.Image{\n\t\tRequest: id,\n\t\tTrees:   trees,\n\t\tLayers:  layers,\n\t}, nil\n}\n\nfunc ExtractFromImage(tarFile io.ReadCloser, l string, p string) error {\n\ttarReader := tar.NewReader(tarFile)\n\n\tfor {\n\t\theader, err := tarReader.Next()\n\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tname := header.Name\n\n\t\tswitch header.Typeflag {\n\t\tcase tar.TypeReg:\n\t\t\tif name == l {\n\t\t\t\terr = extractInner(tar.NewReader(tarReader), p)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc extractInner(reader *tar.Reader, p string) error {\n\ttarget := strings.TrimPrefix(p, \"/\")\n\n\tfor {\n\t\theader, err := reader.Next()\n\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tname := header.Name\n\n\t\tswitch header.Typeflag {\n\t\tcase tar.TypeReg:\n\t\t\tif strings.HasPrefix(name, target) {\n\t\t\t\terr := os.MkdirAll(filepath.Dir(name), 0755)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tout, err := os.Create(name)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t_, err = io.Copy(out, reader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "dive/image/docker/image_archive_analysis_test.go",
    "content": "package docker\n\nimport (\n\t\"testing\"\n)\n\nfunc Test_Analysis(t *testing.T) {\n\n\ttable := map[string]struct {\n\t\tefficiency    float64\n\t\tsizeBytes     uint64\n\t\tuserSizeBytes uint64\n\t\twastedBytes   uint64\n\t\twastedPercent float64\n\t\tpath          string\n\t}{\n\t\t\"docker-image\": {0.9844212134184309, 1220598, 66237, 32025, 0.4834911001404049, \"../../../.data/test-docker-image.tar\"},\n\t}\n\n\tfor name, test := range table {\n\t\tresult := TestAnalysisFromArchive(t, test.path)\n\n\t\tif result.SizeBytes != test.sizeBytes {\n\t\t\tt.Errorf(\"%s.%s: expected sizeBytes=%v, got %v\", t.Name(), name, test.sizeBytes, result.SizeBytes)\n\t\t}\n\n\t\tif result.UserSizeByes != test.userSizeBytes {\n\t\t\tt.Errorf(\"%s.%s: expected userSizeBytes=%v, got %v\", t.Name(), name, test.userSizeBytes, result.UserSizeByes)\n\t\t}\n\n\t\tif result.WastedBytes != test.wastedBytes {\n\t\t\tt.Errorf(\"%s.%s: expected wasterBytes=%v, got %v\", t.Name(), name, test.wastedBytes, result.WastedBytes)\n\t\t}\n\n\t\tif result.WastedUserPercent != test.wastedPercent {\n\t\t\tt.Errorf(\"%s.%s: expected wastedPercent=%v, got %v\", t.Name(), name, test.wastedPercent, result.WastedUserPercent)\n\t\t}\n\n\t\tif result.Efficiency != test.efficiency {\n\t\t\tt.Errorf(\"%s.%s: expected efficiency=%v, got %v\", t.Name(), name, test.efficiency, result.Efficiency)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "dive/image/docker/layer.go",
    "content": "package docker\n\nimport (\n\t\"strings\"\n\n\t\"github.com/wagoodman/dive/dive/filetree\"\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\n// Layer represents a Docker image layer and metadata\ntype layer struct {\n\thistory historyEntry\n\tindex   int\n\ttree    *filetree.FileTree\n}\n\n// String represents a layer in a columnar format.\nfunc (l *layer) ToLayer() *image.Layer {\n\tid := strings.Split(l.tree.Name, \"/\")[0]\n\treturn &image.Layer{\n\t\tId:      id,\n\t\tIndex:   l.index,\n\t\tCommand: strings.TrimPrefix(l.history.CreatedBy, \"/bin/sh -c \"),\n\t\tSize:    l.history.Size,\n\t\tTree:    l.tree,\n\t\t// todo: query docker api for tags\n\t\tNames:  []string{\"(unavailable)\"},\n\t\tDigest: l.history.ID,\n\t}\n}\n"
  },
  {
    "path": "dive/image/docker/manifest.go",
    "content": "package docker\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype manifest struct {\n\tConfigPath    string   `json:\"Config\"`\n\tRepoTags      []string `json:\"RepoTags\"`\n\tLayerTarPaths []string `json:\"Layers\"`\n}\n\nfunc newManifest(manifestBytes []byte) manifest {\n\tvar manifest []manifest\n\terr := json.Unmarshal(manifestBytes, &manifest)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to unmarshal manifest: %w\", err))\n\t}\n\treturn manifest[0]\n}\n"
  },
  {
    "path": "dive/image/docker/testing.go",
    "content": "package docker\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/net/context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\nfunc TestLoadArchive(t testing.TB, tarPath string) (*ImageArchive, error) {\n\tt.Helper()\n\tf, err := os.Open(tarPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\n\treturn NewImageArchive(f)\n}\n\nfunc TestAnalysisFromArchive(t testing.TB, path string) *image.Analysis {\n\tt.Helper()\n\tarchive, err := TestLoadArchive(t, path)\n\trequire.NoError(t, err, \"unable to load archive\")\n\n\timg, err := archive.ToImage(path)\n\trequire.NoError(t, err, \"unable to convert archive to image\")\n\n\tresult, err := image.Analyze(context.Background(), img)\n\trequire.NoError(t, err, \"unable to analyze image\")\n\treturn result\n}\n"
  },
  {
    "path": "dive/image/image.go",
    "content": "package image\n\nimport (\n\t\"github.com/wagoodman/dive/dive/filetree\"\n)\n\ntype Image struct {\n\tRequest string\n\tTrees   []*filetree.FileTree\n\tLayers  []*Layer\n}\n"
  },
  {
    "path": "dive/image/layer.go",
    "content": "package image\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/dustin/go-humanize\"\n\n\t\"github.com/wagoodman/dive/dive/filetree\"\n)\n\nconst (\n\tLayerFormat = \"%7s  %s\"\n)\n\ntype Layer struct {\n\tId      string\n\tIndex   int\n\tCommand string\n\tSize    uint64\n\tTree    *filetree.FileTree\n\tNames   []string\n\tDigest  string\n}\n\nfunc (l *Layer) ShortId() string {\n\trangeBound := 15\n\tid := l.Id\n\tif length := len(id); length < 15 {\n\t\trangeBound = length\n\t}\n\tid = id[0:rangeBound]\n\n\treturn id\n}\n\nfunc (l *Layer) commandPreview() string {\n\t// Layers using heredocs can be multiple lines; rendering relies on\n\t// Layer.String to be a single line.\n\treturn strings.Replace(l.Command, \"\\n\", \"↵\", -1)\n}\n\nfunc (l *Layer) String() string {\n\tif l.Index == 0 {\n\t\treturn fmt.Sprintf(LayerFormat,\n\t\t\thumanize.Bytes(l.Size),\n\t\t\t\"FROM \"+l.ShortId())\n\t}\n\treturn fmt.Sprintf(LayerFormat,\n\t\thumanize.Bytes(l.Size),\n\t\tl.commandPreview())\n}\n"
  },
  {
    "path": "dive/image/podman/build.go",
    "content": "//go:build linux || darwin\n\npackage podman\n\nimport (\n\t\"os\"\n)\n\nfunc buildImageFromCli(buildArgs []string) (string, error) {\n\tiidfile, err := os.CreateTemp(\"/tmp\", \"dive.*.iid\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer os.Remove(iidfile.Name())\n\tdefer iidfile.Close()\n\n\tallArgs := append([]string{\"--iidfile\", iidfile.Name()}, buildArgs...)\n\terr = runPodmanCmd(\"build\", allArgs...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\timageId, err := os.ReadFile(iidfile.Name())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(imageId), nil\n}\n"
  },
  {
    "path": "dive/image/podman/cli.go",
    "content": "//go:build linux || darwin\n\npackage podman\n\nimport (\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/internal/log\"\n\t\"github.com/wagoodman/dive/internal/utils\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\n// runPodmanCmd runs a given Podman command in the current tty\nfunc runPodmanCmd(cmdStr string, args ...string) error {\n\tif !isPodmanClientBinaryAvailable() {\n\t\treturn fmt.Errorf(\"cannot find podman client executable\")\n\t}\n\n\tallArgs := utils.CleanArgs(append([]string{cmdStr}, args...))\n\n\tfullCmd := strings.Join(append([]string{\"docker\"}, allArgs...), \" \")\n\tlog.WithFields(\"cmd\", fullCmd).Trace(\"executing\")\n\n\tcmd := exec.Command(\"podman\", allArgs...)\n\tcmd.Env = os.Environ()\n\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdin = os.Stdin\n\n\treturn cmd.Run()\n}\n\nfunc streamPodmanCmd(args ...string) (error, io.Reader) {\n\tif !isPodmanClientBinaryAvailable() {\n\t\treturn fmt.Errorf(\"cannot find podman client executable\"), nil\n\t}\n\n\tallArgs := utils.CleanArgs(args)\n\tfullCmd := strings.Join(append([]string{\"docker\"}, allArgs...), \" \")\n\tlog.WithFields(\"cmd\", fullCmd).Trace(\"executing (streaming)\")\n\n\tcmd := exec.Command(\"podman\", allArgs...)\n\tcmd.Env = os.Environ()\n\n\treader, writer, err := os.Pipe()\n\tif err != nil {\n\t\treturn err, nil\n\t}\n\tdefer writer.Close()\n\n\tcmd.Stdout = writer\n\tcmd.Stderr = os.Stderr\n\n\treturn cmd.Start(), reader\n}\n\nfunc isPodmanClientBinaryAvailable() bool {\n\t_, err := exec.LookPath(\"podman\")\n\treturn err == nil\n}\n"
  },
  {
    "path": "dive/image/podman/resolver.go",
    "content": "//go:build linux || darwin\n\npackage podman\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/dive/image/docker\"\n)\n\ntype resolver struct{}\n\nfunc NewResolverFromEngine() *resolver {\n\treturn &resolver{}\n}\n\n// Name returns the name of the resolver to display to the user.\nfunc (r *resolver) Name() string {\n\treturn \"podman\"\n}\n\nfunc (r *resolver) Build(ctx context.Context, args []string) (*image.Image, error) {\n\tid, err := buildImageFromCli(args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn r.Fetch(ctx, id)\n}\n\nfunc (r *resolver) Fetch(ctx context.Context, id string) (*image.Image, error) {\n\t// todo: add podman fetch attempt via varlink first...\n\n\timg, err := r.resolveFromDockerArchive(id)\n\tif err == nil {\n\t\treturn img, err\n\t}\n\n\treturn nil, fmt.Errorf(\"unable to resolve image %q: %+v\", id, err)\n}\n\nfunc (r *resolver) Extract(ctx context.Context, id string, l string, p string) error {\n\t// todo: add podman fetch attempt via varlink first...\n\n\terr, reader := streamPodmanCmd(\"image\", \"save\", id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := docker.ExtractFromImage(io.NopCloser(reader), l, p); err == nil {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"unable to extract from image %q: %+v\", id, err)\n}\n\nfunc (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) {\n\terr, reader := streamPodmanCmd(\"image\", \"save\", id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\timg, err := docker.NewImageArchive(io.NopCloser(reader))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn img.ToImage(id)\n}\n"
  },
  {
    "path": "dive/image/podman/resolver_unsupported.go",
    "content": "//go:build !linux && !darwin\n// +build !linux,!darwin\n\npackage podman\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/wagoodman/dive/dive/image\"\n)\n\ntype resolver struct{}\n\nfunc NewResolverFromEngine() *resolver {\n\treturn &resolver{}\n}\n\n// Name returns the name of the resolver to display to the user.\nfunc (r *resolver) Name() string {\n\treturn \"podman\"\n}\nfunc (r *resolver) Build(ctx context.Context, args []string) (*image.Image, error) {\n\treturn nil, fmt.Errorf(\"unsupported platform\")\n}\n\nfunc (r *resolver) Fetch(ctx context.Context, id string) (*image.Image, error) {\n\treturn nil, fmt.Errorf(\"unsupported platform\")\n}\n\nfunc (r *resolver) Extract(ctx context.Context, id string, l string, p string) error {\n\treturn fmt.Errorf(\"unsupported platform\")\n}\n"
  },
  {
    "path": "dive/image/resolver.go",
    "content": "package image\n\nimport \"golang.org/x/net/context\"\n\ntype Resolver interface {\n\tName() string\n\tFetch(ctx context.Context, id string) (*Image, error)\n\tBuild(ctx context.Context, options []string) (*Image, error)\n\tContentReader\n}\n\ntype ContentReader interface {\n\tExtract(ctx context.Context, id string, layer string, path string) error\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/wagoodman/dive\n\ngo 1.24\n\nrequire (\n\tgithub.com/anchore/clio v0.0.0-20250401141128-4c1d6bd1e872\n\tgithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722\n\tgithub.com/awesome-gocui/gocui v1.1.0\n\tgithub.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f\n\tgithub.com/cespare/xxhash/v2 v2.3.0\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/docker/cli v28.1.1+incompatible\n\tgithub.com/docker/docker v28.1.1+incompatible\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/gkampitakis/go-snaps v0.5.11\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/klauspost/compress v1.18.0\n\tgithub.com/lunixbochs/vtclean v1.0.0\n\tgithub.com/muesli/termenv v0.16.0\n\tgithub.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee\n\tgithub.com/scylladb/go-set v1.0.2\n\tgithub.com/spf13/afero v1.14.0\n\tgithub.com/spf13/cobra v1.9.1\n\tgithub.com/stretchr/testify v1.10.0\n\tgithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651\n\tgithub.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0\n\tgo.uber.org/atomic v1.11.0\n\tgolang.org/x/net v0.40.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tdario.cat/mergo v1.0.1 // indirect\n\tgithub.com/Microsoft/go-winio v0.4.14 // indirect\n\tgithub.com/adrg/xdg v0.5.3 // indirect\n\tgithub.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe // indirect\n\tgithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect\n\tgithub.com/charmbracelet/x/ansi v0.8.0 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect\n\tgithub.com/charmbracelet/x/term v0.2.1 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/docker-credential-helpers v0.8.2 // indirect\n\tgithub.com/docker/go-connections v0.4.0 // indirect\n\tgithub.com/docker/go-units v0.4.0 // indirect\n\tgithub.com/felixge/fgprof v0.9.3 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.8.0 // indirect\n\tgithub.com/fvbommel/sortorder v1.1.0 // indirect\n\tgithub.com/gdamore/encoding v1.0.0 // indirect\n\tgithub.com/gdamore/tcell/v2 v2.4.0 // indirect\n\tgithub.com/gkampitakis/ciinfo v0.3.1 // indirect\n\tgithub.com/gkampitakis/go-diff v1.3.2 // indirect\n\tgithub.com/go-logr/logr v1.4.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.2.1 // indirect\n\tgithub.com/goccy/go-yaml v1.15.13 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect\n\tgithub.com/gookit/color v1.5.4 // indirect\n\tgithub.com/hashicorp/errwrap v1.0.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.0 // indirect\n\tgithub.com/iancoleman/strcase v0.3.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/maruel/natural v1.1.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/sys/atomicwriter v0.1.0 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/term v0.5.0 // indirect\n\tgithub.com/morikuni/aec v1.0.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.0.2 // indirect\n\tgithub.com/pborman/indent v1.2.1 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.3 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pkg/profile v1.7.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rogpeppe/go-internal v1.13.1 // indirect\n\tgithub.com/sagikazarmark/locafero v0.7.0 // indirect\n\tgithub.com/sergi/go-diff v1.3.1 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/cast v1.7.1 // indirect\n\tgithub.com/spf13/pflag v1.0.6 // indirect\n\tgithub.com/spf13/viper v1.20.1 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/tidwall/sjson v1.2.5 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect\n\tgo.opentelemetry.io/otel v1.31.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.31.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.31.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.31.0 // indirect\n\tgo.uber.org/multierr v1.9.0 // indirect\n\tgolang.org/x/sys v0.33.0 // indirect\n\tgolang.org/x/term v0.32.0 // indirect\n\tgolang.org/x/text v0.25.0 // indirect\n\tgolang.org/x/time v0.11.0 // indirect\n\tgotest.tools/v3 v3.5.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=\ndario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=\ngithub.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=\ngithub.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=\ngithub.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=\ngithub.com/anchore/clio v0.0.0-20250401141128-4c1d6bd1e872 h1:iEF0xhHUuh3J8FrlPsZAQVaMpTa2j4lvLRI5XrXzge4=\ngithub.com/anchore/clio v0.0.0-20250401141128-4c1d6bd1e872/go.mod h1:Utb9i4kwiCWvqAIxZaJeMIXFO9uOgQXlvH2BfbfO/zI=\ngithub.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe h1:qv/xxpjF5RdKPqZjx8RM0aBi3HUCAO0DhRBMs2xhY1I=\ngithub.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe/go.mod h1:vrcYMDps9YXwwx2a9AsvipM6Fi5H9//9bymGb8G8BIQ=\ngithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc=\ngithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50=\ngithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM=\ngithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=\ngithub.com/awesome-gocui/gocui v0.5.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=\ngithub.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=\ngithub.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg=\ngithub.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f h1:u5xQfLwWC98BFToYDifqEcgK2ht2FFlbvRlzRnMb0cQ=\ngithub.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f/go.mod h1:z0TyCwIhaT97yU+becTse8Dqh2CvYT0FLw0R0uTk0ag=\ngithub.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=\ngithub.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=\ngithub.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=\ngithub.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=\ngithub.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=\ngithub.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=\ngithub.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=\ngithub.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=\ngithub.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=\ngithub.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=\ngithub.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=\ngithub.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=\ngithub.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=\ngithub.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=\ngithub.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=\ngithub.com/gkampitakis/ciinfo v0.3.1 h1:lzjbemlGI4Q+XimPg64ss89x8Mf3xihJqy/0Mgagapo=\ngithub.com/gkampitakis/ciinfo v0.3.1/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=\ngithub.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=\ngithub.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=\ngithub.com/gkampitakis/go-snaps v0.5.11 h1:LFG0ggUKR+KEiiaOvFCmLgJ5NO2zf93AxxddkBn3LdQ=\ngithub.com/gkampitakis/go-snaps v0.5.11/go.mod h1:PcKmy8q5Se7p48ywpogN5Td13reipz1Iivah4wrTIvY=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=\ngithub.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=\ngithub.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=\ngithub.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=\ngithub.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=\ngithub.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=\ngithub.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=\ngithub.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=\ngithub.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=\ngithub.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=\ngithub.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=\ngithub.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=\ngithub.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=\ngithub.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=\ngithub.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=\ngithub.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=\ngithub.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=\ngithub.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=\ngithub.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=\ngithub.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE=\ngithub.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=\ngithub.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=\ngithub.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=\ngithub.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=\ngithub.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=\ngithub.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=\ngithub.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=\ngithub.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=\ngithub.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=\ngithub.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=\ngithub.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=\ngithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=\ngithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20=\ngithub.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8=\ngithub.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=\ngo.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=\ngo.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=\ngo.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=\ngo.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=\ngo.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=\ngo.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=\ngo.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=\ngo.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=\ngo.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=\ngo.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=\ngo.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=\ngolang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=\ngolang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=\ngolang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=\ngolang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=\ngolang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=\ngolang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=\ngoogle.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=\ngoogle.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=\ngoogle.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=\ngoogle.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=\ngotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=\n"
  },
  {
    "path": "internal/bus/bus.go",
    "content": "package bus\n\nimport \"github.com/wagoodman/go-partybus\"\n\nvar publisher partybus.Publisher\n\nfunc Set(p partybus.Publisher) {\n\tpublisher = p\n}\n\nfunc Get() partybus.Publisher {\n\treturn publisher\n}\n\nfunc Publish(e partybus.Event) {\n\tif publisher != nil {\n\t\tpublisher.Publish(e)\n\t}\n}\n"
  },
  {
    "path": "internal/bus/event/event.go",
    "content": "package event\n\nimport (\n\t\"github.com/wagoodman/go-partybus\"\n)\n\nconst (\n\ttypePrefix = \"dive-cli\"\n\n\t// TaskStarted encompasses all events that are related to the analysis of a docker image (build, fetch, analyze)\n\tTaskStarted partybus.EventType = typePrefix + \"-task-started\"\n\n\t// ExploreAnalysis is a partybus event that occurs when an analysis result is ready for presentation to stdout\n\tExploreAnalysis partybus.EventType = typePrefix + \"-analysis\"\n\n\t// Report is a partybus event that occurs when an analysis result is ready for final presentation to stdout\n\tReport partybus.EventType = typePrefix + \"-report\"\n\n\t// Notification is a partybus event that occurs when auxiliary information is ready for presentation to stderr\n\tNotification partybus.EventType = typePrefix + \"-notification\"\n)\n"
  },
  {
    "path": "internal/bus/event/parser/parsers.go",
    "content": "package parser\n\nimport (\n\t\"fmt\"\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/bus/event\"\n\t\"github.com/wagoodman/dive/internal/bus/event/payload\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n)\n\ntype ErrBadPayload struct {\n\tType  partybus.EventType\n\tField string\n\tValue interface{}\n}\n\nfunc (e *ErrBadPayload) Error() string {\n\treturn fmt.Sprintf(\"event='%s' has bad event payload field=%q: %q\", string(e.Type), e.Field, e.Value)\n}\n\nfunc newPayloadErr(t partybus.EventType, field string, value interface{}) error {\n\treturn &ErrBadPayload{\n\t\tType:  t,\n\t\tField: field,\n\t\tValue: value,\n\t}\n}\n\nfunc checkEventType(actual, expected partybus.EventType) error {\n\tif actual != expected {\n\t\treturn newPayloadErr(expected, \"Type\", actual)\n\t}\n\treturn nil\n}\n\nfunc ParseTaskStarted(e partybus.Event) (progress.StagedProgressable, *payload.GenericTask, error) {\n\tif err := checkEventType(e.Type, event.TaskStarted); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tvar mon progress.StagedProgressable\n\n\tsource, ok := e.Source.(payload.GenericTask)\n\tif !ok {\n\t\treturn nil, nil, newPayloadErr(e.Type, \"Source\", e.Source)\n\t}\n\n\tmon, ok = e.Value.(progress.StagedProgressable)\n\tif !ok {\n\t\tmon = nil\n\t}\n\n\treturn mon, &source, nil\n}\n\nfunc ParseExploreAnalysis(e partybus.Event) (image.Analysis, image.ContentReader, error) {\n\tif err := checkEventType(e.Type, event.ExploreAnalysis); err != nil {\n\t\treturn image.Analysis{}, nil, err\n\t}\n\n\tex, ok := e.Value.(payload.Explore)\n\tif !ok {\n\t\treturn image.Analysis{}, nil, newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn ex.Analysis, ex.Content, nil\n}\n\nfunc ParseReport(e partybus.Event) (string, string, error) {\n\tif err := checkEventType(e.Type, event.Report); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tcontext, ok := e.Source.(string)\n\tif !ok {\n\t\t// this is optional\n\t\tcontext = \"\"\n\t}\n\n\treport, ok := e.Value.(string)\n\tif !ok {\n\t\treturn \"\", \"\", newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn context, report, nil\n}\n\nfunc ParseNotification(e partybus.Event) (string, string, error) {\n\tif err := checkEventType(e.Type, event.Notification); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tcontext, ok := e.Source.(string)\n\tif !ok {\n\t\t// this is optional\n\t\tcontext = \"\"\n\t}\n\n\tnotification, ok := e.Value.(string)\n\tif !ok {\n\t\treturn \"\", \"\", newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn context, notification, nil\n}\n"
  },
  {
    "path": "internal/bus/event/payload/explore.go",
    "content": "package payload\n\nimport \"github.com/wagoodman/dive/dive/image\"\n\ntype Explore struct {\n\tAnalysis image.Analysis\n\tContent  image.ContentReader\n}\n"
  },
  {
    "path": "internal/bus/event/payload/generic.go",
    "content": "package payload\n\nimport (\n\t\"context\"\n\t\"github.com/wagoodman/go-progress\"\n)\n\ntype genericProgressKey struct{}\n\nfunc SetGenericProgressToContext(ctx context.Context, mon *GenericProgress) context.Context {\n\treturn context.WithValue(ctx, genericProgressKey{}, mon)\n}\n\nfunc GetGenericProgressFromContext(ctx context.Context) *GenericProgress {\n\tmon, ok := ctx.Value(genericProgressKey{}).(*GenericProgress)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn mon\n}\n\ntype GenericTask struct {\n\t// required fields\n\n\tTitle Title\n\n\t// optional format fields\n\n\tHideOnSuccess      bool\n\tHideStageOnSuccess bool\n\n\t// optional fields\n\n\tID       string\n\tParentID string\n\tContext  string\n}\n\ntype GenericProgress struct {\n\t*progress.AtomicStage\n\t*progress.Manual\n}\n\ntype Title struct {\n\tDefault      string\n\tWhileRunning string\n\tOnSuccess    string\n}\n"
  },
  {
    "path": "internal/bus/helpers.go",
    "content": "package bus\n\nimport (\n\t\"github.com/wagoodman/dive/dive/image\"\n\t\"github.com/wagoodman/dive/internal/bus/event\"\n\t\"github.com/wagoodman/dive/internal/bus/event/payload\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n)\n\nfunc Report(report string) {\n\tif len(report) == 0 {\n\t\treturn\n\t}\n\tPublish(partybus.Event{\n\t\tType:  event.Report,\n\t\tValue: report,\n\t})\n}\n\nfunc Notify(message string) {\n\tPublish(partybus.Event{\n\t\tType:  event.Notification,\n\t\tValue: message,\n\t})\n}\n\nfunc StartTask(info payload.GenericTask) *payload.GenericProgress {\n\tt := &payload.GenericProgress{\n\t\tAtomicStage: progress.NewAtomicStage(\"\"),\n\t\tManual:      progress.NewManual(-1),\n\t}\n\n\tPublish(partybus.Event{\n\t\tType:   event.TaskStarted,\n\t\tSource: info,\n\t\tValue:  progress.StagedProgressable(t),\n\t})\n\n\treturn t\n}\n\nfunc StartSizedTask(info payload.GenericTask, size int64, initialStage string) *payload.GenericProgress {\n\tt := &payload.GenericProgress{\n\t\tAtomicStage: progress.NewAtomicStage(initialStage),\n\t\tManual:      progress.NewManual(size),\n\t}\n\n\tPublish(partybus.Event{\n\t\tType:   event.TaskStarted,\n\t\tSource: info,\n\t\tValue:  progress.StagedProgressable(t),\n\t})\n\n\treturn t\n}\n\nfunc ExploreAnalysis(analysis image.Analysis, reader image.ContentReader) {\n\tPublish(partybus.Event{\n\t\tType:  event.ExploreAnalysis,\n\t\tValue: payload.Explore{Analysis: analysis, Content: reader},\n\t})\n}\n"
  },
  {
    "path": "internal/log/log.go",
    "content": "package log\n\nimport (\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/go-logger/adapter/discard\"\n)\n\n// log is the singleton used to facilitate logging internally within\nvar log = discard.New()\n\n// Set replaces the default logger with the provided logger.\nfunc Set(l logger.Logger) {\n\tlog = l\n}\n\n// Get returns the current logger instance.\nfunc Get() logger.Logger {\n\treturn log\n}\n\n// Errorf takes a formatted template string and template arguments for the error logging level.\nfunc Errorf(format string, args ...interface{}) {\n\tlog.Errorf(format, args...)\n}\n\n// Error logs the given arguments at the error logging level.\nfunc Error(args ...interface{}) {\n\tlog.Error(args...)\n}\n\n// Warnf takes a formatted template string and template arguments for the warning logging level.\nfunc Warnf(format string, args ...interface{}) {\n\tlog.Warnf(format, args...)\n}\n\n// Warn logs the given arguments at the warning logging level.\nfunc Warn(args ...interface{}) {\n\tlog.Warn(args...)\n}\n\n// Infof takes a formatted template string and template arguments for the info logging level.\nfunc Infof(format string, args ...interface{}) {\n\tlog.Infof(format, args...)\n}\n\n// Info logs the given arguments at the info logging level.\nfunc Info(args ...interface{}) {\n\tlog.Info(args...)\n}\n\n// Debugf takes a formatted template string and template arguments for the debug logging level.\nfunc Debugf(format string, args ...interface{}) {\n\tlog.Debugf(format, args...)\n}\n\n// Debug logs the given arguments at the debug logging level.\nfunc Debug(args ...interface{}) {\n\tlog.Debug(args...)\n}\n\n// Tracef takes a formatted template string and template arguments for the trace logging level.\nfunc Tracef(format string, args ...interface{}) {\n\tlog.Tracef(format, args...)\n}\n\n// Trace logs the given arguments at the trace logging level.\nfunc Trace(args ...interface{}) {\n\tlog.Trace(args...)\n}\n\n// WithFields returns a message logger with multiple key-value fields.\nfunc WithFields(fields ...interface{}) logger.MessageLogger {\n\treturn log.WithFields(fields...)\n}\n\n// Nested returns a new logger with hard coded key-value pairs\nfunc Nested(fields ...interface{}) logger.Logger {\n\treturn log.Nested(fields...)\n}\n"
  },
  {
    "path": "internal/utils/format.go",
    "content": "package utils\n\nimport (\n\t\"strings\"\n)\n\n// CleanArgs trims the whitespace from the given set of strings.\nfunc CleanArgs(s []string) []string {\n\tvar r []string\n\tfor _, str := range s {\n\t\tif str != \"\" {\n\t\t\tr = append(r, strings.Trim(str, \" \"))\n\t\t}\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "internal/utils/view.go",
    "content": "package utils\n\nimport (\n\t\"errors\"\n\t\"github.com/awesome-gocui/gocui\"\n\t\"github.com/wagoodman/dive/internal/log\"\n)\n\n// IsNewView determines if a view has already been created based on the set of errors given (a bit hokie)\nfunc IsNewView(errs ...error) bool {\n\tfor _, err := range errs {\n\t\tif err == nil {\n\t\t\treturn false\n\t\t}\n\t\tif !errors.Is(err, gocui.ErrUnknownView) {\n\t\t\tlog.WithFields(\"error\", err).Error(\"IsNewView() unexpected error\")\n\t\t\treturn true\n\t\t}\n\t}\n\treturn true\n}\n"
  }
]