[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: schollz\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: File a bug report to help us improve croc\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report! Please provide as much detail as possible to help us reproduce and fix the issue.\n\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: A clear and concise description of what the bug is.\n      placeholder: Tell us what you see!\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: What did you expect to happen?\n      description: A clear and concise description of what you expected to happen.\n      placeholder: Tell us what you expected!\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction-steps\n    attributes:\n      label: Steps to reproduce\n      description: Steps to reproduce the behavior\n      placeholder: |\n        1. Go to '...'\n        2. Click on '...'\n        3. Scroll down to '...'\n        4. See error\n    validations:\n      required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: croc version\n      description: What version of croc are you running?\n      placeholder: Run `croc --version` and paste the output here\n    validations:\n      required: true\n\n  - type: dropdown\n    id: os\n    attributes:\n      label: Operating System\n      description: What operating system are you using?\n      options:\n        - Linux\n        - macOS\n        - Windows\n        - Other (please specify in additional context)\n    validations:\n      required: true\n\n  - type: input\n    id: os-version\n    attributes:\n      label: OS Version\n      description: What version of your operating system are you using?\n      placeholder: e.g., Ubuntu 22.04, macOS 14.0, Windows 11\n    validations:\n      required: true\n\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: |\n        Please copy and paste any relevant log output. You can enable debug logging with `croc --debug` \n        This will be automatically formatted into code, so no need for backticks.\n      render: shell\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional context\n      description: Add any other context about the problem here, such as screenshots, configuration files, or anything else that might help us understand the issue.\n    validations:\n      required: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  unit-tests:\n    name: Go unit tests\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '^1.26.0'\n      \n      - name: Display Go version\n        run: go version\n\n      - name: Run unit tests\n        run: go test -v ./...\n\n      - name: Build files\n        run: |\n          go version\n          CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags \"-static\"' -o croc-windows-amd64.exe\n          CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags \"-static\"' -o croc-windows-386.exe\n          CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags \"-static\"' -o croc-windows-arm.exe\n          CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags \"-static\"' -o croc-windows-arm64.exe\n          CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags \"-static\"' -o croc-linux-amd64\n          CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags \"-static\"' -o croc-linux-386\n          CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags \"-static\"' -o croc-linux-arm\n          GOARM=5 CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags \"-static\"' -o croc-linux-arm5\n          CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags \"-static\"' -o croc-linux-arm64\n          CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags '-extldflags \"-static\"' -o croc-linux-riscv64\n          CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags \"-sectcreate __TEXT __info_plist Info.plist\"' -o croc-darwin-amd64\n          CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags \"-sectcreate __TEXT __info_plist Info.plist\"' -o croc-darwin-arm64\n          CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc-freebsd-amd64\n          CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc-freebsd-arm64\n          CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc-openbsd-amd64\n          CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc-openbsd-arm64\n      - name: Check static build of the linux version\n        run: |\n          if ldd croc-linux-amd64 2>&1 | grep -q \"not a dynamic executable\"; then\n            echo \"Static build confirmed.\"\n          else\n            echo \"Error: croc-linux-amd64 is a dynamic executable.\"\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy Docker\n\non:\n  release:\n    types: [created]\n  workflow_dispatch:\n\njobs:\n  docker:\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n      \n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: schollz/croc\n          tags: |\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=sha\n      \n      - name: Setup QEMU\n        uses: docker/setup-qemu-action@v4\n      \n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n      \n      - name: Login to DockerHub\n        if: github.event_name != 'pull_request'\n        uses: docker/login-action@v4\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      \n      - name: Build and push\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./Dockerfile\n          platforms: linux/amd64,linux/arm,linux/arm64,linux/386\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types: [created]\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  prepare-source:\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '^1.24.0'\n\n      - name: Prepare source tarball\n        run: |\n          git clone -b ${{ github.event.release.name }} --depth 1 https://github.com/schollz/croc croc-${{ github.event.release.name }}\n          cd croc-${{ github.event.release.name }} && go mod tidy && go mod vendor\n          cd .. && tar -czvf croc_${{ github.event.release.name }}_src.tar.gz croc-${{ github.event.release.name }}\n\n      - name: Upload source artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: source-tarball\n          path: \"*.tar.gz\"\n\n  build:\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        include:\n          # Windows builds\n          - goos: windows\n            goarch: amd64\n            name: Windows-64bit\n            ext: .exe\n            archive: zip\n          - goos: windows\n            goarch: \"386\"\n            name: Windows-32bit\n            ext: .exe\n            archive: zip\n          - goos: windows\n            goarch: arm\n            name: Windows-ARM\n            ext: .exe\n            archive: zip\n          - goos: windows\n            goarch: arm64\n            name: Windows-ARM64\n            ext: .exe\n            archive: zip\n          \n          # Linux builds\n          - goos: linux\n            goarch: amd64\n            name: Linux-64bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: linux\n            goarch: \"386\"\n            name: Linux-32bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: linux\n            goarch: arm\n            name: Linux-ARM\n            ext: \"\"\n            archive: tar.gz\n          - goos: linux\n            goarch: arm\n            goarm: \"5\"\n            name: Linux-ARMv5\n            ext: \"\"\n            archive: tar.gz\n          - goos: linux\n            goarch: arm64\n            name: Linux-ARM64\n            ext: \"\"\n            archive: tar.gz\n          - goos: linux\n            goarch: riscv64\n            name: Linux-RISCV64\n            ext: \"\"\n            archive: tar.gz\n          \n          # macOS builds\n          - goos: darwin\n            goarch: amd64\n            name: macOS-64bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: darwin\n            goarch: arm64\n            name: macOS-ARM64\n            ext: \"\"\n            archive: tar.gz\n          \n          # BSD builds\n          - goos: dragonfly\n            goarch: amd64\n            name: DragonFlyBSD-64bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: freebsd\n            goarch: amd64\n            name: FreeBSD-64bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: freebsd\n            goarch: arm64\n            name: FreeBSD-ARM64\n            ext: \"\"\n            archive: tar.gz\n          - goos: netbsd\n            goarch: \"386\"\n            name: NetBSD-32bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: netbsd\n            goarch: amd64\n            name: NetBSD-64bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: netbsd\n            goarch: arm64\n            name: NetBSD-ARM64\n            ext: \"\"\n            archive: tar.gz\n          - goos: openbsd\n            goarch: amd64\n            name: OpenBSD-64bit\n            ext: \"\"\n            archive: tar.gz\n          - goos: openbsd\n            goarch: arm64\n            name: OpenBSD-ARM64\n            ext: \"\"\n            archive: tar.gz\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '^1.24.0'\n\n      - name: Build binary\n        env:\n          CGO_ENABLED: 0\n          GOOS: ${{ matrix.goos }}\n          GOARCH: ${{ matrix.goarch }}\n          GOARM: ${{ matrix.goarm }}\n        run: |\n          # Set LDFLAGS based on platform\n          case \"${{ matrix.goos }}\" in\n            \"darwin\")\n              LDFLAGS='-s -extldflags \"-sectcreate __TEXT __info_plist Info.plist\"'\n              ;;\n            \"dragonfly\"|\"freebsd\"|\"netbsd\"|\"openbsd\")\n              LDFLAGS=\"\"\n              ;;\n            *)\n              LDFLAGS='-extldflags \"-static\"'\n              ;;\n          esac\n          \n          echo \"Building for ${{ matrix.goos }}/${{ matrix.goarch }} with LDFLAGS: $LDFLAGS\"\n          go build -ldflags \"$LDFLAGS\" -o croc${{ matrix.ext }}\n\n      - name: Create archive\n        run: |\n          if [ \"${{ matrix.archive }}\" = \"zip\" ]; then\n            zip croc_${{ github.event.release.name }}_${{ matrix.name }}.zip croc${{ matrix.ext }} LICENSE\n          else\n            tar -czvf croc_${{ github.event.release.name }}_${{ matrix.name }}.tar.gz croc${{ matrix.ext }} LICENSE\n          fi\n\n      - name: Upload build artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: build-${{ matrix.name }}\n          path: |\n            *.zip\n            *.tar.gz\n\n  release:\n    needs: [prepare-source, build]\n    runs-on: ubuntu-24.04\n    if: github.event_name == 'release'\n    steps:\n      - name: Download all artifacts\n        uses: actions/download-artifact@v8\n        with:\n          merge-multiple: true\n\n      - name: Generate checksums\n        run: |\n          # Generate SHA256 checksums for all archives\n          sha256sum *.zip *.tar.gz > croc_${{ github.event.release.name }}_checksums.txt\n          \n          # Display the checksums file for verification\n          echo \"Generated checksums:\"\n          cat croc_${{ github.event.release.name }}_checksums.txt\n\n      - name: Upload release assets\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            *.zip\n            *.tar.gz\n            *_checksums.txt\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Mark stale issues and pull requests\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n\njobs:\n  stale:\n    runs-on: ubuntu-24.04\n    permissions:\n      issues: write\n      pull-requests: write\n\n    steps:\n      - name: Stale\n        uses: actions/stale@v10\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          stale-issue-message: 'This issue has been marked stale because it has been open for 60 days with no activity.'\n          stale-pr-message: 'This pull request has been marked stale because it has been open for 60 days with no activity.'\n          stale-issue-label: 'no-issue-activity'\n          stale-pr-label: 'no-pr-activity'\n"
  },
  {
    "path": ".github/workflows/winget.yml",
    "content": "name: Publish to Winget\r\n\r\non:\r\n  release:\r\n    types: [released]\r\n  workflow_dispatch:\r\n\r\njobs:\r\n  publish:\r\n    runs-on: ubuntu-24.04\r\n\r\n    steps:\r\n      - name: Publish to Winget\r\n        uses: vedantmgoyal9/winget-releaser@v2\r\n        with:\r\n          identifier: schollz.croc\r\n          installers-regex: '.*Windows.*\\.zip$'\r\n          token: ${{ secrets.WINGET_TOKEN }}\r\n\r\n"
  },
  {
    "path": ".gitignore",
    "content": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\ngo.work.sum\n\n# Environment variables file\n.env\n\n# Croc builds\n/croc\ncroc_v*\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "project_name: croc\n\nbuild:\n  main: main.go\n  binary: croc\n  ldflags: -s -w -X main.Version=\"v{{.Version}}-{{.Date}}\"\n  env:\n    - CGO_ENABLED=0\n  goos:\n    - darwin\n    - linux\n    - windows\n    - freebsd\n    - netbsd\n    - openbsd\n    - dragonfly\n  goarch:\n    - amd64\n    - 386\n    - arm\n    - arm64\n  ignore:\n    - goos: darwin\n      goarch: 386\n    - goos: freebsd\n      goarch: arm\n  goarm:\n    - 7\n\nnfpms:\n  - formats:\n      - deb\n    vendor: \"schollz.com\"\n    homepage: \"https://schollz.com/software/croc/\"\n    maintainer: \"Zack Scholl <zack.scholl@gmail.com>\"\n    description: \"A simple, secure, and fast way to transfer data.\"\n    license: \"MIT\"\n    file_name_template: \"{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}\"\n    replacements:\n      amd64: 64bit\n      386: 32bit\n      arm: ARM\n      arm64: ARM64\n      darwin: macOS\n      linux: Linux\n      windows: Windows\n      openbsd: OpenBSD\n      netbsd: NetBSD\n      freebsd: FreeBSD\n      dragonfly: DragonFlyBSD\n\narchives:\n  - format: tar.gz\n    format_overrides:\n      - goos: windows\n        format: zip\n    name_template: \"{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}\"\n    replacements:\n      amd64: 64bit\n      386: 32bit\n      arm: ARM\n      arm64: ARM64\n      darwin: macOS\n      linux: Linux\n      windows: Windows\n      openbsd: OpenBSD\n      netbsd: NetBSD\n      freebsd: FreeBSD\n      dragonfly: DragonFlyBSD\n    files:\n      - README.md\n      - LICENSE\n      - zsh_autocomplete\n      - bash_autocomplete\n\nbrews:\n  - tap:\n      owner: schollz\n      name: homebrew-tap\n    folder: Formula\n    description: \"croc is a tool that allows any two computers to simply and securely transfer files and folders.\"\n    homepage: \"https://schollz.com/software/croc/\"\n    install: |\n      bin.install \"croc\"\n    test: |\n      system \"#{bin}/croc --version\"\n\nscoop:\n  bucket:\n    owner: schollz\n    name: scoop-bucket\n  homepage: \"https://schollz.com/software/croc/\"\n  description: \"croc is a tool that allows any two computers to simply and securely transfer files and folders.\"\n  license: MIT\n\nannounce:\n  twitter:\n    enabled: false\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: go\n\ngo:\n  - tip\n\nenv:\n  - \"PATH=/home/travis/gopath/bin:$PATH\"\n\ninstall: true\n\nscript:\n  - env GO111MODULE=on go build -v\n  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/compress\n  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/croc\n  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/crypt\n  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/tcp\n  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/utils\n  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/comm\n\nbranches:\n  except:\n    - dev\n    - win\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.24-alpine AS builder\n\nRUN apk add --no-cache git gcc musl-dev\n\nWORKDIR /go/croc\n\nCOPY . .\n\nRUN go build -v -ldflags=\"-s -w\"\n\nFROM alpine:latest\n\nEXPOSE 9009\nEXPOSE 9010\nEXPOSE 9011\nEXPOSE 9012\nEXPOSE 9013\n\nCOPY --from=builder /go/croc/croc /go/croc/croc-entrypoint.sh /\n\nUSER nobody\n\n# Simple TCP health check with nc!\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n    CMD nc -z localhost 9009 || exit 1\n\nENTRYPOINT [\"/croc-entrypoint.sh\"]\nCMD [\"relay\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017-2025 Zack\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": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg\" width=\"408px\" border=\"0\" alt=\"croc\">\n  <br>\n  <a href=\"https://github.com/schollz/croc/releases/latest\"><img src=\"https://img.shields.io/github/v/release/schollz/croc\" alt=\"Version\"></a>\n  <a href=\"https://github.com/schollz/croc/actions/workflows/ci.yml\"><img src=\"https://github.com/schollz/croc/actions/workflows/ci.yml/badge.svg\" alt=\"Build Status\"></a>\n  <a href=\"https://github.com/sponsors/schollz\"><img alt=\"GitHub Sponsors\" src=\"https://img.shields.io/github/sponsors/schollz\"></a>\n</p>\n<p align=\"center\">\n  <strong>This project’s future depends on community support. <a href=\"https://github.com/sponsors/schollz\">Become a sponsor today</a>.</strong>\n</p>\n\n## About\n\n`croc` is a tool that allows any two computers to simply and securely transfer files and folders. AFAIK, *croc* is the only CLI file-transfer tool that does **all** of the following:\n\n- Allows **any two computers** to transfer data (using a relay)\n- Provides **end-to-end encryption** (using PAKE)\n- Enables easy **cross-platform** transfers (Windows, Linux, Mac)\n- Allows **multiple file** transfers\n- Allows **resuming transfers** that are interrupted\n- No need for local server or port-forwarding\n- **IPv6-first** with IPv4 fallback\n- Can **use a proxy**, like Tor\n\nFor more information about `croc`, see [my blog post](https://schollz.com/tinker/croc6/) or read a [recent interview I did](https://console.substack.com/p/console-91).\n\n![Example](src/install/customization.gif)\n\n## Install\n\nYou can download [the latest release for your system](https://github.com/schollz/croc/releases/latest), or install a release from the command-line:\n\n```bash\ncurl https://getcroc.schollz.com | bash\n```\n\n### On macOS\n\nUsing [Homebrew](https://brew.sh/):\n\n```bash\nbrew install croc\n```\n\nUsing [MacPorts](https://www.macports.org/):\n\n```bash\nsudo port selfupdate\nsudo port install croc\n```\n\n### On Windows\n\nYou can install the latest release with [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), or [Winget](https://learn.microsoft.com/windows/package-manager/):\n\n```bash\nscoop install croc\n```\n\n```bash\nchoco install croc\n```\n\n```bash\nwinget install schollz.croc\n```\n\n### Using nix-env\n\nYou can install the latest release with [Nix](https://nixos.org/):\n\n```bash\nnix-env -i croc\n```\n\n### On NixOS\n\nYou can add this to your [configuration.nix](https://nixos.org/manual/nixos/stable/#ch-configuration):\n\n```nix\nenvironment.systemPackages = [\n  pkgs.croc\n];\n```\n\n### On Alpine Linux\n\nFirst, install dependencies:\n\n```bash\napk add bash coreutils\nwget -qO- https://getcroc.schollz.com | bash\n```\n\n### On Arch Linux\n\nInstall with `pacman`:\n\n```bash\npacman -S croc\n```\n\n### On Fedora\n\nInstall with `dnf`:\n\n```bash\ndnf install croc\n```\n\n### On Gentoo\n\nInstall with `portage`:\n\n```bash\nemerge net-misc/croc\n```\n\n### On Termux\n\nInstall with `pkg`:\n\n```bash\npkg install croc\n```\n\n### On FreeBSD\n\nInstall with `pkg`:\n\n```bash\npkg install croc\n```\n\n### On Linux, macOS, and Windows via Conda\n\nYou can install from [conda-forge](https://github.com/conda-forge/croc-feedstock) globally with [`pixi`](https://pixi.sh/):\n\n```bash\npixi global install croc\n```\n\nOr install into a particular environment with [`conda`](https://docs.conda.io/projects/conda/):\n\n```bash\nconda install --channel conda-forge croc\n```\n\n### On Linux, macOS via Docker \n\nAdd the following one-liner function to your ~/.profile (works with any POSIX-compliant shell):\n\n```bash\ncroc() { [ $# -eq 0 ] && set -- \"\"; mkdir -p \"$HOME/.config/croc\"; docker run --rm -it --user \"$(id -u):$(id -g)\" -v \"$(pwd):/c\" -v \"$HOME/.config/croc:/.config/croc\" -w /c -e CROC_SECRET docker.io/schollz/croc \"$@\"; }\n```\n\nYou can also just paste it in the terminal for current session. On first run Docker will pull the image. `croc` via Docker will only work within the current directory and its subdirectories.\n\n### Build from Source\n\nIf you prefer, you can [install Go](https://go.dev/dl/) and build from source (requires Go 1.22+):\n\n```bash\ngo install github.com/schollz/croc/v10@latest\n```\n\n### On Android\n\nThere is a 3rd-party F-Droid app [available to download](https://f-droid.org/packages/com.github.howeyc.crocgui/).\n\n## Usage\n\nTo send a file, simply do:\n\n```bash\n$ croc send [file(s)-or-folder]\nSending 'file-or-folder' (X MB)\nCode is: code-phrase\n```\n\nThen, to receive the file (or folder) on another computer, run:\n\n```bash\ncroc code-phrase\n```\n\nThe code phrase is used to establish password-authenticated key agreement ([PAKE](https://en.wikipedia.org/wiki/Password-authenticated_key_agreement)) which generates a secret key for the sender and recipient to use for end-to-end encryption.\n\n### Customizations & Options\n\n#### Using `croc` on Linux or macOS\n\nOn Linux and macOS, the sending and receiving process is slightly different to avoid [leaking the secret via the process name](https://nvd.nist.gov/vuln/detail/CVE-2023-43621). You will need to run `croc` with the secret as an environment variable. For example, to receive with the secret `***`:\n\n```bash\nCROC_SECRET=*** croc\n```\n\nFor single-user systems, the default behavior can be permanently enabled by running:\n\n```bash\ncroc --classic\n```\n\n#### Custom Code Phrase\n\nYou can send with your own code phrase (must be more than 6 characters):\n\n```bash\ncroc send --code [code-phrase] [file(s)-or-folder]\n```\n\n#### Allow Overwriting Without Prompt\n\nTo automatically overwrite files without prompting, use the `--overwrite` flag:\n\n```bash\ncroc --yes --overwrite <code>\n```\n\n#### Excluding Folders\n\nTo exclude folders from being sent, use the `--exclude` flag with comma-delimited exclusions:\n\n```bash\ncroc send --exclude \"node_modules,.venv\" [folder]\n```\n\n#### Use Pipes - stdin and stdout\n\nYou can pipe to `croc`:\n\n```bash\ncat [filename] | croc send\n```\n\nTo receive the file to `stdout`, you can use:\n\n```bash\ncroc --yes [code-phrase] > out\n```\n\n#### Send Text\n\nTo send URLs or short text, use:\n\n```bash\ncroc send --text \"hello world\"\n```\n\n#### Send Multiple Files\n\nYou can send multiple files directly by listing the files and/or folders:\n\n```bash\ncroc send [file1] [file2] [file3] [folder1] [folder2]\n```\n\n#### Show QR Code\n\nTo show QR code (for mobile devices), use:\n\n```bash\ncroc send --qr [file(s)-or-folder]\n```\n\n#### Use a Proxy\n\nYou can send files via a proxy by adding `--socks5`:\n\n```bash\ncroc --socks5 \"127.0.0.1:9050\" send SOMEFILE\n```\n\n#### Change Encryption Curve\n\nTo choose a different elliptic curve for encryption, use the `--curve` flag:\n\n```bash\ncroc --curve p521 <codephrase>\n```\n\n#### Change Hash Algorithm\n\nFor faster hashing, use the `imohash` algorithm:\n\n```bash\ncroc send --hash imohash SOMEFILE\n```\n\n#### Clipboard Options\n\nBy default, the code phrase is copied to your clipboard. To disable this:\n\n```bash\ncroc --disable-clipboard send [filename]\n```\n\nTo copy the full command with the secret as an environment variable (useful on Linux/macOS):\n\n```bash\ncroc --extended-clipboard send [filename]\n```\n\nThis copies the full command like `CROC_SECRET=\"code-phrase\" croc` (including any relay/pass flags).\n\n#### Quiet Mode\n\nTo suppress all output (useful for scripts and automation):\n\n```bash\ncroc --quiet send [filename]\n```\n\n#### Self-host Relay\n\nYou can run your own relay:\n\n```bash\ncroc relay\n```\n\nBy default, it uses TCP ports 9009-9013. You can customize the ports (e.g., `croc relay --ports 1111,1112`), but at least **2** ports are required.\n\nTo send files using your relay:\n\n```bash\ncroc --relay \"myrelay.example.com:9009\" send [filename]\n```\n\n#### Self-host Relay with Docker\n\nYou can also run a relay with Docker:\n\n```bash\ndocker run -d -p 9009-9013:9009-9013 -e CROC_PASS='YOURPASSWORD' docker.io/schollz/croc\n```\n\nTo send files using your custom relay:\n\n```bash\ncroc --pass YOURPASSWORD --relay \"myreal.example.com:9009\" send [filename]\n```\n\n## Acknowledgements\n\n`croc` has evolved through many iterations, and I am thankful for the contributions! Special thanks to:\n\n- [@warner](https://github.com/warner) for the [idea](https://github.com/magic-wormhole/magic-wormhole)\n- [@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28)\n- [@skorokithakis](https://github.com/skorokithakis) for [proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/)\n\nAnd many more!\n"
  },
  {
    "path": "croc-entrypoint.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ -n \"$CROC_PASS\" ]; then\n    set -- --pass \"$CROC_PASS\" \"$@\"\nfi\n\nexec /croc \"$@\"\n"
  },
  {
    "path": "croc.service",
    "content": "[Unit]\nDescription=croc relay\nAfter=network.target\n\n[Service]\nType=simple\nDynamicUser=yes\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\nExecStart=/usr/bin/croc relay\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/schollz/croc/v10\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0\n\tgithub.com/chzyer/readline v1.5.1\n\tgithub.com/denisbrodbeck/machineid v1.0.1\n\tgithub.com/kalafut/imohash v1.1.1\n\tgithub.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b\n\tgithub.com/minio/highwayhash v1.0.3\n\tgithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06\n\tgithub.com/schollz/cli/v2 v2.2.1\n\tgithub.com/schollz/logger v1.2.0\n\tgithub.com/schollz/pake/v3 v3.1.1\n\tgithub.com/schollz/peerdiscovery v1.7.6\n\tgithub.com/schollz/progressbar/v3 v3.19.0\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/crypto v0.48.0\n\tgolang.org/x/net v0.51.0\n\tgolang.org/x/sys v0.41.0\n\tgolang.org/x/term v0.40.0\n\tgolang.org/x/time v0.14.0\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 // indirect\n\tgithub.com/twmb/murmur3 v1.1.8 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\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/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=\ngithub.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=\ngithub.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=\ngithub.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kalafut/imohash v1.1.1 h1:G/HYtKgteQSVU96LidSJEbUGoZOMiBcuXYxbeb2W9e4=\ngithub.com/kalafut/imohash v1.1.1/go.mod h1:6cn9lU0Sj8M4eu9UaQm1kR/5y3k/ayB68yntRhGloL4=\ngithub.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=\ngithub.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=\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/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=\ngithub.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=\ngithub.com/schollz/cli/v2 v2.2.1 h1:ou22Mj7ZPjrKz+8k2iDTWaHskEEV5NiAxGrdsCL36VU=\ngithub.com/schollz/cli/v2 v2.2.1/go.mod h1:My6bfphRLZUhZdlFUK8scAxMWHydE7k4s2ed2Dtnn+s=\ngithub.com/schollz/logger v1.2.0 h1:5WXfINRs3lEUTCZ7YXhj0uN+qukjizvITLm3Ca2m0Ho=\ngithub.com/schollz/logger v1.2.0/go.mod h1:P6F4/dGMGcx8wh+kG1zrNEd4vnNpEBY/mwEMd/vn6AM=\ngithub.com/schollz/pake/v3 v3.1.1 h1:lyoU5uNQ3thfjEzrahgxWWBm6+pbI1F2KAZ3gs6LIV8=\ngithub.com/schollz/pake/v3 v3.1.1/go.mod h1:420+m3AakXcS0n7Uwc7eRs2CosQ2YfE/vKcIkilvqZc=\ngithub.com/schollz/peerdiscovery v1.7.6 h1:HJjU1cXcNGfZgenC/vbry9F6CH9B8f+QYcTipZLbtDg=\ngithub.com/schollz/peerdiscovery v1.7.6/go.mod h1:iTa0MWSPy49jJ2HcXL5oSSnFsd6olEUorAFljxbnj2I=\ngithub.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=\ngithub.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 h1:sDWDZkwYqX0jvLWstKzFwh+pYhQNaVg65BgSkCP/f7U=\ngithub.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=\ngithub.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=\ngithub.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=\ngithub.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\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=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\n//go:generate go run src/install/updateversion.go\n//go:generate git commit -am \"bump $VERSION\"\n//go:generate git tag -af v$VERSION -m \"v$VERSION\"\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/schollz/croc/v10/src/cli\"\n\t\"github.com/schollz/croc/v10/src/utils\"\n)\n\nfunc main() {\n\t// \"github.com/pkg/profile\"\n\t// go func() {\n\t// \tfor {\n\t// \t\tf, err := os.Create(\"croc.pprof\")\n\t// \t\tif err != nil {\n\t// \t\t\tpanic(err)\n\t// \t\t}\n\t// \t\truntime.GC() // get up-to-date statistics\n\t// \t\tif err := pprof.WriteHeapProfile(f); err != nil {\n\t// \t\t\tpanic(err)\n\t// \t\t}\n\t// \t\tf.Close()\n\t// \t\ttime.Sleep(3 * time.Second)\n\t// \t\tfmt.Println(\"wrote profile\")\n\t// \t}\n\t// }()\n\n\t// Create a channel to receive OS signals\n\tsigs := make(chan os.Signal, 1)\n\tsignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)\n\n\tgo func() {\n\t\tif err := cli.Run(); err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\t// Exit the program gracefully\n\t\tutils.RemoveMarkedFiles()\n\t\tos.Exit(0)\n\t}()\n\n\t// Wait for a termination signal\n\t<-sigs\n\tutils.RemoveMarkedFiles()\n\n\t// Exit the program gracefully\n\tos.Exit(0)\n}\n"
  },
  {
    "path": "src/cli/cli.go",
    "content": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/chzyer/readline\"\n\t\"github.com/schollz/cli/v2\"\n\t\"github.com/schollz/croc/v10/src/comm\"\n\t\"github.com/schollz/croc/v10/src/croc\"\n\t\"github.com/schollz/croc/v10/src/mnemonicode\"\n\t\"github.com/schollz/croc/v10/src/models\"\n\t\"github.com/schollz/croc/v10/src/tcp\"\n\t\"github.com/schollz/croc/v10/src/utils\"\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/schollz/pake/v3\"\n)\n\n// Version specifies the version\nvar Version string\n\n// Run will run the command line program\nfunc Run() (err error) {\n\t// use all of the processors\n\truntime.GOMAXPROCS(runtime.NumCPU())\n\n\tapp := cli.NewApp()\n\tapp.Name = \"croc\"\n\tif Version == \"\" {\n\t\tVersion = \"v10.4.2\"\n\t}\n\tapp.Version = Version\n\tapp.Compiled = time.Now()\n\tapp.Usage = \"easily and securely transfer stuff from one computer to another\"\n\tapp.UsageText = `croc [GLOBAL OPTIONS] [COMMAND] [COMMAND OPTIONS] [filename(s) or folder]\n\n   USAGE EXAMPLES:\n   Send a file:\n      croc send file.txt\n\n      -git to respect your .gitignore\n   Send multiple files:\n      croc send file1.txt file2.txt file3.txt\n    or\n      croc send *.jpg\n\n   Send everything in a folder:\n      croc send example-folder-name\n\n   Send a file with a custom code:\n      croc send --code secret-code file.txt\n\n   Receive a file using code:\n      croc secret-code`\n\tapp.Commands = []*cli.Command{\n\t\t{\n\t\t\tName:        \"send\",\n\t\t\tUsage:       \"send file(s), or folder (see options with croc send -h)\",\n\t\t\tDescription: \"send file(s), or folder, over the relay\",\n\t\t\tArgsUsage:   \"[filename(s) or folder]\",\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{Name: \"zip\", Usage: \"zip folder before sending\"},\n\t\t\t\t&cli.StringFlag{Name: \"code\", Aliases: []string{\"c\"}, Usage: \"codephrase used to connect to relay\"},\n\t\t\t\t&cli.StringFlag{Name: \"hash\", Value: \"xxhash\", Usage: \"hash algorithm (xxhash, imohash, md5)\"},\n\t\t\t\t&cli.StringFlag{Name: \"text\", Aliases: []string{\"t\"}, Usage: \"send some text\"},\n\t\t\t\t&cli.BoolFlag{Name: \"no-local\", Usage: \"disable local relay when sending\"},\n\t\t\t\t&cli.BoolFlag{Name: \"no-multi\", Usage: \"disable multiplexing\"},\n\t\t\t\t&cli.BoolFlag{Name: \"git\", Usage: \"enable .gitignore respect / don't send ignored files\"},\n\t\t\t\t&cli.IntFlag{Name: \"port\", Value: 9009, Usage: \"base port for the relay\"},\n\t\t\t\t&cli.IntFlag{Name: \"transfers\", Value: 4, Usage: \"number of ports to use for transfers\"},\n\t\t\t\t&cli.BoolFlag{Name: \"qrcode\", Aliases: []string{\"qr\"}, Usage: \"show receive code as a qrcode\"},\n\t\t\t\t&cli.StringFlag{Name: \"exclude\", Value: \"\", Usage: \"exclude files if they contain any of the comma separated strings\"},\n\t\t\t\t&cli.StringFlag{Name: \"socks5\", Value: \"\", Usage: \"add a socks5 proxy\", EnvVars: []string{\"SOCKS5_PROXY\"}},\n\t\t\t\t&cli.StringFlag{Name: \"connect\", Value: \"\", Usage: \"add a http proxy\", EnvVars: []string{\"HTTP_PROXY\"}},\n\t\t\t},\n\t\t\tHelpName: \"croc send\",\n\t\t\tAction:   send,\n\t\t},\n\t\t{\n\t\t\tName:        \"relay\",\n\t\t\tUsage:       \"start your own relay (optional)\",\n\t\t\tDescription: \"start relay\",\n\t\t\tHelpName:    \"croc relay\",\n\t\t\tAction:      relay,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{Name: \"host\", Usage: \"host of the relay\"},\n\t\t\t\t&cli.StringFlag{Name: \"ports\", Value: \"9009,9010,9011,9012,9013\", Usage: \"ports of the relay\"},\n\t\t\t\t&cli.IntFlag{Name: \"port\", Value: 9009, Usage: \"base port for the relay\"},\n\t\t\t\t&cli.IntFlag{Name: \"transfers\", Value: 5, Usage: \"number of ports to use for relay\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"generate-fish-completion\",\n\t\t\tUsage:  \"generate fish completion and output to stdout\",\n\t\t\tHidden: true,\n\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\tcompletion, err := ctx.App.ToFishCompletion()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Print(completion)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t}\n\tapp.Flags = []cli.Flag{\n\t\t&cli.BoolFlag{Name: \"internal-dns\", Usage: \"use a built-in DNS stub resolver rather than the host operating system\"},\n\t\t&cli.BoolFlag{Name: \"classic\", Usage: \"toggle between the classic mode (insecure due to local attack vector) and new mode (secure)\"},\n\t\t&cli.BoolFlag{Name: \"remember\", Usage: \"save these settings to reuse next time\"},\n\t\t&cli.BoolFlag{Name: \"debug\", Usage: \"toggle debug mode\"},\n\t\t&cli.BoolFlag{Name: \"yes\", Usage: \"automatically agree to all prompts\"},\n\t\t&cli.BoolFlag{Name: \"stdout\", Usage: \"redirect file to stdout\"},\n\t\t&cli.BoolFlag{Name: \"no-compress\", Usage: \"disable compression\"},\n\t\t&cli.BoolFlag{Name: \"ask\", Usage: \"make sure sender and recipient are prompted\"},\n\t\t&cli.BoolFlag{Name: \"local\", Usage: \"force to use only local connections\"},\n\t\t&cli.BoolFlag{Name: \"ignore-stdin\", Usage: \"ignore piped stdin\"},\n\t\t&cli.BoolFlag{Name: \"overwrite\", Usage: \"do not prompt to overwrite or resume\"},\n\t\t&cli.BoolFlag{Name: \"testing\", Usage: \"flag for testing purposes\"},\n\t\t&cli.BoolFlag{Name: \"quiet\", Usage: \"disable all output\"},\n\t\t&cli.BoolFlag{Name: \"disable-clipboard\", Usage: \"disable copy to clipboard\"},\n\t\t&cli.BoolFlag{Name: \"extended-clipboard\", Usage: \"copy full command with secret as env variable to clipboard\"},\n\t\t&cli.StringFlag{Name: \"multicast\", Value: \"239.255.255.250\", Usage: \"multicast address to use for local discovery\"},\n\t\t&cli.StringFlag{Name: \"curve\", Value: \"p256\", Usage: \"choose an encryption curve (\" + strings.Join(pake.AvailableCurves(), \", \") + \")\"},\n\t\t&cli.StringFlag{Name: \"ip\", Value: \"\", Usage: \"set sender ip if known e.g. 10.0.0.1:9009, [::1]:9009\"},\n\t\t&cli.StringFlag{Name: \"relay\", Value: models.DEFAULT_RELAY, Usage: \"address of the relay\", EnvVars: []string{\"CROC_RELAY\"}},\n\t\t&cli.StringFlag{Name: \"relay6\", Value: models.DEFAULT_RELAY6, Usage: \"ipv6 address of the relay\", EnvVars: []string{\"CROC_RELAY6\"}},\n\t\t&cli.StringFlag{Name: \"out\", Value: \".\", Usage: \"specify an output folder to receive the file\"},\n\t\t&cli.StringFlag{Name: \"pass\", Value: models.DEFAULT_PASSPHRASE, Usage: \"password for the relay\", EnvVars: []string{\"CROC_PASS\"}},\n\t\t&cli.StringFlag{Name: \"socks5\", Value: \"\", Usage: \"add a socks5 proxy\", EnvVars: []string{\"SOCKS5_PROXY\"}},\n\t\t&cli.StringFlag{Name: \"connect\", Value: \"\", Usage: \"add a http proxy\", EnvVars: []string{\"HTTP_PROXY\"}},\n\t\t&cli.StringFlag{Name: \"throttleUpload\", Value: \"\", Usage: \"throttle the upload speed e.g. 500k\"},\n\t}\n\tapp.EnableBashCompletion = true\n\tapp.HideHelp = false\n\tapp.HideVersion = false\n\tapp.Action = func(c *cli.Context) error {\n\t\tallStringsAreFiles := func(strs []string) bool {\n\t\t\tfor _, str := range strs {\n\t\t\t\tif !utils.Exists(str) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\n\t\t// check if \"classic\" is set\n\t\tclassicFile := getClassicConfigFile(true)\n\t\tclassicInsecureMode := utils.Exists(classicFile)\n\t\tif c.Bool(\"classic\") {\n\t\t\tif classicInsecureMode {\n\t\t\t\t// classic mode not enabled\n\t\t\t\tfmt.Print(`Classic mode is currently ENABLED.\n\nDisabling this mode will prevent the shared secret from being visible\non the host's process list when passed via the command line. On a\nmulti-user system, this will help ensure that other local users cannot\naccess the shared secret and receive the files instead of the intended\nrecipient.\n\nDo you wish to continue to DISABLE the classic mode? (y/N) `)\n\t\t\t\tchoice := strings.ToLower(utils.GetInput(\"\"))\n\t\t\t\tif choice == \"y\" || choice == \"yes\" {\n\t\t\t\t\tos.Remove(classicFile)\n\t\t\t\t\tfmt.Print(\"\\nClassic mode DISABLED.\\n\\n\")\n\t\t\t\t\tfmt.Print(`To send and receive, export the CROC_SECRET variable with the code phrase:\n\n  Send:    CROC_SECRET=*** croc send file.txt\n\n  Receive: CROC_SECRET=*** croc` + \"\\n\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Print(\"\\nClassic mode ENABLED.\\n\")\n\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// enable classic mode\n\t\t\t\t// touch the file\n\t\t\t\tfmt.Print(`Classic mode is currently DISABLED.\n\nPlease note that enabling this mode will make the shared secret visible\non the host's process list when passed via the command line. On a\nmulti-user system, this could allow other local users to access the\nshared secret and receive the files instead of the intended recipient.\n\nDo you wish to continue to enable the classic mode? (y/N) `)\n\t\t\t\tchoice := strings.ToLower(utils.GetInput(\"\"))\n\t\t\t\tif choice == \"y\" || choice == \"yes\" {\n\t\t\t\t\tfmt.Print(\"\\nClassic mode ENABLED.\\n\\n\")\n\t\t\t\t\tos.WriteFile(classicFile, []byte(\"enabled\"), 0o644)\n\t\t\t\t\tfmt.Print(`To send and receive, use the code phrase:\n\n  Send:    croc send --code *** file.txt\n\n  Receive: croc ***` + \"\\n\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Print(\"\\nClassic mode DISABLED.\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\t// if trying to send but forgot send, let the user know\n\t\tif c.Args().Present() && allStringsAreFiles(c.Args().Slice()) {\n\t\t\tfnames := []string{}\n\t\t\tfor _, fpath := range c.Args().Slice() {\n\t\t\t\t_, basename := filepath.Split(fpath)\n\t\t\t\tfnames = append(fnames, \"'\"+basename+\"'\")\n\t\t\t}\n\t\t\tpromptMessage := fmt.Sprintf(\"Did you mean to send %s? (Y/n) \", strings.Join(fnames, \", \"))\n\t\t\tchoice := strings.ToLower(utils.GetInput(promptMessage))\n\t\t\tif choice == \"\" || choice == \"y\" || choice == \"yes\" {\n\t\t\t\treturn send(c)\n\t\t\t}\n\t\t}\n\n\t\treturn receive(c)\n\t}\n\n\treturn app.Run(os.Args)\n}\n\nfunc setDebugLevel(c *cli.Context) {\n\tif c.Bool(\"quiet\") {\n\t\tlog.SetLevel(\"error\")\n\t} else if c.Bool(\"debug\") {\n\t\tlog.SetLevel(\"debug\")\n\t\tlog.Debug(\"debug mode on\")\n\t\t// print the public IP address\n\t\tip, err := utils.PublicIP()\n\t\tif err == nil {\n\t\t\tlog.Debugf(\"public IP address: %s\", ip)\n\t\t} else {\n\t\t\tlog.Debug(err)\n\t\t}\n\n\t} else {\n\t\tlog.SetLevel(\"info\")\n\t}\n}\n\nfunc getSendConfigFile(requireValidPath bool) string {\n\tconfigFile, err := utils.GetConfigDir(requireValidPath)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn \"\"\n\t}\n\treturn path.Join(configFile, \"send.json\")\n}\n\nfunc getClassicConfigFile(requireValidPath bool) string {\n\tconfigFile, err := utils.GetConfigDir(requireValidPath)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn \"\"\n\t}\n\treturn path.Join(configFile, \"classic_enabled\")\n}\n\nfunc getReceiveConfigFile(requireValidPath bool) (string, error) {\n\tconfigFile, err := utils.GetConfigDir(requireValidPath)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn \"\", err\n\t}\n\treturn path.Join(configFile, \"receive.json\"), nil\n}\n\nfunc determinePass(c *cli.Context) (pass string) {\n\tpass = c.String(\"pass\")\n\tb, err := os.ReadFile(pass)\n\tif err == nil {\n\t\tpass = strings.TrimSpace(string(b))\n\t}\n\treturn\n}\n\nfunc send(c *cli.Context) (err error) {\n\tsetDebugLevel(c)\n\tcomm.Socks5Proxy = c.String(\"socks5\")\n\tcomm.HttpProxy = c.String(\"connect\")\n\n\tportParam := c.Int(\"port\")\n\tif portParam == 0 {\n\t\tportParam = 9009\n\t}\n\ttransfersParam := c.Int(\"transfers\")\n\tif transfersParam == 0 {\n\t\ttransfersParam = 4\n\t}\n\texcludeStrings := []string{}\n\tfor _, v := range strings.Split(c.String(\"exclude\"), \",\") {\n\t\tv = strings.ToLower(strings.TrimSpace(v))\n\t\tif v != \"\" {\n\t\t\texcludeStrings = append(excludeStrings, v)\n\t\t}\n\t}\n\n\tports := make([]string, transfersParam+1)\n\tfor i := 0; i <= transfersParam; i++ {\n\t\tports[i] = strconv.Itoa(portParam + i)\n\t}\n\n\tcrocOptions := croc.Options{\n\t\tSharedSecret:      c.String(\"code\"),\n\t\tIsSender:          true,\n\t\tDebug:             c.Bool(\"debug\"),\n\t\tNoPrompt:          c.Bool(\"yes\"),\n\t\tRelayAddress:      c.String(\"relay\"),\n\t\tRelayAddress6:     c.String(\"relay6\"),\n\t\tStdout:            c.Bool(\"stdout\"),\n\t\tDisableLocal:      c.Bool(\"no-local\"),\n\t\tOnlyLocal:         c.Bool(\"local\"),\n\t\tIgnoreStdin:       c.Bool(\"ignore-stdin\"),\n\t\tRelayPorts:        ports,\n\t\tAsk:               c.Bool(\"ask\"),\n\t\tNoMultiplexing:    c.Bool(\"no-multi\"),\n\t\tRelayPassword:     determinePass(c),\n\t\tSendingText:       c.String(\"text\") != \"\",\n\t\tNoCompress:        c.Bool(\"no-compress\"),\n\t\tOverwrite:         c.Bool(\"overwrite\"),\n\t\tCurve:             c.String(\"curve\"),\n\t\tHashAlgorithm:     c.String(\"hash\"),\n\t\tThrottleUpload:    c.String(\"throttleUpload\"),\n\t\tZipFolder:         c.Bool(\"zip\"),\n\t\tGitIgnore:         c.Bool(\"git\"),\n\t\tShowQrCode:        c.Bool(\"qrcode\"),\n\t\tMulticastAddress:  c.String(\"multicast\"),\n\t\tExclude:           excludeStrings,\n\t\tQuiet:             c.Bool(\"quiet\"),\n\t\tDisableClipboard:  c.Bool(\"disable-clipboard\"),\n\t\tExtendedClipboard: c.Bool(\"extended-clipboard\"),\n\t}\n\tif crocOptions.RelayAddress != models.DEFAULT_RELAY {\n\t\tcrocOptions.RelayAddress6 = \"\"\n\t} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {\n\t\tcrocOptions.RelayAddress = \"\"\n\t}\n\tb, errOpen := os.ReadFile(getSendConfigFile(false))\n\tif errOpen == nil && !c.Bool(\"remember\") {\n\t\tvar rememberedOptions croc.Options\n\t\terr = json.Unmarshal(b, &rememberedOptions)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\t// update anything that isn't explicitly set\n\t\tif !c.IsSet(\"no-local\") {\n\t\t\tcrocOptions.DisableLocal = rememberedOptions.DisableLocal\n\t\t}\n\t\tif !c.IsSet(\"ports\") && len(rememberedOptions.RelayPorts) > 0 {\n\t\t\tcrocOptions.RelayPorts = rememberedOptions.RelayPorts\n\t\t}\n\t\tif !c.IsSet(\"code\") {\n\t\t\tcrocOptions.SharedSecret = rememberedOptions.SharedSecret\n\t\t}\n\t\tif !c.IsSet(\"pass\") && rememberedOptions.RelayPassword != \"\" {\n\t\t\tcrocOptions.RelayPassword = rememberedOptions.RelayPassword\n\t\t}\n\t\tif !c.IsSet(\"overwrite\") {\n\t\t\tcrocOptions.Overwrite = rememberedOptions.Overwrite\n\t\t}\n\t\tif !c.IsSet(\"curve\") && rememberedOptions.Curve != \"\" {\n\t\t\tcrocOptions.Curve = rememberedOptions.Curve\n\t\t}\n\t\tif !c.IsSet(\"local\") {\n\t\t\tcrocOptions.OnlyLocal = rememberedOptions.OnlyLocal\n\t\t}\n\t\tif !c.IsSet(\"hash\") {\n\t\t\tcrocOptions.HashAlgorithm = rememberedOptions.HashAlgorithm\n\t\t}\n\t\tif !c.IsSet(\"git\") {\n\t\t\tcrocOptions.GitIgnore = rememberedOptions.GitIgnore\n\t\t}\n\t\tif !c.IsSet(\"relay\") && strings.HasPrefix(rememberedOptions.RelayAddress, \"non-default:\") {\n\t\t\tvar rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress, \"non-default:\")\n\t\t\trememberedAddr = strings.TrimSpace(rememberedAddr)\n\t\t\tcrocOptions.RelayAddress = rememberedAddr\n\t\t}\n\t\tif !c.IsSet(\"relay6\") && strings.HasPrefix(rememberedOptions.RelayAddress6, \"non-default:\") {\n\t\t\tvar rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress6, \"non-default:\")\n\t\t\trememberedAddr = strings.TrimSpace(rememberedAddr)\n\t\t\tcrocOptions.RelayAddress6 = rememberedAddr\n\t\t}\n\t}\n\n\tvar fnames []string\n\tstat, _ := os.Stdin.Stat()\n\tif ((stat.Mode() & os.ModeCharDevice) == 0) && !c.Bool(\"ignore-stdin\") {\n\t\tfnames, err = getStdin()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tutils.MarkFileForRemoval(fnames[0])\n\t\tdefer func() {\n\t\t\te := os.Remove(fnames[0])\n\t\t\tif e != nil {\n\t\t\t\tlog.Error(e)\n\t\t\t}\n\t\t}()\n\t} else if c.String(\"text\") != \"\" {\n\t\tfnames, err = makeTempFileWithString(c.String(\"text\"))\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tutils.MarkFileForRemoval(fnames[0])\n\t\tdefer func() {\n\t\t\te := os.Remove(fnames[0])\n\t\t\tif e != nil {\n\t\t\t\tlog.Error(e)\n\t\t\t}\n\t\t}()\n\n\t} else {\n\t\tfnames = c.Args().Slice()\n\t}\n\tif len(fnames) == 0 {\n\t\treturn errors.New(\"must specify file: croc send [filename(s) or folder]\")\n\t}\n\n\tclassicInsecureMode := utils.Exists(getClassicConfigFile(true))\n\tif !classicInsecureMode {\n\t\t// if operating system is UNIX, then use environmental variable to set the code\n\t\tif (!(runtime.GOOS == \"windows\") && c.IsSet(\"code\")) || os.Getenv(\"CROC_SECRET\") != \"\" {\n\t\t\tcrocOptions.SharedSecret = os.Getenv(\"CROC_SECRET\")\n\t\t\tif crocOptions.SharedSecret == \"\" {\n\t\t\t\tfmt.Printf(`On UNIX systems, to send with a custom code phrase,\nyou need to set the environmental variable CROC_SECRET:\n\n  CROC_SECRET=**** croc send file.txt\n\nOr you can have the code phrase automatically generated:\n\n  croc send file.txt\n\nOr you can go back to the classic croc behavior by enabling classic mode:\n\n  croc --classic\n\n`)\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(crocOptions.SharedSecret) == 0 {\n\t\t// generate code phrase\n\t\tcrocOptions.SharedSecret = utils.GetRandomName()\n\t}\n\tminimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfo(fnames, crocOptions.ZipFolder, crocOptions.GitIgnore, crocOptions.Exclude)\n\tif err != nil {\n\t\treturn\n\t}\n\tif len(crocOptions.Exclude) > 0 {\n\t\tminimalFileInfosInclude := []croc.FileInfo{}\n\t\temptyFoldersToTransferInclude := []croc.FileInfo{}\n\t\tfor _, f := range minimalFileInfos {\n\t\t\texclude := false\n\t\t\tfor _, exclusion := range crocOptions.Exclude {\n\t\t\t\tif strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) {\n\t\t\t\t\texclude = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !exclude {\n\t\t\t\tminimalFileInfosInclude = append(minimalFileInfosInclude, f)\n\t\t\t}\n\t\t}\n\t\tfor _, f := range emptyFoldersToTransfer {\n\t\t\texclude := false\n\t\t\tfor _, exclusion := range crocOptions.Exclude {\n\t\t\t\tif strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) {\n\t\t\t\t\texclude = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !exclude {\n\t\t\t\temptyFoldersToTransferInclude = append(emptyFoldersToTransferInclude, f)\n\t\t\t}\n\t\t}\n\t\ttotalNumberFolders = 0\n\t\tfolderMap := make(map[string]bool)\n\t\tfor _, f := range minimalFileInfosInclude {\n\t\t\tfolderMap[f.FolderRemote] = true\n\t\t}\n\t\tfor _, f := range emptyFoldersToTransferInclude {\n\t\t\tfolderMap[f.FolderRemote] = true\n\t\t}\n\t\ttotalNumberFolders = len(folderMap)\n\t\tminimalFileInfos = minimalFileInfosInclude\n\t\temptyFoldersToTransfer = emptyFoldersToTransferInclude\n\t}\n\n\tcr, err := croc.New(crocOptions)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// save the config\n\tsaveConfig(c, crocOptions)\n\terr = cr.Send(minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders)\n\treturn\n}\n\nfunc getStdin() (fnames []string, err error) {\n\tf, err := os.CreateTemp(\".\", \"croc-stdin-\")\n\tif err != nil {\n\t\treturn\n\t}\n\t_, err = io.Copy(f, os.Stdin)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = f.Close()\n\tif err != nil {\n\t\treturn\n\t}\n\tfnames = []string{f.Name()}\n\treturn\n}\n\nfunc makeTempFileWithString(s string) (fnames []string, err error) {\n\tf, err := os.CreateTemp(\".\", \"croc-stdin-\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\t_, err = f.WriteString(s)\n\tif err != nil {\n\t\treturn\n\t}\n\n\terr = f.Close()\n\tif err != nil {\n\t\treturn\n\t}\n\tfnames = []string{f.Name()}\n\treturn\n}\n\nfunc saveConfig(c *cli.Context, crocOptions croc.Options) {\n\tif c.Bool(\"remember\") {\n\t\tconfigFile := getSendConfigFile(true)\n\t\tlog.Debug(\"saving config file\")\n\t\tvar bConfig []byte\n\t\t// if the code wasn't set, don't save it\n\t\tif c.String(\"code\") == \"\" {\n\t\t\tcrocOptions.SharedSecret = \"\"\n\t\t}\n\t\tif c.String(\"relay\") != models.DEFAULT_RELAY {\n\t\t\tcrocOptions.RelayAddress = \"non-default: \" + c.String(\"relay\")\n\t\t} else {\n\t\t\tcrocOptions.RelayAddress = \"default\"\n\t\t}\n\t\tif c.String(\"relay6\") != models.DEFAULT_RELAY6 {\n\t\t\tcrocOptions.RelayAddress6 = \"non-default: \" + c.String(\"relay6\")\n\t\t} else {\n\t\t\tcrocOptions.RelayAddress6 = \"default\"\n\t\t}\n\t\tbConfig, err := json.MarshalIndent(crocOptions, \"\", \"    \")\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\terr = os.WriteFile(configFile, bConfig, 0o644)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\tlog.Debugf(\"wrote %s\", configFile)\n\t}\n}\n\ntype TabComplete struct{}\n\nfunc (t TabComplete) Do(line []rune, pos int) ([][]rune, int) {\n\tvar words = strings.SplitAfter(string(line), \"-\")\n\tvar lastPartialWord = words[len(words)-1]\n\tvar nbCharacter = len(lastPartialWord)\n\tif nbCharacter == 0 {\n\t\t// No completion\n\t\treturn [][]rune{[]rune(\"\")}, 0\n\t}\n\tif len(words) == 1 && nbCharacter == utils.NbPinNumbers {\n\t\t// Check if word is indeed a number\n\t\t_, err := strconv.Atoi(lastPartialWord)\n\t\tif err == nil {\n\t\t\treturn [][]rune{[]rune(\"-\")}, nbCharacter\n\t\t}\n\t}\n\tvar strArray [][]rune\n\tfor _, s := range mnemonicode.WordList {\n\t\tif strings.HasPrefix(s, lastPartialWord) {\n\t\t\tvar completionCandidate = s[nbCharacter:]\n\t\t\tif len(words) <= mnemonicode.WordsRequired(utils.NbBytesWords) {\n\t\t\t\tcompletionCandidate += \"-\"\n\t\t\t}\n\t\t\tstrArray = append(strArray, []rune(completionCandidate))\n\t\t}\n\t}\n\treturn strArray, nbCharacter\n}\n\nfunc receive(c *cli.Context) (err error) {\n\tcomm.Socks5Proxy = c.String(\"socks5\")\n\tcomm.HttpProxy = c.String(\"connect\")\n\tcrocOptions := croc.Options{\n\t\tSharedSecret:      c.String(\"code\"),\n\t\tIsSender:          false,\n\t\tDebug:             c.Bool(\"debug\"),\n\t\tNoPrompt:          c.Bool(\"yes\"),\n\t\tRelayAddress:      c.String(\"relay\"),\n\t\tRelayAddress6:     c.String(\"relay6\"),\n\t\tStdout:            c.Bool(\"stdout\"),\n\t\tAsk:               c.Bool(\"ask\"),\n\t\tRelayPassword:     determinePass(c),\n\t\tOnlyLocal:         c.Bool(\"local\"),\n\t\tIP:                c.String(\"ip\"),\n\t\tOverwrite:         c.Bool(\"overwrite\"),\n\t\tCurve:             c.String(\"curve\"),\n\t\tTestFlag:          c.Bool(\"testing\"),\n\t\tMulticastAddress:  c.String(\"multicast\"),\n\t\tQuiet:             c.Bool(\"quiet\"),\n\t\tDisableClipboard:  c.Bool(\"disable-clipboard\"),\n\t\tExtendedClipboard: c.Bool(\"extended-clipboard\"),\n\t}\n\tif crocOptions.RelayAddress != models.DEFAULT_RELAY {\n\t\tcrocOptions.RelayAddress6 = \"\"\n\t} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {\n\t\tcrocOptions.RelayAddress = \"\"\n\t}\n\n\tswitch c.Args().Len() {\n\tcase 1:\n\t\tcrocOptions.SharedSecret = c.Args().First()\n\tcase 3:\n\t\tfallthrough\n\tcase 4:\n\t\tvar phrase []string\n\t\tphrase = append(phrase, c.Args().First())\n\t\tphrase = append(phrase, c.Args().Tail()...)\n\t\tcrocOptions.SharedSecret = strings.Join(phrase, \"-\")\n\t}\n\n\t// load options here\n\tsetDebugLevel(c)\n\n\tdoRemember := c.Bool(\"remember\")\n\tconfigFile, err := getReceiveConfigFile(doRemember)\n\tif err != nil && doRemember {\n\t\treturn\n\t}\n\tb, errOpen := os.ReadFile(configFile)\n\tif errOpen == nil && !doRemember {\n\t\tvar rememberedOptions croc.Options\n\t\terr = json.Unmarshal(b, &rememberedOptions)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\t// update anything that isn't explicitly Globally set\n\t\tif !c.IsSet(\"yes\") {\n\t\t\tcrocOptions.NoPrompt = rememberedOptions.NoPrompt\n\t\t}\n\t\tif crocOptions.SharedSecret == \"\" {\n\t\t\tcrocOptions.SharedSecret = rememberedOptions.SharedSecret\n\t\t}\n\t\tif !c.IsSet(\"pass\") && rememberedOptions.RelayPassword != \"\" {\n\t\t\tcrocOptions.RelayPassword = rememberedOptions.RelayPassword\n\t\t}\n\t\tif !c.IsSet(\"overwrite\") {\n\t\t\tcrocOptions.Overwrite = rememberedOptions.Overwrite\n\t\t}\n\t\tif !c.IsSet(\"curve\") && rememberedOptions.Curve != \"\" {\n\t\t\tcrocOptions.Curve = rememberedOptions.Curve\n\t\t}\n\t\tif !c.IsSet(\"local\") {\n\t\t\tcrocOptions.OnlyLocal = rememberedOptions.OnlyLocal\n\t\t}\n\t\tif !c.IsSet(\"relay\") && strings.HasPrefix(rememberedOptions.RelayAddress, \"non-default:\") {\n\t\t\tvar rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress, \"non-default:\")\n\t\t\trememberedAddr = strings.TrimSpace(rememberedAddr)\n\t\t\tcrocOptions.RelayAddress = rememberedAddr\n\t\t}\n\t\tif !c.IsSet(\"relay6\") && strings.HasPrefix(rememberedOptions.RelayAddress6, \"non-default:\") {\n\t\t\tvar rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress6, \"non-default:\")\n\t\t\trememberedAddr = strings.TrimSpace(rememberedAddr)\n\t\t\tcrocOptions.RelayAddress6 = rememberedAddr\n\t\t}\n\t}\n\n\tclassicInsecureMode := utils.Exists(getClassicConfigFile(true))\n\tif crocOptions.SharedSecret == \"\" && os.Getenv(\"CROC_SECRET\") != \"\" {\n\t\tcrocOptions.SharedSecret = os.Getenv(\"CROC_SECRET\")\n\t} else if !(runtime.GOOS == \"windows\") && crocOptions.SharedSecret != \"\" && !classicInsecureMode {\n\t\tcrocOptions.SharedSecret = os.Getenv(\"CROC_SECRET\")\n\t\tif crocOptions.SharedSecret == \"\" {\n\t\t\tfmt.Printf(`On UNIX systems, to receive with croc you either need\nto set a code phrase using your environmental variables:\n\n  CROC_SECRET=**** croc\n\nOr you can specify the code phrase when you run croc without\ndeclaring the secret on the command line:\n\n  croc\n  Enter receive code: ****\n\nOr you can go back to the classic croc behavior by enabling classic mode:\n\n  croc --classic\n\n`)\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\tif crocOptions.SharedSecret == \"\" {\n\t\tl, err := readline.NewEx(&readline.Config{\n\t\t\tPrompt:       \"Enter receive code: \",\n\t\t\tAutoComplete: TabComplete{},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcrocOptions.SharedSecret, err = l.Readline()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif c.String(\"out\") != \"\" {\n\t\tif err = os.Chdir(c.String(\"out\")); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcr, err := croc.New(crocOptions)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// save the config\n\tif doRemember {\n\t\tlog.Debug(\"saving config file\")\n\t\tvar bConfig []byte\n\t\tif c.String(\"relay\") != models.DEFAULT_RELAY {\n\t\t\tcrocOptions.RelayAddress = \"non-default: \" + c.String(\"relay\")\n\t\t} else {\n\t\t\tcrocOptions.RelayAddress = \"default\"\n\t\t}\n\t\tif c.String(\"relay6\") != models.DEFAULT_RELAY6 {\n\t\t\tcrocOptions.RelayAddress6 = \"non-default: \" + c.String(\"relay6\")\n\t\t} else {\n\t\t\tcrocOptions.RelayAddress6 = \"default\"\n\t\t}\n\t\tbConfig, err = json.MarshalIndent(crocOptions, \"\", \"    \")\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\terr = os.WriteFile(configFile, bConfig, 0o644)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\tlog.Debugf(\"wrote %s\", configFile)\n\t}\n\n\terr = cr.Receive()\n\treturn\n}\n\nfunc relay(c *cli.Context) (err error) {\n\tlog.Infof(\"starting croc relay version %v\", Version)\n\tdebugString := \"info\"\n\tif c.Bool(\"debug\") {\n\t\tdebugString = \"debug\"\n\t}\n\thost := c.String(\"host\")\n\tvar ports []string\n\n\tif c.IsSet(\"ports\") {\n\t\tports = strings.Split(c.String(\"ports\"), \",\")\n\t} else {\n\t\tportString := c.Int(\"port\")\n\t\tif portString == 0 {\n\t\t\tportString = 9009\n\t\t}\n\t\ttransfersString := c.Int(\"transfers\")\n\t\tif transfersString == 0 {\n\t\t\ttransfersString = 4\n\t\t}\n\t\tports = make([]string, transfersString)\n\t\tfor i := range ports {\n\t\t\tports[i] = strconv.Itoa(portString + i)\n\t\t}\n\t}\n\tif len(ports) < 2 {\n\t\treturn fmt.Errorf(\"relay requires at least two ports; specify --ports with two or more ports or set --transfers to 2+\")\n\t}\n\n\ttcpPorts := strings.Join(ports[1:], \",\")\n\tfor i, port := range ports {\n\t\tif i == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tgo func(portStr string) {\n\t\t\terr := tcp.Run(debugString, host, portStr, determinePass(c))\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}(port)\n\t}\n\treturn tcp.Run(debugString, host, ports[0], determinePass(c), tcpPorts)\n}\n"
  },
  {
    "path": "src/comm/comm.go",
    "content": "package comm\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/magisterquis/connectproxy\"\n\t\"github.com/schollz/croc/v10/src/utils\"\n\tlog \"github.com/schollz/logger\"\n\t\"golang.org/x/net/proxy\"\n)\n\nvar Socks5Proxy = \"\"\nvar HttpProxy = \"\"\n\nvar MAGIC_BYTES = []byte(\"croc\")\n\nconst maxReadMessageSize = 64 * 1024 * 1024\n\n// Comm is some basic TCP communication\ntype Comm struct {\n\tconnection net.Conn\n}\n\n// NewConnection gets a new comm to a tcp address\nfunc NewConnection(address string, timelimit ...time.Duration) (c *Comm, err error) {\n\ttlimit := 30 * time.Second\n\tif len(timelimit) > 0 {\n\t\ttlimit = timelimit[0]\n\t}\n\tvar connection net.Conn\n\tif Socks5Proxy != \"\" && !utils.IsLocalIP(address) {\n\t\tvar dialer proxy.Dialer\n\t\t// prepend schema if no schema is given\n\t\tif !strings.Contains(Socks5Proxy, `://`) {\n\t\t\tSocks5Proxy = `socks5://` + Socks5Proxy\n\t\t}\n\t\tsocks5ProxyURL, urlParseError := url.Parse(Socks5Proxy)\n\t\tif urlParseError != nil {\n\t\t\terr = fmt.Errorf(\"unable to parse socks proxy url: %s\", urlParseError)\n\t\t\tlog.Debug(err)\n\t\t\treturn\n\t\t}\n\t\tdialer, err = proxy.FromURL(socks5ProxyURL, proxy.Direct)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"proxy failed: %w\", err)\n\t\t\tlog.Debug(err)\n\t\t\treturn\n\t\t}\n\t\tlog.Debug(\"dialing with dialer.Dial\")\n\t\tconnection, err = dialer.Dial(\"tcp\", address)\n\t} else if HttpProxy != \"\" && !utils.IsLocalIP(address) {\n\t\tvar dialer proxy.Dialer\n\t\t// prepend schema if no schema is given\n\t\tif !strings.Contains(HttpProxy, `://`) {\n\t\t\tHttpProxy = `http://` + HttpProxy\n\t\t}\n\t\tHttpProxyURL, urlParseError := url.Parse(HttpProxy)\n\t\tif urlParseError != nil {\n\t\t\terr = fmt.Errorf(\"unable to parse http proxy url: %s\", urlParseError)\n\t\t\tlog.Debug(err)\n\t\t\treturn\n\t\t}\n\t\tdialer, err = connectproxy.New(HttpProxyURL, proxy.Direct)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"proxy failed: %w\", err)\n\t\t\tlog.Debug(err)\n\t\t\treturn\n\t\t}\n\t\tlog.Debug(\"dialing with dialer.Dial\")\n\t\tconnection, err = dialer.Dial(\"tcp\", address)\n\n\t} else {\n\t\tlog.Debugf(\"dialing to %s with timelimit %s\", address, tlimit)\n\t\tconnection, err = net.DialTimeout(\"tcp\", address, tlimit)\n\t}\n\tif err != nil {\n\t\terr = fmt.Errorf(\"comm.NewConnection failed: %w\", err)\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tc = New(connection)\n\tlog.Debugf(\"connected to '%s'\", address)\n\treturn\n}\n\n// New returns a new comm\nfunc New(c net.Conn) *Comm {\n\tif err := c.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {\n\t\tlog.Warnf(\"error setting read deadline: %v\", err)\n\t}\n\tif err := c.SetDeadline(time.Now().Add(3 * time.Hour)); err != nil {\n\t\tlog.Warnf(\"error setting overall deadline: %v\", err)\n\t}\n\tif err := c.SetWriteDeadline(time.Now().Add(3 * time.Hour)); err != nil {\n\t\tlog.Errorf(\"error setting write deadline: %v\", err)\n\t}\n\tcomm := new(Comm)\n\tcomm.connection = c\n\treturn comm\n}\n\n// Connection returns the net.Conn connection\nfunc (c *Comm) Connection() net.Conn {\n\treturn c.connection\n}\n\n// Close closes the connection\nfunc (c *Comm) Close() {\n\tif err := c.connection.Close(); err != nil {\n\t\tlog.Warnf(\"error closing connection: %v\", err)\n\t}\n}\n\nfunc (c *Comm) Write(b []byte) (n int, err error) {\n\theader := new(bytes.Buffer)\n\terr = binary.Write(header, binary.LittleEndian, uint32(len(b)))\n\tif err != nil {\n\t\tfmt.Println(\"binary.Write failed:\", err)\n\t}\n\ttmpCopy := append(header.Bytes(), b...)\n\ttmpCopy = append(MAGIC_BYTES, tmpCopy...)\n\tn, err = c.connection.Write(tmpCopy)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"connection.Write failed: %w\", err)\n\t\treturn\n\t}\n\tif n != len(tmpCopy) {\n\t\terr = fmt.Errorf(\"wanted to write %d but wrote %d\", len(b), n)\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {\n\t// long read deadline in case waiting for file\n\tif err = c.connection.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {\n\t\tlog.Warnf(\"error setting read deadline: %v\", err)\n\t}\n\t// must clear the timeout setting\n\tif err := c.connection.SetDeadline(time.Time{}); err != nil {\n\t\tlog.Warnf(\"failed to clear deadline: %v\", err)\n\t}\n\n\t// read until we get 4 bytes for the magic\n\theader := make([]byte, 4)\n\t_, err = io.ReadFull(c.connection, header)\n\tif err != nil {\n\t\tlog.Debugf(\"initial read error: %v\", err)\n\t\treturn\n\t}\n\tif !bytes.Equal(header, MAGIC_BYTES) {\n\t\terr = fmt.Errorf(\"initial bytes are not magic: %x\", header)\n\t\treturn\n\t}\n\n\t// read until we get 4 bytes for the header\n\theader = make([]byte, 4)\n\t_, err = io.ReadFull(c.connection, header)\n\tif err != nil {\n\t\tlog.Debugf(\"initial read error: %v\", err)\n\t\treturn\n\t}\n\n\tvar numBytesUint32 uint32\n\trbuf := bytes.NewReader(header)\n\terr = binary.Read(rbuf, binary.LittleEndian, &numBytesUint32)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"binary.Read failed: %w\", err)\n\t\tlog.Debug(err.Error())\n\t\treturn\n\t}\n\tif numBytesUint32 > uint32(maxReadMessageSize) {\n\t\terr = fmt.Errorf(\"message too large: %d > %d\", numBytesUint32, maxReadMessageSize)\n\t\tlog.Debug(err.Error())\n\t\treturn\n\t}\n\tnumBytes = int(numBytesUint32)\n\n\t// shorten the reading deadline in case getting weird data\n\tif err = c.connection.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {\n\t\tlog.Warnf(\"error setting read deadline: %v\", err)\n\t}\n\tbuf = make([]byte, numBytes)\n\t_, err = io.ReadFull(c.connection, buf)\n\tif err != nil {\n\t\tlog.Debugf(\"consecutive read error: %v\", err)\n\t\treturn\n\t}\n\treturn\n}\n\n// Send a message\nfunc (c *Comm) Send(message []byte) (err error) {\n\t_, err = c.Write(message)\n\treturn\n}\n\n// Receive a message\nfunc (c *Comm) Receive() (b []byte, err error) {\n\tb, _, _, err = c.Read()\n\treturn\n}\n"
  },
  {
    "path": "src/comm/comm_test.go",
    "content": "package comm\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/binary\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestComm(t *testing.T) {\n\ttoken := make([]byte, 3000)\n\tif _, err := rand.Read(token); err != nil {\n\t\tt.Error(err)\n\t}\n\n\t// Use dynamic port allocation to avoid conflicts\n\tlistener, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tport := listener.Addr().(*net.TCPAddr).Port\n\tportStr := listener.Addr().String()\n\tlistener.Close() // Close the listener so we can reopen it in the goroutine\n\n\tgo func() {\n\t\tlog.Debug(\"starting TCP server on \" + portStr)\n\t\tserver, err := net.Listen(\"tcp\", portStr)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\tdefer func() {\n\t\t\tif err := server.Close(); err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t}()\n\t\t// spawn a new goroutine whenever a client connects\n\t\tfor {\n\t\t\tconnection, err := server.Accept()\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\tlog.Debugf(\"client %s connected\", connection.RemoteAddr().String())\n\t\t\tgo func(_ int, connection net.Conn) {\n\t\t\t\tc := New(connection)\n\t\t\t\terr = c.Send([]byte(\"hello, world\"))\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tdata, err := c.Receive()\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equal(t, []byte(\"hello, computer\"), data)\n\t\t\t\tdata, err = c.Receive()\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equal(t, []byte{'\\x00'}, data)\n\t\t\t\tdata, err = c.Receive()\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equal(t, token, data)\n\t\t\t}(port, connection)\n\t\t}\n\t}()\n\n\ttime.Sleep(300 * time.Millisecond)\n\ta, err := NewConnection(portStr, 10*time.Minute)\n\tassert.Nil(t, err)\n\tdata, err := a.Receive()\n\tassert.Equal(t, []byte(\"hello, world\"), data)\n\tassert.Nil(t, err)\n\tassert.Nil(t, a.Send([]byte(\"hello, computer\")))\n\tassert.Nil(t, a.Send([]byte{'\\x00'}))\n\n\tassert.Nil(t, a.Send(token))\n\t_ = a.Connection()\n\ta.Close()\n\tassert.NotNil(t, a.Send(token))\n\t_, err = a.Write(token)\n\tassert.NotNil(t, err)\n}\n\nfunc TestReceiveRejectsOversizedMessage(t *testing.T) {\n\tclientConn, serverConn := net.Pipe()\n\tdefer clientConn.Close()\n\tdefer serverConn.Close()\n\n\tc := New(clientConn)\n\n\twriteErr := make(chan error, 1)\n\tgo func() {\n\t\theader := new(bytes.Buffer)\n\t\theader.Write(MAGIC_BYTES)\n\t\tif err := binary.Write(header, binary.LittleEndian, uint32(maxReadMessageSize+1)); err != nil {\n\t\t\twriteErr <- err\n\t\t\treturn\n\t\t}\n\t\t_, err := serverConn.Write(header.Bytes())\n\t\twriteErr <- err\n\t}()\n\n\t_, err := c.Receive()\n\tassert.NotNil(t, err)\n\tassert.Contains(t, err.Error(), \"message too large\")\n\tassert.Nil(t, <-writeErr)\n}\n"
  },
  {
    "path": "src/compress/compress.go",
    "content": "package compress\n\nimport (\n\t\"bytes\"\n\t\"compress/flate\"\n\t\"io\"\n\n\tlog \"github.com/schollz/logger\"\n)\n\n// CompressWithOption returns compressed data using the specified level\nfunc CompressWithOption(src []byte, level int) []byte {\n\tcompressedData := new(bytes.Buffer)\n\tcompress(src, compressedData, level)\n\treturn compressedData.Bytes()\n}\n\n// Compress returns a compressed byte slice.\nfunc Compress(src []byte) []byte {\n\tcompressedData := new(bytes.Buffer)\n\tcompress(src, compressedData, flate.HuffmanOnly)\n\treturn compressedData.Bytes()\n}\n\n// Decompress returns a decompressed byte slice.\nfunc Decompress(src []byte) []byte {\n\tcompressedData := bytes.NewBuffer(src)\n\tdeCompressedData := new(bytes.Buffer)\n\tdecompress(compressedData, deCompressedData)\n\treturn deCompressedData.Bytes()\n}\n\n// compress uses flate to compress a byte slice to a corresponding level\nfunc compress(src []byte, dest io.Writer, level int) {\n\tcompressor, err := flate.NewWriter(dest, level)\n\tif err != nil {\n\t\tlog.Debugf(\"error level data: %v\", err)\n\t\treturn\n\t}\n\tif _, err := compressor.Write(src); err != nil {\n\t\tlog.Debugf(\"error writing data: %v\", err)\n\t}\n\tcompressor.Close()\n}\n\n// decompress uses flate to decompress an io.Reader\nfunc decompress(src io.Reader, dest io.Writer) {\n\tdecompressor := flate.NewReader(src)\n\tif _, err := io.Copy(dest, decompressor); err != nil {\n\t\tlog.Debugf(\"error copying data: %v\", err)\n\t}\n\tdecompressor.Close()\n}\n"
  },
  {
    "path": "src/compress/compress_test.go",
    "content": "package compress\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar fable = []byte(`The Frog and the Crocodile\nOnce, there was a frog who lived in the middle of a swamp. His entire family had lived in that swamp for generations, but this particular frog decided that he had had quite enough wetness to last him a lifetime. He decided that he was going to find a dry place to live instead.\n\nThe only thing that separated him from dry land was a swampy, muddy, swiftly flowing river. But the river was home to all sorts of slippery, slittering snakes that loved nothing better than a good, plump frog for dinner, so Frog didn't dare try to swim across.\n\nSo for many days, the frog stayed put, hopping along the bank, trying to think of a way to get across.\n\nThe snakes hissed and jeered at him, daring him to come closer, but he refused. Occasionally they would slither closer, jaws open to attack, but the frog always leaped out of the way. But no matter how far upstream he searched or how far downstream, the frog wasn't able to find a way across the water.\n\nHe had felt certain that there would be a bridge, or a place where the banks came together, yet all he found was more reeds and water. After a while, even the snakes stopped teasing him and went off in search of easier prey.\n\nThe frog sighed in frustration and sat to sulk in the rushes. Suddenly, he spotted two big eyes staring at him from the water. The giant log-shaped animal opened its mouth and asked him, \"What are you doing, Frog? Surely there are enough flies right there for a meal.\"\n\nThe frog croaked in surprise and leaped away from the crocodile. That creature could swallow him whole in a moment without thinking about it! Once he was a satisfied that he was a safe distance away, he answered. \"I'm tired of living in swampy waters, and I want to travel to the other side of the river. But if I swim across, the snakes will eat me.\"\n\nThe crocodile harrumphed in agreement and sat, thinking, for a while. \"Well, if you're afraid of the snakes, I could give you a ride across,\" he suggested.\n\n\"Oh no, I don't think so,\" Frog answered quickly. \"You'd eat me on the way over, or go underwater so the snakes could get me!\"\n\n\"Now why would I let the snakes get you? I think they're a terrible nuisance with all their hissing and slithering! The river would be much better off without them altogether! Anyway, if you're so worried that I might eat you, you can ride on my tail.\"\n\nThe frog considered his offer. He did want to get to dry ground very badly, and there didn't seem to be any other way across the river. He looked at the crocodile from his short, squat buggy eyes and wondered about the crocodile's motives. But if he rode on the tail, the croc couldn't eat him anyway. And he was right about the snakes--no self-respecting crocodile would give a meal to the snakes.\n\n\"Okay, it sounds like a good plan to me. Turn around so I can hop on your tail.\"\n\nThe crocodile flopped his tail into the marshy mud and let the frog climb on, then he waddled out to the river. But he couldn't stick his tail into the water as a rudder because the frog was on it -- and if he put his tail in the water, the snakes would eat the frog. They clumsily floated downstream for a ways, until the crocodile said, \"Hop onto my back so I can steer straight with my tail.\" The frog moved, and the journey smoothed out.\n\nFrom where he was sitting, the frog couldn't see much except the back of Crocodile's head. \"Why don't you hop up on my head so you can see everything around us?\" Crocodile invited. `)\n\nfunc BenchmarkCompressLevelMinusTwo(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tCompressWithOption(fable, -2)\n\t}\n}\n\nfunc BenchmarkCompressLevelNine(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tCompressWithOption(fable, 9)\n\t}\n}\n\nfunc BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {\n\tdata := make([]byte, 1000000)\n\tif _, err := rand.Read(data); err != nil {\n\t\tb.Fatal(err)\n\t}\n\tfor i := 0; i < b.N; i++ {\n\t\tCompressWithOption(data, -2)\n\t}\n}\n\nfunc BenchmarkCompressLevelNineBinary(b *testing.B) {\n\tdata := make([]byte, 1000000)\n\tif _, err := rand.Read(data); err != nil {\n\t\tb.Fatal(err)\n\t}\n\tfor i := 0; i < b.N; i++ {\n\t\tCompressWithOption(data, 9)\n\t}\n}\n\nfunc TestCompress(t *testing.T) {\n\tcompressedB := CompressWithOption(fable, 9)\n\tdataRateSavings := 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))\n\tfmt.Printf(\"Level 9: %2.0f%% percent space savings\\n\", dataRateSavings)\n\tassert.True(t, len(compressedB) < len(fable))\n\tassert.Equal(t, fable, Decompress(compressedB))\n\n\tcompressedB = CompressWithOption(fable, -2)\n\tdataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))\n\tfmt.Printf(\"Level -2: %2.0f%% percent space savings\\n\", dataRateSavings)\n\tassert.True(t, len(compressedB) < len(fable))\n\n\tcompressedB = Compress(fable)\n\tdataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))\n\tfmt.Printf(\"Level -2: %2.0f%% percent space savings\\n\", dataRateSavings)\n\tassert.True(t, len(compressedB) < len(fable))\n\n\tdata := make([]byte, 4096)\n\tif _, err := rand.Read(data); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcompressedB = CompressWithOption(data, -2)\n\tdataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))\n\tfmt.Printf(\"random, Level -2: %2.0f%% percent space savings\\n\", dataRateSavings)\n\n\tif _, err := rand.Read(data); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcompressedB = CompressWithOption(data, 9)\n\tdataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))\n\n\tfmt.Printf(\"random, Level 9: %2.0f%% percent space savings\\n\", dataRateSavings)\n\n}\n"
  },
  {
    "path": "src/croc/croc.go",
    "content": "package croc\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/denisbrodbeck/machineid\"\n\tignore \"github.com/sabhiram/go-gitignore\"\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/schollz/pake/v3\"\n\t\"github.com/schollz/peerdiscovery\"\n\t\"github.com/schollz/progressbar/v3\"\n\t\"github.com/skip2/go-qrcode\"\n\t\"golang.org/x/term\"\n\t\"golang.org/x/time/rate\"\n\n\t\"github.com/schollz/croc/v10/src/comm\"\n\t\"github.com/schollz/croc/v10/src/compress\"\n\t\"github.com/schollz/croc/v10/src/crypt\"\n\t\"github.com/schollz/croc/v10/src/message\"\n\t\"github.com/schollz/croc/v10/src/models\"\n\t\"github.com/schollz/croc/v10/src/tcp\"\n\t\"github.com/schollz/croc/v10/src/utils\"\n)\n\nvar (\n\tipRequest        = []byte(\"ips?\")\n\thandshakeRequest = []byte(\"handshake\")\n)\n\nfunc init() {\n\tlog.SetLevel(\"debug\")\n}\n\n// Debug toggles debug mode\nfunc Debug(debug bool) {\n\tif debug {\n\t\tlog.SetLevel(\"debug\")\n\t} else {\n\t\tlog.SetLevel(\"warn\")\n\t}\n}\n\n// Options specifies user specific options\ntype Options struct {\n\tIsSender          bool\n\tSharedSecret      string\n\tRoomName          string\n\tDebug             bool\n\tRelayAddress      string\n\tRelayAddress6     string\n\tRelayPorts        []string\n\tRelayPassword     string\n\tStdout            bool\n\tNoPrompt          bool\n\tNoMultiplexing    bool\n\tDisableLocal      bool\n\tOnlyLocal         bool\n\tIgnoreStdin       bool\n\tAsk               bool\n\tSendingText       bool\n\tNoCompress        bool\n\tIP                string\n\tOverwrite         bool\n\tCurve             string\n\tHashAlgorithm     string\n\tThrottleUpload    string\n\tZipFolder         bool\n\tTestFlag          bool\n\tGitIgnore         bool\n\tMulticastAddress  string\n\tShowQrCode        bool\n\tExclude           []string\n\tQuiet             bool\n\tDisableClipboard  bool\n\tExtendedClipboard bool\n}\n\ntype SimpleMessage struct {\n\tBytes []byte\n\tKind  string\n}\n\n// Client holds the state of the croc transfer\ntype Client struct {\n\tOptions                         Options\n\tPake                            *pake.Pake\n\tKey                             []byte\n\tExternalIP, ExternalIPConnected string\n\n\t// steps involved in forming relationship\n\tStep1ChannelSecured       bool\n\tStep2FileInfoTransferred  bool\n\tStep3RecipientRequestFile bool\n\tStep4FileTransferred      bool\n\tStep5CloseChannels        bool\n\tSuccessfulTransfer        bool\n\n\t// send / receive information of all files\n\tFilesToTransfer           []FileInfo\n\tEmptyFoldersToTransfer    []FileInfo\n\tTotalNumberOfContents     int\n\tTotalNumberFolders        int\n\tFilesToTransferCurrentNum int\n\tFilesHasFinished          map[int]struct{}\n\tTotalFilesIgnored         int\n\n\t// send / receive information of current file\n\tCurrentFile            *os.File\n\tCurrentFileChunkRanges []int64\n\tCurrentFileChunks      []int64\n\tCurrentFileIsClosed    bool\n\tLastFolder             string\n\n\tTotalSent              int64\n\tTotalChunksTransferred int\n\tchunkMap               map[uint64]struct{}\n\tlimiter                *rate.Limiter\n\n\t// tcp connections\n\tconn []*comm.Comm\n\n\tbar             *progressbar.ProgressBar\n\tlongestFilename int\n\tfirstSend       bool\n\n\tmutex                    *sync.Mutex\n\tfread                    *os.File\n\tnumfinished              int\n\tquit                     chan bool\n\tfinishedNum              int\n\tnumberOfTransferredFiles int\n\n\t// ctx.go for graceful shutdown\n\t*stop\n}\n\n// Chunk contains information about the\n// needed bytes\ntype Chunk struct {\n\tBytes    []byte `json:\"b,omitempty\"`\n\tLocation int64  `json:\"l,omitempty\"`\n}\n\n// FileInfo registers the information about the file\ntype FileInfo struct {\n\tName         string      `json:\"n,omitempty\"`\n\tFolderRemote string      `json:\"fr,omitempty\"`\n\tFolderSource string      `json:\"fs,omitempty\"`\n\tHash         []byte      `json:\"h,omitempty\"`\n\tSize         int64       `json:\"s,omitempty\"`\n\tModTime      time.Time   `json:\"m,omitempty\"`\n\tIsCompressed bool        `json:\"c,omitempty\"`\n\tIsEncrypted  bool        `json:\"e,omitempty\"`\n\tSymlink      string      `json:\"sy,omitempty\"`\n\tMode         os.FileMode `json:\"md,omitempty\"`\n\tTempFile     bool        `json:\"tf,omitempty\"`\n\tIsIgnored    bool        `json:\"ig,omitempty\"`\n}\n\n// RemoteFileRequest requests specific bytes\ntype RemoteFileRequest struct {\n\tCurrentFileChunkRanges    []int64\n\tFilesToTransferCurrentNum int\n\tMachineID                 string\n}\n\n// SenderInfo lists the files to be transferred\ntype SenderInfo struct {\n\tFilesToTransfer        []FileInfo\n\tEmptyFoldersToTransfer []FileInfo\n\tTotalNumberFolders     int\n\tMachineID              string\n\tAsk                    bool\n\tSendingText            bool\n\tNoCompress             bool\n\tHashAlgorithm          string\n}\n\n// New establishes a new connection for transferring files between two instances.\nfunc New(ops Options) (c *Client, err error) {\n\tc = new(Client)\n\tc.FilesHasFinished = make(map[int]struct{})\n\n\t// setup basic info\n\tc.Options = ops\n\tDebug(c.Options.Debug)\n\n\t// redirect stderr to null if quiet mode is enabled\n\tif c.Options.Quiet {\n\t\tdevNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)\n\t\tif err == nil {\n\t\t\tos.Stderr = devNull\n\t\t}\n\t}\n\n\tif len(c.Options.SharedSecret) < 6 {\n\t\terr = fmt.Errorf(\"code is too short\")\n\t\treturn\n\t}\n\t// Create a hash of part of the shared secret to use as the room name\n\thashExtra := \"croc\"\n\troomNameBytes := sha256.Sum256([]byte(c.Options.SharedSecret[:4] + hashExtra))\n\tc.Options.RoomName = hex.EncodeToString(roomNameBytes[:])\n\n\tc.conn = make([]*comm.Comm, 16)\n\n\t// initialize throttler\n\tif len(c.Options.ThrottleUpload) > 1 && c.Options.IsSender {\n\t\tupload := c.Options.ThrottleUpload[:len(c.Options.ThrottleUpload)-1]\n\t\tvar uploadLimit int64\n\t\tuploadLimit, err = strconv.ParseInt(upload, 10, 64)\n\t\tif err != nil {\n\t\t\tpanic(\"Could not parse given Upload Limit\")\n\t\t}\n\t\tminBurstSize := models.TCP_BUFFER_SIZE\n\t\tvar rt rate.Limit\n\t\tswitch unit := string(c.Options.ThrottleUpload[len(c.Options.ThrottleUpload)-1:]); unit {\n\t\tcase \"g\", \"G\":\n\t\t\tuploadLimit = uploadLimit * 1024 * 1024 * 1024\n\t\tcase \"m\", \"M\":\n\t\t\tuploadLimit = uploadLimit * 1024 * 1024\n\t\tcase \"k\", \"K\":\n\t\t\tuploadLimit = uploadLimit * 1024\n\t\tdefault:\n\t\t\tuploadLimit, err = strconv.ParseInt(c.Options.ThrottleUpload, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tpanic(\"Could not parse given Upload Limit\")\n\t\t\t}\n\t\t}\n\n\t\trt = rate.Every(time.Second / time.Duration(uploadLimit))\n\t\tif int(uploadLimit) > minBurstSize {\n\t\t\tminBurstSize = int(uploadLimit)\n\t\t}\n\t\tc.limiter = rate.NewLimiter(rt, minBurstSize)\n\t\tlog.Debugf(\"Throttling Upload to %#v\", c.limiter.Limit())\n\t}\n\n\t// initialize pake for recipient\n\tif !c.Options.IsSender {\n\t\tc.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\tc.mutex = &sync.Mutex{}\n\tc.stop = newStop(context.Background())\n\treturn\n}\n\n// TransferOptions for sending\ntype TransferOptions struct {\n\tPathToFiles      []string\n\tKeepPathInRemote bool\n}\n\n// helper function checking for an empty folder\nfunc isEmptyFolder(folderPath string) (bool, error) {\n\tf, err := os.Open(folderPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer f.Close()\n\n\t_, err = f.Readdirnames(1)\n\tif err == io.EOF {\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n\n// helper function to walk each subfolder and parses against an ignore file.\n// returns a hashmap Key: Absolute filepath, Value: boolean (true=ignore)\nfunc gitWalk(dir string, gitObj *ignore.GitIgnore, files map[string]bool) {\n\tvar ignoredDir bool\n\tvar current string\n\terr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif isChild(current, path) && ignoredDir {\n\t\t\tfiles[path] = true\n\t\t\treturn nil\n\t\t}\n\t\tif info.IsDir() && filepath.Base(path) == filepath.Base(dir) {\n\t\t\tignoredDir = false // Skip applying ignore rules for root directory\n\t\t\treturn nil\n\t\t}\n\t\tif gitObj.MatchesPath(info.Name()) {\n\t\t\tfiles[path] = true\n\t\t\tignoredDir = true\n\t\t\tcurrent = path\n\t\t\treturn nil\n\t\t} else {\n\t\t\tfiles[path] = false\n\t\t\tignoredDir = false\n\t\t\treturn nil\n\t\t}\n\t})\n\tif err != nil {\n\t\tlog.Errorf(\"filepath error\")\n\t}\n}\n\nfunc isChild(parentPath, childPath string) bool {\n\trelPath, err := filepath.Rel(parentPath, childPath)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn !strings.HasPrefix(relPath, \"..\")\n}\n\n// This function retrieves the important file information\n// for every file that will be transferred\nfunc GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []string) (filesInfo []FileInfo, emptyFolders []FileInfo, totalNumberFolders int, err error) {\n\t// fnames: the relative/absolute paths of files/folders that will be transferred\n\ttotalNumberFolders = 0\n\tvar paths []string\n\tfor _, fname := range fnames {\n\t\t// Support wildcard\n\t\tif strings.Contains(fname, \"*\") {\n\t\t\tmatches, errGlob := filepath.Glob(fname)\n\t\t\tif errGlob != nil {\n\t\t\t\terr = errGlob\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpaths = append(paths, matches...)\n\t\t\tcontinue\n\t\t} else {\n\t\t\tpaths = append(paths, fname)\n\t\t}\n\t}\n\tignoredPaths := make(map[string]bool)\n\tif ignoreGit {\n\t\twd, wdErr := os.Stat(\".gitignore\")\n\t\tif wdErr == nil {\n\t\t\tgitIgnore, gitErr := ignore.CompileIgnoreFile(wd.Name())\n\t\t\tif gitErr == nil {\n\t\t\t\tfor _, path := range paths {\n\t\t\t\t\tabs, absErr := filepath.Abs(path)\n\t\t\t\t\tif absErr != nil {\n\t\t\t\t\t\terr = absErr\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif gitIgnore.MatchesPath(path) {\n\t\t\t\t\t\tignoredPaths[abs] = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor _, path := range paths {\n\t\t\tabs, absErr := filepath.Abs(path)\n\t\t\tif absErr != nil {\n\t\t\t\terr = absErr\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfile, fileErr := os.Stat(path)\n\t\t\tif fileErr == nil && file.IsDir() {\n\t\t\t\t_, subErr := os.Stat(filepath.Join(path, \".gitignore\"))\n\t\t\t\tif subErr == nil {\n\t\t\t\t\tgitObj, gitObjErr := ignore.CompileIgnoreFile(filepath.Join(path, \".gitignore\"))\n\t\t\t\t\tif gitObjErr != nil {\n\t\t\t\t\t\terr = gitObjErr\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tgitWalk(abs, gitObj, ignoredPaths)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor _, fpath := range paths {\n\t\tstat, errStat := os.Lstat(fpath)\n\n\t\tif errStat != nil {\n\t\t\terr = errStat\n\t\t\treturn\n\t\t}\n\n\t\tabsPath, errAbs := filepath.Abs(fpath)\n\n\t\tif errAbs != nil {\n\t\t\terr = errAbs\n\t\t\treturn\n\t\t}\n\t\tif stat.IsDir() && zipfolder {\n\t\t\tif fpath[len(fpath)-1:] != \"/\" {\n\t\t\t\tfpath += \"/\"\n\t\t\t}\n\t\t\tfpath = filepath.Dir(fpath)\n\t\t\tdest := filepath.Base(fpath) + \".zip\"\n\t\t\tutils.ZipDirectory(dest, fpath)\n\t\t\tutils.MarkFileForRemoval(dest)\n\t\t\tstat, errStat = os.Lstat(dest)\n\t\t\tif errStat != nil {\n\t\t\t\terr = errStat\n\t\t\t\treturn\n\t\t\t}\n\t\t\tabsPath, errAbs = filepath.Abs(dest)\n\t\t\tif errAbs != nil {\n\t\t\t\terr = errAbs\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfInfo := FileInfo{\n\t\t\t\tName:         stat.Name(),\n\t\t\t\tFolderRemote: \"./\",\n\t\t\t\tFolderSource: filepath.Dir(absPath),\n\t\t\t\tSize:         stat.Size(),\n\t\t\t\tModTime:      stat.ModTime(),\n\t\t\t\tMode:         stat.Mode(),\n\t\t\t\tTempFile:     true,\n\t\t\t\tIsIgnored:    ignoredPaths[absPath],\n\t\t\t}\n\t\t\tif fInfo.IsIgnored {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfilesInfo = append(filesInfo, fInfo)\n\t\t\tcontinue\n\t\t}\n\n\t\tif stat.IsDir() {\n\t\t\terr = filepath.Walk(absPath,\n\t\t\t\tfunc(pathName string, info os.FileInfo, err error) error {\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tabsPathWithSeparator := filepath.Dir(absPath)\n\t\t\t\t\tif !strings.HasSuffix(absPathWithSeparator, string(os.PathSeparator)) {\n\t\t\t\t\t\tabsPathWithSeparator += string(os.PathSeparator)\n\t\t\t\t\t}\n\t\t\t\t\tif strings.HasSuffix(absPathWithSeparator, string(os.PathSeparator)+string(os.PathSeparator)) {\n\t\t\t\t\t\tabsPathWithSeparator = strings.TrimSuffix(absPathWithSeparator, string(os.PathSeparator))\n\t\t\t\t\t}\n\t\t\t\t\tremoteFolder := strings.TrimPrefix(filepath.Dir(pathName), absPathWithSeparator)\n\t\t\t\t\tif !info.IsDir() {\n\t\t\t\t\t\tfInfo := FileInfo{\n\t\t\t\t\t\t\tName:         info.Name(),\n\t\t\t\t\t\t\tFolderRemote: strings.ReplaceAll(remoteFolder, string(os.PathSeparator), \"/\") + \"/\",\n\t\t\t\t\t\t\tFolderSource: filepath.Dir(pathName),\n\t\t\t\t\t\t\tSize:         info.Size(),\n\t\t\t\t\t\t\tModTime:      info.ModTime(),\n\t\t\t\t\t\t\tMode:         info.Mode(),\n\t\t\t\t\t\t\tTempFile:     false,\n\t\t\t\t\t\t\tIsIgnored:    ignoredPaths[pathName],\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif fInfo.IsIgnored && ignoreGit {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfilesInfo = append(filesInfo, fInfo)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif ignoredPaths[pathName] {\n\t\t\t\t\t\t\treturn filepath.SkipDir\n\t\t\t\t\t\t}\n\t\t\t\t\t\tisEmptyFolder, _ := isEmptyFolder(pathName)\n\t\t\t\t\t\ttotalNumberFolders++\n\t\t\t\t\t\tif isEmptyFolder {\n\t\t\t\t\t\t\temptyFolders = append(emptyFolders, FileInfo{\n\t\t\t\t\t\t\t\t// Name: info.Name(),\n\t\t\t\t\t\t\t\tFolderRemote: strings.ReplaceAll(strings.TrimPrefix(pathName,\n\t\t\t\t\t\t\t\t\tfilepath.Dir(absPath)+string(os.PathSeparator)), string(os.PathSeparator), \"/\") + \"/\",\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t} else {\n\t\t\tfInfo := FileInfo{\n\t\t\t\tName:         stat.Name(),\n\t\t\t\tFolderRemote: \"./\",\n\t\t\t\tFolderSource: filepath.Dir(absPath),\n\t\t\t\tSize:         stat.Size(),\n\t\t\t\tModTime:      stat.ModTime(),\n\t\t\t\tMode:         stat.Mode(),\n\t\t\t\tTempFile:     false,\n\t\t\t\tIsIgnored:    ignoredPaths[absPath],\n\t\t\t}\n\t\t\tif fInfo.IsIgnored && ignoreGit {\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\tfilesInfo = append(filesInfo, fInfo)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *Client) sendCollectFiles(filesInfo []FileInfo) (err error) {\n\tc.FilesToTransfer = filesInfo\n\ttotalFilesSize := int64(0)\n\n\tfor i, fileInfo := range c.FilesToTransfer {\n\t\tvar fullPath string\n\t\tfullPath = fileInfo.FolderSource + string(os.PathSeparator) + fileInfo.Name\n\t\tfullPath = filepath.Clean(fullPath)\n\n\t\tif len(fileInfo.Name) > c.longestFilename {\n\t\t\tc.longestFilename = len(fileInfo.Name)\n\t\t}\n\n\t\tif fileInfo.Mode&os.ModeSymlink != 0 {\n\t\t\tlog.Debugf(\"%s is symlink\", fileInfo.Name)\n\t\t\tc.FilesToTransfer[i].Symlink, err = os.Readlink(fullPath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"error getting symlink: %s\", err.Error())\n\t\t\t}\n\t\t\tlog.Debugf(\"%+v\", c.FilesToTransfer[i])\n\t\t}\n\n\t\tif c.Options.HashAlgorithm == \"\" {\n\t\t\tc.Options.HashAlgorithm = \"xxhash\"\n\t\t}\n\n\t\tc.FilesToTransfer[i].Hash, err = c.stop.hash(fullPath, c.Options.HashAlgorithm, fileInfo.Size > 1e7)\n\t\tlog.Debugf(\"hashed %s to %x using %s\", fullPath, c.FilesToTransfer[i].Hash, c.Options.HashAlgorithm)\n\t\ttotalFilesSize += fileInfo.Size\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tlog.Debugf(\"file %d info: %+v\", i, c.FilesToTransfer[i])\n\t\tfmt.Fprintf(os.Stderr, \"\\r                                 \")\n\t\tfmt.Fprintf(os.Stderr, \"\\rSending %d files (%s)\", i, utils.ByteCountDecimal(totalFilesSize))\n\t}\n\tlog.Debugf(\"longestFilename: %+v\", c.longestFilename)\n\tfname := fmt.Sprintf(\"%d files\", len(c.FilesToTransfer))\n\tfolderName := fmt.Sprintf(\"%d folders\", c.TotalNumberFolders)\n\tif len(c.FilesToTransfer) == 1 {\n\t\tfname = fmt.Sprintf(\"'%s'\", c.FilesToTransfer[0].Name)\n\t}\n\tif strings.HasPrefix(fname, \"'croc-stdin-\") {\n\t\tfname = \"'stdin'\"\n\t\tif c.Options.SendingText {\n\t\t\tfname = \"'text'\"\n\t\t}\n\t}\n\n\tfmt.Fprintf(os.Stderr, \"\\r                                 \")\n\tif c.TotalNumberFolders > 0 {\n\t\tfmt.Fprintf(os.Stderr, \"\\rSending %s and %s (%s)\\n\", fname, folderName, utils.ByteCountDecimal(totalFilesSize))\n\t} else {\n\t\tfmt.Fprintf(os.Stderr, \"\\rSending %s (%s)\\n\", fname, utils.ByteCountDecimal(totalFilesSize))\n\t}\n\treturn\n}\n\nfunc (c *Client) setupLocalRelay() {\n\t// setup the relay locally\n\tfirstPort, _ := strconv.Atoi(c.Options.RelayPorts[0])\n\topenPorts := utils.FindOpenPorts(\"127.0.0.1\", firstPort, len(c.Options.RelayPorts))\n\tif len(openPorts) < len(c.Options.RelayPorts) {\n\t\tpanic(\"not enough open ports to run local relay\")\n\t}\n\tfor i, port := range openPorts {\n\t\tc.Options.RelayPorts[i] = fmt.Sprint(port)\n\t}\n\tfor _, port := range c.Options.RelayPorts {\n\t\tgo func(portStr string) {\n\t\t\tdebugString := \"warn\"\n\t\t\tif c.Options.Debug {\n\t\t\t\tdebugString = \"debug\"\n\t\t\t}\n\t\t\terr := c.stop.run(\n\t\t\t\tdebugString,\n\t\t\t\t\"127.0.0.1\",\n\t\t\t\tportStr,\n\t\t\t\tc.Options.RelayPassword,\n\t\t\t\tstrings.Join(c.Options.RelayPorts[1:], \",\"))\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}(port)\n\t}\n}\n\nfunc (c *Client) broadcastOnLocalNetwork(useipv6 bool) {\n\tvar timeLimit time.Duration\n\t// if we don't use an external relay, the broadcast messages need to be sent continuously\n\tif c.Options.OnlyLocal {\n\t\ttimeLimit = -1 * time.Second\n\t} else {\n\t\ttimeLimit = 30 * time.Second\n\t}\n\t// look for peers first\n\tsettings := peerdiscovery.Settings{\n\t\tLimit:     -1,\n\t\tPayload:   []byte(\"croc\" + c.Options.RelayPorts[0]),\n\t\tDelay:     20 * time.Millisecond,\n\t\tTimeLimit: timeLimit,\n\t\tStopChan:  c.stop.stopChan,\n\t}\n\tif useipv6 {\n\t\tsettings.IPVersion = peerdiscovery.IPv6\n\t} else {\n\t\tsettings.MulticastAddress = c.Options.MulticastAddress\n\t}\n\n\tdiscoveries, err := peerdiscovery.Discover(settings)\n\tlog.Debugf(\"discoveries: %+v\", discoveries)\n\n\tif err != nil {\n\t\tlog.Debug(err)\n\t}\n}\n\nfunc (c *Client) transferOverLocalRelay(errchan chan<- error) {\n\ttime.Sleep(500 * time.Millisecond)\n\tlog.Debug(\"establishing connection\")\n\tvar banner string\n\tconn, banner, ipaddr, err := tcp.ConnectToTCPServer(\"127.0.0.1:\"+c.Options.RelayPorts[0], c.Options.RelayPassword, c.Options.RoomName)\n\tlog.Debugf(\"banner: %s\", banner)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"could not connect to 127.0.0.1:%s: %w\", c.Options.RelayPorts[0], err)\n\t\tlog.Debug(err)\n\t\t// not really an error because it will try to connect over the actual relay\n\t\treturn\n\t}\n\tlog.Debugf(\"local connection established: %+v\", conn)\n\tfor {\n\t\tif err := c.ctxErr(); err != nil {\n\t\t\terrchan <- err\n\t\t\treturn\n\t\t}\n\t\tdata, _ := conn.Receive()\n\t\tif bytes.Equal(data, handshakeRequest) {\n\t\t\tbreak\n\t\t} else if bytes.Equal(data, []byte{1}) {\n\t\t\tlog.Trace(\"got ping\")\n\t\t} else {\n\t\t\tlog.Debugf(\"instead of handshake got: %s\", data)\n\t\t}\n\t}\n\tc.conn[0] = conn\n\tlog.Debug(\"exchanged header message\")\n\tc.Options.RelayAddress = \"127.0.0.1\"\n\tc.Options.RelayPorts = strings.Split(banner, \",\")\n\tif c.Options.NoMultiplexing {\n\t\tlog.Debug(\"no multiplexing\")\n\t\tc.Options.RelayPorts = []string{c.Options.RelayPorts[0]}\n\t}\n\tc.ExternalIP = ipaddr\n\terrchan <- c.transfer()\n}\n\n// Send will send the specified file\nfunc (c *Client) Send(filesInfo []FileInfo, emptyFoldersToTransfer []FileInfo, totalNumberFolders int) (err error) {\n\tgo c.stop.done()\n\tdefer c.stop.Cancel()\n\tc.EmptyFoldersToTransfer = emptyFoldersToTransfer\n\tc.TotalNumberFolders = totalNumberFolders\n\tc.TotalNumberOfContents = len(filesInfo)\n\terr = c.sendCollectFiles(filesInfo)\n\tif err != nil {\n\t\treturn\n\t}\n\tflags := &strings.Builder{}\n\tif c.Options.RelayAddress != models.DEFAULT_RELAY && !c.Options.OnlyLocal {\n\t\tflags.WriteString(\"--relay \" + c.Options.RelayAddress + \" \")\n\t}\n\tif c.Options.RelayPassword != models.DEFAULT_PASSPHRASE {\n\t\tflags.WriteString(\"--pass \" + c.Options.RelayPassword + \" \")\n\t}\n\tfmt.Fprintf(os.Stderr, `Code is: %[1]s\n\nOn the other computer run:\n(For Windows)\n    croc %[2]s%[1]s\n(For Linux/macOS)\n    CROC_SECRET=%[1]q croc %[2]s\n`, c.Options.SharedSecret, flags.String())\n\tif !c.Options.DisableClipboard {\n\t\tclipboardText := c.Options.SharedSecret\n\t\tif c.Options.ExtendedClipboard {\n\t\t\tclipboardText = fmt.Sprintf(\"CROC_SECRET=%q croc %s\", c.Options.SharedSecret, strings.TrimSpace(flags.String()))\n\t\t}\n\t\tcopyToClipboard(clipboardText, c.Options.Quiet, c.Options.ExtendedClipboard)\n\t}\n\tif c.Options.ShowQrCode {\n\t\tshowReceiveCommandQrCode(fmt.Sprintf(\"%[1]s\", c.Options.SharedSecret))\n\t}\n\tif c.Options.Ask {\n\t\tmachid, _ := machineid.ID()\n\t\tfmt.Fprintf(os.Stderr, \"\\rYour machine ID is '%s'\\n\", machid)\n\t}\n\t// c.spinner.Suffix = \" waiting for recipient...\"\n\t// c.spinner.Start()\n\t// create channel for quitting\n\t// connect to the relay for messaging\n\terrchan := make(chan error, 1)\n\n\tif !c.Options.DisableLocal {\n\t\t// add two things to the error channel\n\t\terrchan = make(chan error, 2)\n\t\tc.setupLocalRelay()\n\t\t// broadcast on ipv4\n\t\tgo c.broadcastOnLocalNetwork(false)\n\t\t// broadcast on ipv6\n\t\tgo c.broadcastOnLocalNetwork(true)\n\t\tgo c.transferOverLocalRelay(errchan)\n\t}\n\n\tif !c.Options.OnlyLocal {\n\t\tgo func() {\n\t\t\tvar ipaddr, banner string\n\t\t\tvar conn *comm.Comm\n\t\t\tdurations := []time.Duration{100 * time.Millisecond, 5 * time.Second}\n\t\t\tfor i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} {\n\t\t\t\tif address == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\thost, port, _ := net.SplitHostPort(address)\n\t\t\t\tlog.Debugf(\"host: '%s', port: '%s'\", host, port)\n\t\t\t\t// Default port to :9009\n\t\t\t\tif port == \"\" {\n\t\t\t\t\thost = address\n\t\t\t\t\tport = models.DEFAULT_PORT\n\t\t\t\t}\n\t\t\t\tlog.Debugf(\"got host '%v' and port '%v'\", host, port)\n\t\t\t\taddress = net.JoinHostPort(host, port)\n\t\t\t\tlog.Debugf(\"trying connection to %s\", address)\n\t\t\t\tconn, banner, ipaddr, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RoomName, durations[i])\n\t\t\t\tif err == nil {\n\t\t\t\t\tc.Options.RelayAddress = address\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlog.Debugf(\"could not establish '%s'\", address)\n\t\t\t}\n\t\t\tif conn == nil && err == nil {\n\t\t\t\terr = fmt.Errorf(\"could not connect\")\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"could not connect to %s: %w\", c.Options.RelayAddress, err)\n\t\t\t\tlog.Debug(err)\n\t\t\t\terrchan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Debugf(\"banner: %s\", banner)\n\t\t\tlog.Debugf(\"connection established: %+v\", conn)\n\t\t\tvar kB []byte\n\t\t\tB, _ := pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, c.Options.Curve)\n\t\t\tfor {\n\t\t\t\tif err := c.ctxErr(); err != nil {\n\t\t\t\t\terrchan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar dataMessage SimpleMessage\n\t\t\t\tlog.Trace(\"waiting for bytes\")\n\t\t\t\tdata, errConn := conn.Receive()\n\t\t\t\tif errConn != nil {\n\t\t\t\t\tlog.Tracef(\"[%+v] had error: %s\", conn, errConn.Error())\n\t\t\t\t}\n\t\t\t\tjson.Unmarshal(data, &dataMessage)\n\t\t\t\tlog.Tracef(\"data: %+v '%s'\", data, data)\n\t\t\t\tlog.Tracef(\"dataMessage: %s\", dataMessage)\n\t\t\t\tlog.Tracef(\"kB: %x\", kB)\n\t\t\t\t// if kB not null, then use it to decrypt\n\t\t\t\tif kB != nil {\n\t\t\t\t\tvar decryptErr error\n\t\t\t\t\tvar dataDecrypt []byte\n\t\t\t\t\tdataDecrypt, decryptErr = crypt.Decrypt(data, kB)\n\t\t\t\t\tif decryptErr != nil {\n\t\t\t\t\t\tlog.Tracef(\"error decrypting: %v: '%s'\", decryptErr, data)\n\t\t\t\t\t\t// relay sent a message encrypted with an invalid key.\n\t\t\t\t\t\t// consider this a security issue and abort\n\t\t\t\t\t\tif strings.Contains(decryptErr.Error(), \"message authentication failed\") {\n\t\t\t\t\t\t\terrchan <- decryptErr\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// copy dataDecrypt to data\n\t\t\t\t\t\tdata = dataDecrypt\n\t\t\t\t\t\tlog.Tracef(\"decrypted: %s\", data)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif bytes.Equal(data, ipRequest) {\n\t\t\t\t\tlog.Tracef(\"got ipRequest\")\n\t\t\t\t\t// recipient wants to try to connect to local ips\n\t\t\t\t\tvar ips []string\n\t\t\t\t\t// only get local ips if the local is enabled\n\t\t\t\t\tif !c.Options.DisableLocal {\n\t\t\t\t\t\t// get list of local ips\n\t\t\t\t\t\tips, err = utils.GetLocalIPs()\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Tracef(\"error getting local ips: %v\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// prepend the port that is being listened to\n\t\t\t\t\t\tips = append([]string{c.Options.RelayPorts[0]}, ips...)\n\t\t\t\t\t}\n\t\t\t\t\tlog.Tracef(\"sending ips: %+v\", ips)\n\t\t\t\t\tbips, errIps := json.Marshal(ips)\n\t\t\t\t\tif errIps != nil {\n\t\t\t\t\t\tlog.Tracef(\"error marshalling ips: %v\", errIps)\n\t\t\t\t\t}\n\t\t\t\t\tbips, errIps = crypt.Encrypt(bips, kB)\n\t\t\t\t\tif errIps != nil {\n\t\t\t\t\t\tlog.Tracef(\"error encrypting ips: %v\", errIps)\n\t\t\t\t\t}\n\t\t\t\t\tif err = conn.Send(bips); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"error sending: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if dataMessage.Kind == \"pake1\" {\n\t\t\t\t\tlog.Trace(\"got pake1\")\n\t\t\t\t\tvar pakeError error\n\t\t\t\t\tpakeError = B.Update(dataMessage.Bytes)\n\t\t\t\t\tif pakeError == nil {\n\t\t\t\t\t\tkB, pakeError = B.SessionKey()\n\t\t\t\t\t\tif pakeError == nil {\n\t\t\t\t\t\t\tlog.Tracef(\"dataMessage kB: %x\", kB)\n\t\t\t\t\t\t\tdataMessage.Bytes = B.Bytes()\n\t\t\t\t\t\t\tdataMessage.Kind = \"pake2\"\n\t\t\t\t\t\t\tdata, _ = json.Marshal(dataMessage)\n\t\t\t\t\t\t\tif pakeError = conn.Send(data); err != nil {\n\t\t\t\t\t\t\t\tlog.Errorf(\"dataMessage error sending: %v\", err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t} else if bytes.Equal(data, handshakeRequest) {\n\t\t\t\t\tlog.Trace(\"got handshake\")\n\t\t\t\t\tbreak\n\t\t\t\t} else if bytes.Equal(data, []byte{1}) {\n\t\t\t\t\tlog.Trace(\"got ping\")\n\t\t\t\t\tcontinue\n\t\t\t\t} else {\n\t\t\t\t\tlog.Tracef(\"[%+v] got weird bytes: %+v\", conn, data)\n\t\t\t\t\t// throttle the reading\n\t\t\t\t\terrchan <- fmt.Errorf(\"gracefully refusing using the public relay\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tc.conn[0] = conn\n\t\t\tc.Options.RelayPorts = strings.Split(banner, \",\")\n\t\t\tif c.Options.NoMultiplexing {\n\t\t\t\tlog.Debug(\"no multiplexing\")\n\t\t\t\tc.Options.RelayPorts = []string{c.Options.RelayPorts[0]}\n\t\t\t}\n\t\t\tc.ExternalIP = ipaddr\n\t\t\tlog.Debug(\"exchanged header message\")\n\t\t\terrchan <- c.transfer()\n\t\t}()\n\t}\n\n\terr = <-errchan\n\tif err == nil {\n\t\treturn // no error\n\t} else {\n\t\tlog.Debugf(\"error from errchan: %v\", err)\n\t\tif strings.Contains(err.Error(), \"could not secure channel\") {\n\t\t\treturn err\n\t\t}\n\t}\n\tif !c.Options.DisableLocal {\n\t\tif strings.Contains(err.Error(), \"refusing files\") || strings.Contains(err.Error(), \"EOF\") || strings.Contains(err.Error(), \"bad password\") || strings.Contains(err.Error(), \"message authentication failed\") {\n\t\t\terrchan <- err\n\t\t}\n\t\terr = <-errchan\n\t}\n\treturn err\n}\n\nfunc showReceiveCommandQrCode(command string) {\n\tqrCode, err := qrcode.New(command, qrcode.Medium)\n\tif err == nil {\n\t\tfmt.Println(qrCode.ToSmallString(false))\n\t}\n}\n\n// Receive will receive a file\nfunc (c *Client) Receive() (err error) {\n\tgo c.stop.done()\n\tdefer c.stop.Cancel()\n\tfmt.Fprintf(os.Stderr, \"connecting...\")\n\t// recipient will look for peers first\n\t// and continue if it doesn't find any within 100 ms\n\tusingLocal := false\n\tisIPset := false\n\n\tif c.Options.OnlyLocal || c.Options.IP != \"\" {\n\t\tc.Options.RelayAddress = \"\"\n\t\tc.Options.RelayAddress6 = \"\"\n\t}\n\n\tif c.Options.IP != \"\" {\n\t\t// check ip version\n\t\tif strings.Count(c.Options.IP, \":\") >= 2 {\n\t\t\tlog.Debug(\"assume ipv6\")\n\t\t\tc.Options.RelayAddress6 = c.Options.IP\n\t\t}\n\t\tif strings.Contains(c.Options.IP, \".\") {\n\t\t\tlog.Debug(\"assume ipv4\")\n\t\t\tc.Options.RelayAddress = c.Options.IP\n\t\t}\n\t\tisIPset = true\n\t}\n\n\tif !c.Options.DisableLocal && !isIPset {\n\t\tlog.Debug(\"attempt to discover peers\")\n\t\tvar discoveries []peerdiscovery.Discovered\n\t\tvar wgDiscovery sync.WaitGroup\n\t\tvar dmux sync.Mutex\n\t\twgDiscovery.Add(2)\n\t\tgo func() {\n\t\t\tdefer wgDiscovery.Done()\n\t\t\tipv4discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{\n\t\t\t\tLimit:            1,\n\t\t\t\tPayload:          []byte(\"ok\"),\n\t\t\t\tDelay:            20 * time.Millisecond,\n\t\t\t\tTimeLimit:        200 * time.Millisecond,\n\t\t\t\tMulticastAddress: c.Options.MulticastAddress,\n\t\t\t\tStopChan:         c.stop.stopChan,\n\t\t\t})\n\t\t\tif err1 == nil && len(ipv4discoveries) > 0 {\n\t\t\t\tdmux.Lock()\n\t\t\t\terr = err1\n\t\t\t\tdiscoveries = append(discoveries, ipv4discoveries...)\n\t\t\t\tdmux.Unlock()\n\t\t\t}\n\t\t}()\n\t\tgo func() {\n\t\t\tdefer wgDiscovery.Done()\n\t\t\tipv6discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{\n\t\t\t\tLimit:     1,\n\t\t\t\tPayload:   []byte(\"ok\"),\n\t\t\t\tDelay:     20 * time.Millisecond,\n\t\t\t\tTimeLimit: 200 * time.Millisecond,\n\t\t\t\tIPVersion: peerdiscovery.IPv6,\n\t\t\t\tStopChan:  c.stop.stopChan,\n\t\t\t})\n\t\t\tif err1 == nil && len(ipv6discoveries) > 0 {\n\t\t\t\tdmux.Lock()\n\t\t\t\terr = err1\n\t\t\t\tdiscoveries = append(discoveries, ipv6discoveries...)\n\t\t\t\tdmux.Unlock()\n\t\t\t}\n\t\t}()\n\t\twgDiscovery.Wait()\n\n\t\tif err == nil && len(discoveries) > 0 {\n\t\t\tlog.Debugf(\"all discoveries: %+v\", discoveries)\n\t\t\tfor i := 0; i < len(discoveries); i++ {\n\t\t\t\tlog.Debugf(\"discovery %d has payload: %+v\", i, discoveries[i])\n\t\t\t\tif !bytes.HasPrefix(discoveries[i].Payload, []byte(\"croc\")) {\n\t\t\t\t\tlog.Debug(\"skipping discovery\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlog.Debug(\"switching to local\")\n\t\t\t\tportToUse := string(bytes.TrimPrefix(discoveries[i].Payload, []byte(\"croc\")))\n\t\t\t\tif portToUse == \"\" {\n\t\t\t\t\tportToUse = models.DEFAULT_PORT\n\t\t\t\t}\n\t\t\t\taddress := net.JoinHostPort(discoveries[i].Address, portToUse)\n\t\t\t\terrPing := tcp.PingServer(address)\n\t\t\t\tif errPing == nil {\n\t\t\t\t\tlog.Debugf(\"successfully pinged '%s'\", address)\n\t\t\t\t\tc.Options.RelayAddress = address\n\t\t\t\t\tc.ExternalIPConnected = c.Options.RelayAddress\n\t\t\t\t\tc.Options.RelayAddress6 = \"\"\n\t\t\t\t\tusingLocal = true\n\t\t\t\t\tbreak\n\t\t\t\t} else {\n\t\t\t\t\tlog.Debugf(\"could not ping: %+v\", errPing)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlog.Debugf(\"discoveries: %+v\", discoveries)\n\t\tlog.Debug(\"establishing connection\")\n\t}\n\tvar banner string\n\tdurations := []time.Duration{200 * time.Millisecond, 5 * time.Second}\n\terr = fmt.Errorf(\"found no addresses to connect\")\n\tfor i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} {\n\t\tif address == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tvar host, port string\n\t\thost, port, _ = net.SplitHostPort(address)\n\t\t// Default port to :9009\n\t\tif port == \"\" {\n\t\t\thost = address\n\t\t\tport = models.DEFAULT_PORT\n\t\t}\n\t\tlog.Debugf(\"got host '%v' and port '%v'\", host, port)\n\t\taddress = net.JoinHostPort(host, port)\n\t\tlog.Debugf(\"trying connection to %s\", address)\n\t\tc.conn[0], banner, c.ExternalIP, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RoomName, durations[i])\n\t\tif err == nil {\n\t\t\tc.Options.RelayAddress = address\n\t\t\tbreak\n\t\t}\n\t\tlog.Debugf(\"could not establish '%s'\", address)\n\t}\n\tif err != nil {\n\t\terr = fmt.Errorf(\"could not connect to %s: %w\", c.Options.RelayAddress, err)\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tlog.Debugf(\"receiver connection established: %+v\", c.conn[0])\n\tlog.Debugf(\"banner: %s\", banner)\n\n\tif c.Options.TestFlag {\n\t\tlog.Debugf(\"TEST FLAG ENABLED, TESTING LOCAL IPS\")\n\t}\n\tif c.Options.TestFlag || (!usingLocal && !c.Options.DisableLocal && !isIPset) {\n\t\t// ask the sender for their local ips and port\n\t\t// and try to connect to them\n\t\tvar ips []string\n\t\terr = func() (err error) {\n\t\t\tvar A *pake.Pake\n\t\t\tvar data []byte\n\t\t\tA, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdataMessage := SimpleMessage{\n\t\t\t\tBytes: A.Bytes(),\n\t\t\t\tKind:  \"pake1\",\n\t\t\t}\n\t\t\tdata, _ = json.Marshal(dataMessage)\n\t\t\tif err = c.conn[0].Send(data); err != nil {\n\t\t\t\tlog.Errorf(\"dataMessage send error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdata, err = c.conn[0].Receive()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\terr = json.Unmarshal(data, &dataMessage)\n\t\t\tif err != nil || dataMessage.Kind != \"pake2\" {\n\t\t\t\tlog.Debugf(\"data: %s\", data)\n\t\t\t\treturn fmt.Errorf(\"dataMessage %s pake failed\", ipRequest)\n\t\t\t}\n\t\t\terr = A.Update(dataMessage.Bytes)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar kA []byte\n\t\t\tkA, err = A.SessionKey()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Debugf(\"dataMessage kA: %x\", kA)\n\n\t\t\t// secure ipRequest\n\t\t\tdata, err = crypt.Encrypt([]byte(ipRequest), kA)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Debug(\"sending ips?\")\n\t\t\tif err = c.conn[0].Send(data); err != nil {\n\t\t\t\tlog.Errorf(\"ips send error: %v\", err)\n\t\t\t}\n\t\t\tdata, err = c.conn[0].Receive()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdata, err = crypt.Decrypt(data, kA)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Debugf(\"ips data: %s\", data)\n\t\t\tif err = json.Unmarshal(data, &ips); err != nil {\n\t\t\t\tlog.Debugf(\"ips unmarshal error: %v\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}()\n\n\t\tif len(ips) > 1 {\n\t\t\tport := ips[0]\n\t\t\tips = ips[1:]\n\t\t\tfor _, ip := range ips {\n\t\t\t\tipv4Addr, ipv4Net, errNet := net.ParseCIDR(fmt.Sprintf(\"%s/24\", ip))\n\t\t\t\tlog.Debugf(\"ipv4Add4: %+v, ipv4Net: %+v, err: %+v\", ipv4Addr, ipv4Net, errNet)\n\n\t\t\t\t// For peer-to-peer connectivity within a LAN, the sender and receiver don't need to be on the same subnet.\n\t\t\t\t// Even with NAT routers in their respective local networks,\n\t\t\t\t// a receiver behind NAT can establish direct access to the sender without requiring internet connectivity.\n\t\t\t\t// Conversely, the local networks on the sender and receiver may overlap but not be connected.\n\t\t\t\t// This often occurs with 192.168.0.0/30 and 192.168.1.0/30 subnets.\n\n\t\t\t\t// localIps, _ := utils.GetLocalIPs()\n\t\t\t\t// haveLocalIP := false\n\t\t\t\t// for _, localIP := range localIps {\n\t\t\t\t// \tlocalIPparsed := net.ParseIP(localIP)\n\t\t\t\t// \tlog.Debugf(\"localIP: %+v, localIPparsed: %+v\", localIP, localIPparsed)\n\t\t\t\t// \tif ipv4Net.Contains(localIPparsed) {\n\t\t\t\t// \t\thaveLocalIP = true\n\t\t\t\t// \t\tlog.Debugf(\"ip: %+v is a local IP\", ip)\n\t\t\t\t// \t\tbreak\n\t\t\t\t// \t}\n\t\t\t\t// }\n\t\t\t\t// if !haveLocalIP {\n\t\t\t\t// \tlog.Debugf(\"%s is not a local IP, skipping\", ip)\n\t\t\t\t// \tcontinue\n\t\t\t\t// }\n\n\t\t\t\tserverTry := net.JoinHostPort(ip, port)\n\t\t\t\tconn, banner2, externalIP, errConn := tcp.ConnectToTCPServer(serverTry, c.Options.RelayPassword, c.Options.RoomName, 500*time.Millisecond)\n\t\t\t\tif errConn != nil {\n\t\t\t\t\tlog.Debug(errConn)\n\t\t\t\t\tlog.Debug(\"could not connect to \" + serverTry)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlog.Debugf(\"local connection established to %s\", serverTry)\n\t\t\t\tlog.Debugf(\"banner: %s\", banner2)\n\t\t\t\t// reset to the local port\n\t\t\t\tbanner = banner2\n\t\t\t\tc.Options.RelayAddress = serverTry\n\t\t\t\tc.ExternalIP = externalIP\n\t\t\t\tc.conn[0].Close()\n\t\t\t\tc.conn[0] = nil\n\t\t\t\tc.conn[0] = conn\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif err = c.conn[0].Send(handshakeRequest); err != nil {\n\t\tlog.Errorf(\"handshake send error: %v\", err)\n\t}\n\tc.Options.RelayPorts = strings.Split(banner, \",\")\n\tif c.Options.NoMultiplexing {\n\t\tlog.Debug(\"no multiplexing\")\n\t\tc.Options.RelayPorts = []string{c.Options.RelayPorts[0]}\n\t}\n\tlog.Debug(\"exchanged header message\")\n\tfmt.Fprintf(os.Stderr, \"\\rsecuring channel...\")\n\terr = c.transfer()\n\tif err == nil {\n\t\tif c.numberOfTransferredFiles+len(c.EmptyFoldersToTransfer) == 0 {\n\t\t\tfmt.Fprintf(os.Stderr, \"\\rNo files transferred.\\n\")\n\t\t}\n\t} else {\n\t\tc.SendError()\n\t}\n\treturn\n}\n\nfunc (c *Client) transfer() (err error) {\n\t// connect to the server\n\n\t// quit with c.quit <- true\n\tc.quit = make(chan bool)\n\n\t// if recipient, initialize with sending pake information\n\tlog.Debug(\"ready\")\n\tif !c.Options.IsSender && !c.Step1ChannelSecured {\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType:   message.TypePAKE,\n\t\t\tBytes:  c.Pake.Bytes(),\n\t\t\tBytes2: []byte(c.Options.Curve),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\t// listen for incoming messages and process them\n\tfor {\n\t\tif e := c.ctxErr(); e != nil {\n\t\t\tlog.Tracef(\"transfer: %v\", e)\n\t\t\terr = e\n\t\t\tbreak\n\t\t}\n\t\tvar data []byte\n\t\tvar done bool\n\t\tdata, err = c.conn[0].Receive()\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"got error receiving: %v\", err)\n\t\t\tif !c.Step1ChannelSecured {\n\t\t\t\terr = fmt.Errorf(\"could not secure channel\")\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tdone, err = c.processMessage(data)\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"data: %s\", data)\n\t\t\tlog.Debugf(\"got error processing: %v\", err)\n\t\t\tbreak\n\t\t}\n\t\tif done {\n\t\t\tbreak\n\t\t}\n\t}\n\tif err := c.ctxErr(); err != nil && c.SuccessfulTransfer {\n\t\tc.SuccessfulTransfer = false\n\t\tlog.Tracef(\"SuccessfulTransfer: %v\", err)\n\t}\n\t// purge errors that come from successful transfer\n\tif c.SuccessfulTransfer {\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"purging error: %s\", err)\n\t\t}\n\t\terr = nil\n\t}\n\tif c.Options.IsSender && c.SuccessfulTransfer {\n\t\tfor _, file := range c.FilesToTransfer {\n\t\t\tif file.TempFile {\n\t\t\t\tfmt.Println(\"Removing \" + file.Name)\n\t\t\t\tos.Remove(file.Name)\n\t\t\t}\n\t\t}\n\t}\n\n\tif c.SuccessfulTransfer && !c.Options.IsSender {\n\t\tfor _, file := range c.FilesToTransfer {\n\t\t\tif file.TempFile {\n\t\t\t\tif unzipErr := utils.UnzipDirectory(\".\", file.Name); unzipErr != nil {\n\t\t\t\t\tc.SuccessfulTransfer = false\n\t\t\t\t\terr = fmt.Errorf(\"failed to unzip received archive %s: %w\", file.Name, unzipErr)\n\t\t\t\t\tlog.Error(err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif removeErr := os.Remove(file.Name); removeErr != nil {\n\t\t\t\t\tlog.Warnf(\"error removing %s: %v\", file.Name, removeErr)\n\t\t\t\t} else {\n\t\t\t\t\tlog.Debugf(\"Removing %s\\n\", file.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif c.Options.Stdout && !c.Options.IsSender && len(c.FilesToTransfer) > 0 && c.FilesToTransferCurrentNum < len(c.FilesToTransfer) {\n\t\tpathToFile := path.Join(\n\t\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,\n\t\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].Name,\n\t\t)\n\t\tlog.Debugf(\"pathToFile: %s\", pathToFile)\n\t\t// close if not closed already\n\t\tif !c.CurrentFileIsClosed {\n\t\t\tc.CurrentFile.Close()\n\t\t\tc.CurrentFileIsClosed = true\n\t\t}\n\t\tif err = os.Remove(pathToFile); err != nil {\n\t\t\tlog.Warnf(\"error removing %s: %v\", pathToFile, err)\n\t\t}\n\t\tfmt.Fprint(os.Stderr, \"\\n\")\n\t}\n\tif err != nil && strings.Contains(err.Error(), \"pake not successful\") {\n\t\tlog.Debugf(\"pake error: %s\", err.Error())\n\t\terr = fmt.Errorf(\"password mismatch\")\n\t}\n\tif err != nil && strings.Contains(err.Error(), \"unexpected end of JSON input\") {\n\t\tlog.Debugf(\"error: %s\", err.Error())\n\t\terr = fmt.Errorf(\"room (secure channel) not ready, maybe peer disconnected\")\n\t}\n\tif err != nil {\n\t\tc.SendError()\n\t}\n\treturn\n}\n\nfunc (c *Client) createEmptyFolder(i int) (err error) {\n\terr = os.MkdirAll(c.EmptyFoldersToTransfer[i].FolderRemote, os.ModePerm)\n\tif err != nil {\n\t\treturn\n\t}\n\tfmt.Fprintf(os.Stderr, \"%s\\n\", c.EmptyFoldersToTransfer[i].FolderRemote)\n\tc.bar = progressbar.NewOptions64(1,\n\t\tprogressbar.OptionOnCompletion(func() {\n\t\t\tc.fmtPrintUpdate()\n\t\t}),\n\t\tprogressbar.OptionSetWidth(20),\n\t\tprogressbar.OptionSetDescription(\" \"),\n\t\tprogressbar.OptionSetRenderBlankState(true),\n\t\tprogressbar.OptionShowBytes(true),\n\t\tprogressbar.OptionShowCount(),\n\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\tprogressbar.OptionSetVisibility(!c.Options.SendingText),\n\t)\n\tc.bar.Finish()\n\treturn\n}\n\nfunc (c *Client) processMessageFileInfo(m message.Message) (done bool, err error) {\n\tvar senderInfo SenderInfo\n\terr = json.Unmarshal(m.Bytes, &senderInfo)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tc.Options.SendingText = senderInfo.SendingText\n\tc.Options.NoCompress = senderInfo.NoCompress\n\tc.Options.HashAlgorithm = senderInfo.HashAlgorithm\n\tc.EmptyFoldersToTransfer = senderInfo.EmptyFoldersToTransfer\n\tc.TotalNumberFolders = senderInfo.TotalNumberFolders\n\tc.FilesToTransfer = senderInfo.FilesToTransfer\n\tfor i, fi := range c.FilesToTransfer {\n\t\t// Issues #593 - sanitize the sender paths and prevent \"..\" from being used\n\t\tc.FilesToTransfer[i].FolderRemote = filepath.Clean(fi.FolderRemote)\n\t\t// Issues #593 - disallow specific folders like .ssh\n\t\tif strings.Contains(c.FilesToTransfer[i].FolderRemote, \".ssh\") {\n\t\t\treturn true, fmt.Errorf(\"invalid path detected: '%s'\", fi.FolderRemote)\n\t\t}\n\t\t// Issue #595 - disallow filenames with invisible characters\n\t\terrFileName := utils.ValidFileName(path.Join(c.FilesToTransfer[i].FolderRemote, fi.Name))\n\t\tif errFileName != nil {\n\t\t\treturn true, errFileName\n\t\t}\n\t}\n\tc.TotalNumberOfContents = 0\n\tif c.FilesToTransfer != nil {\n\t\tc.TotalNumberOfContents += len(c.FilesToTransfer)\n\t}\n\tif c.EmptyFoldersToTransfer != nil {\n\t\tc.TotalNumberOfContents += len(c.EmptyFoldersToTransfer)\n\t}\n\n\tif c.Options.HashAlgorithm == \"\" {\n\t\tc.Options.HashAlgorithm = \"xxhash\"\n\t}\n\tlog.Debugf(\"using hash algorithm: %s\", c.Options.HashAlgorithm)\n\tif c.Options.NoCompress {\n\t\tlog.Debug(\"disabling compression\")\n\t}\n\tif c.Options.SendingText {\n\t\tc.Options.Stdout = true\n\t}\n\n\tfname := fmt.Sprintf(\"%d files\", len(c.FilesToTransfer))\n\tfolderName := fmt.Sprintf(\"%d folders\", c.TotalNumberFolders)\n\tif len(c.FilesToTransfer) == 1 {\n\t\tfname = fmt.Sprintf(\"'%s'\", c.FilesToTransfer[0].Name)\n\t}\n\ttotalSize := int64(0)\n\tfor i, fi := range c.FilesToTransfer {\n\t\ttotalSize += fi.Size\n\t\tif len(fi.Name) > c.longestFilename {\n\t\t\tc.longestFilename = len(fi.Name)\n\t\t}\n\t\tif strings.HasPrefix(fi.Name, \"croc-stdin-\") && c.Options.SendingText {\n\t\t\tc.FilesToTransfer[i].Name, err = utils.RandomFileName()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\t// check the totalSize does not exceed disk space\n\t// usage := diskusage.NewDiskUsage(\".\")\n\t// if usage.Available() < uint64(totalSize) {\n\t// \treturn true, fmt.Errorf(\"not enough disk space\")\n\t// }\n\n\t// c.spinner.Stop()\n\taction := \"Accept\"\n\tif c.Options.SendingText {\n\t\taction = \"Display\"\n\t\tfname = \"text message\"\n\t}\n\tif !c.Options.NoPrompt || c.Options.Ask || senderInfo.Ask {\n\t\tif c.Options.Ask || senderInfo.Ask {\n\t\t\tmachID, _ := machineid.ID()\n\t\t\tfmt.Fprintf(os.Stderr, \"\\rYour machine id is '%s'.\\n%s %s (%s) from '%s'? (Y/n) \", machID, action, fname, utils.ByteCountDecimal(totalSize), senderInfo.MachineID)\n\t\t} else {\n\t\t\tif c.TotalNumberFolders > 0 {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\r%s %s and %s (%s)? (Y/n) \", action, fname, folderName, utils.ByteCountDecimal(totalSize))\n\t\t\t} else {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\r%s %s (%s)? (Y/n) \", action, fname, utils.ByteCountDecimal(totalSize))\n\t\t\t}\n\t\t}\n\t\tchoice := strings.ToLower(utils.GetInput(\"\"))\n\t\tif choice != \"\" && choice != \"y\" && choice != \"yes\" {\n\t\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\t\tType:    message.TypeError,\n\t\t\t\tMessage: \"refusing files\",\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\treturn true, fmt.Errorf(\"refused files\")\n\t\t}\n\t} else {\n\t\tfmt.Fprintf(os.Stderr, \"\\rReceiving %s (%s) \\n\", fname, utils.ByteCountDecimal(totalSize))\n\t}\n\tfmt.Fprintf(os.Stderr, \"\\nReceiving (<-%s)\\n\", c.ExternalIPConnected)\n\n\tfor i := 0; i < len(c.EmptyFoldersToTransfer); i += 1 {\n\t\t_, errExists := os.Stat(c.EmptyFoldersToTransfer[i].FolderRemote)\n\t\tif os.IsNotExist(errExists) {\n\t\t\terr = c.createEmptyFolder(i)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tisEmpty, _ := isEmptyFolder(c.EmptyFoldersToTransfer[i].FolderRemote)\n\t\t\tif !isEmpty {\n\t\t\t\tlog.Debug(\"asking to overwrite\")\n\t\t\t\tprompt := fmt.Sprintf(\"\\n%s already has some content in it. \\nDo you want\"+\n\t\t\t\t\t\" to overwrite it with an empty folder? (y/N) \", c.EmptyFoldersToTransfer[i].FolderRemote)\n\t\t\t\tchoice := strings.ToLower(utils.GetInput(prompt))\n\t\t\t\tif choice == \"y\" || choice == \"yes\" {\n\t\t\t\t\terr = c.createEmptyFolder(i)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// if no files are to be transferred, then we can end the file transfer process\n\tif c.FilesToTransfer == nil {\n\t\tc.SuccessfulTransfer = true\n\t\tc.Step3RecipientRequestFile = true\n\t\tc.Step4FileTransferred = true\n\t\terrStopTransfer := message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType: message.TypeFinished,\n\t\t})\n\t\tif errStopTransfer != nil {\n\t\t\terr = errStopTransfer\n\t\t}\n\t}\n\tlog.Debug(c.FilesToTransfer)\n\tc.Step2FileInfoTransferred = true\n\treturn\n}\n\nfunc (c *Client) processMessagePake(m message.Message) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif c.stop.gui {\n\t\t\t\tlog.Errorf(\"panic: %v\", r)\n\t\t\t\tc.stop.Cancel()\n\t\t\t} else {\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}\n\t}()\n\tlog.Debug(\"received pake payload\")\n\n\tvar salt []byte\n\tif c.Options.IsSender {\n\t\t// initialize curve based on the recipient's choice\n\t\tlog.Debugf(\"using curve %s\", string(m.Bytes2))\n\t\tc.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, string(m.Bytes2))\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\t// update the pake\n\t\terr = c.Pake.Update(m.Bytes)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// generate salt and send it back to recipient\n\t\tlog.Debug(\"generating salt\")\n\t\tsalt = make([]byte, 8)\n\t\tif _, rerr := rand.Read(salt); err != nil {\n\t\t\tlog.Errorf(\"can't generate random numbers: %v\", rerr)\n\t\t\treturn\n\t\t}\n\t\tlog.Debug(\"sender sending pake+salt\")\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType:   message.TypePAKE,\n\t\t\tBytes:  c.Pake.Bytes(),\n\t\t\tBytes2: salt,\n\t\t})\n\t} else {\n\t\terr = c.Pake.Update(m.Bytes)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tsalt = m.Bytes2\n\t}\n\t// generate key\n\tkey, err := c.Pake.SessionKey()\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.Key, _, err = crypt.New(key, salt)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Debugf(\"generated key = %+x with salt %x\", c.Key, salt)\n\n\t// connects to the other ports of the server for transfer\n\tvar wg sync.WaitGroup\n\twg.Add(len(c.Options.RelayPorts))\n\tfor i := 0; i < len(c.Options.RelayPorts); i++ {\n\t\tlog.Debugf(\"port: [%s]\", c.Options.RelayPorts[i])\n\t\tgo func(j int) {\n\t\t\tdefer wg.Done()\n\t\t\tvar host string\n\t\t\tif c.Options.RelayAddress == \"127.0.0.1\" {\n\t\t\t\thost = c.Options.RelayAddress\n\t\t\t} else {\n\t\t\t\thost, _, err = net.SplitHostPort(c.Options.RelayAddress)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"bad relay address %s\", c.Options.RelayAddress)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tserver := net.JoinHostPort(host, c.Options.RelayPorts[j])\n\t\t\tlog.Debugf(\"connecting to %s\", server)\n\t\t\tc.conn[j+1], _, _, err = tcp.ConnectToTCPServer(\n\t\t\t\tserver,\n\t\t\t\tc.Options.RelayPassword,\n\t\t\t\tfmt.Sprintf(\"%s-%d\", c.Options.RoomName, j),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tlog.Debugf(\"connected to %s\", server)\n\t\t\tif !c.Options.IsSender {\n\t\t\t\tgo c.receiveData(j)\n\t\t\t}\n\t\t}(i)\n\t}\n\twg.Wait()\n\tif !c.Options.IsSender {\n\t\tlog.Debug(\"sending external IP\")\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType:    message.TypeExternalIP,\n\t\t\tMessage: c.ExternalIP,\n\t\t\tBytes:   m.Bytes,\n\t\t})\n\t}\n\treturn\n}\n\nfunc (c *Client) processExternalIP(m message.Message) (done bool, err error) {\n\tlog.Debugf(\"received external IP: %+v\", m)\n\tif c.Options.IsSender {\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType:    message.TypeExternalIP,\n\t\t\tMessage: c.ExternalIP,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn true, err\n\t\t}\n\t}\n\tif c.ExternalIPConnected == \"\" {\n\t\t// it can be preset by the local relay\n\t\tc.ExternalIPConnected = m.Message\n\t}\n\tlog.Debugf(\"connected as %s -> %s\", c.ExternalIP, c.ExternalIPConnected)\n\tc.Step1ChannelSecured = true\n\treturn\n}\n\nfunc (c *Client) processMessage(payload []byte) (done bool, err error) {\n\tm, err := message.Decode(c.Key, payload)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"problem with decoding: %w\", err)\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\n\t// only \"pake\" messages should be unencrypted\n\t// if a non-\"pake\" message is received unencrypted something\n\t// is weird\n\tif m.Type != message.TypePAKE && c.Key == nil {\n\t\terr = fmt.Errorf(\"unencrypted communication rejected\")\n\t\tdone = true\n\t\treturn\n\t}\n\n\tswitch m.Type {\n\tcase message.TypeFinished:\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType: message.TypeFinished,\n\t\t})\n\t\tdone = true\n\t\tc.SuccessfulTransfer = true\n\t\treturn\n\tcase message.TypePAKE:\n\t\terr = c.processMessagePake(m)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"pake not successful: %w\", err)\n\t\t\tlog.Debug(err)\n\t\t}\n\tcase message.TypeExternalIP:\n\t\tdone, err = c.processExternalIP(m)\n\tcase message.TypeError:\n\t\t// c.spinner.Stop()\n\t\tlog.Trace(\"Peer initiates interruption of my loops and goroutines\")\n\t\tc.stop.Cancel()\n\t\tfmt.Print(\"\\r\")\n\t\terr = fmt.Errorf(\"peer error: %s\", m.Message)\n\t\treturn true, err\n\tcase message.TypeFileInfo:\n\t\tdone, err = c.processMessageFileInfo(m)\n\tcase message.TypeRecipientReady:\n\t\tvar remoteFile RemoteFileRequest\n\t\terr = json.Unmarshal(m.Bytes, &remoteFile)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tc.FilesToTransferCurrentNum = remoteFile.FilesToTransferCurrentNum\n\t\tc.CurrentFileChunkRanges = remoteFile.CurrentFileChunkRanges\n\t\tc.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges)\n\t\tlog.Debugf(\"current file chunks: %+v\", c.CurrentFileChunks)\n\t\tc.mutex.Lock()\n\t\tc.chunkMap = make(map[uint64]struct{})\n\t\tfor _, chunk := range c.CurrentFileChunks {\n\t\t\tc.chunkMap[uint64(chunk)] = struct{}{}\n\t\t}\n\t\tc.mutex.Unlock()\n\t\tc.Step3RecipientRequestFile = true\n\n\t\tif c.Options.Ask {\n\t\t\tfmt.Fprintf(os.Stderr, \"Send to machine '%s'? (Y/n) \", remoteFile.MachineID)\n\t\t\tchoice := strings.ToLower(utils.GetInput(\"\"))\n\t\t\tif choice != \"\" && choice != \"y\" && choice != \"yes\" {\n\t\t\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\t\t\tType:    message.TypeError,\n\t\t\t\t\tMessage: \"refusing files\",\n\t\t\t\t})\n\t\t\t\tdone = true\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\tcase message.TypeCloseSender:\n\t\tc.bar.Finish()\n\t\tlog.Debug(\"close-sender received...\")\n\t\tc.Step4FileTransferred = false\n\t\tc.Step3RecipientRequestFile = false\n\t\tlog.Debug(\"sending close-recipient\")\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType: message.TypeCloseRecipient,\n\t\t})\n\tcase message.TypeCloseRecipient:\n\t\tc.Step4FileTransferred = false\n\t\tc.Step3RecipientRequestFile = false\n\t}\n\tif err != nil {\n\t\tlog.Debugf(\"got error from processing message: %v\", err)\n\t\treturn\n\t}\n\terr = c.updateState()\n\tif err != nil {\n\t\tlog.Debugf(\"got error from updating state: %v\", err)\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (c *Client) updateIfSenderChannelSecured() (err error) {\n\tif c.Options.IsSender && c.Step1ChannelSecured && !c.Step2FileInfoTransferred {\n\t\tvar b []byte\n\t\tmachID, _ := machineid.ID()\n\t\tb, err = json.Marshal(SenderInfo{\n\t\t\tFilesToTransfer:        c.FilesToTransfer,\n\t\t\tEmptyFoldersToTransfer: c.EmptyFoldersToTransfer,\n\t\t\tMachineID:              machID,\n\t\t\tAsk:                    c.Options.Ask,\n\t\t\tTotalNumberFolders:     c.TotalNumberFolders,\n\t\t\tSendingText:            c.Options.SendingText,\n\t\t\tNoCompress:             c.Options.NoCompress,\n\t\t\tHashAlgorithm:          c.Options.HashAlgorithm,\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType:  message.TypeFileInfo,\n\t\t\tBytes: b,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tc.Step2FileInfoTransferred = true\n\t}\n\treturn\n}\n\nfunc (c *Client) recipientInitializeFile() (err error) {\n\t// start initiating the process to receive a new file\n\tlog.Debugf(\"working on file %d\", c.FilesToTransferCurrentNum)\n\n\t// recipient sets the file\n\tpathToFile := path.Join(\n\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,\n\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].Name,\n\t)\n\tfolderForFile, _ := filepath.Split(pathToFile)\n\tfolderForFileBase := filepath.Base(folderForFile)\n\tif folderForFileBase != \".\" && folderForFileBase != \"\" {\n\t\tif err := os.MkdirAll(folderForFile, os.ModePerm); err != nil {\n\t\t\tlog.Errorf(\"can't create %s: %v\", folderForFile, err)\n\t\t}\n\t}\n\tvar errOpen error\n\tc.CurrentFile, errOpen = os.OpenFile(\n\t\tpathToFile,\n\t\tos.O_WRONLY, 0o666)\n\tvar truncate bool // default false\n\tc.CurrentFileChunks = []int64{}\n\tc.CurrentFileChunkRanges = []int64{}\n\tif errOpen == nil {\n\t\tstat, _ := c.CurrentFile.Stat()\n\t\ttruncate = stat.Size() != c.FilesToTransfer[c.FilesToTransferCurrentNum].Size\n\t\tif !truncate {\n\t\t\t// recipient requests the file and chunks (if empty, then should receive all chunks)\n\t\t\t// TODO: determine the missing chunks\n\t\t\tc.CurrentFileChunkRanges = utils.MissingChunks(\n\t\t\t\tpathToFile,\n\t\t\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].Size,\n\t\t\t\tmodels.TCP_BUFFER_SIZE/2,\n\t\t\t)\n\t\t}\n\t} else {\n\t\tc.CurrentFile, errOpen = os.Create(pathToFile)\n\t\tif errOpen != nil {\n\t\t\terrOpen = fmt.Errorf(\"could not create %s: %w\", pathToFile, errOpen)\n\t\t\tlog.Error(errOpen)\n\t\t\treturn errOpen\n\t\t}\n\t\terrChmod := os.Chmod(pathToFile, c.FilesToTransfer[c.FilesToTransferCurrentNum].Mode.Perm())\n\t\tif errChmod != nil {\n\t\t\tlog.Error(errChmod)\n\t\t}\n\t\ttruncate = true\n\t}\n\tif truncate {\n\t\terr := c.CurrentFile.Truncate(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"could not truncate %s: %w\", pathToFile, err)\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *Client) recipientGetFileReady(finished bool) (err error) {\n\tif finished {\n\t\t// TODO: do the last finishing stuff\n\t\tlog.Debug(\"finished\")\n\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType: message.TypeFinished,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tc.SuccessfulTransfer = true\n\t\tc.FilesHasFinished[c.FilesToTransferCurrentNum] = struct{}{}\n\t\treturn\n\t}\n\n\terr = c.recipientInitializeFile()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tc.TotalSent = 0\n\tc.CurrentFileIsClosed = false\n\tmachID, _ := machineid.ID()\n\tbRequest, _ := json.Marshal(RemoteFileRequest{\n\t\tCurrentFileChunkRanges:    c.CurrentFileChunkRanges,\n\t\tFilesToTransferCurrentNum: c.FilesToTransferCurrentNum,\n\t\tMachineID:                 machID,\n\t})\n\tlog.Debug(\"converting to chunk range\")\n\tc.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges)\n\n\tif !finished {\n\t\t// setup the progressbar\n\t\tc.setBar()\n\t}\n\n\tlog.Debugf(\"sending recipient ready with %d chunks\", len(c.CurrentFileChunks))\n\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\tType:  message.TypeRecipientReady,\n\t\tBytes: bRequest,\n\t})\n\tif err != nil {\n\t\treturn\n\t}\n\tc.Step3RecipientRequestFile = true\n\treturn\n}\n\nfunc formatDescription(description string) string {\n\tconst (\n\t\t// Reserve extra room for variable progress metadata such as [elapsed:remaining].\n\t\tprogressMetaWidth = 78\n\t\tminDescription    = 12\n\t\tdefaultTermWidth  = 80\n\t)\n\n\twidth, _, err := term.GetSize(int(os.Stderr.Fd()))\n\tif err != nil || width <= 0 {\n\t\twidth, _, err = term.GetSize(int(os.Stdout.Fd()))\n\t}\n\tif err != nil || width <= 0 {\n\t\tif envColumns, convErr := strconv.Atoi(os.Getenv(\"COLUMNS\")); convErr == nil && envColumns > 0 {\n\t\t\twidth = envColumns\n\t\t} else {\n\t\t\twidth = defaultTermWidth\n\t\t}\n\t}\n\n\tmaxDescription := width - progressMetaWidth\n\tif maxDescription < minDescription {\n\t\tmaxDescription = minDescription\n\t}\n\n\trunes := []rune(description)\n\tif len(runes) > maxDescription {\n\t\tif maxDescription <= 3 {\n\t\t\treturn string(runes[:maxDescription])\n\t\t}\n\t\treturn string(runes[:maxDescription-3]) + \"...\"\n\t}\n\treturn description\n}\n\nfunc (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (err error) {\n\tlog.Debugf(\"touching file with folder / name\")\n\tif !utils.Exists(fileInfo.FolderRemote) {\n\t\terr = os.MkdirAll(fileInfo.FolderRemote, os.ModePerm)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t}\n\tpathToFile := path.Join(fileInfo.FolderRemote, fileInfo.Name)\n\tif fileInfo.Symlink != \"\" {\n\t\tlog.Debug(\"creating symlink\")\n\t\t// remove symlink if it exists\n\t\tif _, errExists := os.Lstat(pathToFile); errExists == nil {\n\t\t\tos.Remove(pathToFile)\n\t\t}\n\t\terr = os.Symlink(fileInfo.Symlink, pathToFile)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t} else {\n\t\temptyFile, errCreate := os.Create(pathToFile)\n\t\tif errCreate != nil {\n\t\t\tlog.Error(errCreate)\n\t\t\terr = errCreate\n\t\t\treturn\n\t\t}\n\t\temptyFile.Close()\n\t}\n\t// setup the progressbar\n\tdescription := fmt.Sprintf(\"%-*s\", c.longestFilename, c.FilesToTransfer[i].Name)\n\tif len(c.FilesToTransfer) == 1 {\n\t\tdescription = c.FilesToTransfer[i].Name\n\t\t// description = \"\"\n\t} else {\n\t\tdescription = \" \" + description\n\t}\n\tc.bar = progressbar.NewOptions64(1,\n\t\tprogressbar.OptionOnCompletion(func() {\n\t\t\tc.fmtPrintUpdate()\n\t\t}),\n\t\tprogressbar.OptionSetWidth(20),\n\t\tprogressbar.OptionSetDescription(formatDescription(description)),\n\t\tprogressbar.OptionSetRenderBlankState(true),\n\t\tprogressbar.OptionShowBytes(true),\n\t\tprogressbar.OptionShowCount(),\n\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\tprogressbar.OptionSetVisibility(!c.Options.SendingText),\n\t)\n\tc.bar.Finish()\n\treturn\n}\n\nfunc (c *Client) updateIfRecipientHasFileInfo() (err error) {\n\tif c.Options.IsSender || !c.Step2FileInfoTransferred || c.Step3RecipientRequestFile {\n\t\treturn\n\t}\n\t// find the next file to transfer and send that number\n\t// if the files are the same size, then look for missing chunks\n\tfinished := true\n\tfor i, fileInfo := range c.FilesToTransfer {\n\t\tif _, ok := c.FilesHasFinished[i]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tif i < c.FilesToTransferCurrentNum {\n\t\t\tcontinue\n\t\t}\n\t\tlog.Debugf(\"checking %+v\", fileInfo)\n\t\trecipientFileInfo, errRecipientFile := os.Lstat(path.Join(fileInfo.FolderRemote, fileInfo.Name))\n\t\tvar errHash error\n\t\tvar fileHash []byte\n\t\tif errRecipientFile == nil && recipientFileInfo.Size() == fileInfo.Size {\n\t\t\t// the file exists, but is same size, so hash it\n\t\t\tfileHash, errHash = utils.HashFile(path.Join(fileInfo.FolderRemote, fileInfo.Name), c.Options.HashAlgorithm)\n\t\t}\n\t\tif fileInfo.Size == 0 || fileInfo.Symlink != \"\" {\n\t\t\terr = c.createEmptyFileAndFinish(fileInfo, i)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tc.numberOfTransferredFiles++\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tlog.Debugf(\"%s %+x %+x %+v\", fileInfo.Name, fileHash, fileInfo.Hash, errHash)\n\t\tif !bytes.Equal(fileHash, fileInfo.Hash) {\n\t\t\tlog.Debugf(\"hashed %s to %x using %s\", fileInfo.Name, fileHash, c.Options.HashAlgorithm)\n\t\t\tlog.Debugf(\"hashes are not equal %x != %x\", fileHash, fileInfo.Hash)\n\t\t\tif errHash == nil && !c.Options.Overwrite && errRecipientFile == nil && !strings.HasPrefix(fileInfo.Name, \"croc-stdin-\") && !c.Options.SendingText {\n\n\t\t\t\tmissingChunks := utils.ChunkRangesToChunks(utils.MissingChunks(\n\t\t\t\t\tpath.Join(fileInfo.FolderRemote, fileInfo.Name),\n\t\t\t\t\tfileInfo.Size,\n\t\t\t\t\tmodels.TCP_BUFFER_SIZE/2,\n\t\t\t\t))\n\t\t\t\tpercentDone := 100 - float64(len(missingChunks)*models.TCP_BUFFER_SIZE/2)/float64(fileInfo.Size)*100\n\n\t\t\t\tlog.Debug(\"asking to overwrite\")\n\t\t\t\tprompt := fmt.Sprintf(\"\\nOverwrite '%s'? (y/N) (use --overwrite to omit) \", path.Join(fileInfo.FolderRemote, fileInfo.Name))\n\t\t\t\tif percentDone < 99 {\n\t\t\t\t\tprompt = fmt.Sprintf(\"\\nResume '%s' (%2.1f%%)? (y/N)   (use --overwrite to omit) \", path.Join(fileInfo.FolderRemote, fileInfo.Name), percentDone)\n\t\t\t\t}\n\t\t\t\tchoice := strings.ToLower(utils.GetInput(prompt))\n\t\t\t\tif choice != \"y\" && choice != \"yes\" {\n\t\t\t\t\tfmt.Fprintf(os.Stderr, \"Skipping '%s'\\n\", path.Join(fileInfo.FolderRemote, fileInfo.Name))\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Debugf(\"hashes are equal %x == %x\", fileHash, fileInfo.Hash)\n\n\t\t\tif !fileInfo.ModTime.IsZero() {\n\t\t\t\tif err := os.Chtimes(path.Join(fileInfo.FolderRemote, fileInfo.Name), fileInfo.ModTime, fileInfo.ModTime); err != nil {\n\t\t\t\t\tlog.Warnf(\"chtimes %v: %v\", fileInfo.ModTime, err)\n\t\t\t\t} else {\n\t\t\t\t\tlog.Debugf(\"chtimes %v\", fileInfo.ModTime)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif errHash != nil {\n\t\t\t// probably can't find, its okay\n\t\t\tlog.Debug(errHash)\n\t\t}\n\t\tif errHash != nil || !bytes.Equal(fileHash, fileInfo.Hash) {\n\t\t\tfinished = false\n\t\t\tc.FilesToTransferCurrentNum = i\n\t\t\tc.numberOfTransferredFiles++\n\t\t\tnewFolder, _ := filepath.Split(fileInfo.FolderRemote)\n\t\t\tif newFolder != c.LastFolder && len(c.FilesToTransfer) > 0 && !c.Options.SendingText && newFolder != \"./\" {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\r%s\\n\", newFolder)\n\t\t\t}\n\t\t\tc.LastFolder = newFolder\n\t\t\tbreak\n\t\t}\n\t}\n\tc.recipientGetFileReady(finished)\n\treturn\n}\n\nfunc (c *Client) fmtPrintUpdate() {\n\tc.finishedNum++\n\tif c.TotalNumberOfContents > 1 {\n\t\tfmt.Fprintf(os.Stderr, \" %d/%d\\n\", c.finishedNum, c.TotalNumberOfContents)\n\t} else {\n\t\tfmt.Fprintf(os.Stderr, \"\\n\")\n\t}\n}\n\nfunc (c *Client) updateState() (err error) {\n\terr = c.updateIfSenderChannelSecured()\n\tif err != nil {\n\t\treturn\n\t}\n\n\terr = c.updateIfRecipientHasFileInfo()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif c.Options.IsSender && c.Step3RecipientRequestFile && !c.Step4FileTransferred {\n\t\tlog.Debug(\"start sending data!\")\n\n\t\tif !c.firstSend {\n\t\t\tfmt.Fprintf(os.Stderr, \"\\nSending (->%s)\\n\", c.ExternalIPConnected)\n\t\t\tc.firstSend = true\n\t\t\t// if there are empty files, show them as already have been transferred now\n\t\t\tfor i := range c.FilesToTransfer {\n\t\t\t\tif c.FilesToTransfer[i].Size == 0 {\n\t\t\t\t\t// setup the progressbar and takedown the progress bar for empty files\n\t\t\t\t\tdescription := fmt.Sprintf(\"%-*s\", c.longestFilename, c.FilesToTransfer[i].Name)\n\t\t\t\t\tif len(c.FilesToTransfer) == 1 {\n\t\t\t\t\t\tdescription = c.FilesToTransfer[i].Name\n\t\t\t\t\t\t// description = \"\"\n\t\t\t\t\t}\n\n\t\t\t\t\tc.bar = progressbar.NewOptions64(1,\n\t\t\t\t\t\tprogressbar.OptionOnCompletion(func() {\n\t\t\t\t\t\t\tc.fmtPrintUpdate()\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tprogressbar.OptionSetWidth(20),\n\t\t\t\t\t\tprogressbar.OptionSetDescription(formatDescription(description)),\n\t\t\t\t\t\tprogressbar.OptionSetRenderBlankState(true),\n\t\t\t\t\t\tprogressbar.OptionShowBytes(true),\n\t\t\t\t\t\tprogressbar.OptionShowCount(),\n\t\t\t\t\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\t\t\t\t\tprogressbar.OptionSetVisibility(!c.Options.SendingText),\n\t\t\t\t\t)\n\t\t\t\t\tc.bar.Finish()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tc.Step4FileTransferred = true\n\t\t// setup the progressbar\n\t\tc.setBar()\n\t\tc.TotalSent = 0\n\t\tc.CurrentFileIsClosed = false\n\t\tlog.Debug(\"beginning sending comms\")\n\t\tpathToFile := path.Join(\n\t\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].FolderSource,\n\t\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].Name,\n\t\t)\n\t\tc.fread, err = os.Open(pathToFile)\n\t\tc.numfinished = 0\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tfor i := 0; i < len(c.Options.RelayPorts); i++ {\n\t\t\tlog.Debugf(\"starting sending over comm %d\", i)\n\t\t\tgo c.sendData(i)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *Client) setBar() {\n\tdescription := fmt.Sprintf(\"%-*s\", c.longestFilename, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name)\n\tfolder, _ := filepath.Split(c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote)\n\tif folder == \"./\" {\n\t\tdescription = c.FilesToTransfer[c.FilesToTransferCurrentNum].Name\n\t} else if !c.Options.IsSender {\n\t\tdescription = \" \" + description\n\t}\n\tc.bar = progressbar.NewOptions64(\n\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].Size,\n\t\tprogressbar.OptionOnCompletion(func() {\n\t\t\tc.fmtPrintUpdate()\n\t\t}),\n\t\tprogressbar.OptionSetWidth(20),\n\t\tprogressbar.OptionSetDescription(formatDescription(description)),\n\t\tprogressbar.OptionSetRenderBlankState(true),\n\t\tprogressbar.OptionShowBytes(true),\n\t\tprogressbar.OptionShowCount(),\n\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\tprogressbar.OptionThrottle(100*time.Millisecond),\n\t\tprogressbar.OptionSetVisibility(!c.Options.SendingText),\n\t)\n\tbyteToDo := int64(len(c.CurrentFileChunks) * models.TCP_BUFFER_SIZE / 2)\n\tif byteToDo > 0 {\n\t\tbytesDone := c.FilesToTransfer[c.FilesToTransferCurrentNum].Size - byteToDo\n\t\tlog.Debug(byteToDo)\n\t\tlog.Debug(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)\n\t\tlog.Debug(bytesDone)\n\t\tif bytesDone > 0 {\n\t\t\tc.bar.Add64(bytesDone)\n\t\t}\n\t}\n}\n\nfunc (c *Client) receiveData(i int) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif c.stop.gui {\n\t\t\t\tlog.Errorf(\"panic: %v\", r)\n\t\t\t\tc.stop.Cancel()\n\t\t\t} else {\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}\n\t}()\n\tlog.Tracef(\"%d receiving data\", i)\n\tfor {\n\t\tdata, err := c.conn[i+1].Receive()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tlog.Trace(\"got ping\")\n\t\t\tcontinue\n\t\t}\n\n\t\tdata, err = crypt.Decrypt(data, c.Key)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif !c.Options.NoCompress {\n\t\t\tdata = compress.Decompress(data)\n\t\t}\n\n\t\t// get position\n\t\tvar position uint64\n\t\trbuf := bytes.NewReader(data[:8])\n\t\terr = binary.Read(rbuf, binary.LittleEndian, &position)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tpositionInt64 := int64(position)\n\n\t\tc.mutex.Lock()\n\t\tif c.CurrentFileIsClosed || c.CurrentFile == nil {\n\t\t\tc.mutex.Unlock()\n\t\t\tlog.Tracef(\"was closed %d\", i)\n\t\t\treturn\n\t\t}\n\t\tif err := c.ctxErr(); err != nil {\n\t\t\tc.CurrentFileIsClosed = true\n\t\t\tdefer c.mutex.Unlock()\n\t\t\tlog.Tracef(\"stopping: %v\", err)\n\t\t\tif err := c.CurrentFile.Close(); err != nil {\n\t\t\t\tlog.Tracef(\"closing %s: %v\", c.CurrentFile.Name(), err)\n\t\t\t} else {\n\t\t\t\tlog.Tracef(\"Successful closing %s\", c.CurrentFile.Name())\n\t\t\t}\n\t\t\tlog.Tracef(\"sending close-sender\")\n\t\t\tif sendErr := message.Send(c.conn[0], c.Key, message.Message{\n\t\t\t\tType: message.TypeCloseSender,\n\t\t\t}); sendErr != nil {\n\t\t\t\tlog.Tracef(\"sending close-sender: %v\", sendErr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\t_, err = c.CurrentFile.WriteAt(data[8:], positionInt64)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tc.bar.Add(len(data[8:]))\n\t\tc.TotalSent += int64(len(data[8:]))\n\t\tc.TotalChunksTransferred++\n\t\t// log.Debug(len(c.CurrentFileChunks), c.TotalChunksTransferred, c.TotalSent, c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)\n\n\t\tif !c.CurrentFileIsClosed && (c.TotalChunksTransferred == len(c.CurrentFileChunks) || c.TotalSent == c.FilesToTransfer[c.FilesToTransferCurrentNum].Size) {\n\t\t\tc.CurrentFileIsClosed = true\n\t\t\tlog.Debug(\"finished receiving!\")\n\t\t\tif err = c.CurrentFile.Close(); err != nil {\n\t\t\t\tlog.Debugf(\"error closing %s: %v\", c.CurrentFile.Name(), err)\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"Successful closing %s\", c.CurrentFile.Name())\n\t\t\t}\n\t\t\tif c.Options.Stdout || c.Options.SendingText {\n\t\t\t\tpathToFile := path.Join(\n\t\t\t\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,\n\t\t\t\t\tc.FilesToTransfer[c.FilesToTransferCurrentNum].Name,\n\t\t\t\t)\n\t\t\t\tb, _ := os.ReadFile(pathToFile)\n\t\t\t\tfmt.Print(string(b))\n\t\t\t}\n\t\t\tlog.Debug(\"sending close-sender\")\n\t\t\terr = message.Send(c.conn[0], c.Key, message.Message{\n\t\t\t\tType: message.TypeCloseSender,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t\tc.mutex.Unlock()\n\t}\n}\n\nfunc (c *Client) sendData(i int) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif c.stop.gui {\n\t\t\t\tlog.Errorf(\"panic: %v\", r)\n\t\t\t\tc.stop.Cancel()\n\t\t\t} else {\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}\n\t\tlog.Debugf(\"finished with %d\", i)\n\t\tc.numfinished++\n\t\tif c.numfinished == len(c.Options.RelayPorts) {\n\t\t\tlog.Debug(\"closing file\")\n\t\t\tif err := c.fread.Close(); err != nil {\n\t\t\t\tlog.Errorf(\"error closing file: %v\", err)\n\t\t\t}\n\t\t}\n\t}()\n\n\tvar readingPos int64\n\tpos := uint64(0)\n\tcuri := float64(0)\n\tfor {\n\t\tif err := c.ctxErr(); err != nil {\n\t\t\tlog.Tracef(\"stopping send %d: %v\", i, err)\n\t\t\treturn\n\t\t}\n\t\t// Read file\n\t\tvar n int\n\t\tvar errRead error\n\t\tif math.Mod(curi, float64(len(c.Options.RelayPorts))) == float64(i) {\n\t\t\tdata := make([]byte, models.TCP_BUFFER_SIZE/2)\n\t\t\tn, errRead = c.fread.ReadAt(data, readingPos)\n\t\t\tif c.limiter != nil {\n\t\t\t\tr := c.limiter.ReserveN(time.Now(), n)\n\t\t\t\tlog.Debugf(\"Limiting Upload for %d\", r.Delay())\n\t\t\t\ttime.Sleep(r.Delay())\n\t\t\t}\n\t\t\tif n > 0 {\n\t\t\t\t// check to see if this is a chunk that the recipient wants\n\t\t\t\tusableChunk := true\n\t\t\t\tc.mutex.Lock()\n\t\t\t\tif len(c.chunkMap) != 0 {\n\t\t\t\t\tif _, ok := c.chunkMap[pos]; !ok {\n\t\t\t\t\t\tusableChunk = false\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdelete(c.chunkMap, pos)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tc.mutex.Unlock()\n\t\t\t\tif usableChunk {\n\t\t\t\t\t// log.Debugf(\"sending chunk %d\", pos)\n\t\t\t\t\tposByte := make([]byte, 8)\n\t\t\t\t\tbinary.LittleEndian.PutUint64(posByte, pos)\n\t\t\t\t\tvar err error\n\t\t\t\t\tvar dataToSend []byte\n\t\t\t\t\tif c.Options.NoCompress {\n\t\t\t\t\t\tdataToSend, err = crypt.Encrypt(\n\t\t\t\t\t\t\tappend(posByte, data[:n]...),\n\t\t\t\t\t\t\tc.Key,\n\t\t\t\t\t\t)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdataToSend, err = crypt.Encrypt(\n\t\t\t\t\t\t\tcompress.Compress(\n\t\t\t\t\t\t\t\tappend(posByte, data[:n]...),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tc.Key,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\terr = c.conn[i+1].Send(dataToSend)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\t\t\t\t\tc.bar.Add(n)\n\t\t\t\t\tc.TotalSent += int64(n)\n\t\t\t\t\t// time.Sleep(100 * time.Millisecond)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif n == 0 {\n\t\t\tn = models.TCP_BUFFER_SIZE / 2\n\t\t}\n\t\treadingPos += int64(n)\n\t\tcuri++\n\t\tpos += uint64(n)\n\n\t\tif errRead != nil {\n\t\t\tif errRead == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpanic(errRead)\n\t\t}\n\t}\n}\n\n// isExecutableInPath checks for the availability of an executable\nfunc isExecutableInPath(executableName string) bool {\n\t_, err := exec.LookPath(executableName)\n\treturn err == nil\n}\n\n// copyToClipboard tries to send the code to the operating system clipboard\nfunc copyToClipboard(str string, quiet bool, extendedClipboard bool) {\n\tvar cmd *exec.Cmd\n\tswitch runtime.GOOS {\n\t// Windows should always have clip.exe in PATH by default\n\tcase \"windows\":\n\t\tcmd = exec.Command(\"clip\")\n\t// MacOS uses pbcopy\n\tcase \"darwin\":\n\t\tcmd = exec.Command(\"pbcopy\")\n\t// These Unix-like systems are likely using Xorg(with xclip or xsel) or Wayland(with wl-copy or waycopy)\n\tcase \"linux\", \"android\", \"hurd\", \"freebsd\", \"openbsd\", \"netbsd\", \"dragonfly\", \"solaris\", \"illumos\", \"plan9\":\n\t\tif os.Getenv(\"XDG_SESSION_TYPE\") == \"wayland\" { // Wayland running\n\t\t\tif isExecutableInPath(\"wl-copy\") {\n\t\t\t\tcmd = exec.Command(\"wl-copy\")\n\t\t\t} else if isExecutableInPath(\"waycopy\") {\n\t\t\t\tcmd = exec.Command(\"waycopy\")\n\t\t\t}\n\t\t} else if os.Getenv(\"XDG_SESSION_TYPE\") == \"x11\" || os.Getenv(\"XDG_SESSION_TYPE\") == \"xorg\" { // Xorg running\n\t\t\tif isExecutableInPath(\"xclip\") {\n\t\t\t\tcmd = exec.Command(\"xclip\", \"-selection\", \"clipboard\")\n\t\t\t}\n\t\t} else if isExecutableInPath(\"xsel\") {\n\t\t\tcmd = exec.Command(\"xsel\", \"-b\")\n\t\t} else if isExecutableInPath(\"termux-clipboard-set\") {\n\t\t\tcmd = exec.Command(\"termux-clipboard-set\")\n\t\t}\n\tdefault:\n\t\treturn\n\t}\n\t// Nothing has been found\n\tif cmd == nil {\n\t\treturn\n\t}\n\t// Sending stdin into the available clipboard program\n\tcmd.Stdin = bytes.NewReader([]byte(str))\n\tif err := cmd.Run(); err != nil {\n\t\tlog.Debugf(\"error copying to clipboard: %v\", err)\n\t\treturn\n\t}\n\tif !quiet {\n\t\tif extendedClipboard {\n\t\t\tfmt.Fprintf(os.Stderr, \"Command copied to clipboard!\\n\")\n\t\t} else {\n\t\t\tfmt.Fprintf(os.Stderr, \"Code copied to clipboard!\\n\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/croc/croc_test.go",
    "content": "package croc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/schollz/croc/v10/src/tcp\"\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc init() {\n\tlog.SetLevel(\"trace\")\n\n\tgo tcp.Run(\"debug\", \"127.0.0.1\", \"8281\", \"pass123\", \"8282,8283,8284,8285\")\n\tgo tcp.Run(\"debug\", \"127.0.0.1\", \"8282\", \"pass123\")\n\tgo tcp.Run(\"debug\", \"127.0.0.1\", \"8283\", \"pass123\")\n\tgo tcp.Run(\"debug\", \"127.0.0.1\", \"8284\", \"pass123\")\n\tgo tcp.Run(\"debug\", \"127.0.0.1\", \"8285\", \"pass123\")\n\ttime.Sleep(1 * time.Second)\n}\n\nfunc TestCrocReadme(t *testing.T) {\n\tdefer os.Remove(\"README.md\")\n\n\tlog.Debug(\"setting up sender\")\n\tsender, err := New(Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  \"8123-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8281\",\n\t\tRelayPorts:    []string{\"8281\"},\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tlog.Debug(\"setting up receiver\")\n\treceiver, err := New(Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  \"8123-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8281\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{\"../../README.md\"}, false, false, []string{})\n\t\tif errGet != nil {\n\t\t\tt.Errorf(\"failed to get minimal info: %v\", errGet)\n\t\t}\n\t\terr := sender.Send(filesInfo, emptyFolders, totalNumberFolders)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"send failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\ttime.Sleep(100 * time.Millisecond)\n\tgo func() {\n\t\terr := receiver.Receive()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"receive failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\n\twg.Wait()\n}\n\nfunc TestCrocEmptyFolder(t *testing.T) {\n\tpathName := \"../../testEmpty\"\n\tdefer os.RemoveAll(pathName)\n\tdefer os.RemoveAll(\"./testEmpty\")\n\tos.MkdirAll(pathName, 0o755)\n\n\tlog.Debug(\"setting up sender\")\n\tsender, err := New(Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  \"8123-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8281\",\n\t\tRelayPorts:    []string{\"8281\"},\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tlog.Debug(\"setting up receiver\")\n\treceiver, err := New(Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  \"8123-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8281\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false, []string{})\n\t\tif errGet != nil {\n\t\t\tt.Errorf(\"failed to get minimal info: %v\", errGet)\n\t\t}\n\t\terr := sender.Send(filesInfo, emptyFolders, totalNumberFolders)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"send failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\ttime.Sleep(100 * time.Millisecond)\n\tgo func() {\n\t\terr := receiver.Receive()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"receive failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\n\twg.Wait()\n}\n\nfunc TestCrocSymlink(t *testing.T) {\n\tpathName := \"../link-in-folder\"\n\tdefer os.RemoveAll(pathName)\n\tdefer os.RemoveAll(\"./link-in-folder\")\n\tos.MkdirAll(pathName, 0o755)\n\tos.Symlink(\"../../README.md\", filepath.Join(pathName, \"README.link\"))\n\n\tlog.Debug(\"setting up sender\")\n\tsender, err := New(Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  \"8124-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8281\",\n\t\tRelayPorts:    []string{\"8281\"},\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tlog.Debug(\"setting up receiver\")\n\treceiver, err := New(Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  \"8124-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8281\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false, []string{})\n\t\tif errGet != nil {\n\t\t\tt.Errorf(\"failed to get minimal info: %v\", errGet)\n\t\t}\n\t\terr = sender.Send(filesInfo, emptyFolders, totalNumberFolders)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"send failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\ttime.Sleep(100 * time.Millisecond)\n\tgo func() {\n\t\terr = receiver.Receive()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"receive failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\n\twg.Wait()\n\n\ts, err := filepath.EvalSymlinks(path.Join(pathName, \"README.link\"))\n\tif s != \"../../README.md\" && s != \"..\\\\..\\\\README.md\" {\n\t\tlog.Debug(s)\n\t\tt.Errorf(\"symlink failed to transfer in folder\")\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"symlink transfer failed: %s\", err.Error())\n\t}\n}\nfunc TestCrocIgnoreGit(t *testing.T) {\n\tlog.SetLevel(\"trace\")\n\tdefer os.Remove(\".gitignore\")\n\ttime.Sleep(300 * time.Millisecond)\n\n\ttime.Sleep(1 * time.Second)\n\tfile, err := os.Create(\".gitignore\")\n\tif err != nil {\n\t\tlog.Errorf(\"error creating file\")\n\t}\n\t_, err = file.WriteString(\"LICENSE\")\n\tif err != nil {\n\t\tlog.Errorf(\"error writing to file\")\n\t}\n\ttime.Sleep(1 * time.Second)\n\t// due to how files are ignored in this function, all we have to do to test is make sure LICENSE doesn't get included in FilesInfo.\n\tfilesInfo, _, _, errGet := GetFilesInfo([]string{\"../../LICENSE\", \".gitignore\", \"croc.go\"}, false, true, []string{})\n\tif errGet != nil {\n\t\tt.Errorf(\"failed to get minimal info: %v\", errGet)\n\t}\n\tfor _, file := range filesInfo {\n\t\tif strings.Contains(file.Name, \"LICENSE\") {\n\t\t\tt.Errorf(\"test failed, should ignore LICENSE\")\n\t\t}\n\t}\n}\n\nfunc TestCrocLocal(t *testing.T) {\n\tlog.SetLevel(\"trace\")\n\tdefer os.Remove(\"LICENSE\")\n\tdefer os.Remove(\"touched\")\n\ttime.Sleep(300 * time.Millisecond)\n\n\tlog.Debug(\"setting up sender\")\n\tsender, err := New(Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  \"8123-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8181\",\n\t\tRelayPorts:    []string{\"8181\", \"8182\"},\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        true,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  false,\n\t\tCurve:         \"ed25519\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ttime.Sleep(1 * time.Second)\n\n\tlog.Debug(\"setting up receiver\")\n\treceiver, err := New(Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  \"8123-testingthecroc\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8181\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        true,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  false,\n\t\tCurve:         \"ed25519\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar wg sync.WaitGroup\n\tos.Create(\"touched\")\n\twg.Add(2)\n\tgo func() {\n\t\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{\"../../LICENSE\", \"touched\"}, false, false, []string{})\n\t\tif errGet != nil {\n\t\t\tt.Errorf(\"failed to get minimal info: %v\", errGet)\n\t\t}\n\t\terr := sender.Send(filesInfo, emptyFolders, totalNumberFolders)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"send failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\ttime.Sleep(100 * time.Millisecond)\n\tgo func() {\n\t\terr := receiver.Receive()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"send failed: %v\", err)\n\t\t}\n\t\twg.Done()\n\t}()\n\n\twg.Wait()\n}\n\nfunc TestCrocError(t *testing.T) {\n\tcontent := []byte(\"temporary file's content\")\n\ttmpfile, err := os.CreateTemp(\"\", \"example\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdefer os.Remove(tmpfile.Name()) // clean up\n\n\tif _, err = tmpfile.Write(content); err != nil {\n\t\tpanic(err)\n\t}\n\tif err = tmpfile.Close(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tDebug(false)\n\tlog.SetLevel(\"warn\")\n\tsender, _ := New(Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  \"8123-testingthecroc2\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"doesntexistok.com:8381\",\n\t\tRelayPorts:    []string{\"8381\", \"8382\"},\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        true,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tmpfile.Name()}, false, false, []string{})\n\tif errGet != nil {\n\t\tt.Errorf(\"failed to get minimal info: %v\", errGet)\n\t}\n\terr = sender.Send(filesInfo, emptyFolders, totalNumberFolders)\n\tlog.Debug(err)\n\tassert.NotNil(t, err)\n}\n\nfunc TestReceiverStdoutWithInvalidSecret(t *testing.T) {\n\t// Test for issue: panic when receiving with --stdout and invalid CROC_SECRET\n\t// This should fail gracefully without panicking\n\tlog.SetLevel(\"warn\")\n\treceiver, err := New(Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  \"invalid-secret-12345\",\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8281\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        true, // This is the key flag that triggered the panic\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"failed to create receiver: %v\", err)\n\t\treturn\n\t}\n\n\t// This should fail but not panic\n\terr = receiver.Receive()\n\t// We expect an error since the secret is invalid and no sender is present\n\tassert.NotNil(t, err)\n\tlog.Debugf(\"Expected error occurred: %v\", err)\n}\n\nfunc TestCleanUp(t *testing.T) {\n\t// windows allows files to be deleted only if they\n\t// are not open by another program so the remove actions\n\t// from the above tests will not always do a good clean up\n\t// This \"test\" will make sure\n\toperatingSystem := runtime.GOOS\n\tlog.Debugf(\"The operating system is %s\", operatingSystem)\n\tif operatingSystem == \"windows\" {\n\t\ttime.Sleep(1 * time.Second)\n\t\tlog.Debug(\"Full cleanup\")\n\t\tvar err error\n\n\t\tfor _, file := range []string{\"README.md\", \"./README.md\"} {\n\t\t\terr = os.Remove(file)\n\t\t\tif err == nil {\n\t\t\t\tlog.Debugf(\"Successfully purged %s\", file)\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"%s was already purged.\", file)\n\t\t\t}\n\t\t}\n\t\tfor _, folder := range []string{\"./testEmpty\", \"./link-in-folder\"} {\n\t\t\terr = os.RemoveAll(folder)\n\t\t\tif err == nil {\n\t\t\t\tlog.Debugf(\"Successfully purged %s\", folder)\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"%s was already purged.\", folder)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc hashed(c *Client) bool {\n\tif len(c.FilesToTransfer) == 0 {\n\t\treturn false\n\t}\n\tfor _, file := range c.FilesToTransfer {\n\t\tif len(file.Hash) == 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc waitHashed(sender *Client) (err error) {\n\terr = fmt.Errorf(\"not hashed\")\n\tfor i := 0; i < 300; i++ { // Max 3 seconds\n\t\tif hashed(sender) {\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(10 * time.Millisecond)\n\t}\n\treturn\n}\n\nfunc createTestFile(t *testing.T, size int) (string, func()) {\n\ttempFile, err := os.CreateTemp(\"\", \"test-*.dat\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdata := make([]byte, size)\n\tfor i := 0; i < size; i++ {\n\t\tdata[i] = byte(i % 256)\n\t}\n\n\tif _, err := tempFile.Write(data); err != nil {\n\t\ttempFile.Close()\n\t\tos.Remove(tempFile.Name())\n\t\tt.Fatal(err)\n\t}\n\n\tif err := tempFile.Close(); err != nil {\n\t\tos.Remove(tempFile.Name())\n\t\tt.Fatal(err)\n\t}\n\n\treturn tempFile.Name(), func() {\n\t\tos.Remove(tempFile.Name())\n\t}\n}\n\nfunc TestBase(t *testing.T) {\n\ttempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ\n\tdefer cleanup()\n\treceivedFile := filepath.Base(tempFile)\n\tdefer os.Remove(receivedFile)\n\n\tgo tcp.Run(\"debug\", \"127.0.0.1\", \"8286\", \"pass123\", \"8287\")\n\ttime.Sleep(200 * time.Millisecond)\n\tgo tcp.Run(\"debug\", \"127.0.0.1\", \"8287\", \"pass123\")\n\ttime.Sleep(200 * time.Millisecond)\n\n\tuniqueSecret := fmt.Sprintf(\"test-%d-%d\", time.Now().UnixNano(), rand.Intn(10000))\n\n\tsender, err := New(Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8286\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create sender failed: %v\", err)\n\t}\n\n\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})\n\tif errGet != nil {\n\t\tt.Fatalf(\"Get file info failed: %v\", errGet)\n\t}\n\n\treceiver, err := New(Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8286\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create receiver failed: %v\", err)\n\t}\n\n\tfatalErr := make(chan error, 1)\n\n\tfailTest := func(err error) {\n\t\tselect {\n\t\tcase fatalErr <- err:\n\t\tdefault:\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Warn(\"Send\")\n\t\tif err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Send failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif err := waitHashed(sender); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"waitHashed failed: %w\", err))\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warn(\"Receive\")\n\t\tif err := receiver.Receive(); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Receive failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 3000; i++ {\n\t\t\tif sender.Step1ChannelSecured && receiver.Step1ChannelSecured {\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t\tif sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {\n\t\t\t\t\tlog.Warn(\"Step2FileInfoTransferred reached\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Warn(\"Step1ChannelSecured reached\")\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t}\n\t}()\n\n\tdone := make(chan bool, 1)\n\tgo func() {\n\t\twg.Wait()\n\t\tdone <- true\n\t}()\n\n\tselect {\n\tcase err := <-fatalErr:\n\t\tt.Fatal(err)\n\tcase <-done:\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Test timeout after 5 seconds\")\n\t}\n}\n\nfunc TestCtx(t *testing.T) {\n\ttempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ\n\tdefer cleanup()\n\treceivedFile := filepath.Base(tempFile)\n\tdefer os.Remove(receivedFile)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)\n\tdefer cancel()\n\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8288\", \"pass123\", \"8289\")\n\ttime.Sleep(200 * time.Millisecond)\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8289\", \"pass123\")\n\ttime.Sleep(200 * time.Millisecond)\n\n\tuniqueSecret := fmt.Sprintf(\"test-%d-%d\", time.Now().UnixNano(), rand.Intn(10000))\n\n\tsender, err := NewCtx(ctx, Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8288\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create sender failed: %v\", err)\n\t}\n\n\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})\n\tif errGet != nil {\n\t\tt.Fatalf(\"Get file info failed: %v\", errGet)\n\t}\n\n\treceiver, err := NewCtx(ctx, Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8288\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create receiver failed: %v\", err)\n\t}\n\n\tfatalErr := make(chan error, 1)\n\n\tfailTest := func(err error) {\n\t\tselect {\n\t\tcase fatalErr <- err:\n\t\tdefault:\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Warn(\"Send\")\n\t\tif err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Send failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif err := waitHashed(sender); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"waitHashed failed: %w\", err))\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warn(\"Receive\")\n\t\tif err := receiver.Receive(); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Receive failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 3000; i++ {\n\t\t\tif sender.Step1ChannelSecured && receiver.Step1ChannelSecured {\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t\tif sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {\n\t\t\t\t\tlog.Warn(\"Step2FileInfoTransferred reached\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Warn(\"Step1ChannelSecured reached\")\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t}\n\t}()\n\n\tdone := make(chan bool, 1)\n\tgo func() {\n\t\twg.Wait()\n\t\tdone <- true\n\t}()\n\n\tselect {\n\tcase err := <-fatalErr:\n\t\tt.Fatal(err)\n\tcase <-done:\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Test timeout after 5 seconds\")\n\t}\n}\n\nfunc validErrors(err error) bool {\n\ts := err.Error()\n\treturn strings.Contains(s, \"cancel\") ||\n\t\tstrings.Contains(s, \"context\") ||\n\t\tstrings.Contains(s, \"reset\") ||\n\t\tstrings.Contains(s, \"broken\") ||\n\t\tstrings.Contains(s, \"refusing\") ||\n\t\tstrings.Contains(s, \"EOF\") ||\n\t\tstrings.Contains(s, \"closed\")\n}\n\nfunc result(t *testing.T, err error) {\n\tif err != nil {\n\t\tif validErrors(err) {\n\t\t\tt.Logf(\"Expected error during context cancellation: %v\", err)\n\t\t} else {\n\t\t\tt.Errorf(\"Unexpected error during cancellation: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\tt.Error(\"Transfer should have been interrupted by context cancellation\")\n}\n\nfunc TestAllCtx(t *testing.T) {\n\ttempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ\n\tdefer cleanup()\n\treceivedFile := filepath.Base(tempFile)\n\tdefer os.Remove(receivedFile)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)\n\tdefer cancel()\n\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8290\", \"pass123\", \"8291\")\n\ttime.Sleep(200 * time.Millisecond)\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8291\", \"pass123\")\n\ttime.Sleep(200 * time.Millisecond)\n\n\tuniqueSecret := fmt.Sprintf(\"test-%d-%d\", time.Now().UnixNano(), rand.Intn(10000))\n\n\tsender, err := NewCtx(ctx, Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8290\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create sender failed: %v\", err)\n\t}\n\n\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})\n\tif errGet != nil {\n\t\tt.Fatalf(\"Get file info failed: %v\", errGet)\n\t}\n\n\treceiver, err := NewCtx(ctx, Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8290\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create receiver failed: %v\", err)\n\t}\n\n\tfatalErr := make(chan error, 1)\n\n\tfailTest := func(err error) {\n\t\tselect {\n\t\tcase fatalErr <- err:\n\t\tdefault:\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Warn(\"Send\")\n\t\tif err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Send failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif err := waitHashed(sender); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"waitHashed failed: %w\", err))\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warn(\"Receive\")\n\t\tif err := receiver.Receive(); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Receive failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 3000; i++ {\n\t\t\tif sender.Step1ChannelSecured && receiver.Step1ChannelSecured {\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t\tif sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {\n\t\t\t\t\tlog.Warn(\"Step2FileInfoTransferred reached\")\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Warn(\"Step1ChannelSecured reached\")\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t}\n\t}()\n\n\tdone := make(chan bool, 1)\n\tgo func() {\n\t\twg.Wait()\n\t\tdone <- true\n\t}()\n\n\tselect {\n\tcase err := <-fatalErr:\n\t\tresult(t, err)\n\tcase <-done:\n\t\tt.Error(\"Transfer should have been interrupted by context cancellation\")\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Test timeout after 5 seconds\")\n\t}\n}\n\nfunc TestSendCtx(t *testing.T) {\n\ttempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ\n\tdefer cleanup()\n\treceivedFile := filepath.Base(tempFile)\n\tdefer os.Remove(receivedFile)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)\n\tdefer cancel()\n\n\tctx2, cancel2 := context.WithCancel(context.Background())\n\tdefer cancel2()\n\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8292\", \"pass123\", \"8293\")\n\ttime.Sleep(200 * time.Millisecond)\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8293\", \"pass123\")\n\ttime.Sleep(200 * time.Millisecond)\n\n\tuniqueSecret := fmt.Sprintf(\"test-%d-%d\", time.Now().UnixNano(), rand.Intn(10000))\n\n\tsender, err := NewCtx(ctx2, Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8292\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create sender failed: %v\", err)\n\t}\n\n\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})\n\tif errGet != nil {\n\t\tt.Fatalf(\"Get file info failed: %v\", errGet)\n\t}\n\n\treceiver, err := NewCtx(ctx, Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8292\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create receiver failed: %v\", err)\n\t}\n\n\tfatalErr := make(chan error, 1)\n\n\tfailTest := func(err error) {\n\t\tselect {\n\t\tcase fatalErr <- err:\n\t\tdefault:\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Warn(\"Send\")\n\t\tif err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Send failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif err := waitHashed(sender); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"waitHashed failed: %w\", err))\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warn(\"Receive\")\n\t\tif err := receiver.Receive(); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Receive failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 3000; i++ {\n\t\t\tif sender.Step1ChannelSecured && receiver.Step1ChannelSecured {\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t\tif sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {\n\t\t\t\t\tlog.Warn(\"Step2FileInfoTransferred reached\")\n\t\t\t\t\tcancel2()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Warn(\"Step1ChannelSecured reached\")\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t}\n\t}()\n\n\tdone := make(chan bool, 1)\n\tgo func() {\n\t\twg.Wait()\n\t\tdone <- true\n\t}()\n\n\tselect {\n\tcase err := <-fatalErr:\n\t\tresult(t, err)\n\tcase <-done:\n\t\tt.Error(\"Transfer should have been interrupted by context cancellation\")\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Test timeout after 5 seconds\")\n\t}\n}\n\nfunc TestReceiveCtx(t *testing.T) {\n\ttempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ\n\tdefer cleanup()\n\treceivedFile := filepath.Base(tempFile)\n\tdefer os.Remove(receivedFile)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)\n\tdefer cancel()\n\n\tctx2, cancel2 := context.WithCancel(context.Background())\n\tdefer cancel2()\n\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8294\", \"pass123\", \"8295\")\n\ttime.Sleep(200 * time.Millisecond)\n\tgo tcp.RunCtx(ctx, \"debug\", \"127.0.0.1\", \"8295\", \"pass123\")\n\ttime.Sleep(200 * time.Millisecond)\n\n\tuniqueSecret := fmt.Sprintf(\"test-%d-%d\", time.Now().UnixNano(), rand.Intn(10000))\n\n\tsender, err := NewCtx(ctx, Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8294\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create sender failed: %v\", err)\n\t}\n\n\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})\n\tif errGet != nil {\n\t\tt.Fatalf(\"Get file info failed: %v\", errGet)\n\t}\n\n\treceiver, err := NewCtx(ctx2, Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8294\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create receiver failed: %v\", err)\n\t}\n\n\tfatalErr := make(chan error, 1)\n\n\tfailTest := func(err error) {\n\t\tselect {\n\t\tcase fatalErr <- err:\n\t\tdefault:\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Warn(\"Send\")\n\t\tif err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Send failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif err := waitHashed(sender); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"waitHashed failed: %w\", err))\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warn(\"Receive\")\n\t\tif err := receiver.Receive(); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Receive failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 3000; i++ {\n\t\t\tif sender.Step1ChannelSecured && receiver.Step1ChannelSecured {\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t\tif sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {\n\t\t\t\t\tlog.Warn(\"Step2FileInfoTransferred reached\")\n\t\t\t\t\tcancel2()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Warn(\"Step1ChannelSecured reached\")\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t}\n\t}()\n\n\tdone := make(chan bool, 1)\n\tgo func() {\n\t\twg.Wait()\n\t\tdone <- true\n\t}()\n\n\tselect {\n\tcase err := <-fatalErr:\n\t\tresult(t, err)\n\tcase <-done:\n\t\tt.Error(\"Transfer should have been interrupted by context cancellation\")\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Test timeout after 5 seconds\")\n\t}\n}\n\nfunc TestRunCtx(t *testing.T) {\n\ttempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ\n\tdefer cleanup()\n\treceivedFile := filepath.Base(tempFile)\n\tdefer os.Remove(receivedFile)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)\n\tdefer cancel()\n\n\tctx2, cancel2 := context.WithCancel(context.Background())\n\tdefer cancel2()\n\n\tgo tcp.RunCtx(ctx2, \"debug\", \"127.0.0.1\", \"8296\", \"pass123\", \"8297\")\n\ttime.Sleep(200 * time.Millisecond)\n\tgo tcp.RunCtx(ctx2, \"debug\", \"127.0.0.1\", \"8297\", \"pass123\")\n\ttime.Sleep(200 * time.Millisecond)\n\n\tuniqueSecret := fmt.Sprintf(\"test-%d-%d\", time.Now().UnixNano(), rand.Intn(10000))\n\n\tsender, err := NewCtx(ctx, Options{\n\t\tIsSender:      true,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8296\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t\tGitIgnore:     false,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create sender failed: %v\", err)\n\t}\n\n\tfilesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})\n\tif errGet != nil {\n\t\tt.Fatalf(\"Get file info failed: %v\", errGet)\n\t}\n\n\treceiver, err := NewCtx(ctx, Options{\n\t\tIsSender:      false,\n\t\tSharedSecret:  uniqueSecret,\n\t\tDebug:         true,\n\t\tRelayAddress:  \"127.0.0.1:8296\",\n\t\tRelayPassword: \"pass123\",\n\t\tStdout:        false,\n\t\tNoPrompt:      true,\n\t\tDisableLocal:  true,\n\t\tCurve:         \"siec\",\n\t\tOverwrite:     true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Create receiver failed: %v\", err)\n\t}\n\n\tfatalErr := make(chan error, 1)\n\n\tfailTest := func(err error) {\n\t\tselect {\n\t\tcase fatalErr <- err:\n\t\tdefault:\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tlog.Warn(\"Send\")\n\t\tif err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Send failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tif err := waitHashed(sender); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"waitHashed failed: %w\", err))\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warn(\"Receive\")\n\t\tif err := receiver.Receive(); err != nil {\n\t\t\tfailTest(fmt.Errorf(\"Receive failed: %w\", err))\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 3000; i++ {\n\t\t\tif sender.Step1ChannelSecured && receiver.Step1ChannelSecured {\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t\tif sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {\n\t\t\t\t\tlog.Warn(\"Step2FileInfoTransferred reached\")\n\t\t\t\t\tcancel2()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Warn(\"Step1ChannelSecured reached\")\n\t\t\t}\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t}\n\t}()\n\n\tdone := make(chan bool, 1)\n\tgo func() {\n\t\twg.Wait()\n\t\tdone <- true\n\t}()\n\n\tselect {\n\tcase err := <-fatalErr:\n\t\tresult(t, err)\n\tcase <-done:\n\t\tt.Error(\"Transfer should have been interrupted by context cancellation\")\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"Test timeout after 5 seconds\")\n\t}\n}\n"
  },
  {
    "path": "src/croc/ctx.go",
    "content": "// ctx.go\npackage croc\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/schollz/croc/v10/src/message\"\n\t\"github.com/schollz/croc/v10/src/tcp\"\n\t\"github.com/schollz/croc/v10/src/utils\"\n\tlog \"github.com/schollz/logger\"\n)\n\n// stop manages graceful shutdown\ntype stop struct {\n\tctx      context.Context\n\tcancel   context.CancelFunc\n\tstopChan chan struct{} //peerdiscovery\n\trun      func(debugLevel string, host string, port string, password string, banner ...string) (err error)\n\thash     func(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error)\n\tgui      bool\n}\n\n// newStop creates a new stop manager instance\nfunc newStop(ctx context.Context) *stop {\n\ts := &stop{\n\t\tstopChan: make(chan struct{}),\n\t\trun:      tcp.Run,\n\t\thash:     utils.HashFile,\n\t}\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\ts.ctx, s.cancel = context.WithCancel(ctx)\n\n\treturn s\n}\n\nfunc (s *stop) done() {\n\t<-s.ctx.Done()\n\ttime.Sleep(time.Millisecond)\n\tclose(s.stopChan)\n\tlog.Trace(\"croc done\")\n}\n\n// NewCtx creates a client with context support\nfunc NewCtx(ctx context.Context, ops Options) (*Client, error) {\n\t// Create a regular c\n\tc, err := New(ops)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc.stop = newStop(ctx)\n\tc.stop.gui = true\n\tc.stop.run = func(debugLevel string, host string, port string, password string, banner ...string) (err error) {\n\t\treturn tcp.RunCtx(c.stop.ctx, debugLevel, host, port, password, banner...)\n\t}\n\tc.stop.hash = func(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error) {\n\t\treturn utils.HashFileCtx(c.stop.ctx, fname, algorithm, showProgress...)\n\t}\n\n\tgo func() {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlog.Trace(\"parent context canceled\")\n\t\t\tc.SendError()\n\t\tcase <-c.stopChan:\n\t\t\t// for stop goroutine\n\t\t}\n\t\tlog.Trace(\"croc NewCtx done\")\n\t}()\n\n\treturn c, nil\n}\n\n// ctxErr checks whether it is necessary to interrupt my loops and goroutines\nfunc (s *stop) ctxErr() error {\n\tselect {\n\tcase <-s.ctx.Done():\n\t\treturn s.ctx.Err()\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// Cancel initiates interruption of my loops and goroutines\nfunc (s *stop) Cancel() {\n\tlog.Trace(\"croc Cancel\")\n\tif s.cancel != nil {\n\t\ts.cancel()\n\t\ts.cancel = nil\n\t}\n}\n\n// SendError tells the peer to interrupt their loops and goroutines\nfunc (c *Client) SendError() {\n\tif c.Key != nil && len(c.conn) > 0 && c.conn[0] != nil {\n\t\tmessage.Send(c.conn[0], c.Key, message.Message{\n\t\t\tType:    message.TypeError,\n\t\t\tMessage: \"refusing files\",\n\t\t})\n\t\ttime.Sleep(time.Millisecond)\n\t}\n}\n"
  },
  {
    "path": "src/crypt/crypt.go",
    "content": "package crypt\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"golang.org/x/crypto/argon2\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n\t\"golang.org/x/crypto/pbkdf2\"\n)\n\n// New generates a new key based on a passphrase and salt\nfunc New(passphrase []byte, usersalt []byte) (key []byte, salt []byte, err error) {\n\tif len(passphrase) < 1 {\n\t\terr = fmt.Errorf(\"need more than that for passphrase\")\n\t\treturn\n\t}\n\tif usersalt == nil {\n\t\tsalt = make([]byte, 8)\n\t\t// http://www.ietf.org/rfc/rfc2898.txt\n\t\t// Salt.\n\t\tif _, err := rand.Read(salt); err != nil {\n\t\t\tlog.Fatalf(\"can't get random salt: %v\", err)\n\t\t}\n\t} else {\n\t\tsalt = usersalt\n\t}\n\tkey = pbkdf2.Key(passphrase, salt, 100, 32, sha256.New)\n\treturn\n}\n\n// Encrypt will encrypt using the pre-generated key\nfunc Encrypt(plaintext []byte, key []byte) (encrypted []byte, err error) {\n\t// generate a random iv each time\n\t// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf\n\t// Section 8.2\n\tivBytes := make([]byte, 12)\n\tif _, err = rand.Read(ivBytes); err != nil {\n\t\tlog.Fatalf(\"can't initialize crypto: %v\", err)\n\t}\n\tb, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn\n\t}\n\taesgcm, err := cipher.NewGCM(b)\n\tif err != nil {\n\t\treturn\n\t}\n\tencrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)\n\tencrypted = append(ivBytes, encrypted...)\n\treturn\n}\n\n// Decrypt using the pre-generated key\nfunc Decrypt(encrypted []byte, key []byte) (plaintext []byte, err error) {\n\tif len(encrypted) < 13 {\n\t\terr = fmt.Errorf(\"incorrect passphrase\")\n\t\treturn\n\t}\n\tb, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn\n\t}\n\taesgcm, err := cipher.NewGCM(b)\n\tif err != nil {\n\t\treturn\n\t}\n\tplaintext, err = aesgcm.Open(nil, encrypted[:12], encrypted[12:], nil)\n\treturn\n}\n\n// NewArgon2 generates a new key based on a passphrase and salt\n// using argon2\n// https://pkg.go.dev/golang.org/x/crypto/argon2\nfunc NewArgon2(passphrase []byte, usersalt []byte) (aead cipher.AEAD, salt []byte, err error) {\n\tif len(passphrase) < 1 {\n\t\terr = fmt.Errorf(\"need more than that for passphrase\")\n\t\treturn\n\t}\n\tif usersalt == nil {\n\t\tsalt = make([]byte, 8)\n\t\t// http://www.ietf.org/rfc/rfc2898.txt\n\t\t// Salt.\n\t\tif _, err = rand.Read(salt); err != nil {\n\t\t\tlog.Fatalf(\"can't get random salt: %v\", err)\n\t\t}\n\t} else {\n\t\tsalt = usersalt\n\t}\n\taead, err = chacha20poly1305.NewX(argon2.IDKey(passphrase, salt, 1, 64*1024, 4, 32))\n\treturn\n}\n\n// EncryptChaCha will encrypt ChaCha20-Poly1305 using the pre-generated key\n// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305\nfunc EncryptChaCha(plaintext []byte, aead cipher.AEAD) (encrypted []byte, err error) {\n\tnonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead())\n\tif _, err := rand.Read(nonce); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Encrypt the message and append the ciphertext to the nonce.\n\tencrypted = aead.Seal(nonce, nonce, plaintext, nil)\n\treturn\n}\n\n// DecryptChaCha will decrypt ChaCha20-Poly1305 using the pre-generated key\n// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305\nfunc DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (plaintext []byte, err error) {\n\tif len(encryptedMsg) < aead.NonceSize() {\n\t\terr = fmt.Errorf(\"ciphertext too short\")\n\t\treturn\n\t}\n\n\t// Split nonce and ciphertext.\n\tnonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]\n\n\t// Decrypt the message and check it wasn't tampered with.\n\tplaintext, err = aead.Open(nil, nonce, ciphertext, nil)\n\treturn\n}\n"
  },
  {
    "path": "src/crypt/crypt_test.go",
    "content": "package crypt\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc BenchmarkEncrypt(b *testing.B) {\n\tbob, _, _ := New([]byte(\"password\"), nil)\n\tfor i := 0; i < b.N; i++ {\n\t\tEncrypt([]byte(\"hello, world\"), bob)\n\t}\n}\n\nfunc BenchmarkDecrypt(b *testing.B) {\n\tkey, _, _ := New([]byte(\"password\"), nil)\n\tmsg := []byte(\"hello, world\")\n\tenc, _ := Encrypt(msg, key)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tDecrypt(enc, key)\n\t}\n}\n\nfunc BenchmarkNewPbkdf2(b *testing.B) {\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tNew([]byte(\"password\"), nil)\n\t}\n}\n\nfunc BenchmarkNewArgon2(b *testing.B) {\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tNewArgon2([]byte(\"password\"), nil)\n\t}\n}\n\nfunc BenchmarkEncryptChaCha(b *testing.B) {\n\tbob, _, _ := NewArgon2([]byte(\"password\"), nil)\n\tfor i := 0; i < b.N; i++ {\n\t\tEncryptChaCha([]byte(\"hello, world\"), bob)\n\t}\n}\n\nfunc BenchmarkDecryptChaCha(b *testing.B) {\n\tkey, _, _ := NewArgon2([]byte(\"password\"), nil)\n\tmsg := []byte(\"hello, world\")\n\tenc, _ := EncryptChaCha(msg, key)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tDecryptChaCha(enc, key)\n\t}\n}\n\nfunc TestEncryption(t *testing.T) {\n\tkey, salt, err := New([]byte(\"password\"), nil)\n\tassert.Nil(t, err)\n\tmsg := []byte(\"hello, world\")\n\tenc, err := Encrypt(msg, key)\n\tassert.Nil(t, err)\n\tdec, err := Decrypt(enc, key)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, dec)\n\n\t// check reusing the salt\n\tkey2, _, _ := New([]byte(\"password\"), salt)\n\tdec, err = Decrypt(enc, key2)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, dec)\n\n\t// check reusing the salt\n\tkey2, _, _ = New([]byte(\"wrong password\"), salt)\n\tdec, err = Decrypt(enc, key2)\n\tassert.NotNil(t, err)\n\tassert.NotEqual(t, msg, dec)\n\n\t// error with no password\n\t_, err = Decrypt([]byte(\"\"), key)\n\tassert.NotNil(t, err)\n\n\t// error with small password\n\t_, _, err = New([]byte(\"\"), nil)\n\tassert.NotNil(t, err)\n}\n\nfunc TestEncryptionChaCha(t *testing.T) {\n\tkey, salt, err := NewArgon2([]byte(\"password\"), nil)\n\tfmt.Printf(\"key: %x\\n\", key)\n\tassert.Nil(t, err)\n\tmsg := []byte(\"hello, world\")\n\tenc, err := EncryptChaCha(msg, key)\n\tassert.Nil(t, err)\n\tdec, err := DecryptChaCha(enc, key)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, dec)\n\n\t// check reusing the salt\n\tkey2, _, _ := NewArgon2([]byte(\"password\"), salt)\n\tdec, err = DecryptChaCha(enc, key2)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, dec)\n\n\t// check reusing the salt\n\tkey2, _, _ = NewArgon2([]byte(\"wrong password\"), salt)\n\tdec, err = DecryptChaCha(enc, key2)\n\tassert.NotNil(t, err)\n\tassert.NotEqual(t, msg, dec)\n\n\t// error with no password\n\t_, err = DecryptChaCha([]byte(\"\"), key)\n\tassert.NotNil(t, err)\n\n\t// error with small password\n\t_, _, err = NewArgon2([]byte(\"\"), nil)\n\tassert.NotNil(t, err)\n}\n"
  },
  {
    "path": "src/diskusage/diskusage.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage diskusage\n\nimport (\n\t\"golang.org/x/sys/unix\"\n)\n\n// DiskUsage contains usage data and provides user-friendly access methods\ntype DiskUsage struct {\n\tstat *unix.Statfs_t\n}\n\n// NewDiskUsage returns an object holding the disk usage of volumePath\n// or nil in case of error (invalid path, etc)\nfunc NewDiskUsage(volumePath string) *DiskUsage {\n\tstat := unix.Statfs_t{}\n\terr := unix.Statfs(volumePath, &stat)\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn &DiskUsage{&stat}\n}\n\n// Free returns total free bytes on file system\nfunc (du *DiskUsage) Free() uint64 {\n\treturn uint64(du.stat.Bfree) * uint64(du.stat.Bsize)\n}\n\n// Available return total available bytes on file system to an unprivileged user\nfunc (du *DiskUsage) Available() uint64 {\n\treturn uint64(du.stat.Bavail) * uint64(du.stat.Bsize)\n}\n\n// Size returns total size of the file system\nfunc (du *DiskUsage) Size() uint64 {\n\treturn uint64(du.stat.Blocks) * uint64(du.stat.Bsize)\n}\n\n// Used returns total bytes used in file system\nfunc (du *DiskUsage) Used() uint64 {\n\treturn du.Size() - du.Free()\n}\n\n// Usage returns percentage of use on the file system\nfunc (du *DiskUsage) Usage() float32 {\n\treturn float32(du.Used()) / float32(du.Size())\n}\n"
  },
  {
    "path": "src/diskusage/diskusage_test.go",
    "content": "package diskusage\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nvar KB = uint64(1024)\n\nfunc TestNewDiskUsage(t *testing.T) {\n\tusage := NewDiskUsage(\".\")\n\tfmt.Println(\"Free:\", usage.Free()/(KB*KB))\n\tfmt.Println(\"Available:\", usage.Available()/(KB*KB))\n\tfmt.Println(\"Size:\", usage.Size()/(KB*KB))\n\tfmt.Println(\"Used:\", usage.Used()/(KB*KB))\n\tfmt.Println(\"Usage:\", usage.Usage()*100, \"%\")\n}\n"
  },
  {
    "path": "src/diskusage/diskusage_windows.go",
    "content": "package diskusage\n\nimport (\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\ntype DiskUsage struct {\n\tfreeBytes  int64\n\ttotalBytes int64\n\tavailBytes int64\n}\n\n// NewDiskUsage returns an object holding the disk usage of volumePath\n// or nil in case of error (invalid path, etc)\nfunc NewDiskUsage(volumePath string) *DiskUsage {\n\th := windows.MustLoadDLL(\"kernel32.dll\")\n\tc := h.MustFindProc(\"GetDiskFreeSpaceExW\")\n\n\tdu := &DiskUsage{}\n\n\tc.Call(\n\t\tuintptr(unsafe.Pointer(windows.StringToUTF16Ptr(volumePath))),\n\t\tuintptr(unsafe.Pointer(&du.freeBytes)),\n\t\tuintptr(unsafe.Pointer(&du.totalBytes)),\n\t\tuintptr(unsafe.Pointer(&du.availBytes)))\n\n\treturn du\n}\n\n// Free returns total free bytes on file system\nfunc (du *DiskUsage) Free() uint64 {\n\treturn uint64(du.freeBytes)\n}\n\n// Available returns total available bytes on file system to an unprivileged user\nfunc (du *DiskUsage) Available() uint64 {\n\treturn uint64(du.availBytes)\n}\n\n// Size returns total size of the file system\nfunc (du *DiskUsage) Size() uint64 {\n\treturn uint64(du.totalBytes)\n}\n\n// Used returns total bytes used in file system\nfunc (du *DiskUsage) Used() uint64 {\n\treturn du.Size() - du.Free()\n}\n\n// Usage returns percentage of use on the file system\nfunc (du *DiskUsage) Usage() float32 {\n\treturn float32(du.Used()) / float32(du.Size())\n}\n"
  },
  {
    "path": "src/install/Makefile",
    "content": "# VERSION=8.X.Y make release\n\nrelease:\n\tcd ../../ && go run src/install/updateversion.go\n\tgit commit -am \"bump ${VERSION}\"\n\tgit tag -af v${VERSION} -m \"v${VERSION}\"\t\n\tgit push\n\tgit push --tags\n\tcp zsh_autocomplete ../../\n\tcp bash_autocomplete ../../\n\tcd ../../ && goreleaser release\n\tcd ../../ && ./src/install/prepare-sources-tarball.sh\n\tcd ../../ && ./src/install/upload-src-tarball.sh\n\ntest:\n\tcp zsh_autocomplete ../../\n\tcp bash_autocomplete ../../\n\tcd ../../ && go generate\n\tcd ../../ && goreleaser release --skip-publish\n"
  },
  {
    "path": "src/install/bash_autocomplete",
    "content": ": ${PROG:=$(basename ${BASH_SOURCE})}\n\n_cli_bash_autocomplete() {\n  if [[ \"${COMP_WORDS[0]}\" != \"source\" ]]; then\n    local cur opts base\n    COMPREPLY=()\n    cur=\"${COMP_WORDS[COMP_CWORD]}\"\n    if [[ \"$cur\" == \"-\"* ]]; then\n      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )\n    else\n      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )\n    fi\n    COMPREPLY=( $(compgen -W \"${opts}\" -- ${cur}) )\n    return 0\n  fi\n}\n\ncomplete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG\nunset PROG\n"
  },
  {
    "path": "src/install/default.txt",
    "content": "#!/bin/bash - \n#===============================================================================\n#\n#          FILE: default.txt\n# \n#         USAGE: curl https://getcroc.schollz.com | bash\n#                 OR\n#                wget -qO- https://getcroc.schollz.com | bash\n# \n#   DESCRIPTION: croc Installer Script.\n#\n#                This script installs croc into a specified prefix.\n#                Default prefix = /usr/local/bin\n#\n#       OPTIONS: -p, --prefix \"${INSTALL_PREFIX}\"\n#                      Prefix to install croc into.  Defaults to /usr/local/bin\n#  REQUIREMENTS: bash, uname, tar/unzip, curl/wget, sudo/doas (if not run\n#                as root), install, mktemp, sha256sum/shasum/sha256\n#\n#          BUGS: ...hopefully not.  Please report.\n#\n#         NOTES: Homepage: https://schollz.com/software/croc\n#                  Issues: https://github.com/schollz/croc/issues\n#\n#       CREATED: 08/10/2019 16:41\n#      REVISION: 0.9.2\n#===============================================================================\nset -o nounset                              # Treat unset variables as an error\n\n#-------------------------------------------------------------------------------\n# DEFAULTS\n#-------------------------------------------------------------------------------\nPREFIX=\"${PREFIX:-}\"\nANDROID_ROOT=\"${ANDROID_ROOT:-}\"\n\n# Termux on Android has ${PREFIX} set which already ends with '/usr'\nif [[ -n \"${ANDROID_ROOT}\" && -n \"${PREFIX}\" ]]; then\n  INSTALL_PREFIX=\"${PREFIX}/bin\"\nelse\n  INSTALL_PREFIX=\"/usr/local/bin\"\nfi\n\n#-------------------------------------------------------------------------------\n# FUNCTIONS\n#-------------------------------------------------------------------------------\n\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  print_banner\n#   DESCRIPTION:  Prints a banner\n#    PARAMETERS:  none\n#       RETURNS:  0\n#-------------------------------------------------------------------------------\nprint_banner() {\n  cat <<-'EOF'\n=================================================\n              ____\n             / ___|_ __ ___   ___\n            | |   | '__/ _ \\ / __|\n            | |___| | | (_) | (__\n             \\____|_|  \\___/ \\___|\n\n       ___           _        _ _\n      |_ _|_ __  ___| |_ __ _| | | ___ _ __\n       | || '_ \\/ __| __/ _` | | |/ _ \\ '__|\n       | || | | \\__ \\ || (_| | | |  __/ |\n      |___|_| |_|___/\\__\\__,_|_|_|\\___|_| \n==================================================\nEOF\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  print_help\n#   DESCRIPTION:  Prints out a help message\n#    PARAMETERS:  none\n#       RETURNS:  0\n#-------------------------------------------------------------------------------\nprint_help() {\n  local help_header\n  local help_message\n\n  help_header=\"croc Installer Script\"\n  help_message=\"Usage:\n  -p INSTALL_PREFIX\n      Prefix to install croc into.  Directory must already exist.\n      Default = /usr/local/bin ('\\${PREFIX}/bin' on Termux for Android)\n  \n  -h\n      Prints this helpful message and exit.\"\n\n  echo \"${help_header}\"\n  echo \"\"\n  echo \"${help_message}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  print_message\n#   DESCRIPTION:  Prints a message all fancy like\n#    PARAMETERS:  $1 = Message to print\n#                 $2 = Severity. info, ok, error, warn\n#       RETURNS:  Formatted Message to stdout\n#-------------------------------------------------------------------------------\nprint_message() {\n  local message\n  local severity\n  local red\n  local green\n  local yellow\n  local nc\n\n  message=\"${1}\"\n  severity=\"${2}\"\n  red='\\e[0;31m'\n  green='\\e[0;32m'\n  yellow='\\e[1;33m'\n  nc='\\e[0m'\n\n  case \"${severity}\" in\n    \"info\" ) echo -e \"${nc}${message}${nc}\";;\n      \"ok\" ) echo -e \"${green}${message}${nc}\";;\n   \"error\" ) echo -e \"${red}${message}${nc}\";;\n    \"warn\" ) echo -e \"${yellow}${message}${nc}\";;\n  esac\n\n\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  make_tempdir\n#   DESCRIPTION:  Makes a temp dir using mktemp if available\n#    PARAMETERS:  $1 = Directory template\n#       RETURNS:  0 = Created temp dir. Also prints temp file path to stdout\n#                 1 = Failed to create temp dir\n#                 20 = Failed to find mktemp\n#-------------------------------------------------------------------------------\nmake_tempdir() {\n  local template\n  local tempdir\n  local tempdir_rcode\n\n  template=\"${1}.XXXXXX\"\n\n  if command -v mktemp >/dev/null 2>&1; then\n    tempdir=\"$(mktemp -d -t \"${template}\")\"\n    tempdir_rcode=\"${?}\"\n    if [[ \"${tempdir_rcode}\" == \"0\" ]]; then\n      echo \"${tempdir}\"\n      return 0\n    else\n      return 1\n    fi\n  else\n    return 20\n  fi\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  determine_os\n#   DESCRIPTION:  Attempts to determine host os using uname\n#    PARAMETERS:  none\n#       RETURNS:  0 = OS Detected. Also prints detected os to stdout\n#                 1 = Unknown OS\n#                 20 = 'uname' not found in path\n#-------------------------------------------------------------------------------\ndetermine_os() {\n  local uname_out\n\n  if command -v uname >/dev/null 2>&1; then\n    uname_out=\"$(uname)\"\n    if [[ \"${uname_out}\" == \"\" ]]; then\n      return 1\n    else\n      echo \"${uname_out}\"\n      return 0\n    fi\n  else\n    return 20\n  fi\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  determine_arch\n#   DESCRIPTION:  Attempt to determine architecture of host\n#    PARAMETERS:  none\n#       RETURNS:  0 = Arch Detected. Also prints detected arch to stdout\n#                 1 = Unknown arch\n#                 20 = 'uname' not found in path\n#-------------------------------------------------------------------------------\ndetermine_arch() {\n  local uname_out\n\n  if command -v uname >/dev/null 2>&1; then\n    uname_out=\"$(uname -m)\"\n    if [[ \"${uname_out}\" == \"\" ]]; then\n      return 1\n    else\n      echo \"${uname_out}\"\n      return 0\n    fi\n  else\n    return 20\n  fi\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  run_as\n#   DESCRIPTION:  Run a command as root if needed. If already root, runs it\n#                 directly; otherwise tries sudo then doas. Returns 21 if\n#                 neither sudo nor doas is available when needed.\n#    PARAMETERS:  $@ = command and args\n#       RETURNS:  Return code of executed command, or 21 if no escalation tool\n#-------------------------------------------------------------------------------\nrun_as() {\n  if [[ \"${EUID}\" == \"0\" ]]; then\n    \"$@\"\n    return $?\n  fi\n\n  if command -v sudo >/dev/null 2>&1; then\n    sudo \"$@\"\n    return $?\n  fi\n\n  if command -v doas >/dev/null 2>&1; then\n    doas \"$@\"\n    return $?\n  fi\n\n  # No escalation tool found\n  return 21\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  download_file\n#   DESCRIPTION:  Downloads a file into the specified directory.  Attempts to\n#                 use curl, then wget.  If neither is found, fail.\n#    PARAMETERS:  $1 = url of file to download\n#                 $2 = location to download file into on host system\n#       RETURNS:  If curl or wget found, returns the return code of curl or wget\n#                 20 = Could not find curl and wget\n#-------------------------------------------------------------------------------\ndownload_file() {\n  local url\n  local dir\n  local filename\n  local rcode\n\n  url=\"${1}\"\n  dir=\"${2}\"\n  filename=\"${3}\"\n\n  if command -v curl >/dev/null 2>&1; then\n    curl -fsSL \"${url}\" -o \"${dir}/${filename}\"\n    rcode=\"${?}\"\n  elif command -v wget >/dev/null 2>&1; then\n    wget --quiet  \"${url}\" -O \"${dir}/${filename}\"\n    rcode=\"${?}\"\n  else\n    rcode=\"20\"\n  fi\n  \n  return \"${rcode}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  checksum_check\n#   DESCRIPTION:  Attempt to verify checksum of downloaded file to ensure\n#                 integrity.  Tries multiple tools before failing.\n#    PARAMETERS:  $1 = path to checksum file\n#                 $2 = location of file to check\n#                 $3 = working directory\n#       RETURNS:  0 = checkusm verified\n#                 1 = checksum verification failed\n#                 20 = failed to determine tool to use to check checksum\n#                 30 = failed to change into or go back from working dir\n#-------------------------------------------------------------------------------\nchecksum_check() {\n  local checksum_file\n  local file\n  local dir\n  local rcode\n  local shasum_1\n  local shasum_2\n  local shasum_c\n\n  checksum_file=\"${1}\"\n  file=\"${2}\"\n  dir=\"${3}\"\n\n  cd \"${dir}\" || return 30\n  if command -v sha256sum >/dev/null 2>&1; then\n    ## Not all sha256sum versions seem to have --ignore-missing, so filter the checksum file\n    ## to only include the file we downloaded.\n    grep \"$(basename \"${file}\")\" \"${checksum_file}\" > filtered_checksum.txt\n    shasum_c=\"$(sha256sum -c \"filtered_checksum.txt\")\"\n    rcode=\"${?}\"\n  elif command -v shasum >/dev/null 2>&1; then\n    ## With shasum on FreeBSD, we don't get to --ignore-missing, so filter the checksum file\n    ## to only include the file we downloaded.\n    grep \"$(basename \"${file}\")\" \"${checksum_file}\" > filtered_checksum.txt\n    shasum_c=\"$(shasum -a 256 -c \"filtered_checksum.txt\")\"\n    rcode=\"${?}\"\n  elif command -v sha256 >/dev/null 2>&1; then\n    ## With sha256 on FreeBSD, we don't get to --ignore-missing, so filter the checksum file\n    ## to only include the file we downloaded.\n    ## Also sha256 -c option seems to fail, so fall back to an if statement\n    grep \"$(basename \"${file}\")\" \"${checksum_file}\" > filtered_checksum.txt\n    shasum_1=\"$(sha256 -q \"${file}\")\"\n    shasum_2=\"$(awk '{print $1}' filtered_checksum.txt)\"\n    if [[ \"${shasum_1}\" == \"${shasum_2}\" ]]; then\n      rcode=\"0\"\n    else\n      rcode=\"1\"\n    fi\n    shasum_c=\"Expected: ${shasum_1}, Got: ${shasum_2}\"\n  else\n    return 20\n  fi\n  cd - >/dev/null 2>&1 || return 30\n  \n  if [[ \"${rcode}\" -gt \"0\" ]]; then\n    echo \"${shasum_c}\"\n  fi\n  return \"${rcode}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  extract_file\n#   DESCRIPTION:  Extracts a file into a location.  Attempts to determine which\n#                 tool to use by checking file extension.\n#    PARAMETERS:  $1 = file to extract\n#                 $2 = location to extract file into\n#                 $3 = extension\n#       RETURNS:  Return code of the tool used to extract the file\n#                 20 = Failed to determine which tool to use\n#                 30 = Failed to find tool in path\n#-------------------------------------------------------------------------------\nextract_file() {\n  local file\n  local dir\n  local ext\n  local rcode\n\n  file=\"${1}\"\n  dir=\"${2}\"\n  ext=\"${3}\"\n\n  case \"${ext}\" in\n       \"zip\" ) if command -v unzip >/dev/null 2>&1; then\n                 unzip \"${file}\" -d \"${dir}\"\n                 rcode=\"${?}\"\n               else\n                 rcode=\"30\"\n               fi\n               ;;\n    \"tar.gz\" ) if command -v tar >/dev/null 2>&1; then\n                 tar -xf \"${file}\" -C \"${dir}\"\n                 rcode=\"${?}\"\n               else\n                 rcode=\"31\"\n               fi\n               ;;\n           * ) rcode=\"20\";;\n  esac\n\n  return \"${rcode}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  create_prefix\n#   DESCRIPTION:  Creates the install prefix (and any parent directories). If\n#                 EUID not 0, then attempt to use sudo/doas (run_as).\n#    PARAMETERS:  $1 = prefix\n#       RETURNS:  Return code of the tool used to make the directory\n#                 0 = Created the directory\n#                 >0 = Failed to create directory  \n#                 20 = Could not find mkdir command\n#                 21 = Could not find sudo/doas command (when needed)\n#-------------------------------------------------------------------------------\ncreate_prefix() {\n  local prefix\n  local rcode\n\n  prefix=\"${1}\"\n\n  if command -v mkdir >/dev/null 2>&1; then\n    run_as mkdir -p \"${prefix}\"\n    rcode=\"${?}\"\n  else\n    rcode=\"20\"\n  fi\n\n  return \"${rcode}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  install_file_freebsd\n#   DESCRIPTION:  Installs a file into a location using 'install'.  If EUID not\n#                 0, then attempt to use sudo/doas (run_as).\n#    PARAMETERS:  $1 = file to install\n#                 $2 = location to install file into\n#       RETURNS:  0 = File Installed\n#                 1 = File not installed\n#                 20 = Could not find install command\n#                 21 = Could not find sudo/doas command (when needed)\n#-------------------------------------------------------------------------------\ninstall_file_freebsd() {\n  local file\n  local prefix\n  local rcode\n\n  file=\"${1}\"\n  prefix=\"${2}\"\n\n  if command -v install >/dev/null 2>&1; then\n    run_as install -C -b -B '_old' -m 755 \"${file}\" \"${prefix}\"\n    rcode=\"${?}\"\n  else\n    rcode=\"20\"\n  fi\n\n  return \"${rcode}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  install_file_linux\n#   DESCRIPTION:  Installs a file into a location using 'install'.  If EUID not\n#                 0, then attempt to use sudo/doas (run_as) (unless on android).\n#                 Falls back to cp/chmod for BusyBox compatibility.\n#    PARAMETERS:  $1 = file to install\n#                 $2 = location to install file into\n#       RETURNS:  0 = File Installed\n#                 1 = File not installed\n#                 20 = Could not find install or cp command\n#                 21 = Could not find sudo/doas command (when needed)\n#-------------------------------------------------------------------------------\ninstall_file_linux() {\n  local file\n  local prefix\n  local rcode\n\n  file=\"${1}\"\n  prefix=\"${2}\"\n\n  if command -v install >/dev/null 2>&1; then\n    if [[ \"${EUID}\" == \"0\" ]]; then\n      # Try GNU install first, fall back to simple install for BusyBox\n      install -C -b -S '_old' -m 755 -t \"${prefix}\" \"${file}\" 2>/dev/null || \\\n        install -m 755 \"${file}\" \"${prefix}/\"\n      rcode=\"${?}\"\n    else\n      # Try sudo, then doas, then (special-case) Android direct install, else fail\n      if command -v sudo >/dev/null 2>&1; then\n        sudo install -C -b -S '_old' -m 755 \"${file}\" \"${prefix}\" 2>/dev/null || \\\n          sudo install -m 755 \"${file}\" \"${prefix}/\"\n        rcode=\"${?}\"\n      elif command -v doas >/dev/null 2>&1; then\n        doas install -C -b -S '_old' -m 755 \"${file}\" \"${prefix}\" 2>/dev/null || \\\n          doas install -m 755 \"${file}\" \"${prefix}/\"\n        rcode=\"${?}\"\n      elif [[ \"${ANDROID_ROOT}\" != \"\" ]]; then\n        install -C -b -S '_old' -m 755 -t \"${prefix}\" \"${file}\" 2>/dev/null || \\\n          install -m 755 \"${file}\" \"${prefix}/\"\n        rcode=\"${?}\"\n      else\n        rcode=\"21\"\n      fi\n    fi\n  elif command -v cp >/dev/null 2>&1; then\n    # Fallback to cp/chmod if install is not available\n    if [[ \"${EUID}\" == \"0\" ]]; then\n      cp \"${file}\" \"${prefix}/\" && chmod 755 \"${prefix}/$(basename \"${file}\")\"\n      rcode=\"${?}\"\n    else\n      if command -v sudo >/dev/null 2>&1; then\n        sudo cp \"${file}\" \"${prefix}/\" && sudo chmod 755 \"${prefix}/$(basename \"${file}\")\"\n        rcode=\"${?}\"\n      elif command -v doas >/dev/null 2>&1; then\n        doas cp \"${file}\" \"${prefix}/\" && doas chmod 755 \"${prefix}/$(basename \"${file}\")\"\n        rcode=\"${?}\"\n      else\n        rcode=\"21\"\n      fi\n    fi\n  else\n    rcode=\"20\"\n  fi\n\n  return \"${rcode}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  install_file_cygwin\n#   DESCRIPTION:  Installs a file into a location using 'install'.  If EUID not\n#                 0, then attempt to use sudo/doas (run_as).\n#                 Not really 100% sure this is how to install croc in cygwin.\n#    PARAMETERS:  $1 = file to install\n#                 $2 = location to install file into\n#       RETURNS:  0 = File Installed\n#                 20 = Could not find install command\n#                 21 = Could not find sudo/doas command (when needed)\n#-------------------------------------------------------------------------------\ninstall_file_cygwin() {\n  local file\n  local prefix\n  local rcode\n\n  file=\"${1}\"\n  prefix=\"${2}\"\n\n  if command -v install >/dev/null 2>&1; then\n    run_as install -m 755 \"${file}\" \"${prefix}\"\n    rcode=\"${?}\"\n  else\n    rcode=\"20\"\n  fi\n\n  return \"${rcode}\"\n}\n\n#---  FUNCTION  ----------------------------------------------------------------\n#          NAME:  main\n#   DESCRIPTION:  Put it all together in a logical way\n#                 ...at least that is the hope...\n#    PARAMETERS:  1 = prefix\n#       RETURNS:  0 = All good\n#                 1 = Something done broke\n#-------------------------------------------------------------------------------\nmain() {\n  local prefix\n  local tmpdir\n  local tmpdir_rcode\n  local croc_arch\n  local croc_arch_rcode\n  local croc_os\n  local croc_os_rcode\n  local croc_base_url\n  local croc_url\n  local croc_file\n  local croc_checksum_file\n  local croc_bin_name\n  local croc_version\n  local croc_dl_ext\n  local download_file_rcode\n  local download_checksum_file_rcode\n  local checksum_check_rcode\n  local extract_file_rcode\n  local install_file_rcode\n  local create_prefix_rcode\n  local bash_autocomplete_file\n  local bash_autocomplete_prefix\n  local zsh_autocomplete_file\n  local zsh_autocomplete_prefix\n  local autocomplete_install_rcode\n\n  croc_bin_name=\"croc\"\n  croc_version=\"10.4.2\"\n  croc_dl_ext=\"tar.gz\"\n  croc_base_url=\"https://github.com/schollz/croc/releases/download\"\n  prefix=\"${1}\"\n  bash_autocomplete_file=\"bash_autocomplete\"\n  bash_autocomplete_prefix=\"/etc/bash_completion.d\"\n  zsh_autocomplete_file=\"zsh_autocomplete\"\n  zsh_autocomplete_prefix=\"/etc/zsh\"\n\n  print_banner\n  print_message \"== Install prefix set to ${prefix}\" \"info\"\n  \n  tmpdir=\"$(make_tempdir \"${croc_bin_name}\")\"\n  tmpdir_rcode=\"${?}\"\n  if [[ \"${tmpdir_rcode}\" == \"0\" ]]; then\n    print_message \"== Created temp dir at ${tmpdir}\" \"info\"\n  elif [[ \"${tmpdir_rcode}\" == \"1\" ]]; then\n    print_message \"== Failed to create temp dir at ${tmpdir}\" \"error\"\n  else\n    print_message \"== 'mktemp' not found in path. Is it installed?\" \"error\"\n    exit 1\n  fi\n\n  croc_arch=\"$(determine_arch)\"\n  croc_arch_rcode=\"${?}\"\n  if [[ \"${croc_arch_rcode}\" == \"0\" ]]; then\n    print_message \"== Architecture detected as ${croc_arch}\" \"info\"\n  elif [[ \"${croc_arch_rcode}\" == \"1\" ]]; then\n    print_message \"== Architecture not detected\" \"error\"\n    exit 1\n  else\n    print_message \"== 'uname' not found in path. Is it installed?\" \"error\"\n    exit 1\n  fi\n\n  croc_os=\"$(determine_os)\"\n  croc_os_rcode=\"${?}\"\n  if [[ \"${croc_os_rcode}\" == \"0\" ]]; then\n    print_message \"== OS detected as ${croc_os}\" \"info\"\n  elif [[ \"${croc_os_rcode}\" == \"1\" ]]; then\n    print_message \"== OS not detected\" \"error\"\n    exit 1\n  else\n    print_message \"== 'uname' not found in path. Is it installed?\" \"error\"\n    exit 1\n  fi\n\n  case \"${croc_os}\" in\n     \"Darwin\" ) croc_os=\"macOS\";;\n    *\"BusyBox\"* )\n        croc_os=\"Linux\"\n        ;;\n    \"MINGW\"* ) croc_os=\"Windows\";\n                croc_dl_ext=\"zip\";;\n    \"CYGWIN\"* ) croc_os=\"Windows\";\n                croc_dl_ext=\"zip\";\n                print_message \"== Cygwin is currently unsupported.\" \"error\";\n                exit 1;;\n  esac\n\n  case \"${croc_arch}\" in\n     \"x86_64\" ) croc_arch=\"64bit\";;\n      \"amd64\" ) croc_arch=\"64bit\";;\n    \"aarch64\" ) croc_arch=\"ARM64\";;\n      \"arm64\" ) croc_arch=\"ARM64\";;\n     \"armv7l\" ) croc_arch=\"ARM\";;\n     \"armv8l\" ) croc_arch=\"ARM\";;\n     \"armv9l\" ) croc_arch=\"ARM\";;\n       \"i686\" ) croc_arch=\"32bit\";;\n    \"riscv64\" ) croc_arch=\"RISCV64\";;\n            * ) croc_arch=\"unknown\";;\n  esac\n\n  croc_file=\"${croc_bin_name}_v${croc_version}_${croc_os}-${croc_arch}.${croc_dl_ext}\"\n  croc_checksum_file=\"${croc_bin_name}_v${croc_version}_checksums.txt\"\n  croc_url=\"${croc_base_url}/v${croc_version}/${croc_file}\"\n  croc_checksum_url=\"${croc_base_url}/v${croc_version}/${croc_checksum_file}\"\n  echo \"${croc_url}\" \"${tmpdir}\" \"${croc_file}\"\n  download_file \"${croc_url}\" \"${tmpdir}\" \"${croc_file}\"\n  download_file_rcode=\"${?}\"\n  if [[ \"${download_file_rcode}\" == \"0\" ]]; then\n    print_message \"== Downloaded croc archive into ${tmpdir}\" \"info\"\n  elif [[ \"${download_file_rcode}\" == \"1\" ]]; then\n    print_message \"== Failed to download croc archive\" \"error\"\n    exit 1\n  elif [[ \"${download_file_rcode}\" == \"20\" ]]; then\n    print_message \"== Failed to locate curl or wget\" \"error\"\n    exit 1\n  else\n    print_message \"== Return code of download tool returned an unexpected value of ${download_file_rcode}\" \"error\"\n    exit 1\n  fi\n  download_file \"${croc_checksum_url}\" \"${tmpdir}\" \"${croc_checksum_file}\"\n  download_checksum_file_rcode=\"${?}\"\n  if [[ \"${download_checksum_file_rcode}\" == \"0\" ]]; then\n    print_message \"== Downloaded croc checksums file into ${tmpdir}\" \"info\"\n  elif [[ \"${download_checksum_file_rcode}\" == \"1\" ]]; then\n    print_message \"== Failed to download croc checksums\" \"error\"\n    exit 1\n  elif [[ \"${download_checksum_file_rcode}\" == \"20\" ]]; then\n    print_message \"== Failed to locate curl or wget\" \"error\"\n    exit 1\n  else\n    print_message \"== Return code of download tool returned an unexpected value of ${download_checksum_file_rcode}\" \"error\"\n    exit 1\n  fi\n\n  checksum_check \"${tmpdir}/${croc_checksum_file}\" \"${tmpdir}/${croc_file}\" \"${tmpdir}\"\n  checksum_check_rcode=\"${?}\"\n  if [[ \"${checksum_check_rcode}\" == \"0\" ]]; then\n    print_message \"== Checksum of ${tmpdir}/${croc_file} verified\" \"ok\"\n  elif [[ \"${checksum_check_rcode}\" == \"1\" ]]; then\n    print_message \"== Failed to verify checksum of ${tmpdir}/${croc_file}\" \"error\"\n    exit 1\n  elif [[ \"${checksum_check_rcode}\" == \"20\" ]]; then\n    print_message \"== Failed to find tool to verify sha256 sums\" \"error\"\n    exit 1\n  elif [[ \"${checksum_check_rcode}\" == \"30\" ]]; then\n    print_message \"== Failed to change into working directory ${tmpdir}\" \"error\"\n    exit 1\n  else\n    print_message \"== Unknown return code returned while checking checksum of ${tmpdir}/${croc_file}. Returned ${checksum_check_rcode}\" \"error\"\n    exit 1\n  fi\n\n  extract_file \"${tmpdir}/${croc_file}\" \"${tmpdir}/\" \"${croc_dl_ext}\"\n  extract_file_rcode=\"${?}\"\n  if [[ \"${extract_file_rcode}\" == \"0\" ]]; then\n    print_message \"== Extracted ${croc_file} to ${tmpdir}/\" \"info\"\n  elif [[ \"${extract_file_rcode}\" == \"1\" ]]; then\n    print_message \"== Failed to extract ${croc_file}\" \"error\"\n    exit 1\n  elif [[ \"${extract_file_rcode}\" == \"20\" ]]; then\n    print_message \"== Failed to determine which extraction tool to use\" \"error\"\n    exit 1\n  elif [[ \"${extract_file_rcode}\" == \"30\" ]]; then\n    print_message \"== Failed to find 'unzip' in path\" \"error\"\n    exit 1\n  elif [[ \"${extract_file_rcode}\" == \"31\" ]]; then\n    print_message \"== Failed to find 'tar' in path\" \"error\"\n    exit 1\n  else\n    print_message \"== Unknown error returned from extraction attempt\" \"error\"\n    exit 1\n  fi\n\n  if [[ ! -d \"${prefix}\" ]]; then\n    create_prefix \"${prefix}\"\n    create_prefix_rcode=\"${?}\"\n    if [[ \"${create_prefix_rcode}\" == \"0\" ]]; then\n      print_message \"== Created install prefix at ${prefix}\" \"info\"\n    elif [[ \"${create_prefix_rcode}\" == \"20\" ]]; then\n      print_message \"== Failed to find mkdir in path\" \"error\"\n      exit 1\n    elif [[ \"${create_prefix_rcode}\" == \"21\" ]]; then\n      print_message \"== Failed to find sudo or doas in path\" \"error\"\n      exit 1\n    else\n      print_message \"== Failed to create the install prefix: ${prefix}\" \"error\"\n      exit 1\n    fi\n  else\n    print_message \"== Install prefix already exists. No need to create it.\" \"info\"\n  fi\n\n  [ ! -d \"${bash_autocomplete_prefix}/croc\" ] && mkdir -p \"${bash_autocomplete_prefix}/croc\" >/dev/null 2>&1\n  case \"${croc_os}\" in\n    \"Linux\" ) install_file_linux \"${tmpdir}/${croc_bin_name}\" \"${prefix}/\";\n              install_file_rcode=\"${?}\";;\n  \"FreeBSD\" ) install_file_freebsd \"${tmpdir}/${croc_bin_name}\" \"${prefix}/\";\n              install_file_rcode=\"${?}\";;\n    \"macOS\" ) install_file_freebsd \"${tmpdir}/${croc_bin_name}\" \"${prefix}/\";\n              install_file_rcode=\"${?}\";;\n  \"Windows\" ) install_file_cygwin \"${tmpdir}/${croc_bin_name}\" \"${prefix}/\";\n              install_file_rcode=\"${?}\";;\n  esac\n\n  if [[ \"${install_file_rcode}\" == \"0\" ]] ; then\n    print_message \"== Installed ${croc_bin_name} to ${prefix}/\" \"ok\"\n  elif [[ \"${install_file_rcode}\" == \"1\" ]]; then\n    print_message \"== Failed to install ${croc_bin_name}\" \"error\"\n    exit 1\n  elif [[ \"${install_file_rcode}\" == \"20\" ]]; then\n    print_message \"== Failed to locate 'install' command\" \"error\"\n    exit 1\n  elif [[ \"${install_file_rcode}\" == \"21\" ]]; then\n    print_message \"== Failed to locate 'sudo' or 'doas' command\" \"error\"\n    exit 1\n  else\n    print_message \"== Install attempt returned an unexpected value of ${install_file_rcode}\" \"error\"\n    exit 1\n  fi\n\n  # case \"$(basename ${SHELL})\" in\n  #   \"bash\" ) install_file_linux \"${tmpdir}/${bash_autocomplete_file}\" \"${bash_autocomplete_prefix}/croc\";\n  #            autocomplete_install_rcode=\"${?}\";;\n  #    \"zsh\" ) install_file_linux \"${tmpdir}/${zsh_autocomplete_file}\" \"${zsh_autocomplete_prefix}/zsh_autocomplete_croc\";\n  #            autocomplete_install_rcode=\"${?}\";\n  #            print_message \"== You will need to add the following to your ~/.zshrc to enable autocompletion\" \"info\";\n  #            print_message \"\\nPROG=croc\\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\\nsource /etc/zsh/zsh_autocomplete_croc\\n\" \"info\";;\n  #    *)      autocomplete_install_rcode=\"1\";;\n  # esac\n\n  # if [[ \"${autocomplete_install_rcode}\" == \"0\" ]] ; then\n  #   print_message \"== Installed autocompletions for $(basename \"${SHELL}\")\" \"ok\"\n  # elif [[ \"${autocomplete_install_rcode}\" == \"1\" ]]; then\n  #   print_message \"== Failed to install ${bash_autocomplete_file}\" \"error\"\n  # elif [[ \"${autocomplete_install_rcode}\" == \"20\" ]]; then\n  #   print_message \"== Failed to locate 'install' command\" \"error\"\n  # elif [[ \"${autocomplete_install_rcode}\" == \"21\" ]]; then\n  #   print_message \"== Failed to locate 'sudo' command\" \"error\"\n  # else\n  #   print_message \"== Install attempt returned an unexpected value of ${autocomplete_install_rcode}\" \"error\"\n  # fi\n\n  print_message \"== Installation complete\" \"ok\"\n  \n  exit 0\n}\n\n#-------------------------------------------------------------------------------\n#  ARGUMENT PARSING\n#-------------------------------------------------------------------------------\nOPTS=\"hp:\"\nwhile getopts \"${OPTS}\" optchar; do\n  case \"${optchar}\" in\n    'h' ) print_help\n          exit 0\n          ;;\n    'p' ) INSTALL_PREFIX=\"${OPTARG}\"\n          ;;\n     /? ) print_message \"Unknown option ${OPTARG}\" \"warn\"\n          ;;\n  esac\ndone\n\n#-------------------------------------------------------------------------------\n# CALL MAIN\n#-------------------------------------------------------------------------------\nmain \"${INSTALL_PREFIX}\"\n"
  },
  {
    "path": "src/install/prepare-sources-tarball.sh",
    "content": "#!/bin/bash\ntmp=$(mktemp -d)\necho $VERSION\ngit clone -b v${VERSION} --depth 1 https://github.com/schollz/croc $tmp/croc-${VERSION}\n(cd $tmp/croc-${VERSION} && go mod tidy && go mod vendor)\n(cd $tmp && tar -cvzf croc_${VERSION}_src.tar.gz croc-${VERSION})\nmv $tmp/croc_${VERSION}_src.tar.gz dist/\n"
  },
  {
    "path": "src/install/updateversion.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nfunc main() {\n\terr := run()\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc run() (err error) {\n\tversionNew := \"v\" + os.Getenv(\"VERSION\")\n\tversionHash, err := exec.Command(\"git\", \"rev-parse\", \"--short\", \"HEAD\").Output()\n\tif err != nil {\n\t\treturn\n\t}\n\tversionHashNew := strings.TrimSpace(string(versionHash))\n\tfmt.Println(versionNew)\n\tfmt.Println(versionHashNew)\n\n\terr = replaceInFile(\"src/cli/cli.go\", `Version = \"`, `\"`, versionNew+\"-\"+versionHashNew)\n\tif err == nil {\n\t\tfmt.Printf(\"updated cli.go to version %s\\n\", versionNew)\n\t}\n\terr = replaceInFile(\"README.md\", `version-`, `-b`, strings.Split(versionNew, \"-\")[0])\n\tif err == nil {\n\t\tfmt.Printf(\"updated README to version %s\\n\", strings.Split(versionNew, \"-\")[0])\n\t}\n\n\terr = replaceInFile(\"src/install/default.txt\", `croc_version=\"`, `\"`, strings.Split(versionNew, \"-\")[0][1:])\n\tif err == nil {\n\t\tfmt.Printf(\"updated default.txt to version %s\\n\", strings.Split(versionNew, \"-\")[0][1:])\n\t}\n\n\treturn\n}\n\nfunc replaceInFile(fname, start, end, replacement string) (err error) {\n\tb, err := os.ReadFile(fname)\n\tif err != nil {\n\t\treturn\n\t}\n\toldVersion := getStringInBetween(string(b), start, end)\n\tif oldVersion == \"\" {\n\t\terr = fmt.Errorf(\"nothing\")\n\t\treturn\n\t}\n\tnewF := strings.Replace(\n\t\tstring(b),\n\t\tfmt.Sprintf(\"%s%s%s\", start, oldVersion, end),\n\t\tfmt.Sprintf(\"%s%s%s\", start, replacement, end),\n\t\t1,\n\t)\n\terr = os.WriteFile(fname, []byte(newF), 0o644)\n\treturn\n}\n\n// getStringInBetween Returns empty string if no start string found\nfunc getStringInBetween(str, start, end string) (result string) {\n\ts := strings.Index(str, start)\n\tif s == -1 {\n\t\treturn\n\t}\n\ts += len(start)\n\te := strings.Index(str[s:], end)\n\tif e == -1 {\n\t\treturn\n\t}\n\te += s\n\treturn str[s:e]\n}\n"
  },
  {
    "path": "src/install/upload-src-tarball.sh",
    "content": "#!/bin/bash\nVERSION=$(cat ./src/cli/cli.go | grep 'Version = \"v' | sed 's/[^0-9.]*\\([0-9.]*\\).*/\\1/')\necho $VERSION\n\n# Check dependencies.\nset -e\nxargs=$(which gxargs || which xargs)\n\n# Validate settings.\n[ \"$TRACE\" ] && set -x\n\nCONFIG=$@\n\nfor line in $CONFIG; do\n  eval \"$line\"\ndone\n\nowner=\"schollz\"\nrepo=\"croc\"\ntag=\"v${VERSION}\"\nfilename=\"dist/croc_${VERSION}_src.tar.gz\"\n\n# Define variables.\nGH_API=\"https://api.github.com\"\nGH_REPO=\"$GH_API/repos/$owner/$repo\"\nGH_TAGS=\"$GH_REPO/releases/tags/$tag\"\nAUTH=\"Authorization: token $GITHUB_TOKEN\"\nWGET_ARGS=\"--content-disposition --auth-no-challenge --no-cookie\"\nCURL_ARGS=\"-LJO#\"\n\nif [[ \"$tag\" == 'LATEST' ]]; then\n  GH_TAGS=\"$GH_REPO/releases/latest\"\nfi\n\n# Validate token.\ncurl -o /dev/null -sH \"$AUTH\" $GH_REPO || { echo \"Error: Invalid repo, token or network issue!\";  exit 1; }\n\n# Read asset tags.\nresponse=$(curl -sH \"$AUTH\" $GH_TAGS)\n\n# Get ID of the asset based on given filename.\neval $(echo \"$response\" | grep -m 1 \"id.:\" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')\n[ \"$id\" ] || { echo \"Error: Failed to get release id for tag: $tag\"; echo \"$response\" | awk 'length($0)<100' >&2; exit 1; }\n\n# Upload asset\necho \"Uploading asset... \"\n\n# Construct url\nGH_ASSET=\"https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)\"\n\ncurl \"$GITHUB_OAUTH_BASIC\" --data-binary @\"$filename\" -H \"Authorization: token $GITHUB_TOKEN\" -H \"Content-Type: application/octet-stream\" $GH_ASSET\n"
  },
  {
    "path": "src/install/zsh_autocomplete",
    "content": "#compdef $PROG\n\n_cli_zsh_autocomplete() {\n\n  local -a opts\n  local cur\n  cur=${words[-1]}\n  if [[ \"$cur\" == \"-\"* ]]; then\n    opts=(\"${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}\")\n  else\n    opts=(\"${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}\")\n  fi\n\n  if [[ \"${opts[1]}\" != \"\" ]]; then\n    _describe 'values' opts\n  else\n    _files\n  fi\n\n  return\n}\n\ncompdef _cli_zsh_autocomplete $PROG\n"
  },
  {
    "path": "src/message/message.go",
    "content": "package message\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/schollz/croc/v10/src/comm\"\n\t\"github.com/schollz/croc/v10/src/compress\"\n\t\"github.com/schollz/croc/v10/src/crypt\"\n\tlog \"github.com/schollz/logger\"\n)\n\n// Type is a message type\ntype Type string\n\nconst (\n\tTypePAKE           Type = \"pake\"\n\tTypeExternalIP     Type = \"externalip\"\n\tTypeFinished       Type = \"finished\"\n\tTypeError          Type = \"error\"\n\tTypeCloseRecipient Type = \"close-recipient\"\n\tTypeCloseSender    Type = \"close-sender\"\n\tTypeRecipientReady Type = \"recipientready\"\n\tTypeFileInfo       Type = \"fileinfo\"\n)\n\n// Message is the possible payload for messaging\ntype Message struct {\n\tType    Type   `json:\"t,omitempty\"`\n\tMessage string `json:\"m,omitempty\"`\n\tBytes   []byte `json:\"b,omitempty\"`\n\tBytes2  []byte `json:\"b2,omitempty\"`\n\tNum     int    `json:\"n,omitempty\"`\n}\n\nfunc (m Message) String() string {\n\tb, _ := json.Marshal(m)\n\treturn string(b)\n}\n\n// Send will send out\nfunc Send(c *comm.Comm, key []byte, m Message) (err error) {\n\tmSend, err := Encode(key, m)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = c.Send(mSend)\n\treturn\n}\n\n// Encode will convert to bytes\nfunc Encode(key []byte, m Message) (b []byte, err error) {\n\tb, err = json.Marshal(m)\n\tif err != nil {\n\t\treturn\n\t}\n\tb = compress.Compress(b)\n\tif key != nil {\n\t\tlog.Debugf(\"writing %s message (encrypted)\", m.Type)\n\t\tb, err = crypt.Encrypt(b, key)\n\t} else {\n\t\tlog.Debugf(\"writing %s message (unencrypted)\", m.Type)\n\t}\n\treturn\n}\n\n// Decode will convert from bytes\nfunc Decode(key []byte, b []byte) (m Message, err error) {\n\tif key != nil {\n\t\tb, err = crypt.Decrypt(b, key)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\tb = compress.Decompress(b)\n\terr = json.Unmarshal(b, &m)\n\tif err == nil {\n\t\tif key != nil {\n\t\t\tlog.Debugf(\"read %s message (encrypted)\", m.Type)\n\t\t} else {\n\t\t\tlog.Debugf(\"read %s message (unencrypted)\", m.Type)\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "src/message/message_test.go",
    "content": "package message\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/schollz/croc/v10/src/comm\"\n\t\"github.com/schollz/croc/v10/src/crypt\"\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar TypeMessage Type = \"message\"\n\nfunc TestMessage(t *testing.T) {\n\tlog.SetLevel(\"debug\")\n\tm := Message{Type: TypeMessage, Message: \"hello, world\"}\n\te, salt, err := crypt.New([]byte(\"pass\"), nil)\n\tassert.Nil(t, err)\n\tfmt.Println(string(salt))\n\tb, err := Encode(e, m)\n\tassert.Nil(t, err)\n\tfmt.Printf(\"%x\\n\", b)\n\n\tm2, err := Decode(e, b)\n\tassert.Nil(t, err)\n\tassert.Equal(t, m, m2)\n\tassert.Equal(t, `{\"t\":\"message\",\"m\":\"hello, world\"}`, m.String())\n\t_, err = Decode([]byte(\"not pass\"), b)\n\tassert.NotNil(t, err)\n\t_, err = Encode([]byte(\"0\"), m)\n\tassert.NotNil(t, err)\n}\n\nfunc TestMessageNoPass(t *testing.T) {\n\tlog.SetLevel(\"debug\")\n\tm := Message{Type: TypeMessage, Message: \"hello, world\"}\n\tb, err := Encode(nil, m)\n\tassert.Nil(t, err)\n\tfmt.Printf(\"%x\\n\", b)\n\n\tm2, err := Decode(nil, b)\n\tassert.Nil(t, err)\n\tassert.Equal(t, m, m2)\n\tassert.Equal(t, `{\"t\":\"message\",\"m\":\"hello, world\"}`, m.String())\n}\n\nfunc TestSend(t *testing.T) {\n\ttoken := make([]byte, 40000000)\n\trand.Read(token)\n\n\tport := \"8801\"\n\tgo func() {\n\t\tlog.Debug(\"starting TCP server on \" + port)\n\t\tserver, err := net.Listen(\"tcp\", \"0.0.0.0:\"+port)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t\tdefer server.Close()\n\t\t// spawn a new goroutine whenever a client connects\n\t\tfor {\n\t\t\tconnection, err := server.Accept()\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\tlog.Debugf(\"client %s connected\", connection.RemoteAddr().String())\n\t\t\tgo func(_ string, connection net.Conn) {\n\t\t\t\tc := comm.New(connection)\n\t\t\t\terr = c.Send([]byte(\"hello, world\"))\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tdata, err := c.Receive()\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equal(t, []byte(\"hello, computer\"), data)\n\t\t\t\tdata, err = c.Receive()\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equal(t, []byte{'\\x00'}, data)\n\t\t\t\tdata, err = c.Receive()\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equal(t, token, data)\n\t\t\t}(port, connection)\n\t\t}\n\t}()\n\n\ttime.Sleep(800 * time.Millisecond)\n\ta, err := comm.NewConnection(\"127.0.0.1:\"+port, 10*time.Minute)\n\tassert.Nil(t, err)\n\tm := Message{Type: TypeMessage, Message: \"hello, world\"}\n\te, salt, err := crypt.New([]byte(\"pass\"), nil)\n\tlog.Debug(salt)\n\tassert.Nil(t, err)\n\n\tassert.Nil(t, Send(a, e, m))\n}\n"
  },
  {
    "path": "src/mnemonicode/mnemonicode.go",
    "content": "// From GitHub version/fork maintained by Stephen Paul Weber available at:\n// https://github.com/singpolyma/mnemonicode\n//\n// Originally from:\n// http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/\n\n/*\n Copyright (c) 2000  Oren Tirosh <oren@hishome.net>\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*/\n\npackage mnemonicode\n\nconst base = 1626\n\n// WordsRequired returns the number of words required to encode input\n// data of length bytes using mnomonic encoding.\n//\n// Every four bytes of input is encoded into three words. If there\n// is an extra one or two bytes they get an extra one or two words\n// respectively. If there is an extra three bytes, they will be encoded\n// into three words with the last word being one of a small set of very\n// short words (only needed to encode the last 3 bits).\nfunc WordsRequired(length int) int {\n\treturn ((length + 1) * 3) / 4\n}\n\n// EncodeWordList encodes src into mnemomic words which are appended to dst.\n// The final wordlist is returned.\n// There will be WordsRequired(len(src)) words appended.\nfunc EncodeWordList(dst []string, src []byte) (result []string) {\n\tif n := len(dst) + WordsRequired(len(src)); cap(dst) < n {\n\t\tresult = make([]string, len(dst), n)\n\t\tcopy(result, dst)\n\t} else {\n\t\tresult = dst\n\t}\n\n\tvar x uint32\n\tfor len(src) >= 4 {\n\t\tx = uint32(src[0])\n\t\tx |= uint32(src[1]) << 8\n\t\tx |= uint32(src[2]) << 16\n\t\tx |= uint32(src[3]) << 24\n\t\tsrc = src[4:]\n\n\t\ti0 := int(x % base)\n\t\ti1 := int(x/base) % base\n\t\ti2 := int(x/base/base) % base\n\t\tresult = append(result, WordList[i0], WordList[i1], WordList[i2])\n\t}\n\tif len(src) > 0 {\n\t\tx = 0\n\t\tfor i := len(src) - 1; i >= 0; i-- {\n\t\t\tx <<= 8\n\t\t\tx |= uint32(src[i])\n\t\t}\n\t\ti := int(x % base)\n\t\tresult = append(result, WordList[i])\n\t\tif len(src) >= 2 {\n\t\t\ti = int(x/base) % base\n\t\t\tresult = append(result, WordList[i])\n\t\t}\n\t\tif len(src) == 3 {\n\t\t\ti = base + int(x/base/base)%7\n\t\t\tresult = append(result, WordList[i])\n\t\t}\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "src/mnemonicode/mnemonicode_test.go",
    "content": "package mnemonicode\n\nimport (\n\t\"testing\"\n)\n\nfunc TestWordsRequired(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tlength int\n\t\twant   int\n\t}{\n\t\t{\"empty\", 0, 0},\n\t\t{\"1 byte\", 1, 1},\n\t\t{\"2 bytes\", 2, 2},\n\t\t{\"3 bytes\", 3, 3},\n\t\t{\"4 bytes\", 4, 3},\n\t\t{\"5 bytes\", 5, 4},\n\t\t{\"8 bytes\", 8, 6},\n\t\t{\"12 bytes\", 12, 9},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := WordsRequired(tt.length); got != tt.want {\n\t\t\t\tt.Errorf(\"WordsRequired(%d) = %d, want %d\", tt.length, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncodeWordList(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdst  []string\n\t\tsrc  []byte\n\t\twant int\n\t}{\n\t\t{\n\t\t\tname: \"empty input\",\n\t\t\tdst:  []string{},\n\t\t\tsrc:  []byte{},\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"single byte\",\n\t\t\tdst:  []string{},\n\t\t\tsrc:  []byte{0x01},\n\t\t\twant: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"two bytes\",\n\t\t\tdst:  []string{},\n\t\t\tsrc:  []byte{0x01, 0x02},\n\t\t\twant: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"three bytes\",\n\t\t\tdst:  []string{},\n\t\t\tsrc:  []byte{0x01, 0x02, 0x03},\n\t\t\twant: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"four bytes\",\n\t\t\tdst:  []string{},\n\t\t\tsrc:  []byte{0x01, 0x02, 0x03, 0x04},\n\t\t\twant: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"eight bytes\",\n\t\t\tdst:  []string{},\n\t\t\tsrc:  []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},\n\t\t\twant: 6,\n\t\t},\n\t\t{\n\t\t\tname: \"with existing dst\",\n\t\t\tdst:  []string{\"existing\"},\n\t\t\tsrc:  []byte{0x01},\n\t\t\twant: 2,\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 := EncodeWordList(tt.dst, tt.src)\n\t\t\tif len(result) != tt.want {\n\t\t\t\tt.Errorf(\"EncodeWordList() returned %d words, want %d\", len(result), tt.want)\n\t\t\t}\n\t\t\t\n\t\t\t// Check that all words are valid\n\t\t\tfor i, word := range result {\n\t\t\t\tif word == \"\" {\n\t\t\t\t\tt.Errorf(\"EncodeWordList() returned empty word at index %d\", i)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncodeWordListConsistency(t *testing.T) {\n\tinput := []byte{0x12, 0x34, 0x56, 0x78}\n\t\n\t// Encode twice with empty dst\n\tresult1 := EncodeWordList([]string{}, input)\n\tresult2 := EncodeWordList([]string{}, input)\n\t\n\tif len(result1) != len(result2) {\n\t\tt.Errorf(\"Inconsistent result lengths: %d vs %d\", len(result1), len(result2))\n\t}\n\t\n\tfor i := range result1 {\n\t\tif result1[i] != result2[i] {\n\t\t\tt.Errorf(\"Inconsistent result at index %d: %s vs %s\", i, result1[i], result2[i])\n\t\t}\n\t}\n}\n\nfunc TestEncodeWordListCapacityHandling(t *testing.T) {\n\t// Test with dst that has sufficient capacity\n\tdst := make([]string, 1, 10)\n\tdst[0] = \"existing\"\n\tinput := []byte{0x01, 0x02}\n\t\n\tresult := EncodeWordList(dst, input)\n\t\n\tif len(result) != 3 { // 1 existing + 2 new\n\t\tt.Errorf(\"Expected 3 words, got %d\", len(result))\n\t}\n\t\n\tif result[0] != \"existing\" {\n\t\tt.Errorf(\"Expected first word to be 'existing', got %s\", result[0])\n\t}\n}\n\nfunc TestEncodeWordListBoundaryValues(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tsrc  []byte\n\t}{\n\t\t{\"max single byte\", []byte{0xFF}},\n\t\t{\"max two bytes\", []byte{0xFF, 0xFF}},\n\t\t{\"max three bytes\", []byte{0xFF, 0xFF, 0xFF}},\n\t\t{\"max four bytes\", []byte{0xFF, 0xFF, 0xFF, 0xFF}},\n\t\t{\"all zeros\", []byte{0x00, 0x00, 0x00, 0x00}},\n\t}\n\t\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := EncodeWordList([]string{}, tt.src)\n\t\t\texpectedLen := WordsRequired(len(tt.src))\n\t\t\t\n\t\t\tif len(result) != expectedLen {\n\t\t\t\tt.Errorf(\"Expected %d words, got %d\", expectedLen, len(result))\n\t\t\t}\n\t\t\t\n\t\t\t// Ensure all words are from the WordList\n\t\t\tfor _, word := range result {\n\t\t\t\tfound := false\n\t\t\t\tfor _, validWord := range WordList {\n\t\t\t\t\tif word == validWord {\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !found {\n\t\t\t\t\tt.Errorf(\"Invalid word generated: %s\", word)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}"
  },
  {
    "path": "src/mnemonicode/wordlist.go",
    "content": "// From GitHub version/fork maintained by Stephen Paul Weber available at:\n// https://github.com/singpolyma/mnemonicode\n//\n// Originally from:\n// http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/\n\n/*\nCopyright (c) 2000  Oren Tirosh <oren@hishome.net>\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\nall copies 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\nTHE SOFTWARE.\n*/\n\npackage mnemonicode\n\n// WordListVersion is the version of compiled in word list.\nconst WordListVersion = \"0.7\"\n\nvar wordMap = make(map[string]int, len(WordList))\n\nfunc init() {\n\tfor i, w := range WordList {\n\t\twordMap[w] = i\n\t}\n}\n\nconst longestWord = 7\n\nvar WordList = []string{\n\t\"academy\", \"acrobat\", \"active\", \"actor\", \"adam\", \"admiral\",\n\t\"adrian\", \"africa\", \"agenda\", \"agent\", \"airline\", \"airport\",\n\t\"aladdin\", \"alarm\", \"alaska\", \"albert\", \"albino\", \"album\",\n\t\"alcohol\", \"alex\", \"algebra\", \"alibi\", \"alice\", \"alien\",\n\t\"alpha\", \"alpine\", \"amadeus\", \"amanda\", \"amazon\", \"amber\",\n\t\"america\", \"amigo\", \"analog\", \"anatomy\", \"angel\", \"animal\",\n\t\"antenna\", \"antonio\", \"apollo\", \"april\", \"archive\", \"arctic\",\n\t\"arizona\", \"arnold\", \"aroma\", \"arthur\", \"artist\", \"asia\",\n\t\"aspect\", \"aspirin\", \"athena\", \"athlete\", \"atlas\", \"audio\",\n\t\"august\", \"austria\", \"axiom\", \"aztec\", \"balance\", \"ballad\",\n\t\"banana\", \"bandit\", \"banjo\", \"barcode\", \"baron\", \"basic\",\n\t\"battery\", \"belgium\", \"berlin\", \"bermuda\", \"bernard\", \"bikini\",\n\t\"binary\", \"bingo\", \"biology\", \"block\", \"blonde\", \"bonus\",\n\t\"boris\", \"boston\", \"boxer\", \"brandy\", \"bravo\", \"brazil\",\n\t\"bronze\", \"brown\", \"bruce\", \"bruno\", \"burger\", \"burma\",\n\t\"cabinet\", \"cactus\", \"cafe\", \"cairo\", \"cake\", \"calypso\",\n\t\"camel\", \"camera\", \"campus\", \"canada\", \"canal\", \"cannon\",\n\t\"canoe\", \"cantina\", \"canvas\", \"canyon\", \"capital\", \"caramel\",\n\t\"caravan\", \"carbon\", \"cargo\", \"carlo\", \"carol\", \"carpet\",\n\t\"cartel\", \"casino\", \"castle\", \"castro\", \"catalog\", \"caviar\",\n\t\"cecilia\", \"cement\", \"center\", \"century\", \"ceramic\", \"chamber\",\n\t\"chance\", \"change\", \"chaos\", \"charlie\", \"charm\", \"charter\",\n\t\"chef\", \"chemist\", \"cherry\", \"chess\", \"chicago\", \"chicken\",\n\t\"chief\", \"china\", \"cigar\", \"cinema\", \"circus\", \"citizen\",\n\t\"city\", \"clara\", \"classic\", \"claudia\", \"clean\", \"client\",\n\t\"climax\", \"clinic\", \"clock\", \"club\", \"cobra\", \"coconut\",\n\t\"cola\", \"collect\", \"colombo\", \"colony\", \"color\", \"combat\",\n\t\"comedy\", \"comet\", \"command\", \"compact\", \"company\", \"complex\",\n\t\"concept\", \"concert\", \"connect\", \"consul\", \"contact\", \"context\",\n\t\"contour\", \"control\", \"convert\", \"copy\", \"corner\", \"corona\",\n\t\"correct\", \"cosmos\", \"couple\", \"courage\", \"cowboy\", \"craft\",\n\t\"crash\", \"credit\", \"cricket\", \"critic\", \"crown\", \"crystal\",\n\t\"cuba\", \"culture\", \"dallas\", \"dance\", \"daniel\", \"david\",\n\t\"decade\", \"decimal\", \"deliver\", \"delta\", \"deluxe\", \"demand\",\n\t\"demo\", \"denmark\", \"derby\", \"design\", \"detect\", \"develop\",\n\t\"diagram\", \"dialog\", \"diamond\", \"diana\", \"diego\", \"diesel\",\n\t\"diet\", \"digital\", \"dilemma\", \"diploma\", \"direct\", \"disco\",\n\t\"disney\", \"distant\", \"doctor\", \"dollar\", \"dominic\", \"domino\",\n\t\"donald\", \"dragon\", \"drama\", \"dublin\", \"duet\", \"dynamic\",\n\t\"east\", \"ecology\", \"economy\", \"edgar\", \"egypt\", \"elastic\",\n\t\"elegant\", \"element\", \"elite\", \"elvis\", \"email\", \"energy\",\n\t\"engine\", \"english\", \"episode\", \"equator\", \"escort\", \"ethnic\",\n\t\"europe\", \"everest\", \"evident\", \"exact\", \"example\", \"exit\",\n\t\"exotic\", \"export\", \"express\", \"extra\", \"fabric\", \"factor\",\n\t\"falcon\", \"family\", \"fantasy\", \"fashion\", \"fiber\", \"fiction\",\n\t\"fidel\", \"fiesta\", \"figure\", \"film\", \"filter\", \"final\",\n\t\"finance\", \"finish\", \"finland\", \"flash\", \"florida\", \"flower\",\n\t\"fluid\", \"flute\", \"focus\", \"ford\", \"forest\", \"formal\",\n\t\"format\", \"formula\", \"fortune\", \"forum\", \"fragile\", \"france\",\n\t\"frank\", \"friend\", \"frozen\", \"future\", \"gabriel\", \"galaxy\",\n\t\"gallery\", \"gamma\", \"garage\", \"garden\", \"garlic\", \"gemini\",\n\t\"general\", \"genetic\", \"genius\", \"germany\", \"global\", \"gloria\",\n\t\"golf\", \"gondola\", \"gong\", \"good\", \"gordon\", \"gorilla\",\n\t\"grand\", \"granite\", \"graph\", \"green\", \"group\", \"guide\",\n\t\"guitar\", \"guru\", \"hand\", \"happy\", \"harbor\", \"harmony\",\n\t\"harvard\", \"havana\", \"hawaii\", \"helena\", \"hello\", \"henry\",\n\t\"hilton\", \"history\", \"horizon\", \"hotel\", \"human\", \"humor\",\n\t\"icon\", \"idea\", \"igloo\", \"igor\", \"image\", \"impact\",\n\t\"import\", \"index\", \"india\", \"indigo\", \"input\", \"insect\",\n\t\"instant\", \"iris\", \"italian\", \"jacket\", \"jacob\", \"jaguar\",\n\t\"janet\", \"japan\", \"jargon\", \"jazz\", \"jeep\", \"john\",\n\t\"joker\", \"jordan\", \"jumbo\", \"june\", \"jungle\", \"junior\",\n\t\"jupiter\", \"karate\", \"karma\", \"kayak\", \"kermit\", \"kilo\",\n\t\"king\", \"koala\", \"korea\", \"labor\", \"lady\", \"lagoon\",\n\t\"laptop\", \"laser\", \"latin\", \"lava\", \"lecture\", \"left\",\n\t\"legal\", \"lemon\", \"level\", \"lexicon\", \"liberal\", \"libra\",\n\t\"limbo\", \"limit\", \"linda\", \"linear\", \"lion\", \"liquid\",\n\t\"liter\", \"little\", \"llama\", \"lobby\", \"lobster\", \"local\",\n\t\"logic\", \"logo\", \"lola\", \"london\", \"lotus\", \"lucas\",\n\t\"lunar\", \"machine\", \"macro\", \"madam\", \"madonna\", \"madrid\",\n\t\"maestro\", \"magic\", \"magnet\", \"magnum\", \"major\", \"mama\",\n\t\"mambo\", \"manager\", \"mango\", \"manila\", \"marco\", \"marina\",\n\t\"market\", \"mars\", \"martin\", \"marvin\", \"master\", \"matrix\",\n\t\"maximum\", \"media\", \"medical\", \"mega\", \"melody\", \"melon\",\n\t\"memo\", \"mental\", \"mentor\", \"menu\", \"mercury\", \"message\",\n\t\"metal\", \"meteor\", \"meter\", \"method\", \"metro\", \"mexico\",\n\t\"miami\", \"micro\", \"million\", \"mineral\", \"minimum\", \"minus\",\n\t\"minute\", \"miracle\", \"mirage\", \"miranda\", \"mister\", \"mixer\",\n\t\"mobile\", \"model\", \"modem\", \"modern\", \"modular\", \"moment\",\n\t\"monaco\", \"monica\", \"monitor\", \"mono\", \"monster\", \"montana\",\n\t\"morgan\", \"motel\", \"motif\", \"motor\", \"mozart\", \"multi\",\n\t\"museum\", \"music\", \"mustang\", \"natural\", \"neon\", \"nepal\",\n\t\"neptune\", \"nerve\", \"neutral\", \"nevada\", \"news\", \"ninja\",\n\t\"nirvana\", \"normal\", \"nova\", \"novel\", \"nuclear\", \"numeric\",\n\t\"nylon\", \"oasis\", \"object\", \"observe\", \"ocean\", \"octopus\",\n\t\"olivia\", \"olympic\", \"omega\", \"opera\", \"optic\", \"optimal\",\n\t\"orange\", \"orbit\", \"organic\", \"orient\", \"origin\", \"orlando\",\n\t\"oscar\", \"oxford\", \"oxygen\", \"ozone\", \"pablo\", \"pacific\",\n\t\"pagoda\", \"palace\", \"pamela\", \"panama\", \"panda\", \"panel\",\n\t\"panic\", \"paradox\", \"pardon\", \"paris\", \"parker\", \"parking\",\n\t\"parody\", \"partner\", \"passage\", \"passive\", \"pasta\", \"pastel\",\n\t\"patent\", \"patriot\", \"patrol\", \"patron\", \"pegasus\", \"pelican\",\n\t\"penguin\", \"pepper\", \"percent\", \"perfect\", \"perfume\", \"period\",\n\t\"permit\", \"person\", \"peru\", \"phone\", \"photo\", \"piano\",\n\t\"picasso\", \"picnic\", \"picture\", \"pigment\", \"pilgrim\", \"pilot\",\n\t\"pirate\", \"pixel\", \"pizza\", \"planet\", \"plasma\", \"plaster\",\n\t\"plastic\", \"plaza\", \"pocket\", \"poem\", \"poetic\", \"poker\",\n\t\"polaris\", \"police\", \"politic\", \"polo\", \"polygon\", \"pony\",\n\t\"popcorn\", \"popular\", \"postage\", \"postal\", \"precise\", \"prefix\",\n\t\"premium\", \"present\", \"price\", \"prince\", \"printer\", \"prism\",\n\t\"private\", \"product\", \"profile\", \"program\", \"project\", \"protect\",\n\t\"proton\", \"public\", \"pulse\", \"puma\", \"pyramid\", \"queen\",\n\t\"radar\", \"radio\", \"random\", \"rapid\", \"rebel\", \"record\",\n\t\"recycle\", \"reflex\", \"reform\", \"regard\", \"regular\", \"relax\",\n\t\"report\", \"reptile\", \"reverse\", \"ricardo\", \"ringo\", \"ritual\",\n\t\"robert\", \"robot\", \"rocket\", \"rodeo\", \"romeo\", \"royal\",\n\t\"russian\", \"safari\", \"salad\", \"salami\", \"salmon\", \"salon\",\n\t\"salute\", \"samba\", \"sandra\", \"santana\", \"sardine\", \"school\",\n\t\"screen\", \"script\", \"second\", \"secret\", \"section\", \"segment\",\n\t\"select\", \"seminar\", \"senator\", \"senior\", \"sensor\", \"serial\",\n\t\"service\", \"sheriff\", \"shock\", \"sierra\", \"signal\", \"silicon\",\n\t\"silver\", \"similar\", \"simon\", \"single\", \"siren\", \"slogan\",\n\t\"social\", \"soda\", \"solar\", \"solid\", \"solo\", \"sonic\",\n\t\"soviet\", \"special\", \"speed\", \"spiral\", \"spirit\", \"sport\",\n\t\"static\", \"station\", \"status\", \"stereo\", \"stone\", \"stop\",\n\t\"street\", \"strong\", \"student\", \"studio\", \"style\", \"subject\",\n\t\"sultan\", \"super\", \"susan\", \"sushi\", \"suzuki\", \"switch\",\n\t\"symbol\", \"system\", \"tactic\", \"tahiti\", \"talent\", \"tango\",\n\t\"tarzan\", \"taxi\", \"telex\", \"tempo\", \"tennis\", \"texas\",\n\t\"textile\", \"theory\", \"thermos\", \"tiger\", \"titanic\", \"tokyo\",\n\t\"tomato\", \"topic\", \"tornado\", \"toronto\", \"torpedo\", \"total\",\n\t\"totem\", \"tourist\", \"tractor\", \"traffic\", \"transit\", \"trapeze\",\n\t\"travel\", \"tribal\", \"trick\", \"trident\", \"trilogy\", \"tripod\",\n\t\"tropic\", \"trumpet\", \"tulip\", \"tuna\", \"turbo\", \"twist\",\n\t\"ultra\", \"uniform\", \"union\", \"uranium\", \"vacuum\", \"valid\",\n\t\"vampire\", \"vanilla\", \"vatican\", \"velvet\", \"ventura\", \"venus\",\n\t\"vertigo\", \"veteran\", \"victor\", \"video\", \"vienna\", \"viking\",\n\t\"village\", \"vincent\", \"violet\", \"violin\", \"virtual\", \"virus\",\n\t\"visa\", \"vision\", \"visitor\", \"visual\", \"vitamin\", \"viva\",\n\t\"vocal\", \"vodka\", \"volcano\", \"voltage\", \"volume\", \"voyage\",\n\t\"water\", \"weekend\", \"welcome\", \"western\", \"window\", \"winter\",\n\t\"wizard\", \"wolf\", \"world\", \"xray\", \"yankee\", \"yoga\",\n\t\"yogurt\", \"yoyo\", \"zebra\", \"zero\", \"zigzag\", \"zipper\",\n\t\"zodiac\", \"zoom\", \"abraham\", \"action\", \"address\", \"alabama\",\n\t\"alfred\", \"almond\", \"ammonia\", \"analyze\", \"annual\", \"answer\",\n\t\"apple\", \"arena\", \"armada\", \"arsenal\", \"atlanta\", \"atomic\",\n\t\"avenue\", \"average\", \"bagel\", \"baker\", \"ballet\", \"bambino\",\n\t\"bamboo\", \"barbara\", \"basket\", \"bazaar\", \"benefit\", \"bicycle\",\n\t\"bishop\", \"blitz\", \"bonjour\", \"bottle\", \"bridge\", \"british\",\n\t\"brother\", \"brush\", \"budget\", \"cabaret\", \"cadet\", \"candle\",\n\t\"capitan\", \"capsule\", \"career\", \"cartoon\", \"channel\", \"chapter\",\n\t\"cheese\", \"circle\", \"cobalt\", \"cockpit\", \"college\", \"compass\",\n\t\"comrade\", \"condor\", \"crimson\", \"cyclone\", \"darwin\", \"declare\",\n\t\"degree\", \"delete\", \"delphi\", \"denver\", \"desert\", \"divide\",\n\t\"dolby\", \"domain\", \"domingo\", \"double\", \"drink\", \"driver\",\n\t\"eagle\", \"earth\", \"echo\", \"eclipse\", \"editor\", \"educate\",\n\t\"edward\", \"effect\", \"electra\", \"emerald\", \"emotion\", \"empire\",\n\t\"empty\", \"escape\", \"eternal\", \"evening\", \"exhibit\", \"expand\",\n\t\"explore\", \"extreme\", \"ferrari\", \"first\", \"flag\", \"folio\",\n\t\"forget\", \"forward\", \"freedom\", \"fresh\", \"friday\", \"fuji\",\n\t\"galileo\", \"garcia\", \"genesis\", \"gold\", \"gravity\", \"habitat\",\n\t\"hamlet\", \"harlem\", \"helium\", \"holiday\", \"house\", \"hunter\",\n\t\"ibiza\", \"iceberg\", \"imagine\", \"infant\", \"isotope\", \"jackson\",\n\t\"jamaica\", \"jasmine\", \"java\", \"jessica\", \"judo\", \"kitchen\",\n\t\"lazarus\", \"letter\", \"license\", \"lithium\", \"loyal\", \"lucky\",\n\t\"magenta\", \"mailbox\", \"manual\", \"marble\", \"mary\", \"maxwell\",\n\t\"mayor\", \"milk\", \"monarch\", \"monday\", \"money\", \"morning\",\n\t\"mother\", \"mystery\", \"native\", \"nectar\", \"nelson\", \"network\",\n\t\"next\", \"nikita\", \"nobel\", \"nobody\", \"nominal\", \"norway\",\n\t\"nothing\", \"number\", \"october\", \"office\", \"oliver\", \"opinion\",\n\t\"option\", \"order\", \"outside\", \"package\", \"pancake\", \"pandora\",\n\t\"panther\", \"papa\", \"patient\", \"pattern\", \"pedro\", \"pencil\",\n\t\"people\", \"phantom\", \"philips\", \"pioneer\", \"pluto\", \"podium\",\n\t\"portal\", \"potato\", \"prize\", \"process\", \"protein\", \"proxy\",\n\t\"pump\", \"pupil\", \"python\", \"quality\", \"quarter\", \"quiet\",\n\t\"rabbit\", \"radical\", \"radius\", \"rainbow\", \"ralph\", \"ramirez\",\n\t\"ravioli\", \"raymond\", \"respect\", \"respond\", \"result\", \"resume\",\n\t\"retro\", \"richard\", \"right\", \"risk\", \"river\", \"roger\",\n\t\"roman\", \"rondo\", \"sabrina\", \"salary\", \"salsa\", \"sample\",\n\t\"samuel\", \"saturn\", \"savage\", \"scarlet\", \"scoop\", \"scorpio\",\n\t\"scratch\", \"scroll\", \"sector\", \"serpent\", \"shadow\", \"shampoo\",\n\t\"sharon\", \"sharp\", \"short\", \"shrink\", \"silence\", \"silk\",\n\t\"simple\", \"slang\", \"smart\", \"smoke\", \"snake\", \"society\",\n\t\"sonar\", \"sonata\", \"soprano\", \"source\", \"sparta\", \"sphere\",\n\t\"spider\", \"sponsor\", \"spring\", \"acid\", \"adios\", \"agatha\",\n\t\"alamo\", \"alert\", \"almanac\", \"aloha\", \"andrea\", \"anita\",\n\t\"arcade\", \"aurora\", \"avalon\", \"baby\", \"baggage\", \"balloon\",\n\t\"bank\", \"basil\", \"begin\", \"biscuit\", \"blue\", \"bombay\",\n\t\"brain\", \"brenda\", \"brigade\", \"cable\", \"carmen\", \"cello\",\n\t\"celtic\", \"chariot\", \"chrome\", \"citrus\", \"civil\", \"cloud\",\n\t\"common\", \"compare\", \"cool\", \"copper\", \"coral\", \"crater\",\n\t\"cubic\", \"cupid\", \"cycle\", \"depend\", \"door\", \"dream\",\n\t\"dynasty\", \"edison\", \"edition\", \"enigma\", \"equal\", \"eric\",\n\t\"event\", \"evita\", \"exodus\", \"extend\", \"famous\", \"farmer\",\n\t\"food\", \"fossil\", \"frog\", \"fruit\", \"geneva\", \"gentle\",\n\t\"george\", \"giant\", \"gilbert\", \"gossip\", \"gram\", \"greek\",\n\t\"grille\", \"hammer\", \"harvest\", \"hazard\", \"heaven\", \"herbert\",\n\t\"heroic\", \"hexagon\", \"husband\", \"immune\", \"inca\", \"inch\",\n\t\"initial\", \"isabel\", \"ivory\", \"jason\", \"jerome\", \"joel\",\n\t\"joshua\", \"journal\", \"judge\", \"juliet\", \"jump\", \"justice\",\n\t\"kimono\", \"kinetic\", \"leonid\", \"lima\", \"maze\", \"medusa\",\n\t\"member\", \"memphis\", \"michael\", \"miguel\", \"milan\", \"mile\",\n\t\"miller\", \"mimic\", \"mimosa\", \"mission\", \"monkey\", \"moral\",\n\t\"moses\", \"mouse\", \"nancy\", \"natasha\", \"nebula\", \"nickel\",\n\t\"nina\", \"noise\", \"orchid\", \"oregano\", \"origami\", \"orinoco\",\n\t\"orion\", \"othello\", \"paper\", \"paprika\", \"prelude\", \"prepare\",\n\t\"pretend\", \"profit\", \"promise\", \"provide\", \"puzzle\", \"remote\",\n\t\"repair\", \"reply\", \"rival\", \"riviera\", \"robin\", \"rose\",\n\t\"rover\", \"rudolf\", \"saga\", \"sahara\", \"scholar\", \"shelter\",\n\t\"ship\", \"shoe\", \"sigma\", \"sister\", \"sleep\", \"smile\",\n\t\"spain\", \"spark\", \"split\", \"spray\", \"square\", \"stadium\",\n\t\"star\", \"storm\", \"story\", \"strange\", \"stretch\", \"stuart\",\n\t\"subway\", \"sugar\", \"sulfur\", \"summer\", \"survive\", \"sweet\",\n\t\"swim\", \"table\", \"taboo\", \"target\", \"teacher\", \"telecom\",\n\t\"temple\", \"tibet\", \"ticket\", \"tina\", \"today\", \"toga\",\n\t\"tommy\", \"tower\", \"trivial\", \"tunnel\", \"turtle\", \"twin\",\n\t\"uncle\", \"unicorn\", \"unique\", \"update\", \"valery\", \"vega\",\n\t\"version\", \"voodoo\", \"warning\", \"william\", \"wonder\", \"year\",\n\t\"yellow\", \"young\", \"absent\", \"absorb\", \"accent\", \"alfonso\",\n\t\"alias\", \"ambient\", \"andy\", \"anvil\", \"appear\", \"apropos\",\n\t\"archer\", \"ariel\", \"armor\", \"arrow\", \"austin\", \"avatar\",\n\t\"axis\", \"baboon\", \"bahama\", \"bali\", \"balsa\", \"bazooka\",\n\t\"beach\", \"beast\", \"beatles\", \"beauty\", \"before\", \"benny\",\n\t\"betty\", \"between\", \"beyond\", \"billy\", \"bison\", \"blast\",\n\t\"bless\", \"bogart\", \"bonanza\", \"book\", \"border\", \"brave\",\n\t\"bread\", \"break\", \"broken\", \"bucket\", \"buenos\", \"buffalo\",\n\t\"bundle\", \"button\", \"buzzer\", \"byte\", \"caesar\", \"camilla\",\n\t\"canary\", \"candid\", \"carrot\", \"cave\", \"chant\", \"child\",\n\t\"choice\", \"chris\", \"cipher\", \"clarion\", \"clark\", \"clever\",\n\t\"cliff\", \"clone\", \"conan\", \"conduct\", \"congo\", \"content\",\n\t\"costume\", \"cotton\", \"cover\", \"crack\", \"current\", \"danube\",\n\t\"data\", \"decide\", \"desire\", \"detail\", \"dexter\", \"dinner\",\n\t\"dispute\", \"donor\", \"druid\", \"drum\", \"easy\", \"eddie\",\n\t\"enjoy\", \"enrico\", \"epoxy\", \"erosion\", \"except\", \"exile\",\n\t\"explain\", \"fame\", \"fast\", \"father\", \"felix\", \"field\",\n\t\"fiona\", \"fire\", \"fish\", \"flame\", \"flex\", \"flipper\",\n\t\"float\", \"flood\", \"floor\", \"forbid\", \"forever\", \"fractal\",\n\t\"frame\", \"freddie\", \"front\", \"fuel\", \"gallop\", \"game\",\n\t\"garbo\", \"gate\", \"gibson\", \"ginger\", \"giraffe\", \"gizmo\",\n\t\"glass\", \"goblin\", \"gopher\", \"grace\", \"gray\", \"gregory\",\n\t\"grid\", \"griffin\", \"ground\", \"guest\", \"gustav\", \"gyro\",\n\t\"hair\", \"halt\", \"harris\", \"heart\", \"heavy\", \"herman\",\n\t\"hippie\", \"hobby\", \"honey\", \"hope\", \"horse\", \"hostel\",\n\t\"hydro\", \"imitate\", \"info\", \"ingrid\", \"inside\", \"invent\",\n\t\"invest\", \"invite\", \"iron\", \"ivan\", \"james\", \"jester\",\n\t\"jimmy\", \"join\", \"joseph\", \"juice\", \"julius\", \"july\",\n\t\"justin\", \"kansas\", \"karl\", \"kevin\", \"kiwi\", \"ladder\",\n\t\"lake\", \"laura\", \"learn\", \"legacy\", \"legend\", \"lesson\",\n\t\"life\", \"light\", \"list\", \"locate\", \"lopez\", \"lorenzo\",\n\t\"love\", \"lunch\", \"malta\", \"mammal\", \"margo\", \"marion\",\n\t\"mask\", \"match\", \"mayday\", \"meaning\", \"mercy\", \"middle\",\n\t\"mike\", \"mirror\", \"modest\", \"morph\", \"morris\", \"nadia\",\n\t\"nato\", \"navy\", \"needle\", \"neuron\", \"never\", \"newton\",\n\t\"nice\", \"night\", \"nissan\", \"nitro\", \"nixon\", \"north\",\n\t\"oberon\", \"octavia\", \"ohio\", \"olga\", \"open\", \"opus\",\n\t\"orca\", \"oval\", \"owner\", \"page\", \"paint\", \"palma\",\n\t\"parade\", \"parent\", \"parole\", \"paul\", \"peace\", \"pearl\",\n\t\"perform\", \"phoenix\", \"phrase\", \"pierre\", \"pinball\", \"place\",\n\t\"plate\", \"plato\", \"plume\", \"pogo\", \"point\", \"polite\",\n\t\"polka\", \"poncho\", \"powder\", \"prague\", \"press\", \"presto\",\n\t\"pretty\", \"prime\", \"promo\", \"quasi\", \"quest\", \"quick\",\n\t\"quiz\", \"quota\", \"race\", \"rachel\", \"raja\", \"ranger\",\n\t\"region\", \"remark\", \"rent\", \"reward\", \"rhino\", \"ribbon\",\n\t\"rider\", \"road\", \"rodent\", \"round\", \"rubber\", \"ruby\",\n\t\"rufus\", \"sabine\", \"saddle\", \"sailor\", \"saint\", \"salt\",\n\t\"satire\", \"scale\", \"scuba\", \"season\", \"secure\", \"shake\",\n\t\"shallow\", \"shannon\", \"shave\", \"shelf\", \"sherman\", \"shine\",\n\t\"shirt\", \"side\", \"sinatra\", \"sincere\", \"size\", \"slalom\",\n\t\"slow\", \"small\", \"snow\", \"sofia\", \"song\", \"sound\",\n\t\"south\", \"speech\", \"spell\", \"spend\", \"spoon\", \"stage\",\n\t\"stamp\", \"stand\", \"state\", \"stella\", \"stick\", \"sting\",\n\t\"stock\", \"store\", \"sunday\", \"sunset\", \"support\", \"sweden\",\n\t\"swing\", \"tape\", \"think\", \"thomas\", \"tictac\", \"time\",\n\t\"toast\", \"tobacco\", \"tonight\", \"torch\", \"torso\", \"touch\",\n\t\"toyota\", \"trade\", \"tribune\", \"trinity\", \"triton\", \"truck\",\n\t\"trust\", \"type\", \"under\", \"unit\", \"urban\", \"urgent\",\n\t\"user\", \"value\", \"vendor\", \"venice\", \"verona\", \"vibrate\",\n\t\"virgo\", \"visible\", \"vista\", \"vital\", \"voice\", \"vortex\",\n\t\"waiter\", \"watch\", \"wave\", \"weather\", \"wedding\", \"wheel\",\n\t\"whiskey\", \"wisdom\", \"deal\", \"null\", \"nurse\", \"quebec\",\n\t\"reserve\", \"reunion\", \"roof\", \"singer\", \"verbal\", \"amen\",\n\t\"ego\", \"fax\", \"jet\", \"job\", \"rio\", \"ski\",\n\t\"yes\",\n}\n"
  },
  {
    "path": "src/models/constants.go",
    "content": "package models\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/schollz/croc/v10/src/utils\"\n\tlog \"github.com/schollz/logger\"\n)\n\n// TCP_BUFFER_SIZE is the maximum packet size\nconst TCP_BUFFER_SIZE = 1024 * 64\n\n// DEFAULT_RELAY is the default relay used (can be set using --relay)\nvar (\n\tDEFAULT_RELAY      = \"croc.schollz.com\"\n\tDEFAULT_RELAY6     = \"croc6.schollz.com\"\n\tDEFAULT_PORT       = \"9009\"\n\tDEFAULT_PASSPHRASE = \"pass123\"\n\tINTERNAL_DNS       = false\n)\n\n// publicDNS are servers to be queried if a local lookup fails\nvar publicDNS = []string{\n\t\"1.0.0.1\",                // Cloudflare\n\t\"1.1.1.1\",                // Cloudflare\n\t\"[2606:4700:4700::1111]\", // Cloudflare\n\t\"[2606:4700:4700::1001]\", // Cloudflare\n\t\"8.8.4.4\",                // Google\n\t\"8.8.8.8\",                // Google\n\t\"[2001:4860:4860::8844]\", // Google\n\t\"[2001:4860:4860::8888]\", // Google\n\t\"9.9.9.9\",                // Quad9\n\t\"149.112.112.112\",        // Quad9\n\t\"[2620:fe::fe]\",          // Quad9\n\t\"[2620:fe::fe:9]\",        // Quad9\n\t\"8.26.56.26\",             // Comodo\n\t\"8.20.247.20\",            // Comodo\n\t\"208.67.220.220\",         // Cisco OpenDNS\n\t\"208.67.222.222\",         // Cisco OpenDNS\n\t\"[2620:119:35::35]\",      // Cisco OpenDNS\n\t\"[2620:119:53::53]\",      // Cisco OpenDNS\n}\n\nfunc getConfigFile(requireValidPath bool) (fname string, err error) {\n\tconfigFile, err := utils.GetConfigDir(requireValidPath)\n\tif err != nil {\n\t\treturn\n\t}\n\tfname = path.Join(configFile, \"internal-dns\")\n\treturn\n}\n\nfunc init() {\n\tlog.SetLevel(\"info\")\n\tlog.SetOutput(os.Stderr)\n\tdoRemember := false\n\tfor _, flag := range os.Args {\n\t\tif flag == \"--internal-dns\" {\n\t\t\tINTERNAL_DNS = true\n\t\t\tbreak\n\t\t}\n\t\tif flag == \"--remember\" {\n\t\t\tdoRemember = true\n\t\t}\n\t}\n\tif doRemember {\n\t\t// save in config file\n\t\tfname, err := getConfigFile(true)\n\t\tif err == nil {\n\t\t\tf, _ := os.Create(fname)\n\t\t\tf.Close()\n\t\t}\n\t}\n\tif !INTERNAL_DNS {\n\t\tfname, err := getConfigFile(false)\n\t\tif err == nil {\n\t\t\tINTERNAL_DNS = utils.Exists(fname)\n\t\t}\n\t}\n\tlog.Trace(\"Using internal DNS: \", INTERNAL_DNS)\n\tvar err error\n\tvar addr string\n\taddr, err = lookup(DEFAULT_RELAY)\n\tif err == nil {\n\t\tDEFAULT_RELAY = net.JoinHostPort(addr, DEFAULT_PORT)\n\t} else {\n\t\tDEFAULT_RELAY = \"\"\n\t}\n\tlog.Tracef(\"Default ipv4 relay: %s\", addr)\n\taddr, err = lookup(DEFAULT_RELAY6)\n\tif err == nil {\n\t\tDEFAULT_RELAY6 = net.JoinHostPort(addr, DEFAULT_PORT)\n\t} else {\n\t\tDEFAULT_RELAY6 = \"\"\n\t}\n\tlog.Tracef(\"Default ipv6 relay: %s\", addr)\n}\n\n// Resolve a hostname to an IP address using DNS.\nfunc lookup(address string) (ipaddress string, err error) {\n\tif !INTERNAL_DNS {\n\t\tlog.Tracef(\"Using local DNS to resolve %s\", address)\n\t\treturn localLookupIP(address)\n\t}\n\ttype Result struct {\n\t\ts   string\n\t\terr error\n\t}\n\tresult := make(chan Result, len(publicDNS))\n\tfor _, dns := range publicDNS {\n\t\tgo func(dns string) {\n\t\t\tvar r Result\n\t\t\tr.s, r.err = remoteLookupIP(address, dns)\n\t\t\tlog.Tracef(\"Resolved %s to %s using %s\", address, r.s, dns)\n\t\t\tresult <- r\n\t\t}(dns)\n\t}\n\tfor i := 0; i < len(publicDNS); i++ {\n\t\tipaddress = (<-result).s\n\t\tlog.Tracef(\"Resolved %s to %s\", address, ipaddress)\n\t\tif ipaddress != \"\" {\n\t\t\treturn\n\t\t}\n\t}\n\terr = fmt.Errorf(\"failed to resolve %s: all DNS servers exhausted\", address)\n\treturn\n}\n\n// localLookupIP returns a host's IP address using the local DNS configuration.\nfunc localLookupIP(address string) (ipaddress string, err error) {\n\t// Create a context with a 500 millisecond timeout\n\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\tdefer cancel()\n\n\tr := &net.Resolver{}\n\n\t// Use the context with timeout in the LookupHost function\n\tip, err := r.LookupHost(ctx, address)\n\tif err != nil {\n\t\treturn\n\t}\n\tipaddress = ip[0]\n\treturn\n}\n\n// remoteLookupIP returns a host's IP address based on a remote DNS server.\nfunc remoteLookupIP(address, dns string) (ipaddress string, err error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\tdefer cancel()\n\n\tr := &net.Resolver{\n\t\tPreferGo: true,\n\t\tDial: func(ctx context.Context, network, _ string) (net.Conn, error) {\n\t\t\td := new(net.Dialer)\n\t\t\treturn d.DialContext(ctx, network, dns+\":53\")\n\t\t},\n\t}\n\tip, err := r.LookupHost(ctx, address)\n\tif err != nil {\n\t\treturn\n\t}\n\tipaddress = ip[0]\n\treturn\n}\n"
  },
  {
    "path": "src/models/models_test.go",
    "content": "package models\n\nimport (\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConstants(t *testing.T) {\n\tif TCP_BUFFER_SIZE != 1024*64 {\n\t\tt.Errorf(\"TCP_BUFFER_SIZE = %d, want %d\", TCP_BUFFER_SIZE, 1024*64)\n\t}\n\n\tif DEFAULT_PORT != \"9009\" {\n\t\tt.Errorf(\"DEFAULT_PORT = %s, want %s\", DEFAULT_PORT, \"9009\")\n\t}\n\n\tif DEFAULT_PASSPHRASE != \"pass123\" {\n\t\tt.Errorf(\"DEFAULT_PASSPHRASE = %s, want %s\", DEFAULT_PASSPHRASE, \"pass123\")\n\t}\n}\n\nfunc TestPublicDNSServers(t *testing.T) {\n\tif len(publicDNS) == 0 {\n\t\tt.Error(\"publicDNS list should not be empty\")\n\t}\n\n\t// Check that we have both IPv4 and IPv6 servers\n\thasIPv4 := false\n\thasIPv6 := false\n\n\tfor _, dns := range publicDNS {\n\t\tif strings.Contains(dns, \"[\") {\n\t\t\thasIPv6 = true\n\t\t} else {\n\t\t\thasIPv4 = true\n\t\t}\n\t}\n\n\tif !hasIPv4 {\n\t\tt.Error(\"publicDNS should contain IPv4 servers\")\n\t}\n\n\tif !hasIPv6 {\n\t\tt.Error(\"publicDNS should contain IPv6 servers\")\n\t}\n\n\t// Verify known DNS servers are present\n\texpectedServers := []string{\n\t\t\"1.1.1.1\",        // Cloudflare\n\t\t\"8.8.8.8\",        // Google\n\t\t\"9.9.9.9\",        // Quad9\n\t\t\"208.67.220.220\", // OpenDNS\n\t}\n\n\tfor _, expected := range expectedServers {\n\t\tfound := false\n\t\tfor _, dns := range publicDNS {\n\t\t\tif dns == expected {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tt.Errorf(\"Expected DNS server %s not found in publicDNS\", expected)\n\t\t}\n\t}\n}\n\nfunc TestLocalLookupIP(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taddress string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"localhost\",\n\t\t\taddress: \"localhost\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid hostname\",\n\t\t\taddress: \"this-hostname-should-not-exist-12345\",\n\t\t\twantErr: 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\tip, err := localLookupIP(tt.address)\n\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"localLookupIP() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !tt.wantErr && ip == \"\" {\n\t\t\t\tt.Error(\"localLookupIP() returned empty IP for valid hostname\")\n\t\t\t}\n\n\t\t\tif !tt.wantErr {\n\t\t\t\t// Verify it's a valid IP address\n\t\t\t\tif net.ParseIP(ip) == nil {\n\t\t\t\t\tt.Errorf(\"localLookupIP() returned invalid IP: %s\", ip)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemoteLookupIPTimeout(t *testing.T) {\n\t// Test with an invalid DNS server that should timeout\n\tstart := time.Now()\n\t_, err := remoteLookupIP(\"example.com\", \"192.0.2.1\")\n\tduration := time.Since(start)\n\n\t// Should timeout within reasonable time (we set 500ms timeout)\n\tif duration > time.Second {\n\t\tt.Errorf(\"remoteLookupIP took too long: %v\", duration)\n\t}\n\n\tif err == nil {\n\t\tt.Error(\"remoteLookupIP should have failed with invalid DNS server\")\n\t}\n}\n\nfunc TestLookupFunction(t *testing.T) {\n\t// Test the main lookup function\n\ttests := []struct {\n\t\tname    string\n\t\taddress string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"localhost\",\n\t\t\taddress: \"localhost\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid hostname\",\n\t\t\taddress: \"this-hostname-should-definitely-not-exist-98765\",\n\t\t\twantErr: 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\tip, err := lookup(tt.address)\n\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"lookup() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !tt.wantErr && ip == \"\" {\n\t\t\t\tt.Error(\"lookup() returned empty IP for valid hostname\")\n\t\t\t}\n\n\t\t\tif !tt.wantErr {\n\t\t\t\t// Verify it's a valid IP address\n\t\t\t\tif net.ParseIP(ip) == nil {\n\t\t\t\t\tt.Errorf(\"lookup() returned invalid IP: %s\", ip)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetConfigFile(t *testing.T) {\n\tfname, err := getConfigFile(false)\n\tif err != nil {\n\t\tt.Skip(\"Could not get config directory\")\n\t}\n\n\tif !strings.HasSuffix(fname, \"internal-dns\") {\n\t\tt.Errorf(\"Expected config file to end with 'internal-dns', got %s\", fname)\n\t}\n}\n"
  },
  {
    "path": "src/tcp/ctx.go",
    "content": "// ctx.go\npackage tcp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\n\tlog \"github.com/schollz/logger\"\n)\n\n// stop manages graceful shutdown of the TCP server\ntype stop struct {\n\tctx    context.Context\n\tcancel context.CancelFunc\n\t// Track connections\n\tserver net.Listener\n\twg     sync.WaitGroup\n\tgui    bool\n}\n\n// newStop creates a new stop manager\nfunc newStop(ctx context.Context) *stop {\n\ts := &stop{}\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\ts.ctx, s.cancel = context.WithCancel(ctx)\n\n\treturn s\n}\n\n// Cancel initiate graceful shutdown\nfunc (s *stop) Cancel() {\n\tlog.Trace(\"tcp Cancel\")\n\tif s.cancel != nil {\n\t\ts.cancel()\n\t\ts.cancel = nil\n\t}\n}\n\nfunc RunCtx(ctx context.Context, debugLevel, host, port, password string, banner ...string) error {\n\treturn RunWithOptionsAsync(host, port, password, WithBanner(banner...), WithLogLevel(debugLevel), WithCtx(ctx))\n}\n\nfunc WithCtx(ctx context.Context) serverOptsFunc {\n\treturn func(s *server) error {\n\t\tif s.stop.cancel != nil {\n\t\t\ts.stop.cancel()\n\t\t}\n\t\ts.stop = newStop(ctx)\n\t\ts.stop.gui = true\n\t\treturn nil\n\t}\n}\n\n// Ignore context cancellation error\nfunc Ignore(err error) error {\n\tif err != nil && (errors.Is(err, context.Canceled) ||\n\t\terrors.Is(err, context.DeadlineExceeded) ||\n\t\t// ignore Listener closed during cancellation\n\t\t// strings.Contains(err.Error(), \"use of closed network connection\") ||\n\t\terrors.Is(err, net.ErrClosed)) {\n\t\tlog.Tracef(\"ignored: %v\", err)\n\t\treturn nil\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "src/tcp/defaults.go",
    "content": "package tcp\n\nimport \"time\"\n\nconst (\n\tDEFAULT_LOG_LEVEL             = \"debug\"\n\tDEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time.Minute\n\tDEFAULT_ROOM_TTL              = 3 * time.Hour\n)\n"
  },
  {
    "path": "src/tcp/options.go",
    "content": "package tcp\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// TODO: maybe export from logger library?\nvar availableLogLevels = []string{\"info\", \"error\", \"warn\", \"debug\", \"trace\"}\n\ntype serverOptsFunc func(s *server) error\n\nfunc WithBanner(banner ...string) serverOptsFunc {\n\treturn func(s *server) error {\n\t\tif len(banner) > 0 {\n\t\t\ts.banner = banner[0]\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc WithLogLevel(level string) serverOptsFunc {\n\treturn func(s *server) error {\n\t\tif !containsSlice(availableLogLevels, level) {\n\t\t\treturn fmt.Errorf(\"invalid log level specified: %s\", level)\n\t\t}\n\t\ts.debugLevel = level\n\t\treturn nil\n\t}\n}\n\nfunc WithRoomCleanupInterval(interval time.Duration) serverOptsFunc {\n\treturn func(s *server) error {\n\t\ts.roomCleanupInterval = interval\n\t\treturn nil\n\t}\n}\n\nfunc WithRoomTTL(ttl time.Duration) serverOptsFunc {\n\treturn func(s *server) error {\n\t\ts.roomTTL = ttl\n\t\treturn nil\n\t}\n}\n\nfunc containsSlice(s []string, e string) bool {\n\tfor _, ss := range s {\n\t\tif e == ss {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "src/tcp/tcp.go",
    "content": "package tcp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/schollz/pake/v3\"\n\n\t\"github.com/schollz/croc/v10/src/comm\"\n\t\"github.com/schollz/croc/v10/src/crypt\"\n\t\"github.com/schollz/croc/v10/src/models\"\n)\n\ntype server struct {\n\thost       string\n\tport       string\n\tdebugLevel string\n\tbanner     string\n\tpassword   string\n\trooms      roomMap\n\n\troomCleanupInterval time.Duration\n\troomTTL             time.Duration\n\n\t// stopRoomCleanup chan struct{}\n\t// replaced by stop ctx.go\n\t*stop\n}\n\ntype roomInfo struct {\n\tfirst  *comm.Comm\n\tsecond *comm.Comm\n\topened time.Time\n\tfull   bool\n}\n\ntype roomMap struct {\n\trooms map[string]roomInfo\n\tsync.Mutex\n}\n\nconst pingRoom = \"pinglkasjdlfjsaldjf\"\n\n// newDefaultServer initializes a new server, with some default configuration options\nfunc newDefaultServer() *server {\n\ts := new(server)\n\ts.roomCleanupInterval = DEFAULT_ROOM_CLEANUP_INTERVAL\n\ts.roomTTL = DEFAULT_ROOM_TTL\n\ts.debugLevel = DEFAULT_LOG_LEVEL\n\t// s.stopRoomCleanup = make(chan struct{}) replaced by stop\n\ts.stop = newStop(context.Background())\n\treturn s\n}\n\n// RunWithOptionsAsync asynchronously starts a TCP listener.\nfunc RunWithOptionsAsync(host, port, password string, opts ...serverOptsFunc) error {\n\ts := newDefaultServer()\n\ts.host = host\n\ts.port = port\n\ts.password = password\n\tfor _, opt := range opts {\n\t\terr := opt(s)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not apply optional configurations: %w\", err)\n\t\t}\n\t}\n\treturn s.start()\n}\n\n// Run starts a tcp listener, run async\nfunc Run(debugLevel, host, port, password string, banner ...string) (err error) {\n\treturn RunWithOptionsAsync(host, port, password, WithBanner(banner...), WithLogLevel(debugLevel))\n}\n\n// Mask our password in logs\nfunc maskedPassword(password string) (s string) {\n\tif len(password) > 2 {\n\t\ts = fmt.Sprintf(\"%c***%c\", password[0], password[len(password)-1])\n\t} else {\n\t\ts = password\n\t}\n\treturn\n}\n\nfunc (s *server) start() (err error) {\n\tlog.SetLevel(s.debugLevel)\n\n\tlog.Debugf(\"starting with password '%s'\", maskedPassword(s.password))\n\n\ts.rooms.Lock()\n\ts.rooms.rooms = make(map[string]roomInfo)\n\ts.rooms.Unlock()\n\n\ts.stop.wg.Add(1)\n\tgo func() {\n\t\tdefer s.stop.wg.Done()\n\t\ts.deleteOldRooms()\n\t}()\n\t// defer s.stopRoomDeletion()\n\tdefer s.stop.Cancel()\n\tif s.stop.gui {\n\t\tdefer s.stop.wg.Wait()\n\t}\n\n\terr = s.run()\n\terr = Ignore(err)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\treturn\n}\n\nfunc (s *server) run() (err error) {\n\tnetwork := \"tcp\"\n\taddr := net.JoinHostPort(s.host, s.port)\n\tif s.host != \"\" {\n\t\tip := net.ParseIP(s.host)\n\t\tif ip == nil {\n\t\t\tvar tcpIP *net.IPAddr\n\t\t\ttcpIP, err = net.ResolveIPAddr(\"ip\", s.host)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tip = tcpIP.IP\n\t\t}\n\t\taddr = net.JoinHostPort(ip.String(), s.port)\n\t\tif ip.To4() != nil {\n\t\t\tnetwork = \"tcp4\"\n\t\t} else {\n\t\t\tnetwork = \"tcp6\"\n\t\t}\n\n\t}\n\taddr = strings.Replace(addr, \"127.0.0.1\", \"0.0.0.0\", 1)\n\tlog.Infof(\"starting TCP server on %s\", addr)\n\tlc := net.ListenConfig{}\n\ts.stop.server, err = lc.Listen(s.stop.ctx, network, addr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error listening on %s: %w\", addr, err)\n\t}\n\tdefer s.stop.server.Close()\n\n\tgo func() {\n\t\tdc := &net.Dialer{\n\t\t\tTimeout: 100 * time.Millisecond,\n\t\t}\n\t\tif conn, err := dc.DialContext(s.stop.ctx, network, addr); err == nil {\n\t\t\tlog.Debugf(\"started TCP server on %s\", addr)\n\t\t\tconn.Close()\n\t\t} else {\n\t\t\tlog.Errorf(\"started TCP server on %s : %v\", addr, err)\n\t\t\ts.stop.Cancel()\n\t\t}\n\t}()\n\n\t// spawn a new goroutine whenever a client connects\n\tfor {\n\t\tconnection, err := s.stop.server.Accept()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"problem accepting connection: %w\", err)\n\t\t}\n\t\tlog.Debugf(\"client %s connected\", connection.RemoteAddr().String())\n\t\ts.stop.wg.Add(1)\n\t\tgo func(connection net.Conn) {\n\t\t\tdefer s.stop.wg.Done()\n\t\t\tc := comm.New(connection)\n\t\t\troom, errCommunication := s.clientCommunication(c)\n\t\t\tlog.Debugf(\"room: %+v\", room)\n\t\t\tlog.Debugf(\"err: %+v\", errCommunication)\n\t\t\tif errCommunication != nil {\n\t\t\t\tlog.Debugf(\"relay-%s: %s\", connection.RemoteAddr().String(), errCommunication.Error())\n\t\t\t\tconnection.Close()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif room == pingRoom {\n\t\t\t\tlog.Debugf(\"got ping\")\n\t\t\t\tconnection.Close()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tticker := time.NewTicker(1 * time.Second)\n\t\t\tdefer ticker.Stop()\n\t\t\tfor {\n\t\t\t\t// check connection\n\t\t\t\tlog.Tracef(\"checking connection of room %s for %+v\", room, c)\n\t\t\t\tdeleteIt := false\n\t\t\t\ts.rooms.Lock()\n\t\t\t\troomData, ok := s.rooms.rooms[room]\n\t\t\t\tif !ok {\n\t\t\t\t\tlog.Debug(\"room is gone\")\n\t\t\t\t\ts.rooms.Unlock()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Tracef(\"room: %+v\", roomData)\n\t\t\t\tif roomData.first != nil && roomData.second != nil {\n\t\t\t\t\tlog.Debug(\"rooms ready\")\n\t\t\t\t\ts.rooms.Unlock()\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif roomData.first != nil {\n\t\t\t\t\terrSend := roomData.first.Send([]byte{1})\n\t\t\t\t\tif errSend != nil {\n\t\t\t\t\t\tlog.Debug(errSend)\n\t\t\t\t\t\tdeleteIt = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ts.rooms.Unlock()\n\t\t\t\tif deleteIt {\n\t\t\t\t\ts.deleteRoom(room)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tselect {\n\t\t\t\tcase <-s.stop.ctx.Done():\n\t\t\t\t\tlog.Tracef(\"check: %v\", s.stop.ctx.Err())\n\t\t\t\t\ts.deleteRoom(room)\n\t\t\t\t\treturn\n\t\t\t\tcase <-ticker.C:\n\t\t\t\t\t// time.Sleep(1 * time.Second)\n\t\t\t\t}\n\t\t\t}\n\t\t}(connection)\n\t}\n}\n\n// deleteOldRooms checks for rooms at a regular interval and removes those that\n// have exceeded their allocated TTL.\nfunc (s *server) deleteOldRooms() {\n\tticker := time.NewTicker(s.roomCleanupInterval)\n\tdefer func() {\n\t\tticker.Stop()\n\t\tlog.Debug(\"room cleanup stopped\")\n\t}()\n\tfor next := true; next; {\n\t\troomsToDelete := []string{}\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\ts.rooms.Lock()\n\t\t\tfor room, roomData := range s.rooms.rooms {\n\t\t\t\tif time.Since(roomData.opened) > s.roomTTL {\n\t\t\t\t\troomsToDelete = append(roomsToDelete, room)\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.rooms.Unlock()\n\t\tcase <-s.stop.ctx.Done():\n\t\t\tif s.server != nil {\n\t\t\t\tlog.Debugf(\"stop TCP server on %s\", s.server.Addr())\n\t\t\t\ts.server.Close()\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t}\n\t\t\tlog.Debug(\"stop room cleanup fired\")\n\t\t\ts.rooms.Lock()\n\t\t\tfor room := range s.rooms.rooms {\n\t\t\t\troomsToDelete = append(roomsToDelete, room)\n\t\t\t}\n\t\t\ts.rooms.Unlock()\n\t\t\tnext = false\n\t\t}\n\t\tfor _, room := range roomsToDelete {\n\t\t\ts.deleteRoom(room)\n\t\t\tlog.Debugf(\"room cleaned up: %s\", room)\n\t\t}\n\t}\n}\n\n// replaced by stop\n// func (s *server) stopRoomDeletion() {\n// \tlog.Debug(\"stop room cleanup fired\")\n// \ts.stopRoomCleanup <- struct{}{}\n// }\n\nvar weakKey = []byte{1, 2, 3}\n\nfunc (s *server) clientCommunication(c *comm.Comm) (room string, err error) {\n\t// establish secure password with PAKE for communication with relay\n\tB, err := pake.InitCurve(weakKey, 1, \"siec\")\n\tif err != nil {\n\t\treturn\n\t}\n\tAbytes, err := c.Receive()\n\tif err != nil {\n\t\treturn\n\t}\n\tlog.Debugf(\"Abytes: %s\", Abytes)\n\tif bytes.Equal(Abytes, []byte(\"ping\")) {\n\t\troom = pingRoom\n\t\tlog.Debug(\"sending back pong\")\n\t\tc.Send([]byte(\"pong\"))\n\t\treturn\n\t}\n\terr = B.Update(Abytes)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = c.Send(B.Bytes())\n\tif err != nil {\n\t\treturn\n\t}\n\tstrongKey, err := B.SessionKey()\n\tif err != nil {\n\t\treturn\n\t}\n\tlog.Debugf(\"strongkey: %x\", strongKey)\n\n\t// receive salt\n\tsalt, err := c.Receive()\n\tif err != nil {\n\t\treturn\n\t}\n\tstrongKeyForEncryption, _, err := crypt.New(strongKey, salt)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tlog.Debugf(\"waiting for password\")\n\tpasswordBytesEnc, err := c.Receive()\n\tif err != nil {\n\t\treturn\n\t}\n\tpasswordBytes, err := crypt.Decrypt(passwordBytesEnc, strongKeyForEncryption)\n\tif err != nil {\n\t\treturn\n\t}\n\tif strings.TrimSpace(string(passwordBytes)) != s.password {\n\t\terr = fmt.Errorf(\"bad password\")\n\t\tenc, _ := crypt.Encrypt([]byte(err.Error()), strongKeyForEncryption)\n\t\tif err = c.Send(enc); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"send error: %w\", err)\n\t\t}\n\t\treturn\n\t}\n\n\t// send ok to tell client they are connected\n\tbanner := s.banner\n\tif len(banner) == 0 {\n\t\tbanner = \"ok\"\n\t}\n\tlog.Debugf(\"sending '%s'\", banner)\n\tbSend, err := crypt.Encrypt([]byte(banner+\"|||\"+c.Connection().RemoteAddr().String()), strongKeyForEncryption)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = c.Send(bSend)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// wait for client to tell me which room they want\n\tlog.Debug(\"waiting for answer\")\n\tenc, err := c.Receive()\n\tif err != nil {\n\t\treturn\n\t}\n\troomBytes, err := crypt.Decrypt(enc, strongKeyForEncryption)\n\tif err != nil {\n\t\treturn\n\t}\n\troom = string(roomBytes)\n\n\ts.rooms.Lock()\n\t// create the room if it is new\n\tif _, ok := s.rooms.rooms[room]; !ok {\n\t\ts.rooms.rooms[room] = roomInfo{\n\t\t\tfirst:  c,\n\t\t\topened: time.Now(),\n\t\t}\n\t\ts.rooms.Unlock()\n\t\t// tell the client that they got the room\n\n\t\tbSend, err = crypt.Encrypt([]byte(\"ok\"), strongKeyForEncryption)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = c.Send(bSend)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\ts.deleteRoom(room)\n\t\t\treturn\n\t\t}\n\t\tlog.Debugf(\"room %s has 1\", room)\n\t\treturn\n\t}\n\tif s.rooms.rooms[room].full {\n\t\ts.rooms.Unlock()\n\t\tbSend, err = crypt.Encrypt([]byte(\"room full\"), strongKeyForEncryption)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = c.Send(bSend)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t\treturn\n\t}\n\tlog.Debugf(\"room %s has 2\", room)\n\ts.rooms.rooms[room] = roomInfo{\n\t\tfirst:  s.rooms.rooms[room].first,\n\t\tsecond: c,\n\t\topened: s.rooms.rooms[room].opened,\n\t\tfull:   true,\n\t}\n\totherConnection := s.rooms.rooms[room].first\n\ts.rooms.Unlock()\n\n\t// second connection is the sender, time to staple connections\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\t// start piping\n\tgo func(com1, com2 *comm.Comm, wg *sync.WaitGroup) {\n\t\tlog.Debug(\"starting pipes\")\n\t\tpipe(com1.Connection(), com2.Connection())\n\t\twg.Done()\n\t\tlog.Debug(\"done piping\")\n\t}(otherConnection, c, &wg)\n\n\t// tell the sender everything is ready\n\tbSend, err = crypt.Encrypt([]byte(\"ok\"), strongKeyForEncryption)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = c.Send(bSend)\n\tif err != nil {\n\t\ts.deleteRoom(room)\n\t\treturn\n\t}\n\twg.Wait()\n\n\t// delete room\n\ts.deleteRoom(room)\n\treturn\n}\n\nfunc (s *server) deleteRoom(room string) {\n\ts.rooms.Lock()\n\tdefer s.rooms.Unlock()\n\troomData, ok := s.rooms.rooms[room]\n\tif !ok {\n\t\treturn\n\t}\n\tlog.Debugf(\"deleting room: %s\", room)\n\tif roomData.first != nil {\n\t\troomData.first.Close()\n\t}\n\tif roomData.second != nil {\n\t\troomData.second.Close()\n\t}\n\tdelete(s.rooms.rooms, room)\n}\n\n// chanFromConn creates a channel from a Conn object, and sends everything it\n//\n//\tRead()s from the socket to the channel.\nfunc chanFromConn(conn net.Conn) chan []byte {\n\tc := make(chan []byte, 1)\n\tif err := conn.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {\n\t\tlog.Warnf(\"can't set read deadline: %v\", err)\n\t}\n\n\tgo func() {\n\t\tb := make([]byte, models.TCP_BUFFER_SIZE)\n\t\tfor {\n\t\t\tn, err := conn.Read(b)\n\t\t\tif n > 0 {\n\t\t\t\tres := make([]byte, n)\n\t\t\t\t// Copy the buffer so it doesn't get changed while read by the recipient.\n\t\t\t\tcopy(res, b[:n])\n\t\t\t\tc <- res\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tlog.Debug(err)\n\t\t\t\tc <- nil\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tlog.Debug(\"exiting\")\n\t}()\n\n\treturn c\n}\n\n// pipe creates a full-duplex pipe between the two sockets and\n// transfers data from one to the other.\nfunc pipe(conn1 net.Conn, conn2 net.Conn) {\n\tchan1 := chanFromConn(conn1)\n\tchan2 := chanFromConn(conn2)\n\n\tfor {\n\t\tselect {\n\t\tcase b1 := <-chan1:\n\t\t\tif b1 == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif _, err := conn2.Write(b1); err != nil {\n\t\t\t\tlog.Errorf(\"write error on channel 1: %v\", err)\n\t\t\t}\n\n\t\tcase b2 := <-chan2:\n\t\t\tif b2 == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif _, err := conn1.Write(b2); err != nil {\n\t\t\t\tlog.Errorf(\"write error on channel 2: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc PingServer(address string) (err error) {\n\tlog.Debugf(\"pinging %s\", address)\n\tc, err := comm.NewConnection(address, 300*time.Millisecond)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\terr = c.Send([]byte(\"ping\"))\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tb, err := c.Receive()\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tif bytes.Equal(b, []byte(\"pong\")) {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"no pong\")\n}\n\n// ConnectToTCPServer will initiate a new connection\n// to the specified address, room with optional time limit\nfunc ConnectToTCPServer(address, password, room string, timelimit ...time.Duration) (c *comm.Comm, banner string, ipaddr string, err error) {\n\tif len(timelimit) > 0 {\n\t\tc, err = comm.NewConnection(address, timelimit[0])\n\t} else {\n\t\tc, err = comm.NewConnection(address)\n\t}\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\n\t// get PAKE connection with server to establish strong key to transfer info\n\tA, err := pake.InitCurve(weakKey, 0, \"siec\")\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\terr = c.Send(A.Bytes())\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tBbytes, err := c.Receive()\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\terr = A.Update(Bbytes)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tstrongKey, err := A.SessionKey()\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tlog.Debugf(\"strong key: %x\", strongKey)\n\n\tstrongKeyForEncryption, salt, err := crypt.New(strongKey, nil)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\t// send salt\n\terr = c.Send(salt)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\n\tlog.Debugf(\"sending password '%s'\", maskedPassword(password))\n\tbSend, err := crypt.Encrypt([]byte(password), strongKeyForEncryption)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\terr = c.Send(bSend)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tlog.Debug(\"waiting for first ok\")\n\tenc, err := c.Receive()\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tdata, err := crypt.Decrypt(enc, strongKeyForEncryption)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tif !strings.Contains(string(data), \"|||\") {\n\t\terr = fmt.Errorf(\"bad response: %s\", string(data))\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tbanner = strings.Split(string(data), \"|||\")[0]\n\tipaddr = strings.Split(string(data), \"|||\")[1]\n\tlog.Debugf(\"sending room; %s\", room)\n\tbSend, err = crypt.Encrypt([]byte(room), strongKeyForEncryption)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\terr = c.Send(bSend)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tlog.Debug(\"waiting for room confirmation\")\n\tenc, err = c.Receive()\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tdata, err = crypt.Decrypt(enc, strongKeyForEncryption)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tif !bytes.Equal(data, []byte(\"ok\")) {\n\t\terr = fmt.Errorf(\"got bad response: %s\", data)\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tlog.Debug(\"all set\")\n\treturn\n}\n"
  },
  {
    "path": "src/tcp/tcp_test.go",
    "content": "package tcp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc BenchmarkConnection(b *testing.B) {\n\tlog.SetLevel(\"trace\")\n\tgo Run(\"debug\", \"127.0.0.1\", \"8283\", \"pass123\", \"8284\")\n\ttime.Sleep(100 * time.Millisecond)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tc, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8283\", \"pass123\", fmt.Sprintf(\"testroom%d\", i), 1*time.Minute)\n\t\tif c != nil {\n\t\t\tc.Close()\n\t\t}\n\t}\n}\n\nfunc TestTCP(t *testing.T) {\n\tlog.SetLevel(\"error\")\n\ttimeToRoomDeletion := 100 * time.Millisecond\n\tgo RunWithOptionsAsync(\"127.0.0.1\", \"8381\", \"pass123\",\n\t\tWithBanner(\"8382\"),\n\t\tWithLogLevel(\"debug\"),\n\t\tWithRoomTTL(timeToRoomDeletion))\n\n\ttime.Sleep(timeToRoomDeletion)\n\terr := PingServer(\"127.0.0.1:8381\")\n\tassert.Nil(t, err)\n\terr = PingServer(\"127.0.0.1:8333\")\n\tassert.NotNil(t, err)\n\n\ttime.Sleep(timeToRoomDeletion)\n\tc1, banner, _, err := ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\", 1*time.Minute)\n\tassert.Equal(t, banner, \"8382\")\n\tassert.Nil(t, err)\n\tc2, _, _, err := ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\")\n\tassert.Nil(t, err)\n\t_, _, _, err = ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\")\n\tassert.NotNil(t, err)\n\t_, _, _, err = ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\", 1*time.Nanosecond)\n\tassert.NotNil(t, err)\n\n\t// try sending data\n\tassert.Nil(t, c1.Send([]byte(\"hello, c2\")))\n\tvar data []byte\n\tfor {\n\t\tdata, err = c2.Receive()\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tassert.Nil(t, err)\n\tassert.Equal(t, []byte(\"hello, c2\"), data)\n\n\tassert.Nil(t, c2.Send([]byte(\"hello, c1\")))\n\tfor {\n\t\tdata, err = c1.Receive()\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tassert.Nil(t, err)\n\tassert.Equal(t, []byte(\"hello, c1\"), data)\n\n\tc1.Close()\n\ttime.Sleep(300 * time.Millisecond)\n}\n\nfunc TestTCPctx(t *testing.T) {\n\tlog.SetLevel(\"error\")\n\t// Set short room TTL for testing cleanup\n\ttimeToRoomDeletion := 100 * time.Millisecond\n\n\t// Create cancelable context\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\t// Start server with custom options\n\tgo RunWithOptionsAsync(\"127.0.0.1\", \"8381\", \"pass123\",\n\t\tWithBanner(\"8382\"),\n\t\tWithLogLevel(\"debug\"),\n\t\tWithRoomTTL(timeToRoomDeletion),\n\t\tWithCtx(ctx),\n\t)\n\n\ttime.Sleep(timeToRoomDeletion)\n\n\t// Test ping to running server\n\terr := PingServer(\"127.0.0.1:8381\")\n\tassert.Nil(t, err)\n\n\t// Test ping to non-existent server\n\terr = PingServer(\"127.0.0.1:8333\")\n\tassert.NotNil(t, err)\n\n\ttime.Sleep(timeToRoomDeletion)\n\n\t// Connect first client to room\n\tc1, banner, _, err := ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\", 1*time.Minute)\n\tassert.Equal(t, banner, \"8382\")\n\tassert.Nil(t, err)\n\n\t// Connect second client to same room\n\tc2, _, _, err := ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\")\n\tassert.Nil(t, err)\n\n\t// Third client should fail - room is full\n\t_, _, _, err = ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\")\n\tassert.NotNil(t, err)\n\n\t// Connection with very short timeout should fail\n\t_, _, _, err = ConnectToTCPServer(\"127.0.0.1:8381\", \"pass123\", \"testRoom\", 1*time.Nanosecond)\n\tassert.NotNil(t, err)\n\n\t// Test data exchange between clients\n\t// Send from c1 to c2\n\tassert.Nil(t, c1.Send([]byte(\"hello, c2\")))\n\tvar data []byte\n\tfor {\n\t\tdata, err = c2.Receive()\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tcontinue // Skip heartbeat\n\t\t}\n\t\tbreak\n\t}\n\tassert.Nil(t, err)\n\tassert.Equal(t, []byte(\"hello, c2\"), data)\n\n\t// Send from c2 to c1\n\tassert.Nil(t, c2.Send([]byte(\"hello, c1\")))\n\tfor {\n\t\tdata, err = c1.Receive()\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tcontinue // Skip heartbeat\n\t\t}\n\t\tbreak\n\t}\n\tassert.Nil(t, err)\n\tassert.Equal(t, []byte(\"hello, c1\"), data)\n\n\t// Close server\n\tcancel()\n\n\t// Test ping to non-existent server\n\terr = PingServer(\"127.0.0.1:8331\")\n\tassert.NotNil(t, err)\n\n\ttime.Sleep(300 * time.Millisecond)\n}\n\nfunc TestWrongPassword(t *testing.T) {\n\tlog.SetLevel(\"error\")\n\tgo Run(\"debug\", \"127.0.0.1\", \"8385\", \"pass123\", \"8386\")\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Attempt to connect with wrong password\n\t_, _, _, err := ConnectToTCPServer(\"127.0.0.1:8385\", \"wrongpass\", \"testRoom\")\n\tassert.NotNil(t, err)\n\tassert.Contains(t, err.Error(), \"bad password\")\n}\n\nfunc TestRoomIsolation(t *testing.T) {\n\tlog.SetLevel(\"error\")\n\tgo Run(\"debug\", \"127.0.0.1\", \"8387\", \"pass123\", \"8388\")\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Room 1\n\tc1, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8387\", \"pass123\", \"room1\")\n\tc2, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8387\", \"pass123\", \"room1\")\n\n\t// Room 2\n\tc3, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8387\", \"pass123\", \"room2\")\n\tc4, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8387\", \"pass123\", \"room2\")\n\n\t// Send data in different rooms\n\tc1.Send([]byte(\"to_room_1\"))\n\tc3.Send([]byte(\"to_room_2\"))\n\n\t// Verify reception\n\tvar data []byte\n\n\t// c2 should receive message from room1\n\tfor {\n\t\tdata, _ = c2.Receive()\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tassert.Equal(t, []byte(\"to_room_1\"), data)\n\n\t// c4 should receive message from room2\n\tfor {\n\t\tdata, _ = c4.Receive()\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tassert.Equal(t, []byte(\"to_room_2\"), data)\n\n\tc1.Close()\n\tc2.Close()\n\tc3.Close()\n\tc4.Close()\n}\n\nfunc TestRoomRecreationAfterTTL(t *testing.T) {\n\tlog.SetLevel(\"error\")\n\tshortTTL := 50 * time.Millisecond\n\n\tgo RunWithOptionsAsync(\"127.0.0.1\", \"8389\", \"pass123\",\n\t\tWithRoomTTL(shortTTL),\n\t\tWithLogLevel(\"error\"))\n\ttime.Sleep(100 * time.Millisecond)\n\n\troomName := \"testRoomRecreate\"\n\n\t// 1. Create a room\n\tc1, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8389\", \"pass123\", roomName)\n\tassert.NotNil(t, c1)\n\n\t// 2. Close first client, room becomes empty\n\tc1.Close()\n\n\t// 3. Wait for room cleanup (TTL + buffer)\n\ttime.Sleep(shortTTL + 50*time.Millisecond)\n\n\t// 4. Try to connect to the same room again.\n\t// If room wasn't deleted, we might get \"room full\" or weird behavior.\n\t// If deleted — connection should succeed as the first client.\n\tc3, _, _, err := ConnectToTCPServer(\"127.0.0.1:8389\", \"pass123\", roomName)\n\tassert.Nil(t, err)\n\tassert.NotNil(t, c3)\n\n\tif c3 != nil {\n\t\tc3.Close()\n\t}\n}\n\nfunc TestLargeDataTransfer(t *testing.T) {\n\tlog.SetLevel(\"error\")\n\tgo Run(\"debug\", \"127.0.0.1\", \"8391\", \"pass123\", \"8392\")\n\ttime.Sleep(100 * time.Millisecond)\n\n\tc1, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8391\", \"pass123\", \"bigRoom\")\n\tc2, _, _, _ := ConnectToTCPServer(\"127.0.0.1:8391\", \"pass123\", \"bigRoom\")\n\n\t// Generate data larger than standard buffer (e.g., 1 MB)\n\tlargeData := make([]byte, 1024*1024)\n\tfor i := range largeData {\n\t\tlargeData[i] = byte(i % 256)\n\t}\n\n\terr := c1.Send(largeData)\n\tassert.Nil(t, err)\n\n\tvar received []byte\n\t// Receive data, as it might arrive in chunks (though chanFromConn buffers it)\n\t// In this case pipe passes full Read packets, but for safety let's verify tail\n\tfor {\n\t\tdata, err := c2.Receive()\n\t\tif bytes.Equal(data, []byte{1}) {\n\t\t\tcontinue\n\t\t}\n\t\tassert.Nil(t, err)\n\t\treceived = data\n\t\tbreak\n\t}\n\n\tassert.True(t, bytes.Equal(largeData, received), \"Large data mismatch\")\n\n\tc1.Close()\n\tc2.Close()\n}\n\nfunc TestServerReleasesPort(t *testing.T) {\n\tlog.SetLevel(\"trace\")\n\thost := \"127.0.0.1\"\n\tport := \"8394\"\n\n\t// 1. Start and automatically stop first server using timeout\n\t// RunCtx blocks the execution, so we don't need 'go' or channels\n\tctx1, cancel1 := context.WithTimeout(context.Background(), 200*time.Millisecond)\n\tdefer cancel1()\n\n\terr := RunCtx(ctx1, \"trace\", host, port, \"pass123\")\n\tassert.Nil(t, err, \"First server should stop gracefully\")\n\n\t// 2. Try to start second server on the same port immediately\n\t// If port is not released, this will fail with \"address already in use\"\n\tctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond)\n\tdefer cancel2()\n\n\terr = RunCtx(ctx2, \"trace\", host, port, \"pass123\")\n\tassert.Nil(t, err, \"Second server should start (port was released)\")\n}\n"
  },
  {
    "path": "src/utils/ctx.go",
    "content": "// ctx.go\npackage utils\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/cespare/xxhash/v2\"\n\t\"github.com/minio/highwayhash\"\n\t\"github.com/schollz/progressbar/v3\"\n)\n\n// ctxFile wraps os.File with context cancellation support.\ntype ctxFile struct {\n\tctx context.Context\n\tf   *os.File\n}\n\n// NewCtxFile creates a new context-aware file wrapper.\nfunc NewCtxFile(ctx context.Context, f *os.File) *ctxFile {\n\treturn &ctxFile{ctx: ctx, f: f}\n}\n\n// Read implements io.Reader interface with context cancellation.\nfunc (c *ctxFile) Read(p []byte) (n int, err error) {\n\tselect {\n\tcase <-c.ctx.Done():\n\t\treturn 0, c.ctx.Err()\n\tdefault:\n\t\tn, err = c.f.Read(p)\n\t\tif c.ctx.Err() != nil {\n\t\t\treturn 0, c.ctx.Err()\n\t\t}\n\t\treturn n, err\n\t}\n}\n\n// ReadAt implements io.ReaderAt interface with context cancellation.\nfunc (c *ctxFile) ReadAt(p []byte, off int64) (n int, err error) {\n\tselect {\n\tcase <-c.ctx.Done():\n\t\treturn 0, c.ctx.Err()\n\tdefault:\n\t\tn, err = c.f.ReadAt(p, off)\n\t\tif c.ctx.Err() != nil {\n\t\t\treturn 0, c.ctx.Err()\n\t\t}\n\t\treturn n, err\n\t}\n}\n\n// Seek implements io.Seeker interface with context cancellation.\nfunc (c *ctxFile) Seek(offset int64, whence int) (n int64, err error) {\n\tselect {\n\tcase <-c.ctx.Done():\n\t\treturn 0, c.ctx.Err()\n\tdefault:\n\t\tn, err = c.f.Seek(offset, whence)\n\t\tif c.ctx.Err() != nil {\n\t\t\treturn 0, c.ctx.Err()\n\t\t}\n\t\treturn n, err\n\t}\n}\n\n// HashFileCtx returns the hash of a file with context cancellation support.\nfunc HashFileCtx(ctx context.Context, fname string, algorithm string, showProgress ...bool) ([]byte, error) {\n\t// Quick context check before starting\n\tif err := ctx.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfstats, err := os.Lstat(fname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Handle symlinks - quick operation, no context needed\n\tif fstats.Mode()&os.ModeSymlink != 0 {\n\t\ttarget, err := os.Readlink(fname)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []byte(SHA256(target)), nil\n\t}\n\n\tf, err := os.Open(fname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\n\t// Get file info for size (now file is opened, following symlinks if any)\n\tfi, err := f.Stat()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Wrap the file with context support\n\tcf := NewCtxFile(ctx, f)\n\tsr := io.NewSectionReader(cf, 0, fi.Size())\n\n\t// Parse showProgress parameter\n\tdoShowProgress := false\n\tif len(showProgress) > 0 {\n\t\tdoShowProgress = showProgress[0]\n\t}\n\n\t// Create progress bar based on algorithm\n\tvar bar *progressbar.ProgressBar\n\tif doShowProgress {\n\t\tfnameShort := path.Base(fname)\n\t\tif len(fnameShort) > 20 {\n\t\t\tfnameShort = fnameShort[:20] + \"...\"\n\t\t}\n\n\t\tif algorithm == \"imohash\" {\n\t\t\t// Spinner for imohash (indeterminate progress, max = -1)\n\t\t\tbar = progressbar.NewOptions64(-1,\n\t\t\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\t\t\tprogressbar.OptionShowBytes(false),\n\t\t\t\tprogressbar.OptionSetDescription(fmt.Sprintf(\"Sampling %s\", fnameShort)),\n\t\t\t\tprogressbar.OptionClearOnFinish(),\n\t\t\t\tprogressbar.OptionFullWidth(),\n\t\t\t\tprogressbar.OptionShowElapsedTimeOnFinish(),\n\t\t\t\tprogressbar.OptionSpinnerType(14),\n\t\t\t\tprogressbar.OptionSetSpinnerChangeInterval(100*time.Millisecond),\n\t\t\t)\n\t\t} else {\n\t\t\t// Regular progress bar for other algorithms\n\t\t\tbar = progressbar.NewOptions64(fi.Size(),\n\t\t\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\t\t\tprogressbar.OptionShowBytes(true),\n\t\t\t\tprogressbar.OptionSetDescription(fmt.Sprintf(\"Hashing %s\", fnameShort)),\n\t\t\t\tprogressbar.OptionClearOnFinish(),\n\t\t\t\tprogressbar.OptionFullWidth(),\n\t\t\t)\n\t\t}\n\t}\n\n\t// Dispatch to appropriate hash function\n\tswitch algorithm {\n\tcase \"imohash\":\n\t\treturn IMOHashReader(sr, bar)\n\tcase \"md5\":\n\t\treturn MD5HashReader(sr, bar)\n\tcase \"xxhash\":\n\t\treturn XXHashReader(sr, bar)\n\tcase \"highway\":\n\t\treturn HighwayHashReader(sr, bar)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported algorithm: %s\", algorithm)\n\t}\n}\n\n// IMOHashReader returns imohash for a SectionReader.\n// Uses spinner progress bar for indeterminate progress.\nfunc IMOHashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) ([]byte, error) {\n\t// Start spinner if provided\n\tif bar != nil {\n\t\t// Add(0) triggers initial render for spinner\n\t\tbar.Add(0)\n\t}\n\n\tb, err := imopartial.SumSectionReader(sr)\n\tif err != nil {\n\t\t// If there's an error, finish the bar to clean up display\n\t\tif bar != nil {\n\t\t\tbar.Exit()\n\t\t}\n\t\treturn nil, err\n\t}\n\n\t// Finish the progress bar\n\tif bar != nil {\n\t\tbar.Finish()\n\t}\n\n\treturn b[:], nil\n}\n\n// IMOHashReaderFull returns full imohash (no sampling) for a SectionReader.\nfunc IMOHashReaderFull(sr *io.SectionReader, bar *progressbar.ProgressBar) ([]byte, error) {\n\t// For full imohash (which reads entire file), use regular progress bar logic\n\tif bar != nil {\n\t\tbar.Add(0) // Start the spinner\n\t}\n\n\tb, err := imofull.SumSectionReader(sr)\n\tif err != nil {\n\t\tif bar != nil {\n\t\t\tbar.Exit()\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tif bar != nil {\n\t\tbar.Finish()\n\t}\n\n\treturn b[:], nil\n}\n\n// MD5HashReader returns MD5 hash for a SectionReader.\nfunc MD5HashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) ([]byte, error) {\n\t// Reset to beginning\n\tif _, err := sr.Seek(0, io.SeekStart); err != nil {\n\t\treturn nil, err\n\t}\n\n\th := md5.New()\n\tif bar != nil {\n\t\t// Copy with progress tracking (like original code)\n\t\tif _, err := io.Copy(io.MultiWriter(h, bar), sr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif _, err := io.Copy(h, sr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn h.Sum(nil), nil\n}\n\n// XXHashReader returns xxhash for a SectionReader.\nfunc XXHashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) ([]byte, error) {\n\tif _, err := sr.Seek(0, io.SeekStart); err != nil {\n\t\treturn nil, err\n\t}\n\n\th := xxhash.New()\n\tif bar != nil {\n\t\tif _, err := io.Copy(io.MultiWriter(h, bar), sr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif _, err := io.Copy(h, sr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn h.Sum(nil), nil\n}\n\n// HighwayHashReader returns highwayhash for a SectionReader.\nfunc HighwayHashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) ([]byte, error) {\n\tif _, err := sr.Seek(0, io.SeekStart); err != nil {\n\t\treturn nil, err\n\t}\n\n\tkey, err := hex.DecodeString(\"1553c5383fb0b86578c3310da665b4f6e0521acf22eb58a99532ffed02a6b115\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\th, err := highwayhash.New(key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create highwayhash: %w\", err)\n\t}\n\n\tif bar != nil {\n\t\tif _, err := io.Copy(io.MultiWriter(h, bar), sr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif _, err := io.Copy(h, sr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn h.Sum(nil), nil\n}\n\n// Helper function to update existing HashFile to use HashFileCtx\n// func HashFile(fname string, algorithm string, showProgress ...bool) ([]byte, error) {\n// \treturn HashFileCtx(context.Background(), fname, algorithm, showProgress...)\n// }\n"
  },
  {
    "path": "src/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"archive/zip\"\n\t\"bufio\"\n\t\"bytes\"\n\t\"compress/flate\"\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/cespare/xxhash/v2\"\n\t\"github.com/kalafut/imohash\"\n\t\"github.com/minio/highwayhash\"\n\t\"github.com/schollz/croc/v10/src/mnemonicode\"\n\tlog \"github.com/schollz/logger\"\n\t\"github.com/schollz/progressbar/v3\"\n)\n\nconst NbPinNumbers = 4\nconst NbBytesWords = 4\n\n// Get or create home directory\nfunc GetConfigDir(requireValidPath bool) (homedir string, err error) {\n\tif envHomedir, isSet := os.LookupEnv(\"CROC_CONFIG_DIR\"); isSet {\n\t\thomedir = envHomedir\n\t} else if xdgConfigHome, isSet := os.LookupEnv(\"XDG_CONFIG_HOME\"); isSet {\n\t\thomedir = path.Join(xdgConfigHome, \"croc\")\n\t} else {\n\t\thomedir, err = os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tif !requireValidPath {\n\t\t\t\terr = nil\n\t\t\t\thomedir = \"\"\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\thomedir = path.Join(homedir, \".config\", \"croc\")\n\t}\n\n\tif requireValidPath {\n\t\tif _, err = os.Stat(homedir); os.IsNotExist(err) {\n\t\t\terr = os.MkdirAll(homedir, 0o700)\n\t\t}\n\t}\n\treturn\n}\n\n// Exists reports whether the named file or directory exists.\nfunc Exists(name string) bool {\n\tif _, err := os.Stat(name); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// GetInput returns the input with a given prompt\nfunc GetInput(prompt string) string {\n\treader := bufio.NewReader(os.Stdin)\n\tfmt.Fprintf(os.Stderr, \"%s\", prompt)\n\ttext, _ := reader.ReadString('\\n')\n\treturn strings.TrimSpace(text)\n}\n\n// HashFile returns the hash of a file or, in case of a symlink, the\n// SHA256 hash of its target. Takes an argument to specify the algorithm to use.\nfunc HashFile(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error) {\n\tdoShowProgress := false\n\tif len(showProgress) > 0 {\n\t\tdoShowProgress = showProgress[0]\n\t}\n\tvar fstats os.FileInfo\n\tfstats, err = os.Lstat(fname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif fstats.Mode()&os.ModeSymlink != 0 {\n\t\tvar target string\n\t\ttarget, err = os.Readlink(fname)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []byte(SHA256(target)), nil\n\t}\n\tswitch algorithm {\n\tcase \"imohash\":\n\t\treturn IMOHashFile(fname)\n\tcase \"md5\":\n\t\treturn MD5HashFile(fname, doShowProgress)\n\tcase \"xxhash\":\n\t\treturn XXHashFile(fname, doShowProgress)\n\tcase \"highway\":\n\t\treturn HighwayHashFile(fname, doShowProgress)\n\t}\n\terr = fmt.Errorf(\"unspecified algorithm\")\n\treturn\n}\n\n// HighwayHashFile returns highwayhash of a file\nfunc HighwayHashFile(fname string, doShowProgress bool) (hashHighway []byte, err error) {\n\tf, err := os.Open(fname)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer f.Close()\n\tkey, err := hex.DecodeString(\"1553c5383fb0b86578c3310da665b4f6e0521acf22eb58a99532ffed02a6b115\")\n\tif err != nil {\n\t\treturn\n\t}\n\th, err := highwayhash.New(key)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"could not create highwayhash: %s\", err.Error())\n\t\treturn\n\t}\n\tif doShowProgress {\n\t\tstat, _ := f.Stat()\n\t\tfnameShort := path.Base(fname)\n\t\tif len(fnameShort) > 20 {\n\t\t\tfnameShort = fnameShort[:20] + \"...\"\n\t\t}\n\t\tbar := progressbar.NewOptions64(stat.Size(),\n\t\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\t\tprogressbar.OptionShowBytes(true),\n\t\t\tprogressbar.OptionSetDescription(fmt.Sprintf(\"Hashing %s\", fnameShort)),\n\t\t\tprogressbar.OptionClearOnFinish(),\n\t\t\tprogressbar.OptionFullWidth(),\n\t\t)\n\t\tif _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif _, err = io.Copy(h, f); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\thashHighway = h.Sum(nil)\n\treturn\n}\n\n// MD5HashFile returns MD5 hash\nfunc MD5HashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {\n\tf, err := os.Open(fname)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer f.Close()\n\n\th := md5.New()\n\tif doShowProgress {\n\t\tstat, _ := f.Stat()\n\t\tfnameShort := path.Base(fname)\n\t\tif len(fnameShort) > 20 {\n\t\t\tfnameShort = fnameShort[:20] + \"...\"\n\t\t}\n\t\tbar := progressbar.NewOptions64(stat.Size(),\n\t\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\t\tprogressbar.OptionShowBytes(true),\n\t\t\tprogressbar.OptionSetDescription(fmt.Sprintf(\"Hashing %s\", fnameShort)),\n\t\t\tprogressbar.OptionClearOnFinish(),\n\t\t\tprogressbar.OptionFullWidth(),\n\t\t)\n\t\tif _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif _, err = io.Copy(h, f); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\thash256 = h.Sum(nil)\n\treturn\n}\n\nvar imofull = imohash.NewCustom(0, 0)\nvar imopartial = imohash.NewCustom(16*16*8*1024, 128*1024)\n\n// IMOHashFile returns imohash\nfunc IMOHashFile(fname string) (hash []byte, err error) {\n\tb, err := imopartial.SumFile(fname)\n\thash = b[:]\n\treturn\n}\n\n// IMOHashFileFull returns imohash of full file\nfunc IMOHashFileFull(fname string) (hash []byte, err error) {\n\tb, err := imofull.SumFile(fname)\n\thash = b[:]\n\treturn\n}\n\n// XXHashFile returns the xxhash of a file\nfunc XXHashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {\n\tf, err := os.Open(fname)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer f.Close()\n\n\th := xxhash.New()\n\tif doShowProgress {\n\t\tstat, _ := f.Stat()\n\t\tfnameShort := path.Base(fname)\n\t\tif len(fnameShort) > 20 {\n\t\t\tfnameShort = fnameShort[:20] + \"...\"\n\t\t}\n\t\tbar := progressbar.NewOptions64(stat.Size(),\n\t\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\t\tprogressbar.OptionShowBytes(true),\n\t\t\tprogressbar.OptionSetDescription(fmt.Sprintf(\"Hashing %s\", fnameShort)),\n\t\t\tprogressbar.OptionClearOnFinish(),\n\t\t\tprogressbar.OptionFullWidth(),\n\t\t)\n\t\tif _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif _, err = io.Copy(h, f); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\thash256 = h.Sum(nil)\n\treturn\n}\n\n// SHA256 returns sha256 sum\nfunc SHA256(s string) string {\n\tsha := sha256.New()\n\tsha.Write([]byte(s))\n\treturn hex.EncodeToString(sha.Sum(nil))\n}\n\n// PublicIP returns public ip address\nfunc PublicIP() (ip string, err error) {\n\t// ask ipv4.icanhazip.com for the public ip\n\t// by making http request\n\t// if the request fails, return nothing\n\tresp, err := http.Get(\"http://ipv4.icanhazip.com\")\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// read the body of the response\n\t// and return the ip address\n\tbuf := new(bytes.Buffer)\n\tbuf.ReadFrom(resp.Body)\n\tip = strings.TrimSpace(buf.String())\n\n\treturn\n}\n\n// LocalIP returns local ip address\nfunc LocalIP() string {\n\tconn, err := net.Dial(\"udp\", \"8.8.8.8:80\")\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn \"\"\n\t}\n\tdefer conn.Close()\n\n\tlocalAddr := conn.LocalAddr().(*net.UDPAddr)\n\n\treturn localAddr.IP.String()\n}\n\n// GenerateRandomPin returns a randomly generated pin with set length\nfunc GenerateRandomPin() string {\n\ts := \"\"\n\tmax := new(big.Int)\n\tmax.SetInt64(9)\n\tfor range NbPinNumbers {\n\t\tv, err := rand.Int(rand.Reader, max)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\ts += fmt.Sprintf(\"%d\", v)\n\t}\n\treturn s\n}\n\n// GetRandomName returns mnemonicoded random name\nfunc GetRandomName() string {\n\tvar result []string\n\tbs := make([]byte, NbBytesWords)\n\trand.Read(bs)\n\tresult = mnemonicode.EncodeWordList(result, bs)\n\treturn GenerateRandomPin() + \"-\" + strings.Join(result, \"-\")\n}\n\n// ByteCountDecimal converts bytes to human readable byte string\nfunc ByteCountDecimal(b int64) string {\n\tconst unit = 1024\n\tif b < unit {\n\t\treturn fmt.Sprintf(\"%d B\", b)\n\t}\n\tdiv, exp := int64(unit), 0\n\tfor n := b / unit; n >= unit; n /= unit {\n\t\tdiv *= unit\n\t\texp++\n\t}\n\treturn fmt.Sprintf(\"%.1f %cB\", float64(b)/float64(div), \"kMGTPE\"[exp])\n}\n\n// MissingChunks returns the positions of missing chunks.\n// If file doesn't exist, it returns an empty chunk list (all chunks).\n// If the file size is not the same as requested, it returns an empty chunk list (all chunks).\nfunc MissingChunks(fname string, fsize int64, chunkSize int) (chunkRanges []int64) {\n\tf, err := os.Open(fname)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer f.Close()\n\n\tfstat, err := os.Stat(fname)\n\tif err != nil || fstat.Size() != fsize {\n\t\treturn\n\t}\n\n\t// Show progress bar for large files (> 10MB)\n\tvar bar *progressbar.ProgressBar\n\tshowProgress := fsize > 10*1024*1024\n\tif showProgress {\n\t\tfnameShort := path.Base(fname)\n\t\tif len(fnameShort) > 20 {\n\t\t\tfnameShort = fnameShort[:20] + \"...\"\n\t\t}\n\t\tbar = progressbar.NewOptions64(fsize,\n\t\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\t\tprogressbar.OptionShowBytes(true),\n\t\t\tprogressbar.OptionSetDescription(fmt.Sprintf(\"Checking %s\", fnameShort)),\n\t\t\tprogressbar.OptionClearOnFinish(),\n\t\t\tprogressbar.OptionFullWidth(),\n\t\t\tprogressbar.OptionThrottle(100*time.Millisecond),\n\t\t)\n\t}\n\n\temptyBuffer := make([]byte, chunkSize)\n\tchunkNum := 0\n\tchunks := make([]int64, int64(math.Ceil(float64(fsize)/float64(chunkSize))))\n\tvar currentLocation int64\n\tfor {\n\t\tbuffer := make([]byte, chunkSize)\n\t\tbytesread, err := f.Read(buffer)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif bytes.Equal(buffer[:bytesread], emptyBuffer[:bytesread]) {\n\t\t\tchunks[chunkNum] = currentLocation\n\t\t\tchunkNum++\n\t\t}\n\t\tcurrentLocation += int64(bytesread)\n\t\tif showProgress && bar != nil {\n\t\t\tbar.Add(bytesread)\n\t\t}\n\t}\n\tif showProgress && bar != nil {\n\t\tbar.Finish()\n\t}\n\tif chunkNum == 0 {\n\t\tchunkRanges = []int64{}\n\t} else {\n\t\tchunks = chunks[:chunkNum]\n\t\tchunkRanges = []int64{int64(chunkSize), chunks[0]}\n\t\tcurCount := 0\n\t\tfor i, chunk := range chunks {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcurCount++\n\t\t\tif chunk-chunks[i-1] > int64(chunkSize) {\n\t\t\t\tchunkRanges = append(chunkRanges, int64(curCount))\n\t\t\t\tchunkRanges = append(chunkRanges, chunk)\n\t\t\t\tcurCount = 0\n\t\t\t}\n\t\t}\n\t\tchunkRanges = append(chunkRanges, int64(curCount+1))\n\t}\n\treturn\n}\n\n// ChunkRangesToChunks converts chunk ranges to list\nfunc ChunkRangesToChunks(chunkRanges []int64) (chunks []int64) {\n\tif len(chunkRanges) == 0 {\n\t\treturn\n\t}\n\tchunkSize := chunkRanges[0]\n\tchunks = []int64{}\n\tfor i := 1; i < len(chunkRanges); i += 2 {\n\t\tfor j := int64(0); j < (chunkRanges[i+1]); j++ {\n\t\t\tchunks = append(chunks, chunkRanges[i]+j*chunkSize)\n\t\t}\n\t}\n\treturn\n}\n\n// GetLocalIPs returns all local ips\nfunc GetLocalIPs() (ips []string, err error) {\n\taddrs, err := net.InterfaceAddrs()\n\tif err != nil {\n\t\tif ip := LocalIP(); ip != \"\" {\n\t\t\treturn []string{ip}, nil\n\t\t}\n\t\treturn\n\t}\n\tips = []string{}\n\tfor _, address := range addrs {\n\t\t// check the address type and if it is not a loopback the display it\n\t\tif ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {\n\t\t\tif ipnet.IP.To4() != nil {\n\t\t\t\tips = append(ips, ipnet.IP.String())\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc RandomFileName() (fname string, err error) {\n\tf, err := os.CreateTemp(\".\", \"croc-stdin-\")\n\tif err != nil {\n\t\treturn\n\t}\n\tfname = f.Name()\n\t_ = f.Close()\n\treturn\n}\n\nfunc FindOpenPorts(host string, portNumStart, numPorts int) (openPorts []int) {\n\topenPorts = []int{}\n\tfor port := portNumStart; port-portNumStart < 200; port++ {\n\t\ttimeout := 100 * time.Millisecond\n\t\tconn, err := net.DialTimeout(\"tcp\", net.JoinHostPort(host, fmt.Sprint(port)), timeout)\n\t\tif conn != nil {\n\t\t\tconn.Close()\n\t\t} else if err != nil {\n\t\t\topenPorts = append(openPorts, port)\n\t\t}\n\t\tif len(openPorts) >= numPorts {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// local ip determination\n// https://stackoverflow.com/questions/41240761/check-if-ip-address-is-in-private-network-space\nvar privateIPBlocks []*net.IPNet\n\nfunc init() {\n\tfor _, cidr := range []string{\n\t\t\"127.0.0.0/8\",    // IPv4 loopback\n\t\t\"10.0.0.0/8\",     // RFC1918\n\t\t\"172.16.0.0/12\",  // RFC1918\n\t\t\"192.168.0.0/16\", // RFC1918\n\t\t\"169.254.0.0/16\", // RFC3927 link-local\n\t\t\"::1/128\",        // IPv6 loopback\n\t\t\"fe80::/10\",      // IPv6 link-local\n\t\t\"fc00::/7\",       // IPv6 unique local addr\n\t} {\n\t\t_, block, err := net.ParseCIDR(cidr)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"parse error on %q: %v\", cidr, err))\n\t\t}\n\t\tprivateIPBlocks = append(privateIPBlocks, block)\n\t}\n}\n\nfunc IsLocalIP(ipaddress string) bool {\n\tif strings.Contains(ipaddress, \"127.0.0.1\") {\n\t\treturn true\n\t}\n\thost, _, _ := net.SplitHostPort(ipaddress)\n\tip := net.ParseIP(host)\n\tif ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {\n\t\treturn true\n\t}\n\tfor _, block := range privateIPBlocks {\n\t\tif block.Contains(ip) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc ZipDirectory(destination string, source string) (err error) {\n\tif _, err = os.Stat(destination); err == nil {\n\t\tlog.Errorf(\"%s file already exists!\\n\", destination)\n\t\treturn fmt.Errorf(\"file already exists: %s\", destination)\n\t}\n\n\t// Check if source directory exists\n\tif _, err := os.Stat(source); os.IsNotExist(err) {\n\t\tlog.Errorf(\"Source directory does not exist: %s\", source)\n\t\treturn fmt.Errorf(\"source directory does not exist: %s\", source)\n\t}\n\n\tfmt.Fprintf(os.Stderr, \"Zipping %s to %s\\n\", source, destination)\n\tfile, err := os.Create(destination)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn fmt.Errorf(\"failed to create zip file: %w\", err)\n\t}\n\tdefer file.Close()\n\twriter := zip.NewWriter(file)\n\t// no compression because croc does its compression on the fly\n\twriter.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {\n\t\treturn flate.NewWriter(out, flate.NoCompression)\n\t})\n\tdefer writer.Close()\n\n\t// Get base name for zip structure\n\tbaseName := strings.TrimSuffix(filepath.Base(destination), \".zip\")\n\n\t// First pass: add the root directory with its modification time\n\trootInfo, err := os.Stat(source)\n\tif err == nil && rootInfo.IsDir() {\n\t\theader, err := zip.FileInfoHeader(rootInfo)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t} else {\n\t\t\theader.Name = baseName + \"/\" // Trailing slash indicates directory\n\t\t\theader.Method = zip.Store\n\t\t\theader.Modified = rootInfo.ModTime()\n\n\t\t\t_, err = writer.CreateHeader(header)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t} else {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\r\\033[2K\")\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\rAdding %s\", baseName+\"/\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Second pass: add all other directories and files\n\terr = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Skip root directory (we already added it)\n\t\tif path == source {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Calculate relative path from source directory\n\t\trelPath, err := filepath.Rel(source, path)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create zip path with base name structure\n\t\tzipPath := filepath.Join(baseName, relPath)\n\t\tzipPath = filepath.ToSlash(zipPath)\n\n\t\tif info.IsDir() {\n\t\t\t// Add directory entry to zip with original modification time\n\t\t\theader, err := zip.FileInfoHeader(info)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\theader.Name = zipPath + \"/\" // Trailing slash indicates directory\n\t\t\theader.Method = zip.Store\n\t\t\t// Preserve the original modification time\n\t\t\theader.Modified = info.ModTime()\n\n\t\t\t_, err = writer.CreateHeader(header)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfmt.Fprintf(os.Stderr, \"\\r\\033[2K\")\n\t\t\tfmt.Fprintf(os.Stderr, \"\\rAdding %s\", zipPath+\"/\")\n\t\t\treturn nil\n\t\t}\n\n\t\tif info.Mode().IsRegular() {\n\t\t\tf1, err := os.Open(path)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdefer f1.Close()\n\n\t\t\t// Create file header with modified time\n\t\t\theader, err := zip.FileInfoHeader(info)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\theader.Name = zipPath\n\t\t\theader.Method = zip.Deflate\n\n\t\t\tw1, err := writer.CreateHeader(header)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif _, err := io.Copy(w1, f1); err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfmt.Fprintf(os.Stderr, \"\\r\\033[2K\")\n\t\t\tfmt.Fprintf(os.Stderr, \"\\rAdding %s\", zipPath)\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn fmt.Errorf(\"error during directory walk: %w\", err)\n\t}\n\tfmt.Fprintf(os.Stderr, \"\\n\")\n\treturn nil\n}\n\nfunc UnzipDirectory(destination string, source string) error {\n\tarchive, err := zip.OpenReader(source)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn fmt.Errorf(\"failed to open zip file: %w\", err)\n\t}\n\tdefer archive.Close()\n\n\t// Pre-validate all paths to avoid partial extraction on malicious archives.\n\tfilePaths := make([]string, len(archive.File))\n\tfor i, f := range archive.File {\n\t\tfilePath, pathErr := resolveUnzipPath(destination, f.Name)\n\t\tif pathErr != nil {\n\t\t\tlog.Errorf(\"Invalid file path %s: %v\\n\", f.Name, pathErr)\n\t\t\treturn fmt.Errorf(\"invalid file path in zip entry %q: %w\", f.Name, pathErr)\n\t\t}\n\t\tfilePaths[i] = filePath\n\t}\n\n\t// Store modification times for all files and directories\n\tmodTimes := make(map[string]time.Time)\n\n\t// First pass: extract all files and directories, store modification times\n\tfor i, f := range archive.File {\n\t\tfilePath := filePaths[i]\n\t\tfmt.Fprintf(os.Stderr, \"\\r\\033[2K\")\n\t\tfmt.Fprintf(os.Stderr, \"\\rUnzipping file %s\", filePath)\n\n\t\t// Store modification time for this entry (BOTH files and directories)\n\t\tmodifiedTime := f.Modified\n\t\tif modifiedTime.IsZero() {\n\t\t\tmodifiedTime = f.FileHeader.Modified\n\t\t}\n\t\tif !modifiedTime.IsZero() {\n\t\t\tmodTimes[filePath] = modifiedTime\n\t\t}\n\n\t\tif f.FileInfo().IsDir() {\n\t\t\tif err := os.MkdirAll(filePath, os.ModePerm); err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// check if file exists\n\t\tif _, err := os.Stat(filePath); err == nil {\n\t\t\tprompt := fmt.Sprintf(\"\\nOverwrite '%s'? (y/N) \", filePath)\n\t\t\tchoice := strings.ToLower(GetInput(prompt))\n\t\t\tif choice != \"y\" && choice != \"yes\" {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Skipping '%s'\\n\", filePath)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tdstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tfileInArchive, err := f.Open()\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tdstFile.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := io.Copy(dstFile, fileInArchive); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\n\t\tdstFile.Close()\n\t\tfileInArchive.Close()\n\t}\n\n\t// Second pass: restore modification times for ALL files and directories\n\tfor path, modTime := range modTimes {\n\t\tif err := os.Chtimes(path, modTime, modTime); err != nil {\n\t\t\tlog.Errorf(\"Failed to set modification time for %s: %v\", path, err)\n\t\t} else {\n\t\t\tfi, err := os.Lstat(path)\n\t\t\tif err != nil ||\n\t\t\t\t!modTime.UTC().Equal(fi.ModTime().UTC()) {\n\t\t\t\tlog.Errorf(\"Failed to set modification time for %s: %v\", path, err)\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Failed to set modification time %s %v: %v\\n\", path, modTime, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tfmt.Fprintf(os.Stderr, \"\\n\")\n\treturn nil\n}\n\nfunc resolveUnzipPath(destination string, entryName string) (string, error) {\n\tif filepath.IsAbs(entryName) || filepath.VolumeName(entryName) != \"\" {\n\t\treturn \"\", fmt.Errorf(\"path escapes destination\")\n\t}\n\n\tdestinationAbs, err := filepath.Abs(destination)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to resolve destination: %w\", err)\n\t}\n\tdestinationAbs = filepath.Clean(destinationAbs)\n\n\tfilePath := filepath.Clean(filepath.Join(destinationAbs, entryName))\n\trelativePath, err := filepath.Rel(destinationAbs, filePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to resolve path %q: %w\", entryName, err)\n\t}\n\tif relativePath == \"..\" || strings.HasPrefix(relativePath, \"..\"+string(os.PathSeparator)) {\n\t\treturn \"\", fmt.Errorf(\"path escapes destination\")\n\t}\n\n\treturn filePath, nil\n}\n\n// ValidFileName checks if a filename is valid\n// by making sure it has no invisible characters\nfunc ValidFileName(fname string) (err error) {\n\t// make sure it doesn't contain unicode or invisible characters\n\tfor _, r := range fname {\n\t\tif !unicode.IsGraphic(r) {\n\t\t\terr = fmt.Errorf(\"non-graphical unicode: %x U+%d in '%x'\", string(r), r, fname)\n\t\t\treturn\n\t\t}\n\t\tif !unicode.IsPrint(r) {\n\t\t\terr = fmt.Errorf(\"non-printable unicode: %x U+%d in '%x'\", string(r), r, fname)\n\t\t\treturn\n\t\t}\n\t}\n\t// make sure basename does not include path separators\n\t_, basename := filepath.Split(fname)\n\tif strings.Contains(basename, string(os.PathSeparator)) {\n\t\terr = fmt.Errorf(\"basename cannot contain path separators: '%s'\", basename)\n\t\treturn\n\t}\n\t// make sure the filename is not an absolute path\n\tif filepath.IsAbs(fname) {\n\t\terr = fmt.Errorf(\"filename cannot be an absolute path: '%s'\", fname)\n\t\treturn\n\t}\n\tif !filepath.IsLocal(fname) {\n\t\terr = fmt.Errorf(\"filename must be a local path: '%s'\", fname)\n\t\treturn\n\t}\n\treturn\n}\n\nconst crocRemovalFile = \"croc-marked-files.txt\"\n\nfunc MarkFileForRemoval(fname string) {\n\t// append the fname to the list of files to remove\n\tf, err := os.OpenFile(crocRemovalFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)\n\tif err != nil {\n\t\tlog.Debug(err)\n\t\treturn\n\t}\n\tdefer f.Close()\n\t_, err = f.WriteString(fname + \"\\n\")\n}\n\nfunc RemoveMarkedFiles() (err error) {\n\t// read the file and remove all the files\n\tf, err := os.Open(crocRemovalFile)\n\tif err != nil {\n\t\treturn\n\t}\n\tscanner := bufio.NewScanner(f)\n\tfor scanner.Scan() {\n\t\tfname := scanner.Text()\n\t\terr = os.Remove(fname)\n\t\tif err == nil {\n\t\t\tlog.Tracef(\"Removed %s\", fname)\n\t\t}\n\t}\n\tf.Close()\n\tos.Remove(crocRemovalFile)\n\treturn\n}\n"
  },
  {
    "path": "src/utils/utils_test.go",
    "content": "package utils\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst TCP_BUFFER_SIZE = 1024 * 64\n\nvar bigFileSize = 75000000\n\nfunc bigFile() {\n\tos.WriteFile(\"bigfile.test\", bytes.Repeat([]byte(\"z\"), bigFileSize), 0o666)\n}\n\nfunc BenchmarkMD5(b *testing.B) {\n\tbigFile()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tMD5HashFile(\"bigfile.test\", false)\n\t}\n}\n\nfunc BenchmarkXXHash(b *testing.B) {\n\tbigFile()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tXXHashFile(\"bigfile.test\", false)\n\t}\n}\n\nfunc BenchmarkImoHash(b *testing.B) {\n\tbigFile()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tIMOHashFile(\"bigfile.test\")\n\t}\n}\n\nfunc BenchmarkHighwayHash(b *testing.B) {\n\tbigFile()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tHighwayHashFile(\"bigfile.test\", false)\n\t}\n}\n\nfunc BenchmarkImoHashFull(b *testing.B) {\n\tbigFile()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tIMOHashFileFull(\"bigfile.test\")\n\t}\n}\n\nfunc BenchmarkSha256(b *testing.B) {\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tSHA256(\"hello,world\")\n\t}\n}\n\nfunc BenchmarkMissingChunks(b *testing.B) {\n\tbigFile()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tMissingChunks(\"bigfile.test\", int64(bigFileSize), TCP_BUFFER_SIZE/2)\n\t}\n}\n\nfunc TestExists(t *testing.T) {\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\tfmt.Println(GetLocalIPs())\n\tassert.True(t, Exists(\"bigfile.test\"))\n\tassert.False(t, Exists(\"doesnotexist\"))\n}\n\nfunc TestMD5HashFile(t *testing.T) {\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\tb, err := MD5HashFile(\"bigfile.test\", false)\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"8304ff018e02baad0e3555bade29a405\", fmt.Sprintf(\"%x\", b))\n\t_, err = MD5HashFile(\"bigfile.test.nofile\", false)\n\tassert.NotNil(t, err)\n}\n\nfunc TestHighwayHashFile(t *testing.T) {\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\tb, err := HighwayHashFile(\"bigfile.test\", false)\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"3c32999529323ed66a67aeac5720c7bf1301dcc5dca87d8d46595e85ff990329\", fmt.Sprintf(\"%x\", b))\n\t_, err = HighwayHashFile(\"bigfile.test.nofile\", false)\n\tassert.NotNil(t, err)\n}\n\nfunc TestIMOHashFile(t *testing.T) {\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\tb, err := IMOHashFile(\"bigfile.test\")\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"c0d1e12301e6c635f6d4a8ea5c897437\", fmt.Sprintf(\"%x\", b))\n}\n\nfunc TestXXHashFile(t *testing.T) {\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\tb, err := XXHashFile(\"bigfile.test\", false)\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"4918740eb5ccb6f7\", fmt.Sprintf(\"%x\", b))\n\t_, err = XXHashFile(\"nofile\", false)\n\tassert.NotNil(t, err)\n}\n\nfunc TestSHA256(t *testing.T) {\n\tassert.Equal(t, \"09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b\", SHA256(\"hello, world\"))\n}\n\nfunc TestByteCountDecimal(t *testing.T) {\n\tassert.Equal(t, \"10.0 kB\", ByteCountDecimal(10240))\n\tassert.Equal(t, \"50 B\", ByteCountDecimal(50))\n\tassert.Equal(t, \"12.4 MB\", ByteCountDecimal(13002343))\n}\n\nfunc TestMissingChunks(t *testing.T) {\n\tfileSize := 100\n\tchunkSize := 10\n\trand.Seed(1)\n\tbigBuff := make([]byte, fileSize)\n\trand.Read(bigBuff)\n\tos.WriteFile(\"missing.test\", bigBuff, 0o644)\n\tempty := make([]byte, chunkSize)\n\tf, err := os.OpenFile(\"missing.test\", os.O_RDWR, 0o644)\n\tassert.Nil(t, err)\n\tfor block := 0; block < fileSize/chunkSize; block++ {\n\t\tif block == 0 || block == 4 || block == 5 || block >= 7 {\n\t\t\tf.WriteAt(empty, int64(block*chunkSize))\n\t\t}\n\t}\n\tf.Close()\n\n\tchunkRanges := MissingChunks(\"missing.test\", int64(fileSize), chunkSize)\n\tassert.Equal(t, []int64{10, 0, 1, 40, 2, 70, 3}, chunkRanges)\n\n\tchunks := ChunkRangesToChunks(chunkRanges)\n\tassert.Equal(t, []int64{0, 40, 50, 70, 80, 90}, chunks)\n\n\tos.Remove(\"missing.test\")\n\n\tcontent := []byte(\"temporary file's content\")\n\ttmpfile, err := os.CreateTemp(\"\", \"example\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tdefer os.Remove(tmpfile.Name()) // clean up\n\n\tif _, err := tmpfile.Write(content); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := tmpfile.Close(); err != nil {\n\t\tpanic(err)\n\t}\n\tchunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)), chunkSize)\n\tassert.Empty(t, chunkRanges)\n\tchunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)+10), chunkSize)\n\tassert.Empty(t, chunkRanges)\n\tchunkRanges = MissingChunks(tmpfile.Name()+\"ok\", int64(len(content)), chunkSize)\n\tassert.Empty(t, chunkRanges)\n\tchunks = ChunkRangesToChunks(chunkRanges)\n\tassert.Empty(t, chunks)\n}\n\n// func Test1(t *testing.T) {\n// \tchunkRanges := MissingChunks(\"../../m/bigfile.test\", int64(75000000), 1024*64/2)\n// \tfmt.Println(chunkRanges)\n// \tfmt.Println(ChunkRangesToChunks((chunkRanges)))\n// \tassert.Nil(t, nil)\n// }\n\nfunc TestHashFile(t *testing.T) {\n\tcontent := []byte(\"temporary file's content\")\n\ttmpfile, err := os.CreateTemp(\"\", \"example\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tdefer os.Remove(tmpfile.Name()) // clean up\n\n\tif _, err = tmpfile.Write(content); err != nil {\n\t\tpanic(err)\n\t}\n\tif err = tmpfile.Close(); err != nil {\n\t\tpanic(err)\n\t}\n\thashed, err := HashFile(tmpfile.Name(), \"xxhash\")\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"e66c561610ad51e2\", fmt.Sprintf(\"%x\", hashed))\n}\n\nfunc TestPublicIP(t *testing.T) {\n\tip, err := PublicIP()\n\tfmt.Println(ip)\n\tassert.True(t, strings.Contains(ip, \".\") || strings.Contains(ip, \":\"))\n\tassert.Nil(t, err)\n}\n\nfunc TestLocalIP(t *testing.T) {\n\tip := LocalIP()\n\tfmt.Println(ip)\n\tassert.True(t, strings.Contains(ip, \".\") || strings.Contains(ip, \":\"))\n}\n\nfunc TestGetRandomName(t *testing.T) {\n\tname := GetRandomName()\n\tfmt.Println(name)\n\tassert.NotEmpty(t, name)\n}\n\nfunc intSliceSame(a, b []int) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc TestFindOpenPorts(t *testing.T) {\n\topenPorts := FindOpenPorts(\"127.0.0.1\", 9009, 4)\n\tif !intSliceSame(openPorts, []int{9009, 9010, 9011, 9012}) && !intSliceSame(openPorts, []int{9014, 9015, 9016, 9017}) {\n\t\tt.Errorf(\"openPorts: %v\", openPorts)\n\n\t}\n}\n\nfunc TestIsLocalIP(t *testing.T) {\n\tassert.True(t, IsLocalIP(\"192.168.0.14:9009\"))\n}\n\nfunc TestValidFileName(t *testing.T) {\n\t// contains regular characters\n\tassert.Nil(t, ValidFileName(\"中文.csl\"))\n\t// contains regular characters\n\tassert.Nil(t, ValidFileName(\"[something].csl\"))\n\t// contains regular characters\n\tassert.Nil(t, ValidFileName(\"[(something)].csl\"))\n\t// contains invisible character\n\terr := ValidFileName(\"D中文.cslouglas​\")\n\tassert.NotNil(t, err)\n\tassert.Equal(t, \"non-graphical unicode: e2808b U+8203 in '44e4b8ade696872e63736c6f75676c6173e2808b'\", err.Error())\n\t// contains \"..\", but not next to a path separator\n\tassert.Nil(t, ValidFileName(\"hi..txt\"))\n\t// contains \"..\", but only next to a path separator on one side\n\tassert.Nil(t, ValidFileName(\"rel\"+string(os.PathSeparator)+\"..txt\"))\n\tassert.Nil(t, ValidFileName(\"rel..\"+string(os.PathSeparator)+\"txt\"))\n\t// contains \"..\" between two path separators, but does not break out of the base directory\n\tassert.Nil(t, ValidFileName(\"hi\"+string(os.PathSeparator)+\"..\"+string(os.PathSeparator)+\"txt\"))\n\t// contains \"..\" between two path separators, and breaks out of the base directory\n\tassert.NotNil(t, ValidFileName(\"hi\"+string(os.PathSeparator)+\"..\"+string(os.PathSeparator)+\"..\"+string(os.PathSeparator)+\"txt\"))\n\t// contains \"..\" between a path separator and the beginning or end of the path\n\tassert.NotNil(t, ValidFileName(\"..\"+string(os.PathSeparator)+\"hi.txt\"))\n\tassert.NotNil(t, ValidFileName(\"hi\"+string(os.PathSeparator)+\"..\"+string(os.PathSeparator)+\"..\"+string(os.PathSeparator)+\"hi.txt\"))\n\tassert.NotNil(t, ValidFileName(\"..\"))\n\t// is an absolute path\n\tassert.NotNil(t, ValidFileName(path.Join(string(os.PathSeparator), \"abs\", string(os.PathSeparator), \"hi.txt\")))\n}\n\n// zip\n\n// TestUnzipDirectory tests the unzip directory functionality\nfunc TestUnzipDirectory(t *testing.T) {\n\t// Create temporary directory for tests\n\ttmpDir, err := os.MkdirTemp(\"\", \"unzip_test\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp dir: %v\", err)\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\n\t// Create test zip and extraction directory\n\tzipPath := filepath.Join(tmpDir, \"test.zip\")\n\textractDir := filepath.Join(tmpDir, \"extracted\")\n\n\t// Create test zip file with proper structure and known mod times\n\texpectedModTime := time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)\n\tif err := createTestZipWithModTime(zipPath, expectedModTime); err != nil {\n\t\tt.Fatalf(\"Failed to create test zip: %v\", err)\n\t}\n\n\t// Test extraction\n\terr = UnzipDirectory(extractDir, zipPath)\n\tif err != nil {\n\t\tt.Fatalf(\"UnzipDirectory failed: %v\", err)\n\t}\n\n\t// Update expected files to match the actual structure from createTestZipWithModTime\n\tbaseName := \"test\"\n\texpectedFiles := []string{\n\t\tbaseName + \"/file1.txt\",\n\t\tbaseName + \"/subdir/file2.txt\",\n\t\tbaseName + \"/subdir2/file3.txt\",\n\t\tbaseName + \"/file4.txt\",\n\t}\n\n\t// Also check directories\n\texpectedDirs := []string{\n\t\tbaseName + \"/\",\n\t\tbaseName + \"/subdir/\",\n\t\tbaseName + \"/subdir2/\",\n\t}\n\n\t// Verify files\n\tfor _, expectedFile := range expectedFiles {\n\t\tfullPath := filepath.Join(extractDir, expectedFile)\n\t\tif _, err := os.Stat(fullPath); os.IsNotExist(err) {\n\t\t\tt.Errorf(\"File was not extracted: %s\", expectedFile)\n\t\t} else {\n\t\t\t// Verify modification time is preserved after extraction\n\t\t\tverifyFileModTime(t, fullPath, expectedModTime)\n\t\t}\n\t}\n\n\t// Verify directories\n\tfor _, expectedDir := range expectedDirs {\n\t\tfullPath := filepath.Join(extractDir, expectedDir)\n\t\tif _, err := os.Stat(fullPath); os.IsNotExist(err) {\n\t\t\tt.Errorf(\"Directory was not extracted: %s\", expectedDir)\n\t\t} else {\n\t\t\t// Verify modification time is preserved after extraction\n\t\t\tverifyFileModTime(t, fullPath, expectedModTime)\n\t\t}\n\t}\n\n\t// Verify file contents after extraction\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/file1.txt\"), \"Test content 1\")\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/subdir/file2.txt\"), \"Test content 2\")\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/subdir2/file3.txt\"), \"Test content 3\")\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/file4.txt\"), \"Test content 4\")\n}\n\n// TestUnzipToNonExistentDirectory tests unzip to non-existent destination\nfunc TestUnzipToNonExistentDirectory(t *testing.T) {\n\ttmpDir, err := os.MkdirTemp(\"\", \"unzip_nonexistent_dest_test\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp dir: %v\", err)\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\n\t// Create test zip\n\tzipPath := filepath.Join(tmpDir, \"test.zip\")\n\texpectedModTime := time.Date(2023, 4, 1, 9, 0, 0, 0, time.UTC)\n\tif err := createTestZipWithModTime(zipPath, expectedModTime); err != nil {\n\t\tt.Fatalf(\"Failed to create test zip: %v\", err)\n\t}\n\n\t// Extract to non-existent directory\n\textractDir := filepath.Join(tmpDir, \"nonexistent\", \"deep\", \"path\")\n\n\terr = UnzipDirectory(extractDir, zipPath)\n\tif err != nil {\n\t\tt.Fatalf(\"UnzipDirectory failed to create destination directory: %v\", err)\n\t}\n\n\t// Update expected files to match the actual structure\n\tbaseName := \"test\"\n\texpectedFiles := []string{\n\t\tbaseName + \"/file1.txt\",\n\t\tbaseName + \"/subdir/file2.txt\",\n\t\tbaseName + \"/subdir2/file3.txt\",\n\t\tbaseName + \"/file4.txt\",\n\t}\n\n\t// Also check directories\n\texpectedDirs := []string{\n\t\tbaseName + \"/\",\n\t\tbaseName + \"/subdir/\",\n\t\tbaseName + \"/subdir2/\",\n\t}\n\n\t// Verify files\n\tfor _, expectedFile := range expectedFiles {\n\t\tfullPath := filepath.Join(extractDir, expectedFile)\n\t\tif _, err := os.Stat(fullPath); os.IsNotExist(err) {\n\t\t\tt.Errorf(\"File was not extracted to non-existent destination: %s\", expectedFile)\n\t\t} else {\n\t\t\t// Verify modification time is preserved\n\t\t\tverifyFileModTime(t, fullPath, expectedModTime)\n\t\t}\n\t}\n\n\t// Verify directories\n\tfor _, expectedDir := range expectedDirs {\n\t\tfullPath := filepath.Join(extractDir, expectedDir)\n\t\tif _, err := os.Stat(fullPath); os.IsNotExist(err) {\n\t\t\tt.Errorf(\"Directory was not extracted to non-existent destination: %s\", expectedDir)\n\t\t} else {\n\t\t\t// Verify modification time is preserved\n\t\t\tverifyFileModTime(t, fullPath, expectedModTime)\n\t\t}\n\t}\n}\n\nfunc TestUnzipDirectoryRejectsPathTraversal(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tzipPath := filepath.Join(tmpDir, \"malicious-relative.zip\")\n\textractDir := filepath.Join(tmpDir, \"extract\")\n\tescapedPath := filepath.Join(tmpDir, \"escaped.txt\")\n\n\terr := createZipWithEntries(zipPath, []zipTestEntry{\n\t\t{name: \"safe/file.txt\", content: \"safe\"},\n\t\t{name: \"../../escaped.txt\", content: \"escape\"},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create malicious zip: %v\", err)\n\t}\n\n\terr = UnzipDirectory(extractDir, zipPath)\n\tassert.NotNil(t, err)\n\tassert.Contains(t, err.Error(), \"invalid file path\")\n\n\t_, statErr := os.Stat(escapedPath)\n\tassert.True(t, os.IsNotExist(statErr), \"path traversal should not create files outside destination\")\n\n\t_, statErr = os.Stat(filepath.Join(extractDir, \"safe\", \"file.txt\"))\n\tassert.True(t, os.IsNotExist(statErr), \"pre-validation should prevent partial extraction\")\n}\n\nfunc TestResolveUnzipPathRejectsAbsolutePathEntry(t *testing.T) {\n\tdestination := t.TempDir()\n\tabsoluteEntry := filepath.Join(string(os.PathSeparator), \"tmp\", \"croc-absolute-escape.txt\")\n\n\t_, err := resolveUnzipPath(destination, absoluteEntry)\n\tassert.NotNil(t, err)\n\tif err != nil {\n\t\tassert.Contains(t, err.Error(), \"path escapes destination\")\n\t}\n}\n\n// TestZipAndUnzipRoundTrip tests complete zip/unzip cycle with proper paths\nfunc TestZipAndUnzipRoundTrip(t *testing.T) {\n\t// Create temporary directory for tests\n\ttmpDir, err := os.MkdirTemp(\"\", \"roundtrip_test\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp dir: %v\", err)\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\n\t// Create source directory with test files\n\tsourceDir := filepath.Join(tmpDir, \"source\")\n\tif err := os.MkdirAll(sourceDir, 0755); err != nil {\n\t\tt.Fatalf(\"Failed to create source directory: %v\", err)\n\t}\n\n\t// Use specific mod times for different items\n\trootModTime := time.Date(2023, 3, 1, 14, 30, 0, 0, time.UTC)\n\tsubdirModTime := time.Date(2023, 3, 1, 14, 29, 0, 0, time.UTC)\n\tsubdir2ModTime := time.Date(2023, 3, 1, 14, 28, 0, 0, time.UTC)\n\tfileModTime := time.Date(2023, 3, 1, 14, 31, 0, 0, time.UTC)\n\n\t// Create directories structure first\n\tdirs := []string{\n\t\t\"subdir\",\n\t\t\"subdir2\",\n\t}\n\n\tfor _, dir := range dirs {\n\t\tfullPath := filepath.Join(sourceDir, dir)\n\t\tif err := os.MkdirAll(fullPath, 0755); err != nil {\n\t\t\tt.Fatalf(\"Failed to create directory: %v\", err)\n\t\t}\n\t}\n\n\t// Create files with specific modification times\n\ttestFiles := map[string]string{\n\t\t\"file1.txt\":         \"Content of file 1\",\n\t\t\"subdir/file2.txt\":  \"Content of file 2 in subdir\",\n\t\t\"subdir2/file3.txt\": \"Content of file 3 in another subdir\",\n\t\t\"file4.txt\":         \"Content of file 4\",\n\t}\n\n\tfor filePath, content := range testFiles {\n\t\tfullPath := filepath.Join(sourceDir, filePath)\n\t\tif err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {\n\t\t\tt.Fatalf(\"Failed to create directory: %v\", err)\n\t\t}\n\t\tif err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {\n\t\t\tt.Fatalf(\"Failed to create test file: %v\", err)\n\t\t}\n\t\tif err := os.Chtimes(fullPath, fileModTime, fileModTime); err != nil {\n\t\t\tt.Fatalf(\"Failed to set file time: %v\", err)\n\t\t}\n\t}\n\n\t// NOW set directory times AFTER creating all files\n\n\t// Set time for root source directory\n\tif err := os.Chtimes(sourceDir, rootModTime, rootModTime); err != nil {\n\t\tt.Fatalf(\"Failed to set source directory time: %v\", err)\n\t}\n\n\t// Set times for subdirectories\n\tdirTimes := map[string]time.Time{\n\t\t\"subdir\":  subdirModTime,\n\t\t\"subdir2\": subdir2ModTime,\n\t}\n\n\tfor dir, modTime := range dirTimes {\n\t\tfullPath := filepath.Join(sourceDir, dir)\n\t\tif err := os.Chtimes(fullPath, modTime, modTime); err != nil {\n\t\t\tt.Fatalf(\"Failed to set directory %s time: %v\", dir, err)\n\t\t}\n\t}\n\n\t// Wait a moment to ensure time changes are applied\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Create zip\n\tzipPath := filepath.Join(tmpDir, \"test.zip\")\n\terr = ZipDirectory(zipPath, sourceDir)\n\tif err != nil {\n\t\tt.Fatalf(\"ZipDirectory failed: %v\", err)\n\t}\n\n\t// Print zip contents using Go's zip reader\n\tfmt.Printf(\"=== ZIP Archive Contents ===\\n\")\n\tarchive, err := zip.OpenReader(zipPath)\n\tif err == nil {\n\t\tdefer archive.Close()\n\t\tfor _, f := range archive.File {\n\t\t\tmodifiedTime := f.Modified\n\t\t\tif modifiedTime.IsZero() {\n\t\t\t\tmodifiedTime = f.FileHeader.Modified\n\t\t\t}\n\t\t\tfmt.Printf(\"  %s (dir: %v) modTime: %v\\n\", f.Name, f.FileInfo().IsDir(), modifiedTime.UTC())\n\t\t}\n\t}\n\n\t// Extract to different directory\n\textractDir := filepath.Join(tmpDir, \"extracted\")\n\terr = UnzipDirectory(extractDir, zipPath)\n\tif err != nil {\n\t\tt.Fatalf(\"UnzipDirectory failed: %v\", err)\n\t}\n\n\t// Expected items (both files and directories)\n\tbaseName := \"test\"\n\texpectedItems := []string{\n\t\tbaseName + \"/\",\n\t\tbaseName + \"/file1.txt\",\n\t\tbaseName + \"/subdir/\",\n\t\tbaseName + \"/subdir/file2.txt\",\n\t\tbaseName + \"/subdir2/\",\n\t\tbaseName + \"/subdir2/file3.txt\",\n\t\tbaseName + \"/file4.txt\",\n\t}\n\n\texpectedExtractedTimes := map[string]time.Time{\n\t\tbaseName + \"/\":                  rootModTime,\n\t\tbaseName + \"/subdir/\":           subdirModTime,\n\t\tbaseName + \"/subdir2/\":          subdir2ModTime,\n\t\tbaseName + \"/file1.txt\":         fileModTime,\n\t\tbaseName + \"/subdir/file2.txt\":  fileModTime,\n\t\tbaseName + \"/subdir2/file3.txt\": fileModTime,\n\t\tbaseName + \"/file4.txt\":         fileModTime,\n\t}\n\n\t// Verify all items exist with correct modification times\n\tfmt.Printf(\"=== Extracted Files Verification ===\\n\")\n\tfor _, itemPath := range expectedItems {\n\t\tfullPath := filepath.Join(extractDir, itemPath)\n\t\tif _, err := os.Stat(fullPath); os.IsNotExist(err) {\n\t\t\tt.Errorf(\"Item was not extracted: %s\", itemPath)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Verify with test assertion\n\t\texpectedTime := expectedExtractedTimes[itemPath]\n\t\tverifyFileModTime(t, fullPath, expectedTime)\n\t}\n\n\t// Verify file contents\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/file1.txt\"), \"Content of file 1\")\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/subdir/file2.txt\"), \"Content of file 2 in subdir\")\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/subdir2/file3.txt\"), \"Content of file 3 in another subdir\")\n\tverifyFileContent(t, filepath.Join(extractDir, baseName+\"/file4.txt\"), \"Content of file 4\")\n}\n\n// Helper function to create test zip file with specific modification time\nfunc createTestZipWithModTime(zipPath string, modTime time.Time) error {\n\tfile, err := os.Create(zipPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\twriter := zip.NewWriter(file)\n\tdefer writer.Close()\n\n\t// Get base name for consistent structure\n\tbaseName := strings.TrimSuffix(filepath.Base(zipPath), \".zip\")\n\n\t// First create entries for directories with modification time\n\tdirs := []string{\n\t\tbaseName + \"/\",\n\t\tbaseName + \"/subdir/\",\n\t\tbaseName + \"/subdir2/\",\n\t}\n\n\tfor _, dir := range dirs {\n\t\theader := &zip.FileHeader{\n\t\t\tName:     filepath.ToSlash(dir),\n\t\t\tModified: modTime,\n\t\t}\n\t\t_, err := writer.CreateHeader(header)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Then create files\n\tfiles := []struct {\n\t\tname    string\n\t\tcontent string\n\t}{\n\t\t{filepath.Join(baseName, \"file1.txt\"), \"Test content 1\"},\n\t\t{filepath.Join(baseName, \"subdir\", \"file2.txt\"), \"Test content 2\"},\n\t\t{filepath.Join(baseName, \"subdir2\", \"file3.txt\"), \"Test content 3\"},\n\t\t{filepath.Join(baseName, \"file4.txt\"), \"Test content 4\"},\n\t}\n\n\tfor _, f := range files {\n\t\theader := &zip.FileHeader{\n\t\t\tName:     filepath.ToSlash(f.name),\n\t\t\tModified: modTime,\n\t\t\tMethod:   zip.Deflate,\n\t\t}\n\n\t\tw, err := writer.CreateHeader(header)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := w.Write([]byte(f.content)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype zipTestEntry struct {\n\tname    string\n\tcontent string\n}\n\nfunc createZipWithEntries(zipPath string, entries []zipTestEntry) error {\n\tfile, err := os.Create(zipPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\twriter := zip.NewWriter(file)\n\tfor _, entry := range entries {\n\t\theader := &zip.FileHeader{\n\t\t\tName:   filepath.ToSlash(entry.name),\n\t\t\tMethod: zip.Deflate,\n\t\t}\n\t\tw, err := writer.CreateHeader(header)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := w.Write([]byte(entry.content)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn writer.Close()\n}\n\n// Helper function to verify file content\nfunc verifyFileContent(t *testing.T, filePath, expectedContent string) {\n\tcontent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\tt.Errorf(\"Failed to read file %s: %v\", filePath, err)\n\t\treturn\n\t}\n\n\tif string(content) != expectedContent {\n\t\tt.Errorf(\"Content mismatch for %s, expected '%s', got '%s'\",\n\t\t\tfilePath, expectedContent, string(content))\n\t}\n}\n\n// Helper function to verify file modification time\nfunc verifyFileModTime(t *testing.T, filePath string, expectedTime time.Time) {\n\tinfo, err := os.Stat(filePath)\n\tif err != nil {\n\t\tt.Errorf(\"Failed to stat file %s: %v\", filePath, err)\n\t\treturn\n\t}\n\n\t// Compare times truncated to seconds (file system precision may vary)\n\texpected := expectedTime.UTC().Truncate(time.Second)\n\tactual := info.ModTime().UTC().Truncate(time.Second)\n\n\tif !actual.Equal(expected) {\n\t\tt.Errorf(\"Modification time mismatch for %s, expected %v, got %v\",\n\t\t\tfilePath, expected, actual)\n\t}\n}\n\n// TestHashFileCtxNoCancellation tests HashFileCtx without cancellation\nfunc TestHashFileCtxNoCancellation(t *testing.T) {\n\t// Use the same bigFile() function as other tests\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\n\tctx := context.Background()\n\n\t// Test each algorithm - using the same expected values from existing tests\n\ttests := []struct {\n\t\tname      string\n\t\talgorithm string\n\t\twantHash  string\n\t}{\n\t\t{\n\t\t\tname:      \"MD5 hash\",\n\t\t\talgorithm: \"md5\",\n\t\t\twantHash:  \"8304ff018e02baad0e3555bade29a405\", // From TestMD5HashFile\n\t\t},\n\t\t{\n\t\t\tname:      \"XXHash\",\n\t\t\talgorithm: \"xxhash\",\n\t\t\twantHash:  \"4918740eb5ccb6f7\", // From TestXXHashFile\n\t\t},\n\t\t{\n\t\t\tname:      \"imohash\",\n\t\t\talgorithm: \"imohash\",\n\t\t\twantHash:  \"c0d1e12301e6c635f6d4a8ea5c897437\", // From TestIMOHashFile\n\t\t},\n\t\t{\n\t\t\tname:      \"highway\",\n\t\t\talgorithm: \"highway\",\n\t\t\twantHash:  \"3c32999529323ed66a67aeac5720c7bf1301dcc5dca87d8d46595e85ff990329\", // From TestHighwayHashFile\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Test without progress bar\n\t\t\thash, err := HashFileCtx(ctx, \"bigfile.test\", tt.algorithm)\n\t\t\tassert.NoError(t, err, \"HashFileCtx should not return error\")\n\t\t\tassert.Equal(t, tt.wantHash, fmt.Sprintf(\"%x\", hash),\n\t\t\t\t\"Hash should match for algorithm %s\", tt.algorithm)\n\n\t\t\t// Test with progress bar (false)\n\t\t\thash, err = HashFileCtx(ctx, \"bigfile.test\", tt.algorithm, false)\n\t\t\tassert.NoError(t, err, \"HashFileCtx with showProgress=false should not return error\")\n\t\t\tassert.Equal(t, tt.wantHash, fmt.Sprintf(\"%x\", hash),\n\t\t\t\t\"Hash should match for algorithm %s with showProgress=false\", tt.algorithm)\n\n\t\t\t// Test with progress bar (true) - only for non-imohash to avoid spinner issues in tests\n\t\t\tif tt.algorithm != \"imohash\" {\n\t\t\t\thash, err = HashFileCtx(ctx, \"bigfile.test\", tt.algorithm, true)\n\t\t\t\tassert.NoError(t, err, \"HashFileCtx with showProgress=true should not return error\")\n\t\t\t\tassert.Equal(t, tt.wantHash, fmt.Sprintf(\"%x\", hash),\n\t\t\t\t\t\"Hash should match for algorithm %s with showProgress=true\", tt.algorithm)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Test symlink handling - match original behavior\n\tt.Run(\"Symlink handling\", func(t *testing.T) {\n\t\t// Create symlink to bigfile.test\n\t\tsymlinkPath := \"bigfile.test.symlink\"\n\t\tdefer os.Remove(symlinkPath)\n\n\t\terr := os.Symlink(\"bigfile.test\", symlinkPath)\n\t\tif err != nil && strings.Contains(err.Error(), \"privilege\") {\n\t\t\tt.Skip(\"Skipping symlink test - requires privilege\")\n\t\t}\n\t\tassert.NoError(t, err, \"Should create symlink\")\n\n\t\t// Hash the symlink\n\t\thash, err := HashFileCtx(ctx, symlinkPath, \"md5\")\n\t\tassert.NoError(t, err, \"Should hash symlink target path\")\n\t\tassert.NotNil(t, hash, \"Should return hash for symlink\")\n\n\t\t// The original HashFile returns []byte(SHA256(target))\n\t\t// SHA256(\"bigfile.test\") = \"3ae29e98bba80ccefc79289c59cc34cb7223954310bb61c6a26147bb9b08c4e4\"\n\t\t// []byte(\"3ae29e98...\") = ASCII bytes of hex string\n\n\t\t// When converted back with fmt.Sprintf(\"%x\", hash):\n\t\t// ASCII '3' = 0x33, 'a' = 0x61, 'e' = 0x65, '2' = 0x32, etc.\n\t\t// So fmt.Sprintf(\"%x\", []byte(\"3ae2...\")) = \"33616532...\"\n\n\t\tactualHex := fmt.Sprintf(\"%x\", hash)\n\n\t\t// Let's compute what we SHOULD get:\n\t\ttargetPath := \"bigfile.test\"\n\t\texpectedSHA256Hex := SHA256(targetPath) // \"3ae29e98...\"\n\t\texpectedBytes := []byte(expectedSHA256Hex)\n\t\texpectedResultHex := fmt.Sprintf(\"%x\", expectedBytes) // hex of ASCII bytes\n\n\t\t// Debug\n\t\tt.Logf(\"Target path: '%s'\", targetPath)\n\t\tt.Logf(\"SHA256(target) hex: %s\", expectedSHA256Hex)\n\t\tt.Logf(\"Expected result (hex of ASCII bytes): %s\", expectedResultHex)\n\t\tt.Logf(\"Actual result: %s\", actualHex)\n\n\t\t// They should match!\n\t\tassert.Equal(t, expectedResultHex, actualHex,\n\t\t\t\"HashFileCtx should behave exactly like HashFile for symlinks\")\n\n\t\t// Also test with original HashFile to ensure consistency\n\t\toriginalHash, err := HashFile(symlinkPath, \"md5\")\n\t\tassert.NoError(t, err)\n\t\toriginalHex := fmt.Sprintf(\"%x\", originalHash)\n\n\t\tassert.Equal(t, originalHex, actualHex,\n\t\t\t\"HashFileCtx should return same result as HashFile for symlinks\")\n\t})\n\t// Test error cases\n\tt.Run(\"Error cases\", func(t *testing.T) {\n\t\t// Non-existent file\n\t\thash, err := HashFileCtx(ctx, \"non_existent_file_12345.test\", \"md5\")\n\t\tassert.Error(t, err, \"Should return error for non-existent file\")\n\t\tassert.Nil(t, hash, \"Hash should be nil on error\")\n\n\t\t// Unsupported algorithm\n\t\thash, err = HashFileCtx(ctx, \"bigfile.test\", \"unsupported_algo\")\n\t\tassert.Error(t, err, \"Should return error for unsupported algorithm\")\n\t\tassert.Contains(t, err.Error(), \"unsupported algorithm\")\n\t\tassert.Nil(t, hash, \"Hash should be nil on error\")\n\t})\n}\n\n// TestHashFileCtxWithCancellation tests HashFileCtx with context cancellation\nfunc TestHashFileCtxWithCancellation(t *testing.T) {\n\t// Use the same bigFile() function\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\n\t// Test 1: Cancel before starting\n\tt.Run(\"Cancel before start\", func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tcancel() // Cancel immediately\n\n\t\thash, err := HashFileCtx(ctx, \"bigfile.test\", \"md5\")\n\t\tassert.Error(t, err, \"Should return error when context cancelled before start\")\n\t\tassert.Equal(t, context.Canceled, err, \"Error should be context.Canceled\")\n\t\tassert.Nil(t, hash, \"Hash should be nil when cancelled\")\n\t})\n\n\t// Test 2: Cancel during operation\n\tt.Run(\"Cancel during operation\", func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\n\t\t// Start hash operation in goroutine\n\t\terrCh := make(chan error, 1)\n\t\thashCh := make(chan []byte, 1)\n\n\t\tgo func() {\n\t\t\thash, err := HashFileCtx(ctx, \"bigfile.test\", \"md5\", false)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\thashCh <- nil\n\t\t\t} else {\n\t\t\t\terrCh <- nil\n\t\t\t\thashCh <- hash\n\t\t\t}\n\t\t}()\n\n\t\t// Cancel after a short delay\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tcancel()\n\n\t\t// Wait for result\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\thash := <-hashCh\n\t\t\t// Either we got an error (cancelled) or a hash (completed before cancellation)\n\t\t\tif err != nil {\n\t\t\t\t// Check if it's a context error\n\t\t\t\tif err == context.Canceled || err == context.DeadlineExceeded {\n\t\t\t\t\tassert.Error(t, err, \"Should return context error when cancelled\")\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, hash, \"Hash should be nil when cancelled\")\n\t\t\t} else {\n\t\t\t\t// Completed successfully before cancellation\n\t\t\t\tassert.NotNil(t, hash, \"If not cancelled, should return hash\")\n\t\t\t\tassert.Equal(t, 16, len(hash), \"MD5 hash should be 16 bytes\")\n\t\t\t\t// Verify it's the correct hash\n\t\t\t\tassert.Equal(t, \"8304ff018e02baad0e3555bade29a405\", fmt.Sprintf(\"%x\", hash))\n\t\t\t}\n\t\tcase <-time.After(5 * time.Second):\n\t\t\tt.Fatal(\"Test timed out\")\n\t\t}\n\t})\n\n\t// Test 3: Cancel with deadline\n\tt.Run(\"Cancel with deadline\", func(t *testing.T) {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)\n\t\tdefer cancel()\n\n\t\t// For a 75MB file, MD5 should take more than 1ms\n\t\thash, err := HashFileCtx(ctx, \"bigfile.test\", \"md5\", false)\n\t\tassert.Error(t, err, \"Should timeout for 75MB file with 1ms deadline\")\n\t\tassert.Equal(t, context.DeadlineExceeded, err, \"Error should be context.DeadlineExceeded\")\n\t\tassert.Nil(t, hash, \"Hash should be nil when deadline exceeded\")\n\t})\n\n\t// Test 4: Imohash should be fast enough to complete before cancellation\n\tt.Run(\"Imohash fast completion\", func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tdefer cancel()\n\n\t\t// Imohash samples the file, so it should complete quickly\n\t\thash, err := HashFileCtx(ctx, \"bigfile.test\", \"imohash\", false)\n\t\tassert.NoError(t, err, \"Imohash should complete before any cancellation\")\n\t\tassert.NotNil(t, hash, \"Should return hash for imohash\")\n\t\tassert.Equal(t, 16, len(hash), \"Imohash should be 16 bytes\")\n\t\t// Verify it's the correct hash\n\t\tassert.Equal(t, \"c0d1e12301e6c635f6d4a8ea5c897437\", fmt.Sprintf(\"%x\", hash))\n\t})\n}\n\n// TestHashFileCtxEquivalence tests that HashFileCtx produces same results as original HashFile\nfunc TestHashFileCtxEquivalence(t *testing.T) {\n\t// Use bigFile() for consistency\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\n\talgorithms := []string{\"md5\", \"xxhash\", \"imohash\", \"highway\"}\n\n\tfor _, algorithm := range algorithms {\n\t\tt.Run(algorithm, func(t *testing.T) {\n\t\t\t// Get hash using original HashFile\n\t\t\toriginalHash, err1 := HashFile(\"bigfile.test\", algorithm)\n\n\t\t\t// Get hash using HashFileCtx with background context\n\t\t\tctxHash, err2 := HashFileCtx(context.Background(), \"bigfile.test\", algorithm)\n\n\t\t\t// Both should succeed or fail together\n\t\t\tif err1 != nil {\n\t\t\t\tassert.Error(t, err2, \"HashFileCtx should also fail if HashFile fails\")\n\t\t\t\tt.Logf(\"Both failed as expected: %v\", err1)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err2, \"HashFileCtx should not fail if HashFile succeeds\")\n\t\t\t\tassert.NotNil(t, originalHash, \"Original hash should not be nil\")\n\t\t\t\tassert.NotNil(t, ctxHash, \"Context hash should not be nil\")\n\n\t\t\t\t// Compare hex representations\n\t\t\t\toriginalHex := fmt.Sprintf(\"%x\", originalHash)\n\t\t\t\tctxHex := fmt.Sprintf(\"%x\", ctxHash)\n\t\t\t\tassert.Equal(t, originalHex, ctxHex,\n\t\t\t\t\t\"HashFile and HashFileCtx should produce same hash for algorithm %s. Got %s vs %s\",\n\t\t\t\t\talgorithm, originalHex, ctxHex)\n\n\t\t\t\t// Also verify against known values from existing tests\n\t\t\t\tswitch algorithm {\n\t\t\t\tcase \"md5\":\n\t\t\t\t\tassert.Equal(t, \"8304ff018e02baad0e3555bade29a405\", originalHex)\n\t\t\t\tcase \"xxhash\":\n\t\t\t\t\tassert.Equal(t, \"4918740eb5ccb6f7\", originalHex)\n\t\t\t\tcase \"imohash\":\n\t\t\t\t\tassert.Equal(t, \"c0d1e12301e6c635f6d4a8ea5c897437\", originalHex)\n\t\t\t\tcase \"highway\":\n\t\t\t\t\tassert.Equal(t, \"3c32999529323ed66a67aeac5720c7bf1301dcc5dca87d8d46595e85ff990329\", originalHex)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestHashFileCtxLargeFile tests with larger files (already using bigfile.test)\nfunc TestHashFileCtxLargeFile(t *testing.T) {\n\t// Skip in short mode\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping large file test in short mode\")\n\t}\n\n\t// Use bigFile()\n\tbigFile()\n\tdefer os.Remove(\"bigfile.test\")\n\n\tctx := context.Background()\n\n\t// Test each algorithm with large file\n\talgorithms := []string{\"md5\", \"xxhash\", \"imohash\", \"highway\"}\n\n\tfor _, algorithm := range algorithms {\n\t\tt.Run(algorithm, func(t *testing.T) {\n\t\t\thash, err := HashFileCtx(ctx, \"bigfile.test\", algorithm, false)\n\t\t\tassert.NoError(t, err, \"Should hash large file with algorithm %s\", algorithm)\n\t\t\tassert.NotNil(t, hash, \"Should return hash for large file\")\n\n\t\t\t// Verify hash size\n\t\t\tswitch algorithm {\n\t\t\tcase \"md5\":\n\t\t\t\tassert.Equal(t, 16, len(hash), \"MD5 should be 16 bytes\")\n\t\t\tcase \"xxhash\":\n\t\t\t\tassert.Equal(t, 8, len(hash), \"XXHash should be 8 bytes\")\n\t\t\tcase \"imohash\":\n\t\t\t\tassert.Equal(t, 16, len(hash), \"Imohash should be 16 bytes\")\n\t\t\tcase \"highway\":\n\t\t\t\tassert.Equal(t, 32, len(hash), \"HighwayHash should be 32 bytes\")\n\t\t\t}\n\n\t\t\t// Verify against known values\n\t\t\tswitch algorithm {\n\t\t\tcase \"md5\":\n\t\t\t\tassert.Equal(t, \"8304ff018e02baad0e3555bade29a405\", fmt.Sprintf(\"%x\", hash))\n\t\t\tcase \"xxhash\":\n\t\t\t\tassert.Equal(t, \"4918740eb5ccb6f7\", fmt.Sprintf(\"%x\", hash))\n\t\t\tcase \"imohash\":\n\t\t\t\tassert.Equal(t, \"c0d1e12301e6c635f6d4a8ea5c897437\", fmt.Sprintf(\"%x\", hash))\n\t\t\tcase \"highway\":\n\t\t\t\tassert.Equal(t, \"3c32999529323ed66a67aeac5720c7bf1301dcc5dca87d8d46595e85ff990329\", fmt.Sprintf(\"%x\", hash))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  }
]