Full Code of schollz/croc for AI

main 9a3d42ae7d4e cached
55 files
335.9 KB
104.8k tokens
299 symbols
1 requests
Download .txt
Showing preview only (353K chars total). Download the full file or copy to clipboard to get everything.
Repository: schollz/croc
Branch: main
Commit: 9a3d42ae7d4e
Files: 55
Total size: 335.9 KB

Directory structure:
gitextract_w4v75_0t/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci.yml
│       ├── deploy.yml
│       ├── release.yml
│       ├── stale.yml
│       └── winget.yml
├── .gitignore
├── .goreleaser.yml
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── croc-entrypoint.sh
├── croc.service
├── go.mod
├── go.sum
├── main.go
└── src/
    ├── cli/
    │   └── cli.go
    ├── comm/
    │   ├── comm.go
    │   └── comm_test.go
    ├── compress/
    │   ├── compress.go
    │   └── compress_test.go
    ├── croc/
    │   ├── croc.go
    │   ├── croc_test.go
    │   └── ctx.go
    ├── crypt/
    │   ├── crypt.go
    │   └── crypt_test.go
    ├── diskusage/
    │   ├── diskusage.go
    │   ├── diskusage_test.go
    │   └── diskusage_windows.go
    ├── install/
    │   ├── Makefile
    │   ├── bash_autocomplete
    │   ├── default.txt
    │   ├── prepare-sources-tarball.sh
    │   ├── updateversion.go
    │   ├── upload-src-tarball.sh
    │   └── zsh_autocomplete
    ├── message/
    │   ├── message.go
    │   └── message_test.go
    ├── mnemonicode/
    │   ├── mnemonicode.go
    │   ├── mnemonicode_test.go
    │   └── wordlist.go
    ├── models/
    │   ├── constants.go
    │   └── models_test.go
    ├── tcp/
    │   ├── ctx.go
    │   ├── defaults.go
    │   ├── options.go
    │   ├── tcp.go
    │   └── tcp_test.go
    └── utils/
        ├── ctx.go
        ├── utils.go
        └── utils_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: schollz


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: File a bug report to help us improve croc
title: "[Bug]: "
labels: ["bug"]
body:
  - type: markdown
    attributes:
      value: |
        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.

  - type: textarea
    id: what-happened
    attributes:
      label: What happened?
      description: A clear and concise description of what the bug is.
      placeholder: Tell us what you see!
    validations:
      required: true

  - type: textarea
    id: expected-behavior
    attributes:
      label: What did you expect to happen?
      description: A clear and concise description of what you expected to happen.
      placeholder: Tell us what you expected!
    validations:
      required: true

  - type: textarea
    id: reproduction-steps
    attributes:
      label: Steps to reproduce
      description: Steps to reproduce the behavior
      placeholder: |
        1. Go to '...'
        2. Click on '...'
        3. Scroll down to '...'
        4. See error
    validations:
      required: true

  - type: input
    id: version
    attributes:
      label: croc version
      description: What version of croc are you running?
      placeholder: Run `croc --version` and paste the output here
    validations:
      required: true

  - type: dropdown
    id: os
    attributes:
      label: Operating System
      description: What operating system are you using?
      options:
        - Linux
        - macOS
        - Windows
        - Other (please specify in additional context)
    validations:
      required: true

  - type: input
    id: os-version
    attributes:
      label: OS Version
      description: What version of your operating system are you using?
      placeholder: e.g., Ubuntu 22.04, macOS 14.0, Windows 11
    validations:
      required: true


  - type: textarea
    id: logs
    attributes:
      label: Relevant log output
      description: |
        Please copy and paste any relevant log output. You can enable debug logging with `croc --debug` 
        This will be automatically formatted into code, so no need for backticks.
      render: shell
    validations:
      required: false

  - type: textarea
    id: additional-context
    attributes:
      label: Additional context
      description: Add any other context about the problem here, such as screenshots, configuration files, or anything else that might help us understand the issue.
    validations:
      required: false

================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "daily"


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
  pull_request:
  workflow_dispatch:

jobs:
  unit-tests:
    name: Go unit tests
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: '^1.26.0'
      
      - name: Display Go version
        run: go version

      - name: Run unit tests
        run: go test -v ./...

      - name: Build files
        run: |
          go version
          CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc-windows-amd64.exe
          CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc-windows-386.exe
          CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc-windows-arm.exe
          CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc-windows-arm64.exe
          CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc-linux-amd64
          CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc-linux-386
          CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc-linux-arm
          GOARM=5 CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc-linux-arm5
          CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc-linux-arm64
          CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -ldflags '-extldflags "-static"' -o croc-linux-riscv64
          CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc-darwin-amd64
          CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc-darwin-arm64
          CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc-freebsd-amd64
          CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc-freebsd-arm64
          CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc-openbsd-amd64
          CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc-openbsd-arm64
      - name: Check static build of the linux version
        run: |
          if ldd croc-linux-amd64 2>&1 | grep -q "not a dynamic executable"; then
            echo "Static build confirmed."
          else
            echo "Error: croc-linux-amd64 is a dynamic executable."
            exit 1
          fi


================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy Docker

on:
  release:
    types: [created]
  workflow_dispatch:

jobs:
  docker:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
      
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: schollz/croc
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha
      
      - name: Setup QEMU
        uses: docker/setup-qemu-action@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      
      - name: Login to DockerHub
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm,linux/arm64,linux/386
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  release:
    types: [created]
  workflow_dispatch:

permissions:
  contents: write

jobs:
  prepare-source:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: '^1.24.0'

      - name: Prepare source tarball
        run: |
          git clone -b ${{ github.event.release.name }} --depth 1 https://github.com/schollz/croc croc-${{ github.event.release.name }}
          cd croc-${{ github.event.release.name }} && go mod tidy && go mod vendor
          cd .. && tar -czvf croc_${{ github.event.release.name }}_src.tar.gz croc-${{ github.event.release.name }}

      - name: Upload source artifact
        uses: actions/upload-artifact@v7
        with:
          name: source-tarball
          path: "*.tar.gz"

  build:
    runs-on: ubuntu-24.04
    strategy:
      matrix:
        include:
          # Windows builds
          - goos: windows
            goarch: amd64
            name: Windows-64bit
            ext: .exe
            archive: zip
          - goos: windows
            goarch: "386"
            name: Windows-32bit
            ext: .exe
            archive: zip
          - goos: windows
            goarch: arm
            name: Windows-ARM
            ext: .exe
            archive: zip
          - goos: windows
            goarch: arm64
            name: Windows-ARM64
            ext: .exe
            archive: zip
          
          # Linux builds
          - goos: linux
            goarch: amd64
            name: Linux-64bit
            ext: ""
            archive: tar.gz
          - goos: linux
            goarch: "386"
            name: Linux-32bit
            ext: ""
            archive: tar.gz
          - goos: linux
            goarch: arm
            name: Linux-ARM
            ext: ""
            archive: tar.gz
          - goos: linux
            goarch: arm
            goarm: "5"
            name: Linux-ARMv5
            ext: ""
            archive: tar.gz
          - goos: linux
            goarch: arm64
            name: Linux-ARM64
            ext: ""
            archive: tar.gz
          - goos: linux
            goarch: riscv64
            name: Linux-RISCV64
            ext: ""
            archive: tar.gz
          
          # macOS builds
          - goos: darwin
            goarch: amd64
            name: macOS-64bit
            ext: ""
            archive: tar.gz
          - goos: darwin
            goarch: arm64
            name: macOS-ARM64
            ext: ""
            archive: tar.gz
          
          # BSD builds
          - goos: dragonfly
            goarch: amd64
            name: DragonFlyBSD-64bit
            ext: ""
            archive: tar.gz
          - goos: freebsd
            goarch: amd64
            name: FreeBSD-64bit
            ext: ""
            archive: tar.gz
          - goos: freebsd
            goarch: arm64
            name: FreeBSD-ARM64
            ext: ""
            archive: tar.gz
          - goos: netbsd
            goarch: "386"
            name: NetBSD-32bit
            ext: ""
            archive: tar.gz
          - goos: netbsd
            goarch: amd64
            name: NetBSD-64bit
            ext: ""
            archive: tar.gz
          - goos: netbsd
            goarch: arm64
            name: NetBSD-ARM64
            ext: ""
            archive: tar.gz
          - goos: openbsd
            goarch: amd64
            name: OpenBSD-64bit
            ext: ""
            archive: tar.gz
          - goos: openbsd
            goarch: arm64
            name: OpenBSD-ARM64
            ext: ""
            archive: tar.gz

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: '^1.24.0'

      - name: Build binary
        env:
          CGO_ENABLED: 0
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ matrix.goarch }}
          GOARM: ${{ matrix.goarm }}
        run: |
          # Set LDFLAGS based on platform
          case "${{ matrix.goos }}" in
            "darwin")
              LDFLAGS='-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"'
              ;;
            "dragonfly"|"freebsd"|"netbsd"|"openbsd")
              LDFLAGS=""
              ;;
            *)
              LDFLAGS='-extldflags "-static"'
              ;;
          esac
          
          echo "Building for ${{ matrix.goos }}/${{ matrix.goarch }} with LDFLAGS: $LDFLAGS"
          go build -ldflags "$LDFLAGS" -o croc${{ matrix.ext }}

      - name: Create archive
        run: |
          if [ "${{ matrix.archive }}" = "zip" ]; then
            zip croc_${{ github.event.release.name }}_${{ matrix.name }}.zip croc${{ matrix.ext }} LICENSE
          else
            tar -czvf croc_${{ github.event.release.name }}_${{ matrix.name }}.tar.gz croc${{ matrix.ext }} LICENSE
          fi

      - name: Upload build artifact
        uses: actions/upload-artifact@v7
        with:
          name: build-${{ matrix.name }}
          path: |
            *.zip
            *.tar.gz

  release:
    needs: [prepare-source, build]
    runs-on: ubuntu-24.04
    if: github.event_name == 'release'
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v8
        with:
          merge-multiple: true

      - name: Generate checksums
        run: |
          # Generate SHA256 checksums for all archives
          sha256sum *.zip *.tar.gz > croc_${{ github.event.release.name }}_checksums.txt
          
          # Display the checksums file for verification
          echo "Generated checksums:"
          cat croc_${{ github.event.release.name }}_checksums.txt

      - name: Upload release assets
        uses: softprops/action-gh-release@v2
        with:
          files: |
            *.zip
            *.tar.gz
            *_checksums.txt


================================================
FILE: .github/workflows/stale.yml
================================================
name: Mark stale issues and pull requests

on:
  schedule:
    - cron: '0 0 * * *'

jobs:
  stale:
    runs-on: ubuntu-24.04
    permissions:
      issues: write
      pull-requests: write

    steps:
      - name: Stale
        uses: actions/stale@v10
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          stale-issue-message: 'This issue has been marked stale because it has been open for 60 days with no activity.'
          stale-pr-message: 'This pull request has been marked stale because it has been open for 60 days with no activity.'
          stale-issue-label: 'no-issue-activity'
          stale-pr-label: 'no-pr-activity'


================================================
FILE: .github/workflows/winget.yml
================================================
name: Publish to Winget

on:
  release:
    types: [released]
  workflow_dispatch:

jobs:
  publish:
    runs-on: ubuntu-24.04

    steps:
      - name: Publish to Winget
        uses: vedantmgoyal9/winget-releaser@v2
        with:
          identifier: schollz.croc
          installers-regex: '.*Windows.*\.zip$'
          token: ${{ secrets.WINGET_TOKEN }}



================================================
FILE: .gitignore
================================================
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# Environment variables file
.env

# Croc builds
/croc
croc_v*


================================================
FILE: .goreleaser.yml
================================================
project_name: croc

build:
  main: main.go
  binary: croc
  ldflags: -s -w -X main.Version="v{{.Version}}-{{.Date}}"
  env:
    - CGO_ENABLED=0
  goos:
    - darwin
    - linux
    - windows
    - freebsd
    - netbsd
    - openbsd
    - dragonfly
  goarch:
    - amd64
    - 386
    - arm
    - arm64
  ignore:
    - goos: darwin
      goarch: 386
    - goos: freebsd
      goarch: arm
  goarm:
    - 7

nfpms:
  - formats:
      - deb
    vendor: "schollz.com"
    homepage: "https://schollz.com/software/croc/"
    maintainer: "Zack Scholl <zack.scholl@gmail.com>"
    description: "A simple, secure, and fast way to transfer data."
    license: "MIT"
    file_name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
    replacements:
      amd64: 64bit
      386: 32bit
      arm: ARM
      arm64: ARM64
      darwin: macOS
      linux: Linux
      windows: Windows
      openbsd: OpenBSD
      netbsd: NetBSD
      freebsd: FreeBSD
      dragonfly: DragonFlyBSD

archives:
  - format: tar.gz
    format_overrides:
      - goos: windows
        format: zip
    name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
    replacements:
      amd64: 64bit
      386: 32bit
      arm: ARM
      arm64: ARM64
      darwin: macOS
      linux: Linux
      windows: Windows
      openbsd: OpenBSD
      netbsd: NetBSD
      freebsd: FreeBSD
      dragonfly: DragonFlyBSD
    files:
      - README.md
      - LICENSE
      - zsh_autocomplete
      - bash_autocomplete

brews:
  - tap:
      owner: schollz
      name: homebrew-tap
    folder: Formula
    description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
    homepage: "https://schollz.com/software/croc/"
    install: |
      bin.install "croc"
    test: |
      system "#{bin}/croc --version"

scoop:
  bucket:
    owner: schollz
    name: scoop-bucket
  homepage: "https://schollz.com/software/croc/"
  description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
  license: MIT

announce:
  twitter:
    enabled: false


================================================
FILE: .travis.yml
================================================
language: go

go:
  - tip

env:
  - "PATH=/home/travis/gopath/bin:$PATH"

install: true

script:
  - env GO111MODULE=on go build -v
  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/compress
  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/croc
  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/crypt
  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/tcp
  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/utils
  - env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/comm

branches:
  except:
    - dev
    - win


================================================
FILE: Dockerfile
================================================
FROM golang:1.24-alpine AS builder

RUN apk add --no-cache git gcc musl-dev

WORKDIR /go/croc

COPY . .

RUN go build -v -ldflags="-s -w"

FROM alpine:latest

EXPOSE 9009
EXPOSE 9010
EXPOSE 9011
EXPOSE 9012
EXPOSE 9013

COPY --from=builder /go/croc/croc /go/croc/croc-entrypoint.sh /

USER nobody

# Simple TCP health check with nc!
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD nc -z localhost 9009 || exit 1

ENTRYPOINT ["/croc-entrypoint.sh"]
CMD ["relay"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017-2025 Zack

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
  <img src="https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg" width="408px" border="0" alt="croc">
  <br>
  <a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/github/v/release/schollz/croc" alt="Version"></a>
  <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>
  <a href="https://github.com/sponsors/schollz"><img alt="GitHub Sponsors" src="https://img.shields.io/github/sponsors/schollz"></a>
</p>
<p align="center">
  <strong>This project’s future depends on community support. <a href="https://github.com/sponsors/schollz">Become a sponsor today</a>.</strong>
</p>

## About

`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:

- Allows **any two computers** to transfer data (using a relay)
- Provides **end-to-end encryption** (using PAKE)
- Enables easy **cross-platform** transfers (Windows, Linux, Mac)
- Allows **multiple file** transfers
- Allows **resuming transfers** that are interrupted
- No need for local server or port-forwarding
- **IPv6-first** with IPv4 fallback
- Can **use a proxy**, like Tor

For 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).

![Example](src/install/customization.gif)

## Install

You can download [the latest release for your system](https://github.com/schollz/croc/releases/latest), or install a release from the command-line:

```bash
curl https://getcroc.schollz.com | bash
```

### On macOS

Using [Homebrew](https://brew.sh/):

```bash
brew install croc
```

Using [MacPorts](https://www.macports.org/):

```bash
sudo port selfupdate
sudo port install croc
```

### On Windows

You can install the latest release with [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), or [Winget](https://learn.microsoft.com/windows/package-manager/):

```bash
scoop install croc
```

```bash
choco install croc
```

```bash
winget install schollz.croc
```

### Using nix-env

You can install the latest release with [Nix](https://nixos.org/):

```bash
nix-env -i croc
```

### On NixOS

You can add this to your [configuration.nix](https://nixos.org/manual/nixos/stable/#ch-configuration):

```nix
environment.systemPackages = [
  pkgs.croc
];
```

### On Alpine Linux

First, install dependencies:

```bash
apk add bash coreutils
wget -qO- https://getcroc.schollz.com | bash
```

### On Arch Linux

Install with `pacman`:

```bash
pacman -S croc
```

### On Fedora

Install with `dnf`:

```bash
dnf install croc
```

### On Gentoo

Install with `portage`:

```bash
emerge net-misc/croc
```

### On Termux

Install with `pkg`:

```bash
pkg install croc
```

### On FreeBSD

Install with `pkg`:

```bash
pkg install croc
```

### On Linux, macOS, and Windows via Conda

You can install from [conda-forge](https://github.com/conda-forge/croc-feedstock) globally with [`pixi`](https://pixi.sh/):

```bash
pixi global install croc
```

Or install into a particular environment with [`conda`](https://docs.conda.io/projects/conda/):

```bash
conda install --channel conda-forge croc
```

### On Linux, macOS via Docker 

Add the following one-liner function to your ~/.profile (works with any POSIX-compliant shell):

```bash
croc() { [ $# -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 "$@"; }
```

You 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.

### Build from Source

If you prefer, you can [install Go](https://go.dev/dl/) and build from source (requires Go 1.22+):

```bash
go install github.com/schollz/croc/v10@latest
```

### On Android

There is a 3rd-party F-Droid app [available to download](https://f-droid.org/packages/com.github.howeyc.crocgui/).

## Usage

To send a file, simply do:

```bash
$ croc send [file(s)-or-folder]
Sending 'file-or-folder' (X MB)
Code is: code-phrase
```

Then, to receive the file (or folder) on another computer, run:

```bash
croc code-phrase
```

The 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.

### Customizations & Options

#### Using `croc` on Linux or macOS

On 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 `***`:

```bash
CROC_SECRET=*** croc
```

For single-user systems, the default behavior can be permanently enabled by running:

```bash
croc --classic
```

#### Custom Code Phrase

You can send with your own code phrase (must be more than 6 characters):

```bash
croc send --code [code-phrase] [file(s)-or-folder]
```

#### Allow Overwriting Without Prompt

To automatically overwrite files without prompting, use the `--overwrite` flag:

```bash
croc --yes --overwrite <code>
```

#### Excluding Folders

To exclude folders from being sent, use the `--exclude` flag with comma-delimited exclusions:

```bash
croc send --exclude "node_modules,.venv" [folder]
```

#### Use Pipes - stdin and stdout

You can pipe to `croc`:

```bash
cat [filename] | croc send
```

To receive the file to `stdout`, you can use:

```bash
croc --yes [code-phrase] > out
```

#### Send Text

To send URLs or short text, use:

```bash
croc send --text "hello world"
```

#### Send Multiple Files

You can send multiple files directly by listing the files and/or folders:

```bash
croc send [file1] [file2] [file3] [folder1] [folder2]
```

#### Show QR Code

To show QR code (for mobile devices), use:

```bash
croc send --qr [file(s)-or-folder]
```

#### Use a Proxy

You can send files via a proxy by adding `--socks5`:

```bash
croc --socks5 "127.0.0.1:9050" send SOMEFILE
```

#### Change Encryption Curve

To choose a different elliptic curve for encryption, use the `--curve` flag:

```bash
croc --curve p521 <codephrase>
```

#### Change Hash Algorithm

For faster hashing, use the `imohash` algorithm:

```bash
croc send --hash imohash SOMEFILE
```

#### Clipboard Options

By default, the code phrase is copied to your clipboard. To disable this:

```bash
croc --disable-clipboard send [filename]
```

To copy the full command with the secret as an environment variable (useful on Linux/macOS):

```bash
croc --extended-clipboard send [filename]
```

This copies the full command like `CROC_SECRET="code-phrase" croc` (including any relay/pass flags).

#### Quiet Mode

To suppress all output (useful for scripts and automation):

```bash
croc --quiet send [filename]
```

#### Self-host Relay

You can run your own relay:

```bash
croc relay
```

By 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.

To send files using your relay:

```bash
croc --relay "myrelay.example.com:9009" send [filename]
```

#### Self-host Relay with Docker

You can also run a relay with Docker:

```bash
docker run -d -p 9009-9013:9009-9013 -e CROC_PASS='YOURPASSWORD' docker.io/schollz/croc
```

To send files using your custom relay:

```bash
croc --pass YOURPASSWORD --relay "myreal.example.com:9009" send [filename]
```

## Acknowledgements

`croc` has evolved through many iterations, and I am thankful for the contributions! Special thanks to:

- [@warner](https://github.com/warner) for the [idea](https://github.com/magic-wormhole/magic-wormhole)
- [@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28)
- [@skorokithakis](https://github.com/skorokithakis) for [proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/)

And many more!


================================================
FILE: croc-entrypoint.sh
================================================
#!/bin/sh
set -e

if [ -n "$CROC_PASS" ]; then
    set -- --pass "$CROC_PASS" "$@"
fi

exec /croc "$@"


================================================
FILE: croc.service
================================================
[Unit]
Description=croc relay
After=network.target

[Service]
Type=simple
DynamicUser=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
ExecStart=/usr/bin/croc relay

[Install]
WantedBy=multi-user.target


================================================
FILE: go.mod
================================================
module github.com/schollz/croc/v10

go 1.25.0

require (
	github.com/cespare/xxhash/v2 v2.3.0
	github.com/chzyer/readline v1.5.1
	github.com/denisbrodbeck/machineid v1.0.1
	github.com/kalafut/imohash v1.1.1
	github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b
	github.com/minio/highwayhash v1.0.3
	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
	github.com/schollz/cli/v2 v2.2.1
	github.com/schollz/logger v1.2.0
	github.com/schollz/pake/v3 v3.1.1
	github.com/schollz/peerdiscovery v1.7.6
	github.com/schollz/progressbar/v3 v3.19.0
	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
	github.com/stretchr/testify v1.11.1
	golang.org/x/crypto v0.48.0
	golang.org/x/net v0.51.0
	golang.org/x/sys v0.41.0
	golang.org/x/term v0.40.0
	golang.org/x/time v0.14.0
)

require (
	filippo.io/edwards25519 v1.2.0 // indirect
	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/rivo/uniseg v0.4.7 // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 // indirect
	github.com/twmb/murmur3 v1.1.8 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kalafut/imohash v1.1.1 h1:G/HYtKgteQSVU96LidSJEbUGoZOMiBcuXYxbeb2W9e4=
github.com/kalafut/imohash v1.1.1/go.mod h1:6cn9lU0Sj8M4eu9UaQm1kR/5y3k/ayB68yntRhGloL4=
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
github.com/schollz/cli/v2 v2.2.1 h1:ou22Mj7ZPjrKz+8k2iDTWaHskEEV5NiAxGrdsCL36VU=
github.com/schollz/cli/v2 v2.2.1/go.mod h1:My6bfphRLZUhZdlFUK8scAxMWHydE7k4s2ed2Dtnn+s=
github.com/schollz/logger v1.2.0 h1:5WXfINRs3lEUTCZ7YXhj0uN+qukjizvITLm3Ca2m0Ho=
github.com/schollz/logger v1.2.0/go.mod h1:P6F4/dGMGcx8wh+kG1zrNEd4vnNpEBY/mwEMd/vn6AM=
github.com/schollz/pake/v3 v3.1.1 h1:lyoU5uNQ3thfjEzrahgxWWBm6+pbI1F2KAZ3gs6LIV8=
github.com/schollz/pake/v3 v3.1.1/go.mod h1:420+m3AakXcS0n7Uwc7eRs2CosQ2YfE/vKcIkilvqZc=
github.com/schollz/peerdiscovery v1.7.6 h1:HJjU1cXcNGfZgenC/vbry9F6CH9B8f+QYcTipZLbtDg=
github.com/schollz/peerdiscovery v1.7.6/go.mod h1:iTa0MWSPy49jJ2HcXL5oSSnFsd6olEUorAFljxbnj2I=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 h1:sDWDZkwYqX0jvLWstKzFwh+pYhQNaVg65BgSkCP/f7U=
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: main.go
================================================
package main

//go:generate go run src/install/updateversion.go
//go:generate git commit -am "bump $VERSION"
//go:generate git tag -af v$VERSION -m "v$VERSION"

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/schollz/croc/v10/src/cli"
	"github.com/schollz/croc/v10/src/utils"
)

func main() {
	// "github.com/pkg/profile"
	// go func() {
	// 	for {
	// 		f, err := os.Create("croc.pprof")
	// 		if err != nil {
	// 			panic(err)
	// 		}
	// 		runtime.GC() // get up-to-date statistics
	// 		if err := pprof.WriteHeapProfile(f); err != nil {
	// 			panic(err)
	// 		}
	// 		f.Close()
	// 		time.Sleep(3 * time.Second)
	// 		fmt.Println("wrote profile")
	// 	}
	// }()

	// Create a channel to receive OS signals
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		if err := cli.Run(); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		// Exit the program gracefully
		utils.RemoveMarkedFiles()
		os.Exit(0)
	}()

	// Wait for a termination signal
	<-sigs
	utils.RemoveMarkedFiles()

	// Exit the program gracefully
	os.Exit(0)
}


================================================
FILE: src/cli/cli.go
================================================
package cli

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/chzyer/readline"
	"github.com/schollz/cli/v2"
	"github.com/schollz/croc/v10/src/comm"
	"github.com/schollz/croc/v10/src/croc"
	"github.com/schollz/croc/v10/src/mnemonicode"
	"github.com/schollz/croc/v10/src/models"
	"github.com/schollz/croc/v10/src/tcp"
	"github.com/schollz/croc/v10/src/utils"
	log "github.com/schollz/logger"
	"github.com/schollz/pake/v3"
)

// Version specifies the version
var Version string

// Run will run the command line program
func Run() (err error) {
	// use all of the processors
	runtime.GOMAXPROCS(runtime.NumCPU())

	app := cli.NewApp()
	app.Name = "croc"
	if Version == "" {
		Version = "v10.4.2"
	}
	app.Version = Version
	app.Compiled = time.Now()
	app.Usage = "easily and securely transfer stuff from one computer to another"
	app.UsageText = `croc [GLOBAL OPTIONS] [COMMAND] [COMMAND OPTIONS] [filename(s) or folder]

   USAGE EXAMPLES:
   Send a file:
      croc send file.txt

      -git to respect your .gitignore
   Send multiple files:
      croc send file1.txt file2.txt file3.txt
    or
      croc send *.jpg

   Send everything in a folder:
      croc send example-folder-name

   Send a file with a custom code:
      croc send --code secret-code file.txt

   Receive a file using code:
      croc secret-code`
	app.Commands = []*cli.Command{
		{
			Name:        "send",
			Usage:       "send file(s), or folder (see options with croc send -h)",
			Description: "send file(s), or folder, over the relay",
			ArgsUsage:   "[filename(s) or folder]",
			Flags: []cli.Flag{
				&cli.BoolFlag{Name: "zip", Usage: "zip folder before sending"},
				&cli.StringFlag{Name: "code", Aliases: []string{"c"}, Usage: "codephrase used to connect to relay"},
				&cli.StringFlag{Name: "hash", Value: "xxhash", Usage: "hash algorithm (xxhash, imohash, md5)"},
				&cli.StringFlag{Name: "text", Aliases: []string{"t"}, Usage: "send some text"},
				&cli.BoolFlag{Name: "no-local", Usage: "disable local relay when sending"},
				&cli.BoolFlag{Name: "no-multi", Usage: "disable multiplexing"},
				&cli.BoolFlag{Name: "git", Usage: "enable .gitignore respect / don't send ignored files"},
				&cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"},
				&cli.IntFlag{Name: "transfers", Value: 4, Usage: "number of ports to use for transfers"},
				&cli.BoolFlag{Name: "qrcode", Aliases: []string{"qr"}, Usage: "show receive code as a qrcode"},
				&cli.StringFlag{Name: "exclude", Value: "", Usage: "exclude files if they contain any of the comma separated strings"},
				&cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}},
				&cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}},
			},
			HelpName: "croc send",
			Action:   send,
		},
		{
			Name:        "relay",
			Usage:       "start your own relay (optional)",
			Description: "start relay",
			HelpName:    "croc relay",
			Action:      relay,
			Flags: []cli.Flag{
				&cli.StringFlag{Name: "host", Usage: "host of the relay"},
				&cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay"},
				&cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"},
				&cli.IntFlag{Name: "transfers", Value: 5, Usage: "number of ports to use for relay"},
			},
		},
		{
			Name:   "generate-fish-completion",
			Usage:  "generate fish completion and output to stdout",
			Hidden: true,
			Action: func(ctx *cli.Context) error {
				completion, err := ctx.App.ToFishCompletion()
				if err != nil {
					return err
				}
				fmt.Print(completion)
				return nil
			},
		},
	}
	app.Flags = []cli.Flag{
		&cli.BoolFlag{Name: "internal-dns", Usage: "use a built-in DNS stub resolver rather than the host operating system"},
		&cli.BoolFlag{Name: "classic", Usage: "toggle between the classic mode (insecure due to local attack vector) and new mode (secure)"},
		&cli.BoolFlag{Name: "remember", Usage: "save these settings to reuse next time"},
		&cli.BoolFlag{Name: "debug", Usage: "toggle debug mode"},
		&cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"},
		&cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"},
		&cli.BoolFlag{Name: "no-compress", Usage: "disable compression"},
		&cli.BoolFlag{Name: "ask", Usage: "make sure sender and recipient are prompted"},
		&cli.BoolFlag{Name: "local", Usage: "force to use only local connections"},
		&cli.BoolFlag{Name: "ignore-stdin", Usage: "ignore piped stdin"},
		&cli.BoolFlag{Name: "overwrite", Usage: "do not prompt to overwrite or resume"},
		&cli.BoolFlag{Name: "testing", Usage: "flag for testing purposes"},
		&cli.BoolFlag{Name: "quiet", Usage: "disable all output"},
		&cli.BoolFlag{Name: "disable-clipboard", Usage: "disable copy to clipboard"},
		&cli.BoolFlag{Name: "extended-clipboard", Usage: "copy full command with secret as env variable to clipboard"},
		&cli.StringFlag{Name: "multicast", Value: "239.255.255.250", Usage: "multicast address to use for local discovery"},
		&cli.StringFlag{Name: "curve", Value: "p256", Usage: "choose an encryption curve (" + strings.Join(pake.AvailableCurves(), ", ") + ")"},
		&cli.StringFlag{Name: "ip", Value: "", Usage: "set sender ip if known e.g. 10.0.0.1:9009, [::1]:9009"},
		&cli.StringFlag{Name: "relay", Value: models.DEFAULT_RELAY, Usage: "address of the relay", EnvVars: []string{"CROC_RELAY"}},
		&cli.StringFlag{Name: "relay6", Value: models.DEFAULT_RELAY6, Usage: "ipv6 address of the relay", EnvVars: []string{"CROC_RELAY6"}},
		&cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"},
		&cli.StringFlag{Name: "pass", Value: models.DEFAULT_PASSPHRASE, Usage: "password for the relay", EnvVars: []string{"CROC_PASS"}},
		&cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}},
		&cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}},
		&cli.StringFlag{Name: "throttleUpload", Value: "", Usage: "throttle the upload speed e.g. 500k"},
	}
	app.EnableBashCompletion = true
	app.HideHelp = false
	app.HideVersion = false
	app.Action = func(c *cli.Context) error {
		allStringsAreFiles := func(strs []string) bool {
			for _, str := range strs {
				if !utils.Exists(str) {
					return false
				}
			}
			return true
		}

		// check if "classic" is set
		classicFile := getClassicConfigFile(true)
		classicInsecureMode := utils.Exists(classicFile)
		if c.Bool("classic") {
			if classicInsecureMode {
				// classic mode not enabled
				fmt.Print(`Classic mode is currently ENABLED.

Disabling this mode will prevent the shared secret from being visible
on the host's process list when passed via the command line. On a
multi-user system, this will help ensure that other local users cannot
access the shared secret and receive the files instead of the intended
recipient.

Do you wish to continue to DISABLE the classic mode? (y/N) `)
				choice := strings.ToLower(utils.GetInput(""))
				if choice == "y" || choice == "yes" {
					os.Remove(classicFile)
					fmt.Print("\nClassic mode DISABLED.\n\n")
					fmt.Print(`To send and receive, export the CROC_SECRET variable with the code phrase:

  Send:    CROC_SECRET=*** croc send file.txt

  Receive: CROC_SECRET=*** croc` + "\n\n")
				} else {
					fmt.Print("\nClassic mode ENABLED.\n")

				}
			} else {
				// enable classic mode
				// touch the file
				fmt.Print(`Classic mode is currently DISABLED.

Please note that enabling this mode will make the shared secret visible
on the host's process list when passed via the command line. On a
multi-user system, this could allow other local users to access the
shared secret and receive the files instead of the intended recipient.

Do you wish to continue to enable the classic mode? (y/N) `)
				choice := strings.ToLower(utils.GetInput(""))
				if choice == "y" || choice == "yes" {
					fmt.Print("\nClassic mode ENABLED.\n\n")
					os.WriteFile(classicFile, []byte("enabled"), 0o644)
					fmt.Print(`To send and receive, use the code phrase:

  Send:    croc send --code *** file.txt

  Receive: croc ***` + "\n\n")
				} else {
					fmt.Print("\nClassic mode DISABLED.\n")
				}
			}
			os.Exit(0)
		}

		// if trying to send but forgot send, let the user know
		if c.Args().Present() && allStringsAreFiles(c.Args().Slice()) {
			fnames := []string{}
			for _, fpath := range c.Args().Slice() {
				_, basename := filepath.Split(fpath)
				fnames = append(fnames, "'"+basename+"'")
			}
			promptMessage := fmt.Sprintf("Did you mean to send %s? (Y/n) ", strings.Join(fnames, ", "))
			choice := strings.ToLower(utils.GetInput(promptMessage))
			if choice == "" || choice == "y" || choice == "yes" {
				return send(c)
			}
		}

		return receive(c)
	}

	return app.Run(os.Args)
}

func setDebugLevel(c *cli.Context) {
	if c.Bool("quiet") {
		log.SetLevel("error")
	} else if c.Bool("debug") {
		log.SetLevel("debug")
		log.Debug("debug mode on")
		// print the public IP address
		ip, err := utils.PublicIP()
		if err == nil {
			log.Debugf("public IP address: %s", ip)
		} else {
			log.Debug(err)
		}

	} else {
		log.SetLevel("info")
	}
}

func getSendConfigFile(requireValidPath bool) string {
	configFile, err := utils.GetConfigDir(requireValidPath)
	if err != nil {
		log.Error(err)
		return ""
	}
	return path.Join(configFile, "send.json")
}

func getClassicConfigFile(requireValidPath bool) string {
	configFile, err := utils.GetConfigDir(requireValidPath)
	if err != nil {
		log.Error(err)
		return ""
	}
	return path.Join(configFile, "classic_enabled")
}

func getReceiveConfigFile(requireValidPath bool) (string, error) {
	configFile, err := utils.GetConfigDir(requireValidPath)
	if err != nil {
		log.Error(err)
		return "", err
	}
	return path.Join(configFile, "receive.json"), nil
}

func determinePass(c *cli.Context) (pass string) {
	pass = c.String("pass")
	b, err := os.ReadFile(pass)
	if err == nil {
		pass = strings.TrimSpace(string(b))
	}
	return
}

func send(c *cli.Context) (err error) {
	setDebugLevel(c)
	comm.Socks5Proxy = c.String("socks5")
	comm.HttpProxy = c.String("connect")

	portParam := c.Int("port")
	if portParam == 0 {
		portParam = 9009
	}
	transfersParam := c.Int("transfers")
	if transfersParam == 0 {
		transfersParam = 4
	}
	excludeStrings := []string{}
	for _, v := range strings.Split(c.String("exclude"), ",") {
		v = strings.ToLower(strings.TrimSpace(v))
		if v != "" {
			excludeStrings = append(excludeStrings, v)
		}
	}

	ports := make([]string, transfersParam+1)
	for i := 0; i <= transfersParam; i++ {
		ports[i] = strconv.Itoa(portParam + i)
	}

	crocOptions := croc.Options{
		SharedSecret:      c.String("code"),
		IsSender:          true,
		Debug:             c.Bool("debug"),
		NoPrompt:          c.Bool("yes"),
		RelayAddress:      c.String("relay"),
		RelayAddress6:     c.String("relay6"),
		Stdout:            c.Bool("stdout"),
		DisableLocal:      c.Bool("no-local"),
		OnlyLocal:         c.Bool("local"),
		IgnoreStdin:       c.Bool("ignore-stdin"),
		RelayPorts:        ports,
		Ask:               c.Bool("ask"),
		NoMultiplexing:    c.Bool("no-multi"),
		RelayPassword:     determinePass(c),
		SendingText:       c.String("text") != "",
		NoCompress:        c.Bool("no-compress"),
		Overwrite:         c.Bool("overwrite"),
		Curve:             c.String("curve"),
		HashAlgorithm:     c.String("hash"),
		ThrottleUpload:    c.String("throttleUpload"),
		ZipFolder:         c.Bool("zip"),
		GitIgnore:         c.Bool("git"),
		ShowQrCode:        c.Bool("qrcode"),
		MulticastAddress:  c.String("multicast"),
		Exclude:           excludeStrings,
		Quiet:             c.Bool("quiet"),
		DisableClipboard:  c.Bool("disable-clipboard"),
		ExtendedClipboard: c.Bool("extended-clipboard"),
	}
	if crocOptions.RelayAddress != models.DEFAULT_RELAY {
		crocOptions.RelayAddress6 = ""
	} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
		crocOptions.RelayAddress = ""
	}
	b, errOpen := os.ReadFile(getSendConfigFile(false))
	if errOpen == nil && !c.Bool("remember") {
		var rememberedOptions croc.Options
		err = json.Unmarshal(b, &rememberedOptions)
		if err != nil {
			log.Error(err)
			return
		}
		// update anything that isn't explicitly set
		if !c.IsSet("no-local") {
			crocOptions.DisableLocal = rememberedOptions.DisableLocal
		}
		if !c.IsSet("ports") && len(rememberedOptions.RelayPorts) > 0 {
			crocOptions.RelayPorts = rememberedOptions.RelayPorts
		}
		if !c.IsSet("code") {
			crocOptions.SharedSecret = rememberedOptions.SharedSecret
		}
		if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
			crocOptions.RelayPassword = rememberedOptions.RelayPassword
		}
		if !c.IsSet("overwrite") {
			crocOptions.Overwrite = rememberedOptions.Overwrite
		}
		if !c.IsSet("curve") && rememberedOptions.Curve != "" {
			crocOptions.Curve = rememberedOptions.Curve
		}
		if !c.IsSet("local") {
			crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
		}
		if !c.IsSet("hash") {
			crocOptions.HashAlgorithm = rememberedOptions.HashAlgorithm
		}
		if !c.IsSet("git") {
			crocOptions.GitIgnore = rememberedOptions.GitIgnore
		}
		if !c.IsSet("relay") && strings.HasPrefix(rememberedOptions.RelayAddress, "non-default:") {
			var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress, "non-default:")
			rememberedAddr = strings.TrimSpace(rememberedAddr)
			crocOptions.RelayAddress = rememberedAddr
		}
		if !c.IsSet("relay6") && strings.HasPrefix(rememberedOptions.RelayAddress6, "non-default:") {
			var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress6, "non-default:")
			rememberedAddr = strings.TrimSpace(rememberedAddr)
			crocOptions.RelayAddress6 = rememberedAddr
		}
	}

	var fnames []string
	stat, _ := os.Stdin.Stat()
	if ((stat.Mode() & os.ModeCharDevice) == 0) && !c.Bool("ignore-stdin") {
		fnames, err = getStdin()
		if err != nil {
			return
		}
		utils.MarkFileForRemoval(fnames[0])
		defer func() {
			e := os.Remove(fnames[0])
			if e != nil {
				log.Error(e)
			}
		}()
	} else if c.String("text") != "" {
		fnames, err = makeTempFileWithString(c.String("text"))
		if err != nil {
			return
		}
		utils.MarkFileForRemoval(fnames[0])
		defer func() {
			e := os.Remove(fnames[0])
			if e != nil {
				log.Error(e)
			}
		}()

	} else {
		fnames = c.Args().Slice()
	}
	if len(fnames) == 0 {
		return errors.New("must specify file: croc send [filename(s) or folder]")
	}

	classicInsecureMode := utils.Exists(getClassicConfigFile(true))
	if !classicInsecureMode {
		// if operating system is UNIX, then use environmental variable to set the code
		if (!(runtime.GOOS == "windows") && c.IsSet("code")) || os.Getenv("CROC_SECRET") != "" {
			crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
			if crocOptions.SharedSecret == "" {
				fmt.Printf(`On UNIX systems, to send with a custom code phrase,
you need to set the environmental variable CROC_SECRET:

  CROC_SECRET=**** croc send file.txt

Or you can have the code phrase automatically generated:

  croc send file.txt

Or you can go back to the classic croc behavior by enabling classic mode:

  croc --classic

`)
				os.Exit(0)
			}
		}
	}

	if len(crocOptions.SharedSecret) == 0 {
		// generate code phrase
		crocOptions.SharedSecret = utils.GetRandomName()
	}
	minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfo(fnames, crocOptions.ZipFolder, crocOptions.GitIgnore, crocOptions.Exclude)
	if err != nil {
		return
	}
	if len(crocOptions.Exclude) > 0 {
		minimalFileInfosInclude := []croc.FileInfo{}
		emptyFoldersToTransferInclude := []croc.FileInfo{}
		for _, f := range minimalFileInfos {
			exclude := false
			for _, exclusion := range crocOptions.Exclude {
				if strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) {
					exclude = true
					break
				}
			}
			if !exclude {
				minimalFileInfosInclude = append(minimalFileInfosInclude, f)
			}
		}
		for _, f := range emptyFoldersToTransfer {
			exclude := false
			for _, exclusion := range crocOptions.Exclude {
				if strings.Contains(path.Join(strings.ToLower(f.FolderRemote), strings.ToLower(f.Name)), exclusion) {
					exclude = true
					break
				}
			}
			if !exclude {
				emptyFoldersToTransferInclude = append(emptyFoldersToTransferInclude, f)
			}
		}
		totalNumberFolders = 0
		folderMap := make(map[string]bool)
		for _, f := range minimalFileInfosInclude {
			folderMap[f.FolderRemote] = true
		}
		for _, f := range emptyFoldersToTransferInclude {
			folderMap[f.FolderRemote] = true
		}
		totalNumberFolders = len(folderMap)
		minimalFileInfos = minimalFileInfosInclude
		emptyFoldersToTransfer = emptyFoldersToTransferInclude
	}

	cr, err := croc.New(crocOptions)
	if err != nil {
		return
	}

	// save the config
	saveConfig(c, crocOptions)
	err = cr.Send(minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders)
	return
}

func getStdin() (fnames []string, err error) {
	f, err := os.CreateTemp(".", "croc-stdin-")
	if err != nil {
		return
	}
	_, err = io.Copy(f, os.Stdin)
	if err != nil {
		return
	}
	err = f.Close()
	if err != nil {
		return
	}
	fnames = []string{f.Name()}
	return
}

func makeTempFileWithString(s string) (fnames []string, err error) {
	f, err := os.CreateTemp(".", "croc-stdin-")
	if err != nil {
		return
	}

	_, err = f.WriteString(s)
	if err != nil {
		return
	}

	err = f.Close()
	if err != nil {
		return
	}
	fnames = []string{f.Name()}
	return
}

func saveConfig(c *cli.Context, crocOptions croc.Options) {
	if c.Bool("remember") {
		configFile := getSendConfigFile(true)
		log.Debug("saving config file")
		var bConfig []byte
		// if the code wasn't set, don't save it
		if c.String("code") == "" {
			crocOptions.SharedSecret = ""
		}
		if c.String("relay") != models.DEFAULT_RELAY {
			crocOptions.RelayAddress = "non-default: " + c.String("relay")
		} else {
			crocOptions.RelayAddress = "default"
		}
		if c.String("relay6") != models.DEFAULT_RELAY6 {
			crocOptions.RelayAddress6 = "non-default: " + c.String("relay6")
		} else {
			crocOptions.RelayAddress6 = "default"
		}
		bConfig, err := json.MarshalIndent(crocOptions, "", "    ")
		if err != nil {
			log.Error(err)
			return
		}
		err = os.WriteFile(configFile, bConfig, 0o644)
		if err != nil {
			log.Error(err)
			return
		}
		log.Debugf("wrote %s", configFile)
	}
}

type TabComplete struct{}

func (t TabComplete) Do(line []rune, pos int) ([][]rune, int) {
	var words = strings.SplitAfter(string(line), "-")
	var lastPartialWord = words[len(words)-1]
	var nbCharacter = len(lastPartialWord)
	if nbCharacter == 0 {
		// No completion
		return [][]rune{[]rune("")}, 0
	}
	if len(words) == 1 && nbCharacter == utils.NbPinNumbers {
		// Check if word is indeed a number
		_, err := strconv.Atoi(lastPartialWord)
		if err == nil {
			return [][]rune{[]rune("-")}, nbCharacter
		}
	}
	var strArray [][]rune
	for _, s := range mnemonicode.WordList {
		if strings.HasPrefix(s, lastPartialWord) {
			var completionCandidate = s[nbCharacter:]
			if len(words) <= mnemonicode.WordsRequired(utils.NbBytesWords) {
				completionCandidate += "-"
			}
			strArray = append(strArray, []rune(completionCandidate))
		}
	}
	return strArray, nbCharacter
}

func receive(c *cli.Context) (err error) {
	comm.Socks5Proxy = c.String("socks5")
	comm.HttpProxy = c.String("connect")
	crocOptions := croc.Options{
		SharedSecret:      c.String("code"),
		IsSender:          false,
		Debug:             c.Bool("debug"),
		NoPrompt:          c.Bool("yes"),
		RelayAddress:      c.String("relay"),
		RelayAddress6:     c.String("relay6"),
		Stdout:            c.Bool("stdout"),
		Ask:               c.Bool("ask"),
		RelayPassword:     determinePass(c),
		OnlyLocal:         c.Bool("local"),
		IP:                c.String("ip"),
		Overwrite:         c.Bool("overwrite"),
		Curve:             c.String("curve"),
		TestFlag:          c.Bool("testing"),
		MulticastAddress:  c.String("multicast"),
		Quiet:             c.Bool("quiet"),
		DisableClipboard:  c.Bool("disable-clipboard"),
		ExtendedClipboard: c.Bool("extended-clipboard"),
	}
	if crocOptions.RelayAddress != models.DEFAULT_RELAY {
		crocOptions.RelayAddress6 = ""
	} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
		crocOptions.RelayAddress = ""
	}

	switch c.Args().Len() {
	case 1:
		crocOptions.SharedSecret = c.Args().First()
	case 3:
		fallthrough
	case 4:
		var phrase []string
		phrase = append(phrase, c.Args().First())
		phrase = append(phrase, c.Args().Tail()...)
		crocOptions.SharedSecret = strings.Join(phrase, "-")
	}

	// load options here
	setDebugLevel(c)

	doRemember := c.Bool("remember")
	configFile, err := getReceiveConfigFile(doRemember)
	if err != nil && doRemember {
		return
	}
	b, errOpen := os.ReadFile(configFile)
	if errOpen == nil && !doRemember {
		var rememberedOptions croc.Options
		err = json.Unmarshal(b, &rememberedOptions)
		if err != nil {
			log.Error(err)
			return
		}
		// update anything that isn't explicitly Globally set
		if !c.IsSet("yes") {
			crocOptions.NoPrompt = rememberedOptions.NoPrompt
		}
		if crocOptions.SharedSecret == "" {
			crocOptions.SharedSecret = rememberedOptions.SharedSecret
		}
		if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
			crocOptions.RelayPassword = rememberedOptions.RelayPassword
		}
		if !c.IsSet("overwrite") {
			crocOptions.Overwrite = rememberedOptions.Overwrite
		}
		if !c.IsSet("curve") && rememberedOptions.Curve != "" {
			crocOptions.Curve = rememberedOptions.Curve
		}
		if !c.IsSet("local") {
			crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
		}
		if !c.IsSet("relay") && strings.HasPrefix(rememberedOptions.RelayAddress, "non-default:") {
			var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress, "non-default:")
			rememberedAddr = strings.TrimSpace(rememberedAddr)
			crocOptions.RelayAddress = rememberedAddr
		}
		if !c.IsSet("relay6") && strings.HasPrefix(rememberedOptions.RelayAddress6, "non-default:") {
			var rememberedAddr = strings.TrimPrefix(rememberedOptions.RelayAddress6, "non-default:")
			rememberedAddr = strings.TrimSpace(rememberedAddr)
			crocOptions.RelayAddress6 = rememberedAddr
		}
	}

	classicInsecureMode := utils.Exists(getClassicConfigFile(true))
	if crocOptions.SharedSecret == "" && os.Getenv("CROC_SECRET") != "" {
		crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
	} else if !(runtime.GOOS == "windows") && crocOptions.SharedSecret != "" && !classicInsecureMode {
		crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
		if crocOptions.SharedSecret == "" {
			fmt.Printf(`On UNIX systems, to receive with croc you either need
to set a code phrase using your environmental variables:

  CROC_SECRET=**** croc

Or you can specify the code phrase when you run croc without
declaring the secret on the command line:

  croc
  Enter receive code: ****

Or you can go back to the classic croc behavior by enabling classic mode:

  croc --classic

`)
			os.Exit(0)
		}
	}
	if crocOptions.SharedSecret == "" {
		l, err := readline.NewEx(&readline.Config{
			Prompt:       "Enter receive code: ",
			AutoComplete: TabComplete{},
		})
		if err != nil {
			return err
		}
		crocOptions.SharedSecret, err = l.Readline()
		if err != nil {
			return err
		}
	}
	if c.String("out") != "" {
		if err = os.Chdir(c.String("out")); err != nil {
			return err
		}
	}

	cr, err := croc.New(crocOptions)
	if err != nil {
		return
	}

	// save the config
	if doRemember {
		log.Debug("saving config file")
		var bConfig []byte
		if c.String("relay") != models.DEFAULT_RELAY {
			crocOptions.RelayAddress = "non-default: " + c.String("relay")
		} else {
			crocOptions.RelayAddress = "default"
		}
		if c.String("relay6") != models.DEFAULT_RELAY6 {
			crocOptions.RelayAddress6 = "non-default: " + c.String("relay6")
		} else {
			crocOptions.RelayAddress6 = "default"
		}
		bConfig, err = json.MarshalIndent(crocOptions, "", "    ")
		if err != nil {
			log.Error(err)
			return
		}
		err = os.WriteFile(configFile, bConfig, 0o644)
		if err != nil {
			log.Error(err)
			return
		}
		log.Debugf("wrote %s", configFile)
	}

	err = cr.Receive()
	return
}

func relay(c *cli.Context) (err error) {
	log.Infof("starting croc relay version %v", Version)
	debugString := "info"
	if c.Bool("debug") {
		debugString = "debug"
	}
	host := c.String("host")
	var ports []string

	if c.IsSet("ports") {
		ports = strings.Split(c.String("ports"), ",")
	} else {
		portString := c.Int("port")
		if portString == 0 {
			portString = 9009
		}
		transfersString := c.Int("transfers")
		if transfersString == 0 {
			transfersString = 4
		}
		ports = make([]string, transfersString)
		for i := range ports {
			ports[i] = strconv.Itoa(portString + i)
		}
	}
	if len(ports) < 2 {
		return fmt.Errorf("relay requires at least two ports; specify --ports with two or more ports or set --transfers to 2+")
	}

	tcpPorts := strings.Join(ports[1:], ",")
	for i, port := range ports {
		if i == 0 {
			continue
		}
		go func(portStr string) {
			err := tcp.Run(debugString, host, portStr, determinePass(c))
			if err != nil {
				panic(err)
			}
		}(port)
	}
	return tcp.Run(debugString, host, ports[0], determinePass(c), tcpPorts)
}


================================================
FILE: src/comm/comm.go
================================================
package comm

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"net/url"
	"strings"
	"time"

	"github.com/magisterquis/connectproxy"
	"github.com/schollz/croc/v10/src/utils"
	log "github.com/schollz/logger"
	"golang.org/x/net/proxy"
)

var Socks5Proxy = ""
var HttpProxy = ""

var MAGIC_BYTES = []byte("croc")

const maxReadMessageSize = 64 * 1024 * 1024

// Comm is some basic TCP communication
type Comm struct {
	connection net.Conn
}

// NewConnection gets a new comm to a tcp address
func NewConnection(address string, timelimit ...time.Duration) (c *Comm, err error) {
	tlimit := 30 * time.Second
	if len(timelimit) > 0 {
		tlimit = timelimit[0]
	}
	var connection net.Conn
	if Socks5Proxy != "" && !utils.IsLocalIP(address) {
		var dialer proxy.Dialer
		// prepend schema if no schema is given
		if !strings.Contains(Socks5Proxy, `://`) {
			Socks5Proxy = `socks5://` + Socks5Proxy
		}
		socks5ProxyURL, urlParseError := url.Parse(Socks5Proxy)
		if urlParseError != nil {
			err = fmt.Errorf("unable to parse socks proxy url: %s", urlParseError)
			log.Debug(err)
			return
		}
		dialer, err = proxy.FromURL(socks5ProxyURL, proxy.Direct)
		if err != nil {
			err = fmt.Errorf("proxy failed: %w", err)
			log.Debug(err)
			return
		}
		log.Debug("dialing with dialer.Dial")
		connection, err = dialer.Dial("tcp", address)
	} else if HttpProxy != "" && !utils.IsLocalIP(address) {
		var dialer proxy.Dialer
		// prepend schema if no schema is given
		if !strings.Contains(HttpProxy, `://`) {
			HttpProxy = `http://` + HttpProxy
		}
		HttpProxyURL, urlParseError := url.Parse(HttpProxy)
		if urlParseError != nil {
			err = fmt.Errorf("unable to parse http proxy url: %s", urlParseError)
			log.Debug(err)
			return
		}
		dialer, err = connectproxy.New(HttpProxyURL, proxy.Direct)
		if err != nil {
			err = fmt.Errorf("proxy failed: %w", err)
			log.Debug(err)
			return
		}
		log.Debug("dialing with dialer.Dial")
		connection, err = dialer.Dial("tcp", address)

	} else {
		log.Debugf("dialing to %s with timelimit %s", address, tlimit)
		connection, err = net.DialTimeout("tcp", address, tlimit)
	}
	if err != nil {
		err = fmt.Errorf("comm.NewConnection failed: %w", err)
		log.Debug(err)
		return
	}
	c = New(connection)
	log.Debugf("connected to '%s'", address)
	return
}

// New returns a new comm
func New(c net.Conn) *Comm {
	if err := c.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
		log.Warnf("error setting read deadline: %v", err)
	}
	if err := c.SetDeadline(time.Now().Add(3 * time.Hour)); err != nil {
		log.Warnf("error setting overall deadline: %v", err)
	}
	if err := c.SetWriteDeadline(time.Now().Add(3 * time.Hour)); err != nil {
		log.Errorf("error setting write deadline: %v", err)
	}
	comm := new(Comm)
	comm.connection = c
	return comm
}

// Connection returns the net.Conn connection
func (c *Comm) Connection() net.Conn {
	return c.connection
}

// Close closes the connection
func (c *Comm) Close() {
	if err := c.connection.Close(); err != nil {
		log.Warnf("error closing connection: %v", err)
	}
}

func (c *Comm) Write(b []byte) (n int, err error) {
	header := new(bytes.Buffer)
	err = binary.Write(header, binary.LittleEndian, uint32(len(b)))
	if err != nil {
		fmt.Println("binary.Write failed:", err)
	}
	tmpCopy := append(header.Bytes(), b...)
	tmpCopy = append(MAGIC_BYTES, tmpCopy...)
	n, err = c.connection.Write(tmpCopy)
	if err != nil {
		err = fmt.Errorf("connection.Write failed: %w", err)
		return
	}
	if n != len(tmpCopy) {
		err = fmt.Errorf("wanted to write %d but wrote %d", len(b), n)
		return
	}
	return
}

func (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {
	// long read deadline in case waiting for file
	if err = c.connection.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
		log.Warnf("error setting read deadline: %v", err)
	}
	// must clear the timeout setting
	if err := c.connection.SetDeadline(time.Time{}); err != nil {
		log.Warnf("failed to clear deadline: %v", err)
	}

	// read until we get 4 bytes for the magic
	header := make([]byte, 4)
	_, err = io.ReadFull(c.connection, header)
	if err != nil {
		log.Debugf("initial read error: %v", err)
		return
	}
	if !bytes.Equal(header, MAGIC_BYTES) {
		err = fmt.Errorf("initial bytes are not magic: %x", header)
		return
	}

	// read until we get 4 bytes for the header
	header = make([]byte, 4)
	_, err = io.ReadFull(c.connection, header)
	if err != nil {
		log.Debugf("initial read error: %v", err)
		return
	}

	var numBytesUint32 uint32
	rbuf := bytes.NewReader(header)
	err = binary.Read(rbuf, binary.LittleEndian, &numBytesUint32)
	if err != nil {
		err = fmt.Errorf("binary.Read failed: %w", err)
		log.Debug(err.Error())
		return
	}
	if numBytesUint32 > uint32(maxReadMessageSize) {
		err = fmt.Errorf("message too large: %d > %d", numBytesUint32, maxReadMessageSize)
		log.Debug(err.Error())
		return
	}
	numBytes = int(numBytesUint32)

	// shorten the reading deadline in case getting weird data
	if err = c.connection.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
		log.Warnf("error setting read deadline: %v", err)
	}
	buf = make([]byte, numBytes)
	_, err = io.ReadFull(c.connection, buf)
	if err != nil {
		log.Debugf("consecutive read error: %v", err)
		return
	}
	return
}

// Send a message
func (c *Comm) Send(message []byte) (err error) {
	_, err = c.Write(message)
	return
}

// Receive a message
func (c *Comm) Receive() (b []byte, err error) {
	b, _, _, err = c.Read()
	return
}


================================================
FILE: src/comm/comm_test.go
================================================
package comm

import (
	"bytes"
	"crypto/rand"
	"encoding/binary"
	"net"
	"testing"
	"time"

	log "github.com/schollz/logger"
	"github.com/stretchr/testify/assert"
)

func TestComm(t *testing.T) {
	token := make([]byte, 3000)
	if _, err := rand.Read(token); err != nil {
		t.Error(err)
	}

	// Use dynamic port allocation to avoid conflicts
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	port := listener.Addr().(*net.TCPAddr).Port
	portStr := listener.Addr().String()
	listener.Close() // Close the listener so we can reopen it in the goroutine

	go func() {
		log.Debug("starting TCP server on " + portStr)
		server, err := net.Listen("tcp", portStr)
		if err != nil {
			log.Error(err)
			return
		}
		defer func() {
			if err := server.Close(); err != nil {
				log.Error(err)
			}
		}()
		// spawn a new goroutine whenever a client connects
		for {
			connection, err := server.Accept()
			if err != nil {
				log.Error(err)
			}
			log.Debugf("client %s connected", connection.RemoteAddr().String())
			go func(_ int, connection net.Conn) {
				c := New(connection)
				err = c.Send([]byte("hello, world"))
				assert.Nil(t, err)
				data, err := c.Receive()
				assert.Nil(t, err)
				assert.Equal(t, []byte("hello, computer"), data)
				data, err = c.Receive()
				assert.Nil(t, err)
				assert.Equal(t, []byte{'\x00'}, data)
				data, err = c.Receive()
				assert.Nil(t, err)
				assert.Equal(t, token, data)
			}(port, connection)
		}
	}()

	time.Sleep(300 * time.Millisecond)
	a, err := NewConnection(portStr, 10*time.Minute)
	assert.Nil(t, err)
	data, err := a.Receive()
	assert.Equal(t, []byte("hello, world"), data)
	assert.Nil(t, err)
	assert.Nil(t, a.Send([]byte("hello, computer")))
	assert.Nil(t, a.Send([]byte{'\x00'}))

	assert.Nil(t, a.Send(token))
	_ = a.Connection()
	a.Close()
	assert.NotNil(t, a.Send(token))
	_, err = a.Write(token)
	assert.NotNil(t, err)
}

func TestReceiveRejectsOversizedMessage(t *testing.T) {
	clientConn, serverConn := net.Pipe()
	defer clientConn.Close()
	defer serverConn.Close()

	c := New(clientConn)

	writeErr := make(chan error, 1)
	go func() {
		header := new(bytes.Buffer)
		header.Write(MAGIC_BYTES)
		if err := binary.Write(header, binary.LittleEndian, uint32(maxReadMessageSize+1)); err != nil {
			writeErr <- err
			return
		}
		_, err := serverConn.Write(header.Bytes())
		writeErr <- err
	}()

	_, err := c.Receive()
	assert.NotNil(t, err)
	assert.Contains(t, err.Error(), "message too large")
	assert.Nil(t, <-writeErr)
}


================================================
FILE: src/compress/compress.go
================================================
package compress

import (
	"bytes"
	"compress/flate"
	"io"

	log "github.com/schollz/logger"
)

// CompressWithOption returns compressed data using the specified level
func CompressWithOption(src []byte, level int) []byte {
	compressedData := new(bytes.Buffer)
	compress(src, compressedData, level)
	return compressedData.Bytes()
}

// Compress returns a compressed byte slice.
func Compress(src []byte) []byte {
	compressedData := new(bytes.Buffer)
	compress(src, compressedData, flate.HuffmanOnly)
	return compressedData.Bytes()
}

// Decompress returns a decompressed byte slice.
func Decompress(src []byte) []byte {
	compressedData := bytes.NewBuffer(src)
	deCompressedData := new(bytes.Buffer)
	decompress(compressedData, deCompressedData)
	return deCompressedData.Bytes()
}

// compress uses flate to compress a byte slice to a corresponding level
func compress(src []byte, dest io.Writer, level int) {
	compressor, err := flate.NewWriter(dest, level)
	if err != nil {
		log.Debugf("error level data: %v", err)
		return
	}
	if _, err := compressor.Write(src); err != nil {
		log.Debugf("error writing data: %v", err)
	}
	compressor.Close()
}

// decompress uses flate to decompress an io.Reader
func decompress(src io.Reader, dest io.Writer) {
	decompressor := flate.NewReader(src)
	if _, err := io.Copy(dest, decompressor); err != nil {
		log.Debugf("error copying data: %v", err)
	}
	decompressor.Close()
}


================================================
FILE: src/compress/compress_test.go
================================================
package compress

import (
	"crypto/rand"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

var fable = []byte(`The Frog and the Crocodile
Once, 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.

The 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.

So for many days, the frog stayed put, hopping along the bank, trying to think of a way to get across.

The 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.

He 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.

The 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."

The 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."

The 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.

"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!"

"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."

The 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.

"Okay, it sounds like a good plan to me. Turn around so I can hop on your tail."

The 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.

From 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. `)

func BenchmarkCompressLevelMinusTwo(b *testing.B) {
	for i := 0; i < b.N; i++ {
		CompressWithOption(fable, -2)
	}
}

func BenchmarkCompressLevelNine(b *testing.B) {
	for i := 0; i < b.N; i++ {
		CompressWithOption(fable, 9)
	}
}

func BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {
	data := make([]byte, 1000000)
	if _, err := rand.Read(data); err != nil {
		b.Fatal(err)
	}
	for i := 0; i < b.N; i++ {
		CompressWithOption(data, -2)
	}
}

func BenchmarkCompressLevelNineBinary(b *testing.B) {
	data := make([]byte, 1000000)
	if _, err := rand.Read(data); err != nil {
		b.Fatal(err)
	}
	for i := 0; i < b.N; i++ {
		CompressWithOption(data, 9)
	}
}

func TestCompress(t *testing.T) {
	compressedB := CompressWithOption(fable, 9)
	dataRateSavings := 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
	fmt.Printf("Level 9: %2.0f%% percent space savings\n", dataRateSavings)
	assert.True(t, len(compressedB) < len(fable))
	assert.Equal(t, fable, Decompress(compressedB))

	compressedB = CompressWithOption(fable, -2)
	dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
	fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
	assert.True(t, len(compressedB) < len(fable))

	compressedB = Compress(fable)
	dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
	fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
	assert.True(t, len(compressedB) < len(fable))

	data := make([]byte, 4096)
	if _, err := rand.Read(data); err != nil {
		t.Fatal(err)
	}
	compressedB = CompressWithOption(data, -2)
	dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))
	fmt.Printf("random, Level -2: %2.0f%% percent space savings\n", dataRateSavings)

	if _, err := rand.Read(data); err != nil {
		t.Fatal(err)
	}
	compressedB = CompressWithOption(data, 9)
	dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))

	fmt.Printf("random, Level 9: %2.0f%% percent space savings\n", dataRateSavings)

}


================================================
FILE: src/croc/croc.go
================================================
package croc

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/denisbrodbeck/machineid"
	ignore "github.com/sabhiram/go-gitignore"
	log "github.com/schollz/logger"
	"github.com/schollz/pake/v3"
	"github.com/schollz/peerdiscovery"
	"github.com/schollz/progressbar/v3"
	"github.com/skip2/go-qrcode"
	"golang.org/x/term"
	"golang.org/x/time/rate"

	"github.com/schollz/croc/v10/src/comm"
	"github.com/schollz/croc/v10/src/compress"
	"github.com/schollz/croc/v10/src/crypt"
	"github.com/schollz/croc/v10/src/message"
	"github.com/schollz/croc/v10/src/models"
	"github.com/schollz/croc/v10/src/tcp"
	"github.com/schollz/croc/v10/src/utils"
)

var (
	ipRequest        = []byte("ips?")
	handshakeRequest = []byte("handshake")
)

func init() {
	log.SetLevel("debug")
}

// Debug toggles debug mode
func Debug(debug bool) {
	if debug {
		log.SetLevel("debug")
	} else {
		log.SetLevel("warn")
	}
}

// Options specifies user specific options
type Options struct {
	IsSender          bool
	SharedSecret      string
	RoomName          string
	Debug             bool
	RelayAddress      string
	RelayAddress6     string
	RelayPorts        []string
	RelayPassword     string
	Stdout            bool
	NoPrompt          bool
	NoMultiplexing    bool
	DisableLocal      bool
	OnlyLocal         bool
	IgnoreStdin       bool
	Ask               bool
	SendingText       bool
	NoCompress        bool
	IP                string
	Overwrite         bool
	Curve             string
	HashAlgorithm     string
	ThrottleUpload    string
	ZipFolder         bool
	TestFlag          bool
	GitIgnore         bool
	MulticastAddress  string
	ShowQrCode        bool
	Exclude           []string
	Quiet             bool
	DisableClipboard  bool
	ExtendedClipboard bool
}

type SimpleMessage struct {
	Bytes []byte
	Kind  string
}

// Client holds the state of the croc transfer
type Client struct {
	Options                         Options
	Pake                            *pake.Pake
	Key                             []byte
	ExternalIP, ExternalIPConnected string

	// steps involved in forming relationship
	Step1ChannelSecured       bool
	Step2FileInfoTransferred  bool
	Step3RecipientRequestFile bool
	Step4FileTransferred      bool
	Step5CloseChannels        bool
	SuccessfulTransfer        bool

	// send / receive information of all files
	FilesToTransfer           []FileInfo
	EmptyFoldersToTransfer    []FileInfo
	TotalNumberOfContents     int
	TotalNumberFolders        int
	FilesToTransferCurrentNum int
	FilesHasFinished          map[int]struct{}
	TotalFilesIgnored         int

	// send / receive information of current file
	CurrentFile            *os.File
	CurrentFileChunkRanges []int64
	CurrentFileChunks      []int64
	CurrentFileIsClosed    bool
	LastFolder             string

	TotalSent              int64
	TotalChunksTransferred int
	chunkMap               map[uint64]struct{}
	limiter                *rate.Limiter

	// tcp connections
	conn []*comm.Comm

	bar             *progressbar.ProgressBar
	longestFilename int
	firstSend       bool

	mutex                    *sync.Mutex
	fread                    *os.File
	numfinished              int
	quit                     chan bool
	finishedNum              int
	numberOfTransferredFiles int

	// ctx.go for graceful shutdown
	*stop
}

// Chunk contains information about the
// needed bytes
type Chunk struct {
	Bytes    []byte `json:"b,omitempty"`
	Location int64  `json:"l,omitempty"`
}

// FileInfo registers the information about the file
type FileInfo struct {
	Name         string      `json:"n,omitempty"`
	FolderRemote string      `json:"fr,omitempty"`
	FolderSource string      `json:"fs,omitempty"`
	Hash         []byte      `json:"h,omitempty"`
	Size         int64       `json:"s,omitempty"`
	ModTime      time.Time   `json:"m,omitempty"`
	IsCompressed bool        `json:"c,omitempty"`
	IsEncrypted  bool        `json:"e,omitempty"`
	Symlink      string      `json:"sy,omitempty"`
	Mode         os.FileMode `json:"md,omitempty"`
	TempFile     bool        `json:"tf,omitempty"`
	IsIgnored    bool        `json:"ig,omitempty"`
}

// RemoteFileRequest requests specific bytes
type RemoteFileRequest struct {
	CurrentFileChunkRanges    []int64
	FilesToTransferCurrentNum int
	MachineID                 string
}

// SenderInfo lists the files to be transferred
type SenderInfo struct {
	FilesToTransfer        []FileInfo
	EmptyFoldersToTransfer []FileInfo
	TotalNumberFolders     int
	MachineID              string
	Ask                    bool
	SendingText            bool
	NoCompress             bool
	HashAlgorithm          string
}

// New establishes a new connection for transferring files between two instances.
func New(ops Options) (c *Client, err error) {
	c = new(Client)
	c.FilesHasFinished = make(map[int]struct{})

	// setup basic info
	c.Options = ops
	Debug(c.Options.Debug)

	// redirect stderr to null if quiet mode is enabled
	if c.Options.Quiet {
		devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
		if err == nil {
			os.Stderr = devNull
		}
	}

	if len(c.Options.SharedSecret) < 6 {
		err = fmt.Errorf("code is too short")
		return
	}
	// Create a hash of part of the shared secret to use as the room name
	hashExtra := "croc"
	roomNameBytes := sha256.Sum256([]byte(c.Options.SharedSecret[:4] + hashExtra))
	c.Options.RoomName = hex.EncodeToString(roomNameBytes[:])

	c.conn = make([]*comm.Comm, 16)

	// initialize throttler
	if len(c.Options.ThrottleUpload) > 1 && c.Options.IsSender {
		upload := c.Options.ThrottleUpload[:len(c.Options.ThrottleUpload)-1]
		var uploadLimit int64
		uploadLimit, err = strconv.ParseInt(upload, 10, 64)
		if err != nil {
			panic("Could not parse given Upload Limit")
		}
		minBurstSize := models.TCP_BUFFER_SIZE
		var rt rate.Limit
		switch unit := string(c.Options.ThrottleUpload[len(c.Options.ThrottleUpload)-1:]); unit {
		case "g", "G":
			uploadLimit = uploadLimit * 1024 * 1024 * 1024
		case "m", "M":
			uploadLimit = uploadLimit * 1024 * 1024
		case "k", "K":
			uploadLimit = uploadLimit * 1024
		default:
			uploadLimit, err = strconv.ParseInt(c.Options.ThrottleUpload, 10, 64)
			if err != nil {
				panic("Could not parse given Upload Limit")
			}
		}

		rt = rate.Every(time.Second / time.Duration(uploadLimit))
		if int(uploadLimit) > minBurstSize {
			minBurstSize = int(uploadLimit)
		}
		c.limiter = rate.NewLimiter(rt, minBurstSize)
		log.Debugf("Throttling Upload to %#v", c.limiter.Limit())
	}

	// initialize pake for recipient
	if !c.Options.IsSender {
		c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)
	}
	if err != nil {
		return
	}

	c.mutex = &sync.Mutex{}
	c.stop = newStop(context.Background())
	return
}

// TransferOptions for sending
type TransferOptions struct {
	PathToFiles      []string
	KeepPathInRemote bool
}

// helper function checking for an empty folder
func isEmptyFolder(folderPath string) (bool, error) {
	f, err := os.Open(folderPath)
	if err != nil {
		return false, err
	}
	defer f.Close()

	_, err = f.Readdirnames(1)
	if err == io.EOF {
		return true, nil
	}
	return false, nil
}

// helper function to walk each subfolder and parses against an ignore file.
// returns a hashmap Key: Absolute filepath, Value: boolean (true=ignore)
func gitWalk(dir string, gitObj *ignore.GitIgnore, files map[string]bool) {
	var ignoredDir bool
	var current string
	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if isChild(current, path) && ignoredDir {
			files[path] = true
			return nil
		}
		if info.IsDir() && filepath.Base(path) == filepath.Base(dir) {
			ignoredDir = false // Skip applying ignore rules for root directory
			return nil
		}
		if gitObj.MatchesPath(info.Name()) {
			files[path] = true
			ignoredDir = true
			current = path
			return nil
		} else {
			files[path] = false
			ignoredDir = false
			return nil
		}
	})
	if err != nil {
		log.Errorf("filepath error")
	}
}

func isChild(parentPath, childPath string) bool {
	relPath, err := filepath.Rel(parentPath, childPath)
	if err != nil {
		return false
	}
	return !strings.HasPrefix(relPath, "..")
}

// This function retrieves the important file information
// for every file that will be transferred
func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []string) (filesInfo []FileInfo, emptyFolders []FileInfo, totalNumberFolders int, err error) {
	// fnames: the relative/absolute paths of files/folders that will be transferred
	totalNumberFolders = 0
	var paths []string
	for _, fname := range fnames {
		// Support wildcard
		if strings.Contains(fname, "*") {
			matches, errGlob := filepath.Glob(fname)
			if errGlob != nil {
				err = errGlob
				return
			}
			paths = append(paths, matches...)
			continue
		} else {
			paths = append(paths, fname)
		}
	}
	ignoredPaths := make(map[string]bool)
	if ignoreGit {
		wd, wdErr := os.Stat(".gitignore")
		if wdErr == nil {
			gitIgnore, gitErr := ignore.CompileIgnoreFile(wd.Name())
			if gitErr == nil {
				for _, path := range paths {
					abs, absErr := filepath.Abs(path)
					if absErr != nil {
						err = absErr
						return
					}
					if gitIgnore.MatchesPath(path) {
						ignoredPaths[abs] = true
					}
				}
			}
		}
		for _, path := range paths {
			abs, absErr := filepath.Abs(path)
			if absErr != nil {
				err = absErr
				return
			}
			file, fileErr := os.Stat(path)
			if fileErr == nil && file.IsDir() {
				_, subErr := os.Stat(filepath.Join(path, ".gitignore"))
				if subErr == nil {
					gitObj, gitObjErr := ignore.CompileIgnoreFile(filepath.Join(path, ".gitignore"))
					if gitObjErr != nil {
						err = gitObjErr
						return
					}
					gitWalk(abs, gitObj, ignoredPaths)
				}
			}
		}
	}
	for _, fpath := range paths {
		stat, errStat := os.Lstat(fpath)

		if errStat != nil {
			err = errStat
			return
		}

		absPath, errAbs := filepath.Abs(fpath)

		if errAbs != nil {
			err = errAbs
			return
		}
		if stat.IsDir() && zipfolder {
			if fpath[len(fpath)-1:] != "/" {
				fpath += "/"
			}
			fpath = filepath.Dir(fpath)
			dest := filepath.Base(fpath) + ".zip"
			utils.ZipDirectory(dest, fpath)
			utils.MarkFileForRemoval(dest)
			stat, errStat = os.Lstat(dest)
			if errStat != nil {
				err = errStat
				return
			}
			absPath, errAbs = filepath.Abs(dest)
			if errAbs != nil {
				err = errAbs
				return
			}

			fInfo := FileInfo{
				Name:         stat.Name(),
				FolderRemote: "./",
				FolderSource: filepath.Dir(absPath),
				Size:         stat.Size(),
				ModTime:      stat.ModTime(),
				Mode:         stat.Mode(),
				TempFile:     true,
				IsIgnored:    ignoredPaths[absPath],
			}
			if fInfo.IsIgnored {
				continue
			}
			filesInfo = append(filesInfo, fInfo)
			continue
		}

		if stat.IsDir() {
			err = filepath.Walk(absPath,
				func(pathName string, info os.FileInfo, err error) error {
					if err != nil {
						return err
					}
					absPathWithSeparator := filepath.Dir(absPath)
					if !strings.HasSuffix(absPathWithSeparator, string(os.PathSeparator)) {
						absPathWithSeparator += string(os.PathSeparator)
					}
					if strings.HasSuffix(absPathWithSeparator, string(os.PathSeparator)+string(os.PathSeparator)) {
						absPathWithSeparator = strings.TrimSuffix(absPathWithSeparator, string(os.PathSeparator))
					}
					remoteFolder := strings.TrimPrefix(filepath.Dir(pathName), absPathWithSeparator)
					if !info.IsDir() {
						fInfo := FileInfo{
							Name:         info.Name(),
							FolderRemote: strings.ReplaceAll(remoteFolder, string(os.PathSeparator), "/") + "/",
							FolderSource: filepath.Dir(pathName),
							Size:         info.Size(),
							ModTime:      info.ModTime(),
							Mode:         info.Mode(),
							TempFile:     false,
							IsIgnored:    ignoredPaths[pathName],
						}
						if fInfo.IsIgnored && ignoreGit {
							return nil
						} else {
							filesInfo = append(filesInfo, fInfo)
						}
					} else {
						if ignoredPaths[pathName] {
							return filepath.SkipDir
						}
						isEmptyFolder, _ := isEmptyFolder(pathName)
						totalNumberFolders++
						if isEmptyFolder {
							emptyFolders = append(emptyFolders, FileInfo{
								// Name: info.Name(),
								FolderRemote: strings.ReplaceAll(strings.TrimPrefix(pathName,
									filepath.Dir(absPath)+string(os.PathSeparator)), string(os.PathSeparator), "/") + "/",
							})
						}
					}
					return nil
				})
			if err != nil {
				return
			}

		} else {
			fInfo := FileInfo{
				Name:         stat.Name(),
				FolderRemote: "./",
				FolderSource: filepath.Dir(absPath),
				Size:         stat.Size(),
				ModTime:      stat.ModTime(),
				Mode:         stat.Mode(),
				TempFile:     false,
				IsIgnored:    ignoredPaths[absPath],
			}
			if fInfo.IsIgnored && ignoreGit {
				continue
			} else {
				filesInfo = append(filesInfo, fInfo)
			}
		}
	}
	return
}

func (c *Client) sendCollectFiles(filesInfo []FileInfo) (err error) {
	c.FilesToTransfer = filesInfo
	totalFilesSize := int64(0)

	for i, fileInfo := range c.FilesToTransfer {
		var fullPath string
		fullPath = fileInfo.FolderSource + string(os.PathSeparator) + fileInfo.Name
		fullPath = filepath.Clean(fullPath)

		if len(fileInfo.Name) > c.longestFilename {
			c.longestFilename = len(fileInfo.Name)
		}

		if fileInfo.Mode&os.ModeSymlink != 0 {
			log.Debugf("%s is symlink", fileInfo.Name)
			c.FilesToTransfer[i].Symlink, err = os.Readlink(fullPath)
			if err != nil {
				log.Debugf("error getting symlink: %s", err.Error())
			}
			log.Debugf("%+v", c.FilesToTransfer[i])
		}

		if c.Options.HashAlgorithm == "" {
			c.Options.HashAlgorithm = "xxhash"
		}

		c.FilesToTransfer[i].Hash, err = c.stop.hash(fullPath, c.Options.HashAlgorithm, fileInfo.Size > 1e7)
		log.Debugf("hashed %s to %x using %s", fullPath, c.FilesToTransfer[i].Hash, c.Options.HashAlgorithm)
		totalFilesSize += fileInfo.Size
		if err != nil {
			return
		}
		log.Debugf("file %d info: %+v", i, c.FilesToTransfer[i])
		fmt.Fprintf(os.Stderr, "\r                                 ")
		fmt.Fprintf(os.Stderr, "\rSending %d files (%s)", i, utils.ByteCountDecimal(totalFilesSize))
	}
	log.Debugf("longestFilename: %+v", c.longestFilename)
	fname := fmt.Sprintf("%d files", len(c.FilesToTransfer))
	folderName := fmt.Sprintf("%d folders", c.TotalNumberFolders)
	if len(c.FilesToTransfer) == 1 {
		fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name)
	}
	if strings.HasPrefix(fname, "'croc-stdin-") {
		fname = "'stdin'"
		if c.Options.SendingText {
			fname = "'text'"
		}
	}

	fmt.Fprintf(os.Stderr, "\r                                 ")
	if c.TotalNumberFolders > 0 {
		fmt.Fprintf(os.Stderr, "\rSending %s and %s (%s)\n", fname, folderName, utils.ByteCountDecimal(totalFilesSize))
	} else {
		fmt.Fprintf(os.Stderr, "\rSending %s (%s)\n", fname, utils.ByteCountDecimal(totalFilesSize))
	}
	return
}

func (c *Client) setupLocalRelay() {
	// setup the relay locally
	firstPort, _ := strconv.Atoi(c.Options.RelayPorts[0])
	openPorts := utils.FindOpenPorts("127.0.0.1", firstPort, len(c.Options.RelayPorts))
	if len(openPorts) < len(c.Options.RelayPorts) {
		panic("not enough open ports to run local relay")
	}
	for i, port := range openPorts {
		c.Options.RelayPorts[i] = fmt.Sprint(port)
	}
	for _, port := range c.Options.RelayPorts {
		go func(portStr string) {
			debugString := "warn"
			if c.Options.Debug {
				debugString = "debug"
			}
			err := c.stop.run(
				debugString,
				"127.0.0.1",
				portStr,
				c.Options.RelayPassword,
				strings.Join(c.Options.RelayPorts[1:], ","))
			if err != nil {
				panic(err)
			}
		}(port)
	}
}

func (c *Client) broadcastOnLocalNetwork(useipv6 bool) {
	var timeLimit time.Duration
	// if we don't use an external relay, the broadcast messages need to be sent continuously
	if c.Options.OnlyLocal {
		timeLimit = -1 * time.Second
	} else {
		timeLimit = 30 * time.Second
	}
	// look for peers first
	settings := peerdiscovery.Settings{
		Limit:     -1,
		Payload:   []byte("croc" + c.Options.RelayPorts[0]),
		Delay:     20 * time.Millisecond,
		TimeLimit: timeLimit,
		StopChan:  c.stop.stopChan,
	}
	if useipv6 {
		settings.IPVersion = peerdiscovery.IPv6
	} else {
		settings.MulticastAddress = c.Options.MulticastAddress
	}

	discoveries, err := peerdiscovery.Discover(settings)
	log.Debugf("discoveries: %+v", discoveries)

	if err != nil {
		log.Debug(err)
	}
}

func (c *Client) transferOverLocalRelay(errchan chan<- error) {
	time.Sleep(500 * time.Millisecond)
	log.Debug("establishing connection")
	var banner string
	conn, banner, ipaddr, err := tcp.ConnectToTCPServer("127.0.0.1:"+c.Options.RelayPorts[0], c.Options.RelayPassword, c.Options.RoomName)
	log.Debugf("banner: %s", banner)
	if err != nil {
		err = fmt.Errorf("could not connect to 127.0.0.1:%s: %w", c.Options.RelayPorts[0], err)
		log.Debug(err)
		// not really an error because it will try to connect over the actual relay
		return
	}
	log.Debugf("local connection established: %+v", conn)
	for {
		if err := c.ctxErr(); err != nil {
			errchan <- err
			return
		}
		data, _ := conn.Receive()
		if bytes.Equal(data, handshakeRequest) {
			break
		} else if bytes.Equal(data, []byte{1}) {
			log.Trace("got ping")
		} else {
			log.Debugf("instead of handshake got: %s", data)
		}
	}
	c.conn[0] = conn
	log.Debug("exchanged header message")
	c.Options.RelayAddress = "127.0.0.1"
	c.Options.RelayPorts = strings.Split(banner, ",")
	if c.Options.NoMultiplexing {
		log.Debug("no multiplexing")
		c.Options.RelayPorts = []string{c.Options.RelayPorts[0]}
	}
	c.ExternalIP = ipaddr
	errchan <- c.transfer()
}

// Send will send the specified file
func (c *Client) Send(filesInfo []FileInfo, emptyFoldersToTransfer []FileInfo, totalNumberFolders int) (err error) {
	go c.stop.done()
	defer c.stop.Cancel()
	c.EmptyFoldersToTransfer = emptyFoldersToTransfer
	c.TotalNumberFolders = totalNumberFolders
	c.TotalNumberOfContents = len(filesInfo)
	err = c.sendCollectFiles(filesInfo)
	if err != nil {
		return
	}
	flags := &strings.Builder{}
	if c.Options.RelayAddress != models.DEFAULT_RELAY && !c.Options.OnlyLocal {
		flags.WriteString("--relay " + c.Options.RelayAddress + " ")
	}
	if c.Options.RelayPassword != models.DEFAULT_PASSPHRASE {
		flags.WriteString("--pass " + c.Options.RelayPassword + " ")
	}
	fmt.Fprintf(os.Stderr, `Code is: %[1]s

On the other computer run:
(For Windows)
    croc %[2]s%[1]s
(For Linux/macOS)
    CROC_SECRET=%[1]q croc %[2]s
`, c.Options.SharedSecret, flags.String())
	if !c.Options.DisableClipboard {
		clipboardText := c.Options.SharedSecret
		if c.Options.ExtendedClipboard {
			clipboardText = fmt.Sprintf("CROC_SECRET=%q croc %s", c.Options.SharedSecret, strings.TrimSpace(flags.String()))
		}
		copyToClipboard(clipboardText, c.Options.Quiet, c.Options.ExtendedClipboard)
	}
	if c.Options.ShowQrCode {
		showReceiveCommandQrCode(fmt.Sprintf("%[1]s", c.Options.SharedSecret))
	}
	if c.Options.Ask {
		machid, _ := machineid.ID()
		fmt.Fprintf(os.Stderr, "\rYour machine ID is '%s'\n", machid)
	}
	// c.spinner.Suffix = " waiting for recipient..."
	// c.spinner.Start()
	// create channel for quitting
	// connect to the relay for messaging
	errchan := make(chan error, 1)

	if !c.Options.DisableLocal {
		// add two things to the error channel
		errchan = make(chan error, 2)
		c.setupLocalRelay()
		// broadcast on ipv4
		go c.broadcastOnLocalNetwork(false)
		// broadcast on ipv6
		go c.broadcastOnLocalNetwork(true)
		go c.transferOverLocalRelay(errchan)
	}

	if !c.Options.OnlyLocal {
		go func() {
			var ipaddr, banner string
			var conn *comm.Comm
			durations := []time.Duration{100 * time.Millisecond, 5 * time.Second}
			for i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} {
				if address == "" {
					continue
				}
				host, port, _ := net.SplitHostPort(address)
				log.Debugf("host: '%s', port: '%s'", host, port)
				// Default port to :9009
				if port == "" {
					host = address
					port = models.DEFAULT_PORT
				}
				log.Debugf("got host '%v' and port '%v'", host, port)
				address = net.JoinHostPort(host, port)
				log.Debugf("trying connection to %s", address)
				conn, banner, ipaddr, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RoomName, durations[i])
				if err == nil {
					c.Options.RelayAddress = address
					break
				}
				log.Debugf("could not establish '%s'", address)
			}
			if conn == nil && err == nil {
				err = fmt.Errorf("could not connect")
			}
			if err != nil {
				err = fmt.Errorf("could not connect to %s: %w", c.Options.RelayAddress, err)
				log.Debug(err)
				errchan <- err
				return
			}
			log.Debugf("banner: %s", banner)
			log.Debugf("connection established: %+v", conn)
			var kB []byte
			B, _ := pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, c.Options.Curve)
			for {
				if err := c.ctxErr(); err != nil {
					errchan <- err
					return
				}
				var dataMessage SimpleMessage
				log.Trace("waiting for bytes")
				data, errConn := conn.Receive()
				if errConn != nil {
					log.Tracef("[%+v] had error: %s", conn, errConn.Error())
				}
				json.Unmarshal(data, &dataMessage)
				log.Tracef("data: %+v '%s'", data, data)
				log.Tracef("dataMessage: %s", dataMessage)
				log.Tracef("kB: %x", kB)
				// if kB not null, then use it to decrypt
				if kB != nil {
					var decryptErr error
					var dataDecrypt []byte
					dataDecrypt, decryptErr = crypt.Decrypt(data, kB)
					if decryptErr != nil {
						log.Tracef("error decrypting: %v: '%s'", decryptErr, data)
						// relay sent a message encrypted with an invalid key.
						// consider this a security issue and abort
						if strings.Contains(decryptErr.Error(), "message authentication failed") {
							errchan <- decryptErr
							return
						}
					} else {
						// copy dataDecrypt to data
						data = dataDecrypt
						log.Tracef("decrypted: %s", data)
					}
				}
				if bytes.Equal(data, ipRequest) {
					log.Tracef("got ipRequest")
					// recipient wants to try to connect to local ips
					var ips []string
					// only get local ips if the local is enabled
					if !c.Options.DisableLocal {
						// get list of local ips
						ips, err = utils.GetLocalIPs()
						if err != nil {
							log.Tracef("error getting local ips: %v", err)
						}
						// prepend the port that is being listened to
						ips = append([]string{c.Options.RelayPorts[0]}, ips...)
					}
					log.Tracef("sending ips: %+v", ips)
					bips, errIps := json.Marshal(ips)
					if errIps != nil {
						log.Tracef("error marshalling ips: %v", errIps)
					}
					bips, errIps = crypt.Encrypt(bips, kB)
					if errIps != nil {
						log.Tracef("error encrypting ips: %v", errIps)
					}
					if err = conn.Send(bips); err != nil {
						log.Errorf("error sending: %v", err)
					}
				} else if dataMessage.Kind == "pake1" {
					log.Trace("got pake1")
					var pakeError error
					pakeError = B.Update(dataMessage.Bytes)
					if pakeError == nil {
						kB, pakeError = B.SessionKey()
						if pakeError == nil {
							log.Tracef("dataMessage kB: %x", kB)
							dataMessage.Bytes = B.Bytes()
							dataMessage.Kind = "pake2"
							data, _ = json.Marshal(dataMessage)
							if pakeError = conn.Send(data); err != nil {
								log.Errorf("dataMessage error sending: %v", err)
							}
						}

					}
				} else if bytes.Equal(data, handshakeRequest) {
					log.Trace("got handshake")
					break
				} else if bytes.Equal(data, []byte{1}) {
					log.Trace("got ping")
					continue
				} else {
					log.Tracef("[%+v] got weird bytes: %+v", conn, data)
					// throttle the reading
					errchan <- fmt.Errorf("gracefully refusing using the public relay")
					return
				}
			}

			c.conn[0] = conn
			c.Options.RelayPorts = strings.Split(banner, ",")
			if c.Options.NoMultiplexing {
				log.Debug("no multiplexing")
				c.Options.RelayPorts = []string{c.Options.RelayPorts[0]}
			}
			c.ExternalIP = ipaddr
			log.Debug("exchanged header message")
			errchan <- c.transfer()
		}()
	}

	err = <-errchan
	if err == nil {
		return // no error
	} else {
		log.Debugf("error from errchan: %v", err)
		if strings.Contains(err.Error(), "could not secure channel") {
			return err
		}
	}
	if !c.Options.DisableLocal {
		if strings.Contains(err.Error(), "refusing files") || strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "bad password") || strings.Contains(err.Error(), "message authentication failed") {
			errchan <- err
		}
		err = <-errchan
	}
	return err
}

func showReceiveCommandQrCode(command string) {
	qrCode, err := qrcode.New(command, qrcode.Medium)
	if err == nil {
		fmt.Println(qrCode.ToSmallString(false))
	}
}

// Receive will receive a file
func (c *Client) Receive() (err error) {
	go c.stop.done()
	defer c.stop.Cancel()
	fmt.Fprintf(os.Stderr, "connecting...")
	// recipient will look for peers first
	// and continue if it doesn't find any within 100 ms
	usingLocal := false
	isIPset := false

	if c.Options.OnlyLocal || c.Options.IP != "" {
		c.Options.RelayAddress = ""
		c.Options.RelayAddress6 = ""
	}

	if c.Options.IP != "" {
		// check ip version
		if strings.Count(c.Options.IP, ":") >= 2 {
			log.Debug("assume ipv6")
			c.Options.RelayAddress6 = c.Options.IP
		}
		if strings.Contains(c.Options.IP, ".") {
			log.Debug("assume ipv4")
			c.Options.RelayAddress = c.Options.IP
		}
		isIPset = true
	}

	if !c.Options.DisableLocal && !isIPset {
		log.Debug("attempt to discover peers")
		var discoveries []peerdiscovery.Discovered
		var wgDiscovery sync.WaitGroup
		var dmux sync.Mutex
		wgDiscovery.Add(2)
		go func() {
			defer wgDiscovery.Done()
			ipv4discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{
				Limit:            1,
				Payload:          []byte("ok"),
				Delay:            20 * time.Millisecond,
				TimeLimit:        200 * time.Millisecond,
				MulticastAddress: c.Options.MulticastAddress,
				StopChan:         c.stop.stopChan,
			})
			if err1 == nil && len(ipv4discoveries) > 0 {
				dmux.Lock()
				err = err1
				discoveries = append(discoveries, ipv4discoveries...)
				dmux.Unlock()
			}
		}()
		go func() {
			defer wgDiscovery.Done()
			ipv6discoveries, err1 := peerdiscovery.Discover(peerdiscovery.Settings{
				Limit:     1,
				Payload:   []byte("ok"),
				Delay:     20 * time.Millisecond,
				TimeLimit: 200 * time.Millisecond,
				IPVersion: peerdiscovery.IPv6,
				StopChan:  c.stop.stopChan,
			})
			if err1 == nil && len(ipv6discoveries) > 0 {
				dmux.Lock()
				err = err1
				discoveries = append(discoveries, ipv6discoveries...)
				dmux.Unlock()
			}
		}()
		wgDiscovery.Wait()

		if err == nil && len(discoveries) > 0 {
			log.Debugf("all discoveries: %+v", discoveries)
			for i := 0; i < len(discoveries); i++ {
				log.Debugf("discovery %d has payload: %+v", i, discoveries[i])
				if !bytes.HasPrefix(discoveries[i].Payload, []byte("croc")) {
					log.Debug("skipping discovery")
					continue
				}
				log.Debug("switching to local")
				portToUse := string(bytes.TrimPrefix(discoveries[i].Payload, []byte("croc")))
				if portToUse == "" {
					portToUse = models.DEFAULT_PORT
				}
				address := net.JoinHostPort(discoveries[i].Address, portToUse)
				errPing := tcp.PingServer(address)
				if errPing == nil {
					log.Debugf("successfully pinged '%s'", address)
					c.Options.RelayAddress = address
					c.ExternalIPConnected = c.Options.RelayAddress
					c.Options.RelayAddress6 = ""
					usingLocal = true
					break
				} else {
					log.Debugf("could not ping: %+v", errPing)
				}
			}
		}
		log.Debugf("discoveries: %+v", discoveries)
		log.Debug("establishing connection")
	}
	var banner string
	durations := []time.Duration{200 * time.Millisecond, 5 * time.Second}
	err = fmt.Errorf("found no addresses to connect")
	for i, address := range []string{c.Options.RelayAddress6, c.Options.RelayAddress} {
		if address == "" {
			continue
		}
		var host, port string
		host, port, _ = net.SplitHostPort(address)
		// Default port to :9009
		if port == "" {
			host = address
			port = models.DEFAULT_PORT
		}
		log.Debugf("got host '%v' and port '%v'", host, port)
		address = net.JoinHostPort(host, port)
		log.Debugf("trying connection to %s", address)
		c.conn[0], banner, c.ExternalIP, err = tcp.ConnectToTCPServer(address, c.Options.RelayPassword, c.Options.RoomName, durations[i])
		if err == nil {
			c.Options.RelayAddress = address
			break
		}
		log.Debugf("could not establish '%s'", address)
	}
	if err != nil {
		err = fmt.Errorf("could not connect to %s: %w", c.Options.RelayAddress, err)
		log.Debug(err)
		return
	}
	log.Debugf("receiver connection established: %+v", c.conn[0])
	log.Debugf("banner: %s", banner)

	if c.Options.TestFlag {
		log.Debugf("TEST FLAG ENABLED, TESTING LOCAL IPS")
	}
	if c.Options.TestFlag || (!usingLocal && !c.Options.DisableLocal && !isIPset) {
		// ask the sender for their local ips and port
		// and try to connect to them
		var ips []string
		err = func() (err error) {
			var A *pake.Pake
			var data []byte
			A, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 0, c.Options.Curve)
			if err != nil {
				return err
			}
			dataMessage := SimpleMessage{
				Bytes: A.Bytes(),
				Kind:  "pake1",
			}
			data, _ = json.Marshal(dataMessage)
			if err = c.conn[0].Send(data); err != nil {
				log.Errorf("dataMessage send error: %v", err)
				return
			}
			data, err = c.conn[0].Receive()
			if err != nil {
				return
			}
			err = json.Unmarshal(data, &dataMessage)
			if err != nil || dataMessage.Kind != "pake2" {
				log.Debugf("data: %s", data)
				return fmt.Errorf("dataMessage %s pake failed", ipRequest)
			}
			err = A.Update(dataMessage.Bytes)
			if err != nil {
				return
			}
			var kA []byte
			kA, err = A.SessionKey()
			if err != nil {
				return
			}
			log.Debugf("dataMessage kA: %x", kA)

			// secure ipRequest
			data, err = crypt.Encrypt([]byte(ipRequest), kA)
			if err != nil {
				return
			}
			log.Debug("sending ips?")
			if err = c.conn[0].Send(data); err != nil {
				log.Errorf("ips send error: %v", err)
			}
			data, err = c.conn[0].Receive()
			if err != nil {
				return
			}
			data, err = crypt.Decrypt(data, kA)
			if err != nil {
				return
			}
			log.Debugf("ips data: %s", data)
			if err = json.Unmarshal(data, &ips); err != nil {
				log.Debugf("ips unmarshal error: %v", err)
			}
			return
		}()

		if len(ips) > 1 {
			port := ips[0]
			ips = ips[1:]
			for _, ip := range ips {
				ipv4Addr, ipv4Net, errNet := net.ParseCIDR(fmt.Sprintf("%s/24", ip))
				log.Debugf("ipv4Add4: %+v, ipv4Net: %+v, err: %+v", ipv4Addr, ipv4Net, errNet)

				// For peer-to-peer connectivity within a LAN, the sender and receiver don't need to be on the same subnet.
				// Even with NAT routers in their respective local networks,
				// a receiver behind NAT can establish direct access to the sender without requiring internet connectivity.
				// Conversely, the local networks on the sender and receiver may overlap but not be connected.
				// This often occurs with 192.168.0.0/30 and 192.168.1.0/30 subnets.

				// localIps, _ := utils.GetLocalIPs()
				// haveLocalIP := false
				// for _, localIP := range localIps {
				// 	localIPparsed := net.ParseIP(localIP)
				// 	log.Debugf("localIP: %+v, localIPparsed: %+v", localIP, localIPparsed)
				// 	if ipv4Net.Contains(localIPparsed) {
				// 		haveLocalIP = true
				// 		log.Debugf("ip: %+v is a local IP", ip)
				// 		break
				// 	}
				// }
				// if !haveLocalIP {
				// 	log.Debugf("%s is not a local IP, skipping", ip)
				// 	continue
				// }

				serverTry := net.JoinHostPort(ip, port)
				conn, banner2, externalIP, errConn := tcp.ConnectToTCPServer(serverTry, c.Options.RelayPassword, c.Options.RoomName, 500*time.Millisecond)
				if errConn != nil {
					log.Debug(errConn)
					log.Debug("could not connect to " + serverTry)
					continue
				}
				log.Debugf("local connection established to %s", serverTry)
				log.Debugf("banner: %s", banner2)
				// reset to the local port
				banner = banner2
				c.Options.RelayAddress = serverTry
				c.ExternalIP = externalIP
				c.conn[0].Close()
				c.conn[0] = nil
				c.conn[0] = conn
				break
			}
		}
	}

	if err = c.conn[0].Send(handshakeRequest); err != nil {
		log.Errorf("handshake send error: %v", err)
	}
	c.Options.RelayPorts = strings.Split(banner, ",")
	if c.Options.NoMultiplexing {
		log.Debug("no multiplexing")
		c.Options.RelayPorts = []string{c.Options.RelayPorts[0]}
	}
	log.Debug("exchanged header message")
	fmt.Fprintf(os.Stderr, "\rsecuring channel...")
	err = c.transfer()
	if err == nil {
		if c.numberOfTransferredFiles+len(c.EmptyFoldersToTransfer) == 0 {
			fmt.Fprintf(os.Stderr, "\rNo files transferred.\n")
		}
	} else {
		c.SendError()
	}
	return
}

func (c *Client) transfer() (err error) {
	// connect to the server

	// quit with c.quit <- true
	c.quit = make(chan bool)

	// if recipient, initialize with sending pake information
	log.Debug("ready")
	if !c.Options.IsSender && !c.Step1ChannelSecured {
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type:   message.TypePAKE,
			Bytes:  c.Pake.Bytes(),
			Bytes2: []byte(c.Options.Curve),
		})
		if err != nil {
			return
		}
	}

	// listen for incoming messages and process them
	for {
		if e := c.ctxErr(); e != nil {
			log.Tracef("transfer: %v", e)
			err = e
			break
		}
		var data []byte
		var done bool
		data, err = c.conn[0].Receive()
		if err != nil {
			log.Debugf("got error receiving: %v", err)
			if !c.Step1ChannelSecured {
				err = fmt.Errorf("could not secure channel")
			}
			break
		}
		done, err = c.processMessage(data)
		if err != nil {
			log.Debugf("data: %s", data)
			log.Debugf("got error processing: %v", err)
			break
		}
		if done {
			break
		}
	}
	if err := c.ctxErr(); err != nil && c.SuccessfulTransfer {
		c.SuccessfulTransfer = false
		log.Tracef("SuccessfulTransfer: %v", err)
	}
	// purge errors that come from successful transfer
	if c.SuccessfulTransfer {
		if err != nil {
			log.Debugf("purging error: %s", err)
		}
		err = nil
	}
	if c.Options.IsSender && c.SuccessfulTransfer {
		for _, file := range c.FilesToTransfer {
			if file.TempFile {
				fmt.Println("Removing " + file.Name)
				os.Remove(file.Name)
			}
		}
	}

	if c.SuccessfulTransfer && !c.Options.IsSender {
		for _, file := range c.FilesToTransfer {
			if file.TempFile {
				if unzipErr := utils.UnzipDirectory(".", file.Name); unzipErr != nil {
					c.SuccessfulTransfer = false
					err = fmt.Errorf("failed to unzip received archive %s: %w", file.Name, unzipErr)
					log.Error(err)
					break
				}
				if removeErr := os.Remove(file.Name); removeErr != nil {
					log.Warnf("error removing %s: %v", file.Name, removeErr)
				} else {
					log.Debugf("Removing %s\n", file.Name)
				}
			}
		}
	}

	if c.Options.Stdout && !c.Options.IsSender && len(c.FilesToTransfer) > 0 && c.FilesToTransferCurrentNum < len(c.FilesToTransfer) {
		pathToFile := path.Join(
			c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
			c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
		)
		log.Debugf("pathToFile: %s", pathToFile)
		// close if not closed already
		if !c.CurrentFileIsClosed {
			c.CurrentFile.Close()
			c.CurrentFileIsClosed = true
		}
		if err = os.Remove(pathToFile); err != nil {
			log.Warnf("error removing %s: %v", pathToFile, err)
		}
		fmt.Fprint(os.Stderr, "\n")
	}
	if err != nil && strings.Contains(err.Error(), "pake not successful") {
		log.Debugf("pake error: %s", err.Error())
		err = fmt.Errorf("password mismatch")
	}
	if err != nil && strings.Contains(err.Error(), "unexpected end of JSON input") {
		log.Debugf("error: %s", err.Error())
		err = fmt.Errorf("room (secure channel) not ready, maybe peer disconnected")
	}
	if err != nil {
		c.SendError()
	}
	return
}

func (c *Client) createEmptyFolder(i int) (err error) {
	err = os.MkdirAll(c.EmptyFoldersToTransfer[i].FolderRemote, os.ModePerm)
	if err != nil {
		return
	}
	fmt.Fprintf(os.Stderr, "%s\n", c.EmptyFoldersToTransfer[i].FolderRemote)
	c.bar = progressbar.NewOptions64(1,
		progressbar.OptionOnCompletion(func() {
			c.fmtPrintUpdate()
		}),
		progressbar.OptionSetWidth(20),
		progressbar.OptionSetDescription(" "),
		progressbar.OptionSetRenderBlankState(true),
		progressbar.OptionShowBytes(true),
		progressbar.OptionShowCount(),
		progressbar.OptionSetWriter(os.Stderr),
		progressbar.OptionSetVisibility(!c.Options.SendingText),
	)
	c.bar.Finish()
	return
}

func (c *Client) processMessageFileInfo(m message.Message) (done bool, err error) {
	var senderInfo SenderInfo
	err = json.Unmarshal(m.Bytes, &senderInfo)
	if err != nil {
		log.Debug(err)
		return
	}
	c.Options.SendingText = senderInfo.SendingText
	c.Options.NoCompress = senderInfo.NoCompress
	c.Options.HashAlgorithm = senderInfo.HashAlgorithm
	c.EmptyFoldersToTransfer = senderInfo.EmptyFoldersToTransfer
	c.TotalNumberFolders = senderInfo.TotalNumberFolders
	c.FilesToTransfer = senderInfo.FilesToTransfer
	for i, fi := range c.FilesToTransfer {
		// Issues #593 - sanitize the sender paths and prevent ".." from being used
		c.FilesToTransfer[i].FolderRemote = filepath.Clean(fi.FolderRemote)
		// Issues #593 - disallow specific folders like .ssh
		if strings.Contains(c.FilesToTransfer[i].FolderRemote, ".ssh") {
			return true, fmt.Errorf("invalid path detected: '%s'", fi.FolderRemote)
		}
		// Issue #595 - disallow filenames with invisible characters
		errFileName := utils.ValidFileName(path.Join(c.FilesToTransfer[i].FolderRemote, fi.Name))
		if errFileName != nil {
			return true, errFileName
		}
	}
	c.TotalNumberOfContents = 0
	if c.FilesToTransfer != nil {
		c.TotalNumberOfContents += len(c.FilesToTransfer)
	}
	if c.EmptyFoldersToTransfer != nil {
		c.TotalNumberOfContents += len(c.EmptyFoldersToTransfer)
	}

	if c.Options.HashAlgorithm == "" {
		c.Options.HashAlgorithm = "xxhash"
	}
	log.Debugf("using hash algorithm: %s", c.Options.HashAlgorithm)
	if c.Options.NoCompress {
		log.Debug("disabling compression")
	}
	if c.Options.SendingText {
		c.Options.Stdout = true
	}

	fname := fmt.Sprintf("%d files", len(c.FilesToTransfer))
	folderName := fmt.Sprintf("%d folders", c.TotalNumberFolders)
	if len(c.FilesToTransfer) == 1 {
		fname = fmt.Sprintf("'%s'", c.FilesToTransfer[0].Name)
	}
	totalSize := int64(0)
	for i, fi := range c.FilesToTransfer {
		totalSize += fi.Size
		if len(fi.Name) > c.longestFilename {
			c.longestFilename = len(fi.Name)
		}
		if strings.HasPrefix(fi.Name, "croc-stdin-") && c.Options.SendingText {
			c.FilesToTransfer[i].Name, err = utils.RandomFileName()
			if err != nil {
				return
			}
		}
	}
	// check the totalSize does not exceed disk space
	// usage := diskusage.NewDiskUsage(".")
	// if usage.Available() < uint64(totalSize) {
	// 	return true, fmt.Errorf("not enough disk space")
	// }

	// c.spinner.Stop()
	action := "Accept"
	if c.Options.SendingText {
		action = "Display"
		fname = "text message"
	}
	if !c.Options.NoPrompt || c.Options.Ask || senderInfo.Ask {
		if c.Options.Ask || senderInfo.Ask {
			machID, _ := machineid.ID()
			fmt.Fprintf(os.Stderr, "\rYour machine id is '%s'.\n%s %s (%s) from '%s'? (Y/n) ", machID, action, fname, utils.ByteCountDecimal(totalSize), senderInfo.MachineID)
		} else {
			if c.TotalNumberFolders > 0 {
				fmt.Fprintf(os.Stderr, "\r%s %s and %s (%s)? (Y/n) ", action, fname, folderName, utils.ByteCountDecimal(totalSize))
			} else {
				fmt.Fprintf(os.Stderr, "\r%s %s (%s)? (Y/n) ", action, fname, utils.ByteCountDecimal(totalSize))
			}
		}
		choice := strings.ToLower(utils.GetInput(""))
		if choice != "" && choice != "y" && choice != "yes" {
			err = message.Send(c.conn[0], c.Key, message.Message{
				Type:    message.TypeError,
				Message: "refusing files",
			})
			if err != nil {
				return false, err
			}
			return true, fmt.Errorf("refused files")
		}
	} else {
		fmt.Fprintf(os.Stderr, "\rReceiving %s (%s) \n", fname, utils.ByteCountDecimal(totalSize))
	}
	fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)\n", c.ExternalIPConnected)

	for i := 0; i < len(c.EmptyFoldersToTransfer); i += 1 {
		_, errExists := os.Stat(c.EmptyFoldersToTransfer[i].FolderRemote)
		if os.IsNotExist(errExists) {
			err = c.createEmptyFolder(i)
			if err != nil {
				return
			}
		} else {
			isEmpty, _ := isEmptyFolder(c.EmptyFoldersToTransfer[i].FolderRemote)
			if !isEmpty {
				log.Debug("asking to overwrite")
				prompt := fmt.Sprintf("\n%s already has some content in it. \nDo you want"+
					" to overwrite it with an empty folder? (y/N) ", c.EmptyFoldersToTransfer[i].FolderRemote)
				choice := strings.ToLower(utils.GetInput(prompt))
				if choice == "y" || choice == "yes" {
					err = c.createEmptyFolder(i)
					if err != nil {
						return
					}
				}
			}
		}
	}

	// if no files are to be transferred, then we can end the file transfer process
	if c.FilesToTransfer == nil {
		c.SuccessfulTransfer = true
		c.Step3RecipientRequestFile = true
		c.Step4FileTransferred = true
		errStopTransfer := message.Send(c.conn[0], c.Key, message.Message{
			Type: message.TypeFinished,
		})
		if errStopTransfer != nil {
			err = errStopTransfer
		}
	}
	log.Debug(c.FilesToTransfer)
	c.Step2FileInfoTransferred = true
	return
}

func (c *Client) processMessagePake(m message.Message) (err error) {
	defer func() {
		if r := recover(); r != nil {
			if c.stop.gui {
				log.Errorf("panic: %v", r)
				c.stop.Cancel()
			} else {
				panic(r)
			}
		}
	}()
	log.Debug("received pake payload")

	var salt []byte
	if c.Options.IsSender {
		// initialize curve based on the recipient's choice
		log.Debugf("using curve %s", string(m.Bytes2))
		c.Pake, err = pake.InitCurve([]byte(c.Options.SharedSecret[5:]), 1, string(m.Bytes2))
		if err != nil {
			log.Error(err)
			return
		}

		// update the pake
		err = c.Pake.Update(m.Bytes)
		if err != nil {
			return
		}

		// generate salt and send it back to recipient
		log.Debug("generating salt")
		salt = make([]byte, 8)
		if _, rerr := rand.Read(salt); err != nil {
			log.Errorf("can't generate random numbers: %v", rerr)
			return
		}
		log.Debug("sender sending pake+salt")
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type:   message.TypePAKE,
			Bytes:  c.Pake.Bytes(),
			Bytes2: salt,
		})
	} else {
		err = c.Pake.Update(m.Bytes)
		if err != nil {
			return
		}
		salt = m.Bytes2
	}
	// generate key
	key, err := c.Pake.SessionKey()
	if err != nil {
		return err
	}
	c.Key, _, err = crypt.New(key, salt)
	if err != nil {
		return err
	}
	log.Debugf("generated key = %+x with salt %x", c.Key, salt)

	// connects to the other ports of the server for transfer
	var wg sync.WaitGroup
	wg.Add(len(c.Options.RelayPorts))
	for i := 0; i < len(c.Options.RelayPorts); i++ {
		log.Debugf("port: [%s]", c.Options.RelayPorts[i])
		go func(j int) {
			defer wg.Done()
			var host string
			if c.Options.RelayAddress == "127.0.0.1" {
				host = c.Options.RelayAddress
			} else {
				host, _, err = net.SplitHostPort(c.Options.RelayAddress)
				if err != nil {
					log.Errorf("bad relay address %s", c.Options.RelayAddress)
					return
				}
			}
			server := net.JoinHostPort(host, c.Options.RelayPorts[j])
			log.Debugf("connecting to %s", server)
			c.conn[j+1], _, _, err = tcp.ConnectToTCPServer(
				server,
				c.Options.RelayPassword,
				fmt.Sprintf("%s-%d", c.Options.RoomName, j),
			)
			if err != nil {
				panic(err)
			}
			log.Debugf("connected to %s", server)
			if !c.Options.IsSender {
				go c.receiveData(j)
			}
		}(i)
	}
	wg.Wait()
	if !c.Options.IsSender {
		log.Debug("sending external IP")
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type:    message.TypeExternalIP,
			Message: c.ExternalIP,
			Bytes:   m.Bytes,
		})
	}
	return
}

func (c *Client) processExternalIP(m message.Message) (done bool, err error) {
	log.Debugf("received external IP: %+v", m)
	if c.Options.IsSender {
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type:    message.TypeExternalIP,
			Message: c.ExternalIP,
		})
		if err != nil {
			return true, err
		}
	}
	if c.ExternalIPConnected == "" {
		// it can be preset by the local relay
		c.ExternalIPConnected = m.Message
	}
	log.Debugf("connected as %s -> %s", c.ExternalIP, c.ExternalIPConnected)
	c.Step1ChannelSecured = true
	return
}

func (c *Client) processMessage(payload []byte) (done bool, err error) {
	m, err := message.Decode(c.Key, payload)
	if err != nil {
		err = fmt.Errorf("problem with decoding: %w", err)
		log.Debug(err)
		return
	}

	// only "pake" messages should be unencrypted
	// if a non-"pake" message is received unencrypted something
	// is weird
	if m.Type != message.TypePAKE && c.Key == nil {
		err = fmt.Errorf("unencrypted communication rejected")
		done = true
		return
	}

	switch m.Type {
	case message.TypeFinished:
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type: message.TypeFinished,
		})
		done = true
		c.SuccessfulTransfer = true
		return
	case message.TypePAKE:
		err = c.processMessagePake(m)
		if err != nil {
			err = fmt.Errorf("pake not successful: %w", err)
			log.Debug(err)
		}
	case message.TypeExternalIP:
		done, err = c.processExternalIP(m)
	case message.TypeError:
		// c.spinner.Stop()
		log.Trace("Peer initiates interruption of my loops and goroutines")
		c.stop.Cancel()
		fmt.Print("\r")
		err = fmt.Errorf("peer error: %s", m.Message)
		return true, err
	case message.TypeFileInfo:
		done, err = c.processMessageFileInfo(m)
	case message.TypeRecipientReady:
		var remoteFile RemoteFileRequest
		err = json.Unmarshal(m.Bytes, &remoteFile)
		if err != nil {
			return
		}
		c.FilesToTransferCurrentNum = remoteFile.FilesToTransferCurrentNum
		c.CurrentFileChunkRanges = remoteFile.CurrentFileChunkRanges
		c.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges)
		log.Debugf("current file chunks: %+v", c.CurrentFileChunks)
		c.mutex.Lock()
		c.chunkMap = make(map[uint64]struct{})
		for _, chunk := range c.CurrentFileChunks {
			c.chunkMap[uint64(chunk)] = struct{}{}
		}
		c.mutex.Unlock()
		c.Step3RecipientRequestFile = true

		if c.Options.Ask {
			fmt.Fprintf(os.Stderr, "Send to machine '%s'? (Y/n) ", remoteFile.MachineID)
			choice := strings.ToLower(utils.GetInput(""))
			if choice != "" && choice != "y" && choice != "yes" {
				err = message.Send(c.conn[0], c.Key, message.Message{
					Type:    message.TypeError,
					Message: "refusing files",
				})
				done = true
				return
			}
		}
	case message.TypeCloseSender:
		c.bar.Finish()
		log.Debug("close-sender received...")
		c.Step4FileTransferred = false
		c.Step3RecipientRequestFile = false
		log.Debug("sending close-recipient")
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type: message.TypeCloseRecipient,
		})
	case message.TypeCloseRecipient:
		c.Step4FileTransferred = false
		c.Step3RecipientRequestFile = false
	}
	if err != nil {
		log.Debugf("got error from processing message: %v", err)
		return
	}
	err = c.updateState()
	if err != nil {
		log.Debugf("got error from updating state: %v", err)
		return
	}
	return
}

func (c *Client) updateIfSenderChannelSecured() (err error) {
	if c.Options.IsSender && c.Step1ChannelSecured && !c.Step2FileInfoTransferred {
		var b []byte
		machID, _ := machineid.ID()
		b, err = json.Marshal(SenderInfo{
			FilesToTransfer:        c.FilesToTransfer,
			EmptyFoldersToTransfer: c.EmptyFoldersToTransfer,
			MachineID:              machID,
			Ask:                    c.Options.Ask,
			TotalNumberFolders:     c.TotalNumberFolders,
			SendingText:            c.Options.SendingText,
			NoCompress:             c.Options.NoCompress,
			HashAlgorithm:          c.Options.HashAlgorithm,
		})
		if err != nil {
			log.Error(err)
			return
		}
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type:  message.TypeFileInfo,
			Bytes: b,
		})
		if err != nil {
			return
		}

		c.Step2FileInfoTransferred = true
	}
	return
}

func (c *Client) recipientInitializeFile() (err error) {
	// start initiating the process to receive a new file
	log.Debugf("working on file %d", c.FilesToTransferCurrentNum)

	// recipient sets the file
	pathToFile := path.Join(
		c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
		c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
	)
	folderForFile, _ := filepath.Split(pathToFile)
	folderForFileBase := filepath.Base(folderForFile)
	if folderForFileBase != "." && folderForFileBase != "" {
		if err := os.MkdirAll(folderForFile, os.ModePerm); err != nil {
			log.Errorf("can't create %s: %v", folderForFile, err)
		}
	}
	var errOpen error
	c.CurrentFile, errOpen = os.OpenFile(
		pathToFile,
		os.O_WRONLY, 0o666)
	var truncate bool // default false
	c.CurrentFileChunks = []int64{}
	c.CurrentFileChunkRanges = []int64{}
	if errOpen == nil {
		stat, _ := c.CurrentFile.Stat()
		truncate = stat.Size() != c.FilesToTransfer[c.FilesToTransferCurrentNum].Size
		if !truncate {
			// recipient requests the file and chunks (if empty, then should receive all chunks)
			// TODO: determine the missing chunks
			c.CurrentFileChunkRanges = utils.MissingChunks(
				pathToFile,
				c.FilesToTransfer[c.FilesToTransferCurrentNum].Size,
				models.TCP_BUFFER_SIZE/2,
			)
		}
	} else {
		c.CurrentFile, errOpen = os.Create(pathToFile)
		if errOpen != nil {
			errOpen = fmt.Errorf("could not create %s: %w", pathToFile, errOpen)
			log.Error(errOpen)
			return errOpen
		}
		errChmod := os.Chmod(pathToFile, c.FilesToTransfer[c.FilesToTransferCurrentNum].Mode.Perm())
		if errChmod != nil {
			log.Error(errChmod)
		}
		truncate = true
	}
	if truncate {
		err := c.CurrentFile.Truncate(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)
		if err != nil {
			err = fmt.Errorf("could not truncate %s: %w", pathToFile, err)
			log.Error(err)
			return err
		}
	}
	return
}

func (c *Client) recipientGetFileReady(finished bool) (err error) {
	if finished {
		// TODO: do the last finishing stuff
		log.Debug("finished")
		err = message.Send(c.conn[0], c.Key, message.Message{
			Type: message.TypeFinished,
		})
		if err != nil {
			panic(err)
		}
		c.SuccessfulTransfer = true
		c.FilesHasFinished[c.FilesToTransferCurrentNum] = struct{}{}
		return
	}

	err = c.recipientInitializeFile()
	if err != nil {
		return
	}

	c.TotalSent = 0
	c.CurrentFileIsClosed = false
	machID, _ := machineid.ID()
	bRequest, _ := json.Marshal(RemoteFileRequest{
		CurrentFileChunkRanges:    c.CurrentFileChunkRanges,
		FilesToTransferCurrentNum: c.FilesToTransferCurrentNum,
		MachineID:                 machID,
	})
	log.Debug("converting to chunk range")
	c.CurrentFileChunks = utils.ChunkRangesToChunks(c.CurrentFileChunkRanges)

	if !finished {
		// setup the progressbar
		c.setBar()
	}

	log.Debugf("sending recipient ready with %d chunks", len(c.CurrentFileChunks))
	err = message.Send(c.conn[0], c.Key, message.Message{
		Type:  message.TypeRecipientReady,
		Bytes: bRequest,
	})
	if err != nil {
		return
	}
	c.Step3RecipientRequestFile = true
	return
}

func formatDescription(description string) string {
	const (
		// Reserve extra room for variable progress metadata such as [elapsed:remaining].
		progressMetaWidth = 78
		minDescription    = 12
		defaultTermWidth  = 80
	)

	width, _, err := term.GetSize(int(os.Stderr.Fd()))
	if err != nil || width <= 0 {
		width, _, err = term.GetSize(int(os.Stdout.Fd()))
	}
	if err != nil || width <= 0 {
		if envColumns, convErr := strconv.Atoi(os.Getenv("COLUMNS")); convErr == nil && envColumns > 0 {
			width = envColumns
		} else {
			width = defaultTermWidth
		}
	}

	maxDescription := width - progressMetaWidth
	if maxDescription < minDescription {
		maxDescription = minDescription
	}

	runes := []rune(description)
	if len(runes) > maxDescription {
		if maxDescription <= 3 {
			return string(runes[:maxDescription])
		}
		return string(runes[:maxDescription-3]) + "..."
	}
	return description
}

func (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (err error) {
	log.Debugf("touching file with folder / name")
	if !utils.Exists(fileInfo.FolderRemote) {
		err = os.MkdirAll(fileInfo.FolderRemote, os.ModePerm)
		if err != nil {
			log.Error(err)
			return
		}
	}
	pathToFile := path.Join(fileInfo.FolderRemote, fileInfo.Name)
	if fileInfo.Symlink != "" {
		log.Debug("creating symlink")
		// remove symlink if it exists
		if _, errExists := os.Lstat(pathToFile); errExists == nil {
			os.Remove(pathToFile)
		}
		err = os.Symlink(fileInfo.Symlink, pathToFile)
		if err != nil {
			return
		}
	} else {
		emptyFile, errCreate := os.Create(pathToFile)
		if errCreate != nil {
			log.Error(errCreate)
			err = errCreate
			return
		}
		emptyFile.Close()
	}
	// setup the progressbar
	description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[i].Name)
	if len(c.FilesToTransfer) == 1 {
		description = c.FilesToTransfer[i].Name
		// description = ""
	} else {
		description = " " + description
	}
	c.bar = progressbar.NewOptions64(1,
		progressbar.OptionOnCompletion(func() {
			c.fmtPrintUpdate()
		}),
		progressbar.OptionSetWidth(20),
		progressbar.OptionSetDescription(formatDescription(description)),
		progressbar.OptionSetRenderBlankState(true),
		progressbar.OptionShowBytes(true),
		progressbar.OptionShowCount(),
		progressbar.OptionSetWriter(os.Stderr),
		progressbar.OptionSetVisibility(!c.Options.SendingText),
	)
	c.bar.Finish()
	return
}

func (c *Client) updateIfRecipientHasFileInfo() (err error) {
	if c.Options.IsSender || !c.Step2FileInfoTransferred || c.Step3RecipientRequestFile {
		return
	}
	// find the next file to transfer and send that number
	// if the files are the same size, then look for missing chunks
	finished := true
	for i, fileInfo := range c.FilesToTransfer {
		if _, ok := c.FilesHasFinished[i]; ok {
			continue
		}
		if i < c.FilesToTransferCurrentNum {
			continue
		}
		log.Debugf("checking %+v", fileInfo)
		recipientFileInfo, errRecipientFile := os.Lstat(path.Join(fileInfo.FolderRemote, fileInfo.Name))
		var errHash error
		var fileHash []byte
		if errRecipientFile == nil && recipientFileInfo.Size() == fileInfo.Size {
			// the file exists, but is same size, so hash it
			fileHash, errHash = utils.HashFile(path.Join(fileInfo.FolderRemote, fileInfo.Name), c.Options.HashAlgorithm)
		}
		if fileInfo.Size == 0 || fileInfo.Symlink != "" {
			err = c.createEmptyFileAndFinish(fileInfo, i)
			if err != nil {
				return
			} else {
				c.numberOfTransferredFiles++
			}
			continue
		}
		log.Debugf("%s %+x %+x %+v", fileInfo.Name, fileHash, fileInfo.Hash, errHash)
		if !bytes.Equal(fileHash, fileInfo.Hash) {
			log.Debugf("hashed %s to %x using %s", fileInfo.Name, fileHash, c.Options.HashAlgorithm)
			log.Debugf("hashes are not equal %x != %x", fileHash, fileInfo.Hash)
			if errHash == nil && !c.Options.Overwrite && errRecipientFile == nil && !strings.HasPrefix(fileInfo.Name, "croc-stdin-") && !c.Options.SendingText {

				missingChunks := utils.ChunkRangesToChunks(utils.MissingChunks(
					path.Join(fileInfo.FolderRemote, fileInfo.Name),
					fileInfo.Size,
					models.TCP_BUFFER_SIZE/2,
				))
				percentDone := 100 - float64(len(missingChunks)*models.TCP_BUFFER_SIZE/2)/float64(fileInfo.Size)*100

				log.Debug("asking to overwrite")
				prompt := fmt.Sprintf("\nOverwrite '%s'? (y/N) (use --overwrite to omit) ", path.Join(fileInfo.FolderRemote, fileInfo.Name))
				if percentDone < 99 {
					prompt = fmt.Sprintf("\nResume '%s' (%2.1f%%)? (y/N)   (use --overwrite to omit) ", path.Join(fileInfo.FolderRemote, fileInfo.Name), percentDone)
				}
				choice := strings.ToLower(utils.GetInput(prompt))
				if choice != "y" && choice != "yes" {
					fmt.Fprintf(os.Stderr, "Skipping '%s'\n", path.Join(fileInfo.FolderRemote, fileInfo.Name))
					continue
				}
			}
		} else {
			log.Debugf("hashes are equal %x == %x", fileHash, fileInfo.Hash)

			if !fileInfo.ModTime.IsZero() {
				if err := os.Chtimes(path.Join(fileInfo.FolderRemote, fileInfo.Name), fileInfo.ModTime, fileInfo.ModTime); err != nil {
					log.Warnf("chtimes %v: %v", fileInfo.ModTime, err)
				} else {
					log.Debugf("chtimes %v", fileInfo.ModTime)
				}
			}
		}
		if errHash != nil {
			// probably can't find, its okay
			log.Debug(errHash)
		}
		if errHash != nil || !bytes.Equal(fileHash, fileInfo.Hash) {
			finished = false
			c.FilesToTransferCurrentNum = i
			c.numberOfTransferredFiles++
			newFolder, _ := filepath.Split(fileInfo.FolderRemote)
			if newFolder != c.LastFolder && len(c.FilesToTransfer) > 0 && !c.Options.SendingText && newFolder != "./" {
				fmt.Fprintf(os.Stderr, "\r%s\n", newFolder)
			}
			c.LastFolder = newFolder
			break
		}
	}
	c.recipientGetFileReady(finished)
	return
}

func (c *Client) fmtPrintUpdate() {
	c.finishedNum++
	if c.TotalNumberOfContents > 1 {
		fmt.Fprintf(os.Stderr, " %d/%d\n", c.finishedNum, c.TotalNumberOfContents)
	} else {
		fmt.Fprintf(os.Stderr, "\n")
	}
}

func (c *Client) updateState() (err error) {
	err = c.updateIfSenderChannelSecured()
	if err != nil {
		return
	}

	err = c.updateIfRecipientHasFileInfo()
	if err != nil {
		return
	}

	if c.Options.IsSender && c.Step3RecipientRequestFile && !c.Step4FileTransferred {
		log.Debug("start sending data!")

		if !c.firstSend {
			fmt.Fprintf(os.Stderr, "\nSending (->%s)\n", c.ExternalIPConnected)
			c.firstSend = true
			// if there are empty files, show them as already have been transferred now
			for i := range c.FilesToTransfer {
				if c.FilesToTransfer[i].Size == 0 {
					// setup the progressbar and takedown the progress bar for empty files
					description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[i].Name)
					if len(c.FilesToTransfer) == 1 {
						description = c.FilesToTransfer[i].Name
						// description = ""
					}

					c.bar = progressbar.NewOptions64(1,
						progressbar.OptionOnCompletion(func() {
							c.fmtPrintUpdate()
						}),
						progressbar.OptionSetWidth(20),
						progressbar.OptionSetDescription(formatDescription(description)),
						progressbar.OptionSetRenderBlankState(true),
						progressbar.OptionShowBytes(true),
						progressbar.OptionShowCount(),
						progressbar.OptionSetWriter(os.Stderr),
						progressbar.OptionSetVisibility(!c.Options.SendingText),
					)
					c.bar.Finish()
				}
			}
		}
		c.Step4FileTransferred = true
		// setup the progressbar
		c.setBar()
		c.TotalSent = 0
		c.CurrentFileIsClosed = false
		log.Debug("beginning sending comms")
		pathToFile := path.Join(
			c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderSource,
			c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
		)
		c.fread, err = os.Open(pathToFile)
		c.numfinished = 0
		if err != nil {
			return
		}
		for i := 0; i < len(c.Options.RelayPorts); i++ {
			log.Debugf("starting sending over comm %d", i)
			go c.sendData(i)
		}
	}
	return
}

func (c *Client) setBar() {
	description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[c.FilesToTransferCurrentNum].Name)
	folder, _ := filepath.Split(c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote)
	if folder == "./" {
		description = c.FilesToTransfer[c.FilesToTransferCurrentNum].Name
	} else if !c.Options.IsSender {
		description = " " + description
	}
	c.bar = progressbar.NewOptions64(
		c.FilesToTransfer[c.FilesToTransferCurrentNum].Size,
		progressbar.OptionOnCompletion(func() {
			c.fmtPrintUpdate()
		}),
		progressbar.OptionSetWidth(20),
		progressbar.OptionSetDescription(formatDescription(description)),
		progressbar.OptionSetRenderBlankState(true),
		progressbar.OptionShowBytes(true),
		progressbar.OptionShowCount(),
		progressbar.OptionSetWriter(os.Stderr),
		progressbar.OptionThrottle(100*time.Millisecond),
		progressbar.OptionSetVisibility(!c.Options.SendingText),
	)
	byteToDo := int64(len(c.CurrentFileChunks) * models.TCP_BUFFER_SIZE / 2)
	if byteToDo > 0 {
		bytesDone := c.FilesToTransfer[c.FilesToTransferCurrentNum].Size - byteToDo
		log.Debug(byteToDo)
		log.Debug(c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)
		log.Debug(bytesDone)
		if bytesDone > 0 {
			c.bar.Add64(bytesDone)
		}
	}
}

func (c *Client) receiveData(i int) {
	defer func() {
		if r := recover(); r != nil {
			if c.stop.gui {
				log.Errorf("panic: %v", r)
				c.stop.Cancel()
			} else {
				panic(r)
			}
		}
	}()
	log.Tracef("%d receiving data", i)
	for {
		data, err := c.conn[i+1].Receive()
		if err != nil {
			break
		}
		if bytes.Equal(data, []byte{1}) {
			log.Trace("got ping")
			continue
		}

		data, err = crypt.Decrypt(data, c.Key)
		if err != nil {
			panic(err)
		}
		if !c.Options.NoCompress {
			data = compress.Decompress(data)
		}

		// get position
		var position uint64
		rbuf := bytes.NewReader(data[:8])
		err = binary.Read(rbuf, binary.LittleEndian, &position)
		if err != nil {
			panic(err)
		}
		positionInt64 := int64(position)

		c.mutex.Lock()
		if c.CurrentFileIsClosed || c.CurrentFile == nil {
			c.mutex.Unlock()
			log.Tracef("was closed %d", i)
			return
		}
		if err := c.ctxErr(); err != nil {
			c.CurrentFileIsClosed = true
			defer c.mutex.Unlock()
			log.Tracef("stopping: %v", err)
			if err := c.CurrentFile.Close(); err != nil {
				log.Tracef("closing %s: %v", c.CurrentFile.Name(), err)
			} else {
				log.Tracef("Successful closing %s", c.CurrentFile.Name())
			}
			log.Tracef("sending close-sender")
			if sendErr := message.Send(c.conn[0], c.Key, message.Message{
				Type: message.TypeCloseSender,
			}); sendErr != nil {
				log.Tracef("sending close-sender: %v", sendErr)
			}
			return
		}
		_, err = c.CurrentFile.WriteAt(data[8:], positionInt64)
		if err != nil {
			panic(err)
		}
		c.bar.Add(len(data[8:]))
		c.TotalSent += int64(len(data[8:]))
		c.TotalChunksTransferred++
		// log.Debug(len(c.CurrentFileChunks), c.TotalChunksTransferred, c.TotalSent, c.FilesToTransfer[c.FilesToTransferCurrentNum].Size)

		if !c.CurrentFileIsClosed && (c.TotalChunksTransferred == len(c.CurrentFileChunks) || c.TotalSent == c.FilesToTransfer[c.FilesToTransferCurrentNum].Size) {
			c.CurrentFileIsClosed = true
			log.Debug("finished receiving!")
			if err = c.CurrentFile.Close(); err != nil {
				log.Debugf("error closing %s: %v", c.CurrentFile.Name(), err)
			} else {
				log.Debugf("Successful closing %s", c.CurrentFile.Name())
			}
			if c.Options.Stdout || c.Options.SendingText {
				pathToFile := path.Join(
					c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
					c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
				)
				b, _ := os.ReadFile(pathToFile)
				fmt.Print(string(b))
			}
			log.Debug("sending close-sender")
			err = message.Send(c.conn[0], c.Key, message.Message{
				Type: message.TypeCloseSender,
			})
			if err != nil {
				panic(err)
			}
		}
		c.mutex.Unlock()
	}
}

func (c *Client) sendData(i int) {
	defer func() {
		if r := recover(); r != nil {
			if c.stop.gui {
				log.Errorf("panic: %v", r)
				c.stop.Cancel()
			} else {
				panic(r)
			}
		}
		log.Debugf("finished with %d", i)
		c.numfinished++
		if c.numfinished == len(c.Options.RelayPorts) {
			log.Debug("closing file")
			if err := c.fread.Close(); err != nil {
				log.Errorf("error closing file: %v", err)
			}
		}
	}()

	var readingPos int64
	pos := uint64(0)
	curi := float64(0)
	for {
		if err := c.ctxErr(); err != nil {
			log.Tracef("stopping send %d: %v", i, err)
			return
		}
		// Read file
		var n int
		var errRead error
		if math.Mod(curi, float64(len(c.Options.RelayPorts))) == float64(i) {
			data := make([]byte, models.TCP_BUFFER_SIZE/2)
			n, errRead = c.fread.ReadAt(data, readingPos)
			if c.limiter != nil {
				r := c.limiter.ReserveN(time.Now(), n)
				log.Debugf("Limiting Upload for %d", r.Delay())
				time.Sleep(r.Delay())
			}
			if n > 0 {
				// check to see if this is a chunk that the recipient wants
				usableChunk := true
				c.mutex.Lock()
				if len(c.chunkMap) != 0 {
					if _, ok := c.chunkMap[pos]; !ok {
						usableChunk = false
					} else {
						delete(c.chunkMap, pos)
					}
				}
				c.mutex.Unlock()
				if usableChunk {
					// log.Debugf("sending chunk %d", pos)
					posByte := make([]byte, 8)
					binary.LittleEndian.PutUint64(posByte, pos)
					var err error
					var dataToSend []byte
					if c.Options.NoCompress {
						dataToSend, err = crypt.Encrypt(
							append(posByte, data[:n]...),
							c.Key,
						)
					} else {
						dataToSend, err = crypt.Encrypt(
							compress.Compress(
								append(posByte, data[:n]...),
							),
							c.Key,
						)
					}
					if err != nil {
						panic(err)
					}

					err = c.conn[i+1].Send(dataToSend)
					if err != nil {
						panic(err)
					}
					c.bar.Add(n)
					c.TotalSent += int64(n)
					// time.Sleep(100 * time.Millisecond)
				}
			}
		}

		if n == 0 {
			n = models.TCP_BUFFER_SIZE / 2
		}
		readingPos += int64(n)
		curi++
		pos += uint64(n)

		if errRead != nil {
			if errRead == io.EOF {
				break
			}
			panic(errRead)
		}
	}
}

// isExecutableInPath checks for the availability of an executable
func isExecutableInPath(executableName string) bool {
	_, err := exec.LookPath(executableName)
	return err == nil
}

// copyToClipboard tries to send the code to the operating system clipboard
func copyToClipboard(str string, quiet bool, extendedClipboard bool) {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	// Windows should always have clip.exe in PATH by default
	case "windows":
		cmd = exec.Command("clip")
	// MacOS uses pbcopy
	case "darwin":
		cmd = exec.Command("pbcopy")
	// These Unix-like systems are likely using Xorg(with xclip or xsel) or Wayland(with wl-copy or waycopy)
	case "linux", "android", "hurd", "freebsd", "openbsd", "netbsd", "dragonfly", "solaris", "illumos", "plan9":
		if os.Getenv("XDG_SESSION_TYPE") == "wayland" { // Wayland running
			if isExecutableInPath("wl-copy") {
				cmd = exec.Command("wl-copy")
			} else if isExecutableInPath("waycopy") {
				cmd = exec.Command("waycopy")
			}
		} else if os.Getenv("XDG_SESSION_TYPE") == "x11" || os.Getenv("XDG_SESSION_TYPE") == "xorg" { // Xorg running
			if isExecutableInPath("xclip") {
				cmd = exec.Command("xclip", "-selection", "clipboard")
			}
		} else if isExecutableInPath("xsel") {
			cmd = exec.Command("xsel", "-b")
		} else if isExecutableInPath("termux-clipboard-set") {
			cmd = exec.Command("termux-clipboard-set")
		}
	default:
		return
	}
	// Nothing has been found
	if cmd == nil {
		return
	}
	// Sending stdin into the available clipboard program
	cmd.Stdin = bytes.NewReader([]byte(str))
	if err := cmd.Run(); err != nil {
		log.Debugf("error copying to clipboard: %v", err)
		return
	}
	if !quiet {
		if extendedClipboard {
			fmt.Fprintf(os.Stderr, "Command copied to clipboard!\n")
		} else {
			fmt.Fprintf(os.Stderr, "Code copied to clipboard!\n")
		}
	}
}


================================================
FILE: src/croc/croc_test.go
================================================
package croc

import (
	"context"
	"fmt"
	"math/rand"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/schollz/croc/v10/src/tcp"
	log "github.com/schollz/logger"
	"github.com/stretchr/testify/assert"
)

func init() {
	log.SetLevel("trace")

	go tcp.Run("debug", "127.0.0.1", "8281", "pass123", "8282,8283,8284,8285")
	go tcp.Run("debug", "127.0.0.1", "8282", "pass123")
	go tcp.Run("debug", "127.0.0.1", "8283", "pass123")
	go tcp.Run("debug", "127.0.0.1", "8284", "pass123")
	go tcp.Run("debug", "127.0.0.1", "8285", "pass123")
	time.Sleep(1 * time.Second)
}

func TestCrocReadme(t *testing.T) {
	defer os.Remove("README.md")

	log.Debug("setting up sender")
	sender, err := New(Options{
		IsSender:      true,
		SharedSecret:  "8123-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8281",
		RelayPorts:    []string{"8281"},
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		panic(err)
	}

	log.Debug("setting up receiver")
	receiver, err := New(Options{
		IsSender:      false,
		SharedSecret:  "8123-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8281",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		panic(err)
	}

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{"../../README.md"}, false, false, []string{})
		if errGet != nil {
			t.Errorf("failed to get minimal info: %v", errGet)
		}
		err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
		if err != nil {
			t.Errorf("send failed: %v", err)
		}
		wg.Done()
	}()
	time.Sleep(100 * time.Millisecond)
	go func() {
		err := receiver.Receive()
		if err != nil {
			t.Errorf("receive failed: %v", err)
		}
		wg.Done()
	}()

	wg.Wait()
}

func TestCrocEmptyFolder(t *testing.T) {
	pathName := "../../testEmpty"
	defer os.RemoveAll(pathName)
	defer os.RemoveAll("./testEmpty")
	os.MkdirAll(pathName, 0o755)

	log.Debug("setting up sender")
	sender, err := New(Options{
		IsSender:      true,
		SharedSecret:  "8123-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8281",
		RelayPorts:    []string{"8281"},
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		panic(err)
	}

	log.Debug("setting up receiver")
	receiver, err := New(Options{
		IsSender:      false,
		SharedSecret:  "8123-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8281",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		panic(err)
	}

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false, []string{})
		if errGet != nil {
			t.Errorf("failed to get minimal info: %v", errGet)
		}
		err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
		if err != nil {
			t.Errorf("send failed: %v", err)
		}
		wg.Done()
	}()
	time.Sleep(100 * time.Millisecond)
	go func() {
		err := receiver.Receive()
		if err != nil {
			t.Errorf("receive failed: %v", err)
		}
		wg.Done()
	}()

	wg.Wait()
}

func TestCrocSymlink(t *testing.T) {
	pathName := "../link-in-folder"
	defer os.RemoveAll(pathName)
	defer os.RemoveAll("./link-in-folder")
	os.MkdirAll(pathName, 0o755)
	os.Symlink("../../README.md", filepath.Join(pathName, "README.link"))

	log.Debug("setting up sender")
	sender, err := New(Options{
		IsSender:      true,
		SharedSecret:  "8124-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8281",
		RelayPorts:    []string{"8281"},
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		panic(err)
	}

	log.Debug("setting up receiver")
	receiver, err := New(Options{
		IsSender:      false,
		SharedSecret:  "8124-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8281",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		panic(err)
	}

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false, []string{})
		if errGet != nil {
			t.Errorf("failed to get minimal info: %v", errGet)
		}
		err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
		if err != nil {
			t.Errorf("send failed: %v", err)
		}
		wg.Done()
	}()
	time.Sleep(100 * time.Millisecond)
	go func() {
		err = receiver.Receive()
		if err != nil {
			t.Errorf("receive failed: %v", err)
		}
		wg.Done()
	}()

	wg.Wait()

	s, err := filepath.EvalSymlinks(path.Join(pathName, "README.link"))
	if s != "../../README.md" && s != "..\\..\\README.md" {
		log.Debug(s)
		t.Errorf("symlink failed to transfer in folder")
	}
	if err != nil {
		t.Errorf("symlink transfer failed: %s", err.Error())
	}
}
func TestCrocIgnoreGit(t *testing.T) {
	log.SetLevel("trace")
	defer os.Remove(".gitignore")
	time.Sleep(300 * time.Millisecond)

	time.Sleep(1 * time.Second)
	file, err := os.Create(".gitignore")
	if err != nil {
		log.Errorf("error creating file")
	}
	_, err = file.WriteString("LICENSE")
	if err != nil {
		log.Errorf("error writing to file")
	}
	time.Sleep(1 * time.Second)
	// 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.
	filesInfo, _, _, errGet := GetFilesInfo([]string{"../../LICENSE", ".gitignore", "croc.go"}, false, true, []string{})
	if errGet != nil {
		t.Errorf("failed to get minimal info: %v", errGet)
	}
	for _, file := range filesInfo {
		if strings.Contains(file.Name, "LICENSE") {
			t.Errorf("test failed, should ignore LICENSE")
		}
	}
}

func TestCrocLocal(t *testing.T) {
	log.SetLevel("trace")
	defer os.Remove("LICENSE")
	defer os.Remove("touched")
	time.Sleep(300 * time.Millisecond)

	log.Debug("setting up sender")
	sender, err := New(Options{
		IsSender:      true,
		SharedSecret:  "8123-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8181",
		RelayPorts:    []string{"8181", "8182"},
		RelayPassword: "pass123",
		Stdout:        true,
		NoPrompt:      true,
		DisableLocal:  false,
		Curve:         "ed25519",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		panic(err)
	}
	time.Sleep(1 * time.Second)

	log.Debug("setting up receiver")
	receiver, err := New(Options{
		IsSender:      false,
		SharedSecret:  "8123-testingthecroc",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8181",
		RelayPassword: "pass123",
		Stdout:        true,
		NoPrompt:      true,
		DisableLocal:  false,
		Curve:         "ed25519",
		Overwrite:     true,
	})
	if err != nil {
		panic(err)
	}

	var wg sync.WaitGroup
	os.Create("touched")
	wg.Add(2)
	go func() {
		filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{"../../LICENSE", "touched"}, false, false, []string{})
		if errGet != nil {
			t.Errorf("failed to get minimal info: %v", errGet)
		}
		err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
		if err != nil {
			t.Errorf("send failed: %v", err)
		}
		wg.Done()
	}()
	time.Sleep(100 * time.Millisecond)
	go func() {
		err := receiver.Receive()
		if err != nil {
			t.Errorf("send failed: %v", err)
		}
		wg.Done()
	}()

	wg.Wait()
}

func TestCrocError(t *testing.T) {
	content := []byte("temporary file's content")
	tmpfile, err := os.CreateTemp("", "example")
	if err != nil {
		panic(err)
	}

	defer os.Remove(tmpfile.Name()) // clean up

	if _, err = tmpfile.Write(content); err != nil {
		panic(err)
	}
	if err = tmpfile.Close(); err != nil {
		panic(err)
	}

	Debug(false)
	log.SetLevel("warn")
	sender, _ := New(Options{
		IsSender:      true,
		SharedSecret:  "8123-testingthecroc2",
		Debug:         true,
		RelayAddress:  "doesntexistok.com:8381",
		RelayPorts:    []string{"8381", "8382"},
		RelayPassword: "pass123",
		Stdout:        true,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tmpfile.Name()}, false, false, []string{})
	if errGet != nil {
		t.Errorf("failed to get minimal info: %v", errGet)
	}
	err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
	log.Debug(err)
	assert.NotNil(t, err)
}

func TestReceiverStdoutWithInvalidSecret(t *testing.T) {
	// Test for issue: panic when receiving with --stdout and invalid CROC_SECRET
	// This should fail gracefully without panicking
	log.SetLevel("warn")
	receiver, err := New(Options{
		IsSender:      false,
		SharedSecret:  "invalid-secret-12345",
		Debug:         true,
		RelayAddress:  "127.0.0.1:8281",
		RelayPassword: "pass123",
		Stdout:        true, // This is the key flag that triggered the panic
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		t.Errorf("failed to create receiver: %v", err)
		return
	}

	// This should fail but not panic
	err = receiver.Receive()
	// We expect an error since the secret is invalid and no sender is present
	assert.NotNil(t, err)
	log.Debugf("Expected error occurred: %v", err)
}

func TestCleanUp(t *testing.T) {
	// windows allows files to be deleted only if they
	// are not open by another program so the remove actions
	// from the above tests will not always do a good clean up
	// This "test" will make sure
	operatingSystem := runtime.GOOS
	log.Debugf("The operating system is %s", operatingSystem)
	if operatingSystem == "windows" {
		time.Sleep(1 * time.Second)
		log.Debug("Full cleanup")
		var err error

		for _, file := range []string{"README.md", "./README.md"} {
			err = os.Remove(file)
			if err == nil {
				log.Debugf("Successfully purged %s", file)
			} else {
				log.Debugf("%s was already purged.", file)
			}
		}
		for _, folder := range []string{"./testEmpty", "./link-in-folder"} {
			err = os.RemoveAll(folder)
			if err == nil {
				log.Debugf("Successfully purged %s", folder)
			} else {
				log.Debugf("%s was already purged.", folder)
			}
		}
	}
}

func hashed(c *Client) bool {
	if len(c.FilesToTransfer) == 0 {
		return false
	}
	for _, file := range c.FilesToTransfer {
		if len(file.Hash) == 0 {
			return false
		}
	}
	return true
}

func waitHashed(sender *Client) (err error) {
	err = fmt.Errorf("not hashed")
	for i := 0; i < 300; i++ { // Max 3 seconds
		if hashed(sender) {
			time.Sleep(100 * time.Millisecond)
			return nil
		}
		time.Sleep(10 * time.Millisecond)
	}
	return
}

func createTestFile(t *testing.T, size int) (string, func()) {
	tempFile, err := os.CreateTemp("", "test-*.dat")
	if err != nil {
		t.Fatal(err)
	}

	data := make([]byte, size)
	for i := 0; i < size; i++ {
		data[i] = byte(i % 256)
	}

	if _, err := tempFile.Write(data); err != nil {
		tempFile.Close()
		os.Remove(tempFile.Name())
		t.Fatal(err)
	}

	if err := tempFile.Close(); err != nil {
		os.Remove(tempFile.Name())
		t.Fatal(err)
	}

	return tempFile.Name(), func() {
		os.Remove(tempFile.Name())
	}
}

func TestBase(t *testing.T) {
	tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
	defer cleanup()
	receivedFile := filepath.Base(tempFile)
	defer os.Remove(receivedFile)

	go tcp.Run("debug", "127.0.0.1", "8286", "pass123", "8287")
	time.Sleep(200 * time.Millisecond)
	go tcp.Run("debug", "127.0.0.1", "8287", "pass123")
	time.Sleep(200 * time.Millisecond)

	uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))

	sender, err := New(Options{
		IsSender:      true,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8286",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		t.Fatalf("Create sender failed: %v", err)
	}

	filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
	if errGet != nil {
		t.Fatalf("Get file info failed: %v", errGet)
	}

	receiver, err := New(Options{
		IsSender:      false,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8286",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		t.Fatalf("Create receiver failed: %v", err)
	}

	fatalErr := make(chan error, 1)

	failTest := func(err error) {
		select {
		case fatalErr <- err:
		default:
		}
	}

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		log.Warn("Send")
		if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
			failTest(fmt.Errorf("Send failed: %w", err))
		}
	}()

	go func() {
		defer wg.Done()

		if err := waitHashed(sender); err != nil {
			failTest(fmt.Errorf("waitHashed failed: %w", err))
			return
		}

		log.Warn("Receive")
		if err := receiver.Receive(); err != nil {
			failTest(fmt.Errorf("Receive failed: %w", err))
		}
	}()

	go func() {
		for i := 0; i < 3000; i++ {
			if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
				time.Sleep(time.Millisecond)
				if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
					log.Warn("Step2FileInfoTransferred reached")
					return
				}
				log.Warn("Step1ChannelSecured reached")
			}
			time.Sleep(time.Millisecond)
		}
	}()

	done := make(chan bool, 1)
	go func() {
		wg.Wait()
		done <- true
	}()

	select {
	case err := <-fatalErr:
		t.Fatal(err)
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("Test timeout after 5 seconds")
	}
}

func TestCtx(t *testing.T) {
	tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
	defer cleanup()
	receivedFile := filepath.Base(tempFile)
	defer os.Remove(receivedFile)

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8288", "pass123", "8289")
	time.Sleep(200 * time.Millisecond)
	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8289", "pass123")
	time.Sleep(200 * time.Millisecond)

	uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))

	sender, err := NewCtx(ctx, Options{
		IsSender:      true,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8288",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		t.Fatalf("Create sender failed: %v", err)
	}

	filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
	if errGet != nil {
		t.Fatalf("Get file info failed: %v", errGet)
	}

	receiver, err := NewCtx(ctx, Options{
		IsSender:      false,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8288",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		t.Fatalf("Create receiver failed: %v", err)
	}

	fatalErr := make(chan error, 1)

	failTest := func(err error) {
		select {
		case fatalErr <- err:
		default:
		}
	}

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		log.Warn("Send")
		if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
			failTest(fmt.Errorf("Send failed: %w", err))
		}
	}()

	go func() {
		defer wg.Done()

		if err := waitHashed(sender); err != nil {
			failTest(fmt.Errorf("waitHashed failed: %w", err))
			return
		}

		log.Warn("Receive")
		if err := receiver.Receive(); err != nil {
			failTest(fmt.Errorf("Receive failed: %w", err))
		}
	}()

	go func() {
		for i := 0; i < 3000; i++ {
			if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
				time.Sleep(time.Millisecond)
				if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
					log.Warn("Step2FileInfoTransferred reached")
					return
				}
				log.Warn("Step1ChannelSecured reached")
			}
			time.Sleep(time.Millisecond)
		}
	}()

	done := make(chan bool, 1)
	go func() {
		wg.Wait()
		done <- true
	}()

	select {
	case err := <-fatalErr:
		t.Fatal(err)
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("Test timeout after 5 seconds")
	}
}

func validErrors(err error) bool {
	s := err.Error()
	return strings.Contains(s, "cancel") ||
		strings.Contains(s, "context") ||
		strings.Contains(s, "reset") ||
		strings.Contains(s, "broken") ||
		strings.Contains(s, "refusing") ||
		strings.Contains(s, "EOF") ||
		strings.Contains(s, "closed")
}

func result(t *testing.T, err error) {
	if err != nil {
		if validErrors(err) {
			t.Logf("Expected error during context cancellation: %v", err)
		} else {
			t.Errorf("Unexpected error during cancellation: %v", err)
		}
		return
	}
	t.Error("Transfer should have been interrupted by context cancellation")
}

func TestAllCtx(t *testing.T) {
	tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
	defer cleanup()
	receivedFile := filepath.Base(tempFile)
	defer os.Remove(receivedFile)

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8290", "pass123", "8291")
	time.Sleep(200 * time.Millisecond)
	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8291", "pass123")
	time.Sleep(200 * time.Millisecond)

	uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))

	sender, err := NewCtx(ctx, Options{
		IsSender:      true,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8290",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		t.Fatalf("Create sender failed: %v", err)
	}

	filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
	if errGet != nil {
		t.Fatalf("Get file info failed: %v", errGet)
	}

	receiver, err := NewCtx(ctx, Options{
		IsSender:      false,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8290",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		t.Fatalf("Create receiver failed: %v", err)
	}

	fatalErr := make(chan error, 1)

	failTest := func(err error) {
		select {
		case fatalErr <- err:
		default:
		}
	}

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		log.Warn("Send")
		if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
			failTest(fmt.Errorf("Send failed: %w", err))
		}
	}()

	go func() {
		defer wg.Done()

		if err := waitHashed(sender); err != nil {
			failTest(fmt.Errorf("waitHashed failed: %w", err))
			return
		}

		log.Warn("Receive")
		if err := receiver.Receive(); err != nil {
			failTest(fmt.Errorf("Receive failed: %w", err))
		}
	}()

	go func() {
		for i := 0; i < 3000; i++ {
			if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
				time.Sleep(time.Millisecond)
				if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
					log.Warn("Step2FileInfoTransferred reached")
					cancel()
					return
				}
				log.Warn("Step1ChannelSecured reached")
			}
			time.Sleep(time.Millisecond)
		}
	}()

	done := make(chan bool, 1)
	go func() {
		wg.Wait()
		done <- true
	}()

	select {
	case err := <-fatalErr:
		result(t, err)
	case <-done:
		t.Error("Transfer should have been interrupted by context cancellation")
	case <-time.After(5 * time.Second):
		t.Fatal("Test timeout after 5 seconds")
	}
}

func TestSendCtx(t *testing.T) {
	tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
	defer cleanup()
	receivedFile := filepath.Base(tempFile)
	defer os.Remove(receivedFile)

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	ctx2, cancel2 := context.WithCancel(context.Background())
	defer cancel2()

	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8292", "pass123", "8293")
	time.Sleep(200 * time.Millisecond)
	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8293", "pass123")
	time.Sleep(200 * time.Millisecond)

	uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))

	sender, err := NewCtx(ctx2, Options{
		IsSender:      true,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8292",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		t.Fatalf("Create sender failed: %v", err)
	}

	filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
	if errGet != nil {
		t.Fatalf("Get file info failed: %v", errGet)
	}

	receiver, err := NewCtx(ctx, Options{
		IsSender:      false,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8292",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		t.Fatalf("Create receiver failed: %v", err)
	}

	fatalErr := make(chan error, 1)

	failTest := func(err error) {
		select {
		case fatalErr <- err:
		default:
		}
	}

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		log.Warn("Send")
		if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
			failTest(fmt.Errorf("Send failed: %w", err))
		}
	}()

	go func() {
		defer wg.Done()

		if err := waitHashed(sender); err != nil {
			failTest(fmt.Errorf("waitHashed failed: %w", err))
			return
		}

		log.Warn("Receive")
		if err := receiver.Receive(); err != nil {
			failTest(fmt.Errorf("Receive failed: %w", err))
		}
	}()

	go func() {
		for i := 0; i < 3000; i++ {
			if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
				time.Sleep(time.Millisecond)
				if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
					log.Warn("Step2FileInfoTransferred reached")
					cancel2()
					return
				}
				log.Warn("Step1ChannelSecured reached")
			}
			time.Sleep(time.Millisecond)
		}
	}()

	done := make(chan bool, 1)
	go func() {
		wg.Wait()
		done <- true
	}()

	select {
	case err := <-fatalErr:
		result(t, err)
	case <-done:
		t.Error("Transfer should have been interrupted by context cancellation")
	case <-time.After(5 * time.Second):
		t.Fatal("Test timeout after 5 seconds")
	}
}

func TestReceiveCtx(t *testing.T) {
	tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
	defer cleanup()
	receivedFile := filepath.Base(tempFile)
	defer os.Remove(receivedFile)

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	ctx2, cancel2 := context.WithCancel(context.Background())
	defer cancel2()

	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8294", "pass123", "8295")
	time.Sleep(200 * time.Millisecond)
	go tcp.RunCtx(ctx, "debug", "127.0.0.1", "8295", "pass123")
	time.Sleep(200 * time.Millisecond)

	uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))

	sender, err := NewCtx(ctx, Options{
		IsSender:      true,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8294",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		t.Fatalf("Create sender failed: %v", err)
	}

	filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
	if errGet != nil {
		t.Fatalf("Get file info failed: %v", errGet)
	}

	receiver, err := NewCtx(ctx2, Options{
		IsSender:      false,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8294",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		t.Fatalf("Create receiver failed: %v", err)
	}

	fatalErr := make(chan error, 1)

	failTest := func(err error) {
		select {
		case fatalErr <- err:
		default:
		}
	}

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		log.Warn("Send")
		if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
			failTest(fmt.Errorf("Send failed: %w", err))
		}
	}()

	go func() {
		defer wg.Done()

		if err := waitHashed(sender); err != nil {
			failTest(fmt.Errorf("waitHashed failed: %w", err))
			return
		}

		log.Warn("Receive")
		if err := receiver.Receive(); err != nil {
			failTest(fmt.Errorf("Receive failed: %w", err))
		}
	}()

	go func() {
		for i := 0; i < 3000; i++ {
			if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
				time.Sleep(time.Millisecond)
				if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
					log.Warn("Step2FileInfoTransferred reached")
					cancel2()
					return
				}
				log.Warn("Step1ChannelSecured reached")
			}
			time.Sleep(time.Millisecond)
		}
	}()

	done := make(chan bool, 1)
	go func() {
		wg.Wait()
		done <- true
	}()

	select {
	case err := <-fatalErr:
		result(t, err)
	case <-done:
		t.Error("Transfer should have been interrupted by context cancellation")
	case <-time.After(5 * time.Second):
		t.Fatal("Test timeout after 5 seconds")
	}
}

func TestRunCtx(t *testing.T) {
	tempFile, cleanup := createTestFile(t, 1024*1024) // 1 МБ
	defer cleanup()
	receivedFile := filepath.Base(tempFile)
	defer os.Remove(receivedFile)

	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	ctx2, cancel2 := context.WithCancel(context.Background())
	defer cancel2()

	go tcp.RunCtx(ctx2, "debug", "127.0.0.1", "8296", "pass123", "8297")
	time.Sleep(200 * time.Millisecond)
	go tcp.RunCtx(ctx2, "debug", "127.0.0.1", "8297", "pass123")
	time.Sleep(200 * time.Millisecond)

	uniqueSecret := fmt.Sprintf("test-%d-%d", time.Now().UnixNano(), rand.Intn(10000))

	sender, err := NewCtx(ctx, Options{
		IsSender:      true,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8296",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
		GitIgnore:     false,
	})
	if err != nil {
		t.Fatalf("Create sender failed: %v", err)
	}

	filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tempFile}, false, false, []string{})
	if errGet != nil {
		t.Fatalf("Get file info failed: %v", errGet)
	}

	receiver, err := NewCtx(ctx, Options{
		IsSender:      false,
		SharedSecret:  uniqueSecret,
		Debug:         true,
		RelayAddress:  "127.0.0.1:8296",
		RelayPassword: "pass123",
		Stdout:        false,
		NoPrompt:      true,
		DisableLocal:  true,
		Curve:         "siec",
		Overwrite:     true,
	})
	if err != nil {
		t.Fatalf("Create receiver failed: %v", err)
	}

	fatalErr := make(chan error, 1)

	failTest := func(err error) {
		select {
		case fatalErr <- err:
		default:
		}
	}

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		log.Warn("Send")
		if err := sender.Send(filesInfo, emptyFolders, totalNumberFolders); err != nil {
			failTest(fmt.Errorf("Send failed: %w", err))
		}
	}()

	go func() {
		defer wg.Done()

		if err := waitHashed(sender); err != nil {
			failTest(fmt.Errorf("waitHashed failed: %w", err))
			return
		}

		log.Warn("Receive")
		if err := receiver.Receive(); err != nil {
			failTest(fmt.Errorf("Receive failed: %w", err))
		}
	}()

	go func() {
		for i := 0; i < 3000; i++ {
			if sender.Step1ChannelSecured && receiver.Step1ChannelSecured {
				time.Sleep(time.Millisecond)
				if sender.Step2FileInfoTransferred && receiver.Step2FileInfoTransferred {
					log.Warn("Step2FileInfoTransferred reached")
					cancel2()
					return
				}
				log.Warn("Step1ChannelSecured reached")
			}
			time.Sleep(time.Millisecond)
		}
	}()

	done := make(chan bool, 1)
	go func() {
		wg.Wait()
		done <- true
	}()

	select {
	case err := <-fatalErr:
		result(t, err)
	case <-done:
		t.Error("Transfer should have been interrupted by context cancellation")
	case <-time.After(5 * time.Second):
		t.Fatal("Test timeout after 5 seconds")
	}
}


================================================
FILE: src/croc/ctx.go
================================================
// ctx.go
package croc

import (
	"context"
	"time"

	"github.com/schollz/croc/v10/src/message"
	"github.com/schollz/croc/v10/src/tcp"
	"github.com/schollz/croc/v10/src/utils"
	log "github.com/schollz/logger"
)

// stop manages graceful shutdown
type stop struct {
	ctx      context.Context
	cancel   context.CancelFunc
	stopChan chan struct{} //peerdiscovery
	run      func(debugLevel string, host string, port string, password string, banner ...string) (err error)
	hash     func(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error)
	gui      bool
}

// newStop creates a new stop manager instance
func newStop(ctx context.Context) *stop {
	s := &stop{
		stopChan: make(chan struct{}),
		run:      tcp.Run,
		hash:     utils.HashFile,
	}
	if ctx == nil {
		ctx = context.Background()
	}
	s.ctx, s.cancel = context.WithCancel(ctx)

	return s
}

func (s *stop) done() {
	<-s.ctx.Done()
	time.Sleep(time.Millisecond)
	close(s.stopChan)
	log.Trace("croc done")
}

// NewCtx creates a client with context support
func NewCtx(ctx context.Context, ops Options) (*Client, error) {
	// Create a regular c
	c, err := New(ops)
	if err != nil {
		return nil, err
	}
	c.stop = newStop(ctx)
	c.stop.gui = true
	c.stop.run = func(debugLevel string, host string, port string, password string, banner ...string) (err error) {
		return tcp.RunCtx(c.stop.ctx, debugLevel, host, port, password, banner...)
	}
	c.stop.hash = func(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error) {
		return utils.HashFileCtx(c.stop.ctx, fname, algorithm, showProgress...)
	}

	go func() {
		select {
		case <-ctx.Done():
			log.Trace("parent context canceled")
			c.SendError()
		case <-c.stopChan:
			// for stop goroutine
		}
		log.Trace("croc NewCtx done")
	}()

	return c, nil
}

// ctxErr checks whether it is necessary to interrupt my loops and goroutines
func (s *stop) ctxErr() error {
	select {
	case <-s.ctx.Done():
		return s.ctx.Err()
	default:
		return nil
	}
}

// Cancel initiates interruption of my loops and goroutines
func (s *stop) Cancel() {
	log.Trace("croc Cancel")
	if s.cancel != nil {
		s.cancel()
		s.cancel = nil
	}
}

// SendError tells the peer to interrupt their loops and goroutines
func (c *Client) SendError() {
	if c.Key != nil && len(c.conn) > 0 && c.conn[0] != nil {
		message.Send(c.conn[0], c.Key, message.Message{
			Type:    message.TypeError,
			Message: "refusing files",
		})
		time.Sleep(time.Millisecond)
	}
}


================================================
FILE: src/crypt/crypt.go
================================================
package crypt

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"log"

	"golang.org/x/crypto/argon2"
	"golang.org/x/crypto/chacha20poly1305"
	"golang.org/x/crypto/pbkdf2"
)

// New generates a new key based on a passphrase and salt
func New(passphrase []byte, usersalt []byte) (key []byte, salt []byte, err error) {
	if len(passphrase) < 1 {
		err = fmt.Errorf("need more than that for passphrase")
		return
	}
	if usersalt == nil {
		salt = make([]byte, 8)
		// http://www.ietf.org/rfc/rfc2898.txt
		// Salt.
		if _, err := rand.Read(salt); err != nil {
			log.Fatalf("can't get random salt: %v", err)
		}
	} else {
		salt = usersalt
	}
	key = pbkdf2.Key(passphrase, salt, 100, 32, sha256.New)
	return
}

// Encrypt will encrypt using the pre-generated key
func Encrypt(plaintext []byte, key []byte) (encrypted []byte, err error) {
	// generate a random iv each time
	// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
	// Section 8.2
	ivBytes := make([]byte, 12)
	if _, err = rand.Read(ivBytes); err != nil {
		log.Fatalf("can't initialize crypto: %v", err)
	}
	b, err := aes.NewCipher(key)
	if err != nil {
		return
	}
	aesgcm, err := cipher.NewGCM(b)
	if err != nil {
		return
	}
	encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
	encrypted = append(ivBytes, encrypted...)
	return
}

// Decrypt using the pre-generated key
func Decrypt(encrypted []byte, key []byte) (plaintext []byte, err error) {
	if len(encrypted) < 13 {
		err = fmt.Errorf("incorrect passphrase")
		return
	}
	b, err := aes.NewCipher(key)
	if err != nil {
		return
	}
	aesgcm, err := cipher.NewGCM(b)
	if err != nil {
		return
	}
	plaintext, err = aesgcm.Open(nil, encrypted[:12], encrypted[12:], nil)
	return
}

// NewArgon2 generates a new key based on a passphrase and salt
// using argon2
// https://pkg.go.dev/golang.org/x/crypto/argon2
func NewArgon2(passphrase []byte, usersalt []byte) (aead cipher.AEAD, salt []byte, err error) {
	if len(passphrase) < 1 {
		err = fmt.Errorf("need more than that for passphrase")
		return
	}
	if usersalt == nil {
		salt = make([]byte, 8)
		// http://www.ietf.org/rfc/rfc2898.txt
		// Salt.
		if _, err = rand.Read(salt); err != nil {
			log.Fatalf("can't get random salt: %v", err)
		}
	} else {
		salt = usersalt
	}
	aead, err = chacha20poly1305.NewX(argon2.IDKey(passphrase, salt, 1, 64*1024, 4, 32))
	return
}

// EncryptChaCha will encrypt ChaCha20-Poly1305 using the pre-generated key
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
func EncryptChaCha(plaintext []byte, aead cipher.AEAD) (encrypted []byte, err error) {
	nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead())
	if _, err := rand.Read(nonce); err != nil {
		panic(err)
	}

	// Encrypt the message and append the ciphertext to the nonce.
	encrypted = aead.Seal(nonce, nonce, plaintext, nil)
	return
}

// DecryptChaCha will decrypt ChaCha20-Poly1305 using the pre-generated key
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (plaintext []byte, err error) {
	if len(encryptedMsg) < aead.NonceSize() {
		err = fmt.Errorf("ciphertext too short")
		return
	}

	// Split nonce and ciphertext.
	nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]

	// Decrypt the message and check it wasn't tampered with.
	plaintext, err = aead.Open(nil, nonce, ciphertext, nil)
	return
}


================================================
FILE: src/crypt/crypt_test.go
================================================
package crypt

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

func BenchmarkEncrypt(b *testing.B) {
	bob, _, _ := New([]byte("password"), nil)
	for i := 0; i < b.N; i++ {
		Encrypt([]byte("hello, world"), bob)
	}
}

func BenchmarkDecrypt(b *testing.B) {
	key, _, _ := New([]byte("password"), nil)
	msg := []byte("hello, world")
	enc, _ := Encrypt(msg, key)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Decrypt(enc, key)
	}
}

func BenchmarkNewPbkdf2(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		New([]byte("password"), nil)
	}
}

func BenchmarkNewArgon2(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		NewArgon2([]byte("password"), nil)
	}
}

func BenchmarkEncryptChaCha(b *testing.B) {
	bob, _, _ := NewArgon2([]byte("password"), nil)
	for i := 0; i < b.N; i++ {
		EncryptChaCha([]byte("hello, world"), bob)
	}
}

func BenchmarkDecryptChaCha(b *testing.B) {
	key, _, _ := NewArgon2([]byte("password"), nil)
	msg := []byte("hello, world")
	enc, _ := EncryptChaCha(msg, key)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		DecryptChaCha(enc, key)
	}
}

func TestEncryption(t *testing.T) {
	key, salt, err := New([]byte("password"), nil)
	assert.Nil(t, err)
	msg := []byte("hello, world")
	enc, err := Encrypt(msg, key)
	assert.Nil(t, err)
	dec, err := Decrypt(enc, key)
	assert.Nil(t, err)
	assert.Equal(t, msg, dec)

	// check reusing the salt
	key2, _, _ := New([]byte("password"), salt)
	dec, err = Decrypt(enc, key2)
	assert.Nil(t, err)
	assert.Equal(t, msg, dec)

	// check reusing the salt
	key2, _, _ = New([]byte("wrong password"), salt)
	dec, err = Decrypt(enc, key2)
	assert.NotNil(t, err)
	assert.NotEqual(t, msg, dec)

	// error with no password
	_, err = Decrypt([]byte(""), key)
	assert.NotNil(t, err)

	// error with small password
	_, _, err = New([]byte(""), nil)
	assert.NotNil(t, err)
}

func TestEncryptionChaCha(t *testing.T) {
	key, salt, err := NewArgon2([]byte("password"), nil)
	fmt.Printf("key: %x\n", key)
	assert.Nil(t, err)
	msg := []byte("hello, world")
	enc, err := EncryptChaCha(msg, key)
	assert.Nil(t, err)
	dec, err := DecryptChaCha(enc, key)
	assert.Nil(t, err)
	assert.Equal(t, msg, dec)

	// check reusing the salt
	key2, _, _ := NewArgon2([]byte("password"), salt)
	dec, err = DecryptChaCha(enc, key2)
	assert.Nil(t, err)
	assert.Equal(t, msg, dec)

	// check reusing the salt
	key2, _, _ = NewArgon2([]byte("wrong password"), salt)
	dec, err = DecryptChaCha(enc, key2)
	assert.NotNil(t, err)
	assert.NotEqual(t, msg, dec)

	// error with no password
	_, err = DecryptChaCha([]byte(""), key)
	assert.NotNil(t, err)

	// error with small password
	_, _, err = NewArgon2([]byte(""), nil)
	assert.NotNil(t, err)
}


================================================
FILE: src/diskusage/diskusage.go
================================================
//go:build !windows
// +build !windows

package diskusage

import (
	"golang.org/x/sys/unix"
)

// DiskUsage contains usage data and provides user-friendly access methods
type DiskUsage struct {
	stat *unix.Statfs_t
}

// NewDiskUsage returns an object holding the disk usage of volumePath
// or nil in case of error (invalid path, etc)
func NewDiskUsage(volumePath string) *DiskUsage {
	stat := unix.Statfs_t{}
	err := unix.Statfs(volumePath, &stat)
	if err != nil {
		return nil
	}
	return &DiskUsage{&stat}
}

// Free returns total free bytes on file system
func (du *DiskUsage) Free() uint64 {
	return uint64(du.stat.Bfree) * uint64(du.stat.Bsize)
}

// Available return total available bytes on file system to an unprivileged user
func (du *DiskUsage) Available() uint64 {
	return uint64(du.stat.Bavail) * uint64(du.stat.Bsize)
}

// Size returns total size of the file system
func (du *DiskUsage) Size() uint64 {
	return uint64(du.stat.Blocks) * uint64(du.stat.Bsize)
}

// Used returns total bytes used in file system
func (du *DiskUsage) Used() uint64 {
	return du.Size() - du.Free()
}

// Usage returns percentage of use on the file system
func (du *DiskUsage) Usage() float32 {
	return float32(du.Used()) / float32(du.Size())
}


================================================
FILE: src/diskusage/diskusage_test.go
================================================
package diskusage

import (
	"fmt"
	"testing"
)

var KB = uint64(1024)

func TestNewDiskUsage(t *testing.T) {
	usage := NewDiskUsage(".")
	fmt.Println("Free:", usage.Free()/(KB*KB))
	fmt.Println("Available:", usage.Available()/(KB*KB))
	fmt.Println("Size:", usage.Size()/(KB*KB))
	fmt.Println("Used:", usage.Used()/(KB*KB))
	fmt.Println("Usage:", usage.Usage()*100, "%")
}


================================================
FILE: src/diskusage/diskusage_windows.go
================================================
package diskusage

import (
	"unsafe"

	"golang.org/x/sys/windows"
)

type DiskUsage struct {
	freeBytes  int64
	totalBytes int64
	availBytes int64
}

// NewDiskUsage returns an object holding the disk usage of volumePath
// or nil in case of error (invalid path, etc)
func NewDiskUsage(volumePath string) *DiskUsage {
	h := windows.MustLoadDLL("kernel32.dll")
	c := h.MustFindProc("GetDiskFreeSpaceExW")

	du := &DiskUsage{}

	c.Call(
		uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(volumePath))),
		uintptr(unsafe.Pointer(&du.freeBytes)),
		uintptr(unsafe.Pointer(&du.totalBytes)),
		uintptr(unsafe.Pointer(&du.availBytes)))

	return du
}

// Free returns total free bytes on file system
func (du *DiskUsage) Free() uint64 {
	return uint64(du.freeBytes)
}

// Available returns total available bytes on file system to an unprivileged user
func (du *DiskUsage) Available() uint64 {
	return uint64(du.availBytes)
}

// Size returns total size of the file system
func (du *DiskUsage) Size() uint64 {
	return uint64(du.totalBytes)
}

// Used returns total bytes used in file system
func (du *DiskUsage) Used() uint64 {
	return du.Size() - du.Free()
}

// Usage returns percentage of use on the file system
func (du *DiskUsage) Usage() float32 {
	return float32(du.Used()) / float32(du.Size())
}


================================================
FILE: src/install/Makefile
================================================
# VERSION=8.X.Y make release

release:
	cd ../../ && go run src/install/updateversion.go
	git commit -am "bump ${VERSION}"
	git tag -af v${VERSION} -m "v${VERSION}"	
	git push
	git push --tags
	cp zsh_autocomplete ../../
	cp bash_autocomplete ../../
	cd ../../ && goreleaser release
	cd ../../ && ./src/install/prepare-sources-tarball.sh
	cd ../../ && ./src/install/upload-src-tarball.sh

test:
	cp zsh_autocomplete ../../
	cp bash_autocomplete ../../
	cd ../../ && go generate
	cd ../../ && goreleaser release --skip-publish


================================================
FILE: src/install/bash_autocomplete
================================================
: ${PROG:=$(basename ${BASH_SOURCE})}

_cli_bash_autocomplete() {
  if [[ "${COMP_WORDS[0]}" != "source" ]]; then
    local cur opts base
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    if [[ "$cur" == "-"* ]]; then
      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
    else
      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
    fi
    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
    return 0
  fi
}

complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
unset PROG


================================================
FILE: src/install/default.txt
================================================
#!/bin/bash - 
#===============================================================================
#
#          FILE: default.txt
# 
#         USAGE: curl https://getcroc.schollz.com | bash
#                 OR
#                wget -qO- https://getcroc.schollz.com | bash
# 
#   DESCRIPTION: croc Installer Script.
#
#                This script installs croc into a specified prefix.
#                Default prefix = /usr/local/bin
#
#       OPTIONS: -p, --prefix "${INSTALL_PREFIX}"
#                      Prefix to install croc into.  Defaults to /usr/local/bin
#  REQUIREMENTS: bash, uname, tar/unzip, curl/wget, sudo/doas (if not run
#                as root), install, mktemp, sha256sum/shasum/sha256
#
#          BUGS: ...hopefully not.  Please report.
#
#         NOTES: Homepage: https://schollz.com/software/croc
#                  Issues: https://github.com/schollz/croc/issues
#
#       CREATED: 08/10/2019 16:41
#      REVISION: 0.9.2
#===============================================================================
set -o nounset                              # Treat unset variables as an error

#-------------------------------------------------------------------------------
# DEFAULTS
#-------------------------------------------------------------------------------
PREFIX="${PREFIX:-}"
ANDROID_ROOT="${ANDROID_ROOT:-}"

# Termux on Android has ${PREFIX} set which already ends with '/usr'
if [[ -n "${ANDROID_ROOT}" && -n "${PREFIX}" ]]; then
  INSTALL_PREFIX="${PREFIX}/bin"
else
  INSTALL_PREFIX="/usr/local/bin"
fi

#-------------------------------------------------------------------------------
# FUNCTIONS
#-------------------------------------------------------------------------------


#---  FUNCTION  ----------------------------------------------------------------
#          NAME:  print_banner
#   DESCRIPTION:  Prints a banner
#    PARAMETERS:  none
#       RETURNS:  0
#-------------------------------------------------------------------------------
print_banner() {
  cat <<-'EOF'
=================================================
              ____
             / ___|_ __ ___   ___
            | |   | '__/ _ \ / __|
            | |___| | | (_) | (__
             \____|_|  \___/ \___|

       ___           _        _ _
      |_ _|_ __  ___| |_ __ _| | | ___ _ __
       | || '_ \/ __| __/ _` | | |/ _ \ '__|
       | || | | \__ \ || (_| | | |  __/ |
      |___|_| |_|___/\__\__,_|_|_|\___|_| 
==================================================
EOF
}

#---  FUNCTION  ----------------------------------------------------------------
#          NAME:  print_help
#   DESCRIPTION:  Prints out a help message
#    PARAMETERS:  none
#       RETURNS:  0
#-------------------------------------------------------------------------------
print_help() {
  local help_header
  local help_message

  help_header="croc Installer Script"
  help_message="Usage:
  -p INSTALL_PREFIX
      Prefix to install croc into.  Directory must already exist.
      Default = /usr/local/bin ('\${PREFIX}/bin' on Termux for Android)
  
  -h
      Prints this helpful message and exit."

  echo "${help_header}"
  echo ""
  echo "${help_message}"
}

#---  FUNCTION  ----------------------------------------------------------------
#          NAME:  print_message
#   DESCRIPTION:  Prints a message all fancy like
#    PARAMETERS:  $1 = Message to print
#                 $2 = Severity. info, ok, error, warn
#       RETURNS:  Formatted Message to stdout
#-------------------------------------------------------------------------------
print_message() {
  local message
  local severity
  local red
  local green
  local yellow
  local nc

  message="${1}"
  severity="${2}"
  red='\e[0;31m'
  green='\e[0;32m'
  yellow='\e[1;33m'
  nc='\e[0m'

  case "${severity}" in
    "info" ) echo -e
Download .txt
gitextract_w4v75_0t/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── config.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci.yml
│       ├── deploy.yml
│       ├── release.yml
│       ├── stale.yml
│       └── winget.yml
├── .gitignore
├── .goreleaser.yml
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── croc-entrypoint.sh
├── croc.service
├── go.mod
├── go.sum
├── main.go
└── src/
    ├── cli/
    │   └── cli.go
    ├── comm/
    │   ├── comm.go
    │   └── comm_test.go
    ├── compress/
    │   ├── compress.go
    │   └── compress_test.go
    ├── croc/
    │   ├── croc.go
    │   ├── croc_test.go
    │   └── ctx.go
    ├── crypt/
    │   ├── crypt.go
    │   └── crypt_test.go
    ├── diskusage/
    │   ├── diskusage.go
    │   ├── diskusage_test.go
    │   └── diskusage_windows.go
    ├── install/
    │   ├── Makefile
    │   ├── bash_autocomplete
    │   ├── default.txt
    │   ├── prepare-sources-tarball.sh
    │   ├── updateversion.go
    │   ├── upload-src-tarball.sh
    │   └── zsh_autocomplete
    ├── message/
    │   ├── message.go
    │   └── message_test.go
    ├── mnemonicode/
    │   ├── mnemonicode.go
    │   ├── mnemonicode_test.go
    │   └── wordlist.go
    ├── models/
    │   ├── constants.go
    │   └── models_test.go
    ├── tcp/
    │   ├── ctx.go
    │   ├── defaults.go
    │   ├── options.go
    │   ├── tcp.go
    │   └── tcp_test.go
    └── utils/
        ├── ctx.go
        ├── utils.go
        └── utils_test.go
Download .txt
SYMBOL INDEX (299 symbols across 30 files)

FILE: main.go
  function main (line 17) | func main() {

FILE: src/cli/cli.go
  function Run (line 32) | func Run() (err error) {
  function setDebugLevel (line 230) | func setDebugLevel(c *cli.Context) {
  function getSendConfigFile (line 249) | func getSendConfigFile(requireValidPath bool) string {
  function getClassicConfigFile (line 258) | func getClassicConfigFile(requireValidPath bool) string {
  function getReceiveConfigFile (line 267) | func getReceiveConfigFile(requireValidPath bool) (string, error) {
  function determinePass (line 276) | func determinePass(c *cli.Context) (pass string) {
  function send (line 285) | func send(c *cli.Context) (err error) {
  function getStdin (line 512) | func getStdin() (fnames []string, err error) {
  function makeTempFileWithString (line 529) | func makeTempFileWithString(s string) (fnames []string, err error) {
  function saveConfig (line 548) | func saveConfig(c *cli.Context, crocOptions croc.Options) {
  type TabComplete (line 581) | type TabComplete struct
    method Do (line 583) | func (t TabComplete) Do(line []rune, pos int) ([][]rune, int) {
  function receive (line 611) | func receive(c *cli.Context) (err error) {
  function relay (line 779) | func relay(c *cli.Context) (err error) {

FILE: src/comm/comm.go
  constant maxReadMessageSize (line 24) | maxReadMessageSize = 64 * 1024 * 1024
  type Comm (line 27) | type Comm struct
    method Connection (line 110) | func (c *Comm) Connection() net.Conn {
    method Close (line 115) | func (c *Comm) Close() {
    method Write (line 121) | func (c *Comm) Write(b []byte) (n int, err error) {
    method Read (line 141) | func (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {
    method Send (line 200) | func (c *Comm) Send(message []byte) (err error) {
    method Receive (line 206) | func (c *Comm) Receive() (b []byte, err error) {
  function NewConnection (line 32) | func NewConnection(address string, timelimit ...time.Duration) (c *Comm,...
  function New (line 94) | func New(c net.Conn) *Comm {

FILE: src/comm/comm_test.go
  function TestComm (line 15) | func TestComm(t *testing.T) {
  function TestReceiveRejectsOversizedMessage (line 83) | func TestReceiveRejectsOversizedMessage(t *testing.T) {

FILE: src/compress/compress.go
  function CompressWithOption (line 12) | func CompressWithOption(src []byte, level int) []byte {
  function Compress (line 19) | func Compress(src []byte) []byte {
  function Decompress (line 26) | func Decompress(src []byte) []byte {
  function compress (line 34) | func compress(src []byte, dest io.Writer, level int) {
  function decompress (line 47) | func decompress(src io.Reader, dest io.Writer) {

FILE: src/compress/compress_test.go
  function BenchmarkCompressLevelMinusTwo (line 40) | func BenchmarkCompressLevelMinusTwo(b *testing.B) {
  function BenchmarkCompressLevelNine (line 46) | func BenchmarkCompressLevelNine(b *testing.B) {
  function BenchmarkCompressLevelMinusTwoBinary (line 52) | func BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {
  function BenchmarkCompressLevelNineBinary (line 62) | func BenchmarkCompressLevelNineBinary(b *testing.B) {
  function TestCompress (line 72) | func TestCompress(t *testing.T) {

FILE: src/croc/croc.go
  function init (line 49) | func init() {
  function Debug (line 54) | func Debug(debug bool) {
  type Options (line 63) | type Options struct
  type SimpleMessage (line 97) | type SimpleMessage struct
  type Client (line 103) | type Client struct
    method sendCollectFiles (line 512) | func (c *Client) sendCollectFiles(filesInfo []FileInfo) (err error) {
    method setupLocalRelay (line 570) | func (c *Client) setupLocalRelay() {
    method broadcastOnLocalNetwork (line 599) | func (c *Client) broadcastOnLocalNetwork(useipv6 bool) {
    method transferOverLocalRelay (line 629) | func (c *Client) transferOverLocalRelay(errchan chan<- error) {
    method Send (line 669) | func (c *Client) Send(filesInfo []FileInfo, emptyFoldersToTransfer []F...
    method Receive (line 893) | func (c *Client) Receive() (err error) {
    method transfer (line 1162) | func (c *Client) transfer() (err error) {
    method createEmptyFolder (line 1276) | func (c *Client) createEmptyFolder(i int) (err error) {
    method processMessageFileInfo (line 1298) | func (c *Client) processMessageFileInfo(m message.Message) (done bool,...
    method processMessagePake (line 1441) | func (c *Client) processMessagePake(m message.Message) (err error) {
    method processExternalIP (line 1546) | func (c *Client) processExternalIP(m message.Message) (done bool, err ...
    method processMessage (line 1566) | func (c *Client) processMessage(payload []byte) (done bool, err error) {
    method updateIfSenderChannelSecured (line 1663) | func (c *Client) updateIfSenderChannelSecured() (err error) {
    method recipientInitializeFile (line 1694) | func (c *Client) recipientInitializeFile() (err error) {
    method recipientGetFileReady (line 1753) | func (c *Client) recipientGetFileReady(finished bool) (err error) {
    method createEmptyFileAndFinish (line 1836) | func (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (e...
    method updateIfRecipientHasFileInfo (line 1889) | func (c *Client) updateIfRecipientHasFileInfo() (err error) {
    method fmtPrintUpdate (line 1975) | func (c *Client) fmtPrintUpdate() {
    method updateState (line 1984) | func (c *Client) updateState() (err error) {
    method setBar (line 2050) | func (c *Client) setBar() {
    method receiveData (line 2084) | func (c *Client) receiveData(i int) {
    method sendData (line 2183) | func (c *Client) sendData(i int) {
  type Chunk (line 158) | type Chunk struct
  type FileInfo (line 164) | type FileInfo struct
  type RemoteFileRequest (line 180) | type RemoteFileRequest struct
  type SenderInfo (line 187) | type SenderInfo struct
  function New (line 199) | func New(ops Options) (c *Client, err error) {
  type TransferOptions (line 272) | type TransferOptions struct
  function isEmptyFolder (line 278) | func isEmptyFolder(folderPath string) (bool, error) {
  function gitWalk (line 294) | func gitWalk(dir string, gitObj *ignore.GitIgnore, files map[string]bool) {
  function isChild (line 325) | func isChild(parentPath, childPath string) bool {
  function GetFilesInfo (line 335) | func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclu...
  function showReceiveCommandQrCode (line 885) | func showReceiveCommandQrCode(command string) {
  function formatDescription (line 1801) | func formatDescription(description string) string {
  function isExecutableInPath (line 2285) | func isExecutableInPath(executableName string) bool {
  function copyToClipboard (line 2291) | func copyToClipboard(str string, quiet bool, extendedClipboard bool) {

FILE: src/croc/croc_test.go
  function init (line 21) | func init() {
  function TestCrocReadme (line 32) | func TestCrocReadme(t *testing.T) {
  function TestCrocEmptyFolder (line 96) | func TestCrocEmptyFolder(t *testing.T) {
  function TestCrocSymlink (line 162) | func TestCrocSymlink(t *testing.T) {
  function TestCrocIgnoreGit (line 238) | func TestCrocIgnoreGit(t *testing.T) {
  function TestCrocLocal (line 265) | func TestCrocLocal(t *testing.T) {
  function TestCrocError (line 334) | func TestCrocError(t *testing.T) {
  function TestReceiverStdoutWithInvalidSecret (line 374) | func TestReceiverStdoutWithInvalidSecret(t *testing.T) {
  function TestCleanUp (line 402) | func TestCleanUp(t *testing.T) {
  function hashed (line 433) | func hashed(c *Client) bool {
  function waitHashed (line 445) | func waitHashed(sender *Client) (err error) {
  function createTestFile (line 457) | func createTestFile(t *testing.T, size int) (string, func()) {
  function TestBase (line 484) | func TestBase(t *testing.T) {
  function TestCtx (line 598) | func TestCtx(t *testing.T) {
  function validErrors (line 715) | func validErrors(err error) bool {
  function result (line 726) | func result(t *testing.T, err error) {
  function TestAllCtx (line 738) | func TestAllCtx(t *testing.T) {
  function TestSendCtx (line 857) | func TestSendCtx(t *testing.T) {
  function TestReceiveCtx (line 979) | func TestReceiveCtx(t *testing.T) {
  function TestRunCtx (line 1101) | func TestRunCtx(t *testing.T) {

FILE: src/croc/ctx.go
  type stop (line 15) | type stop struct
    method done (line 39) | func (s *stop) done() {
    method ctxErr (line 77) | func (s *stop) ctxErr() error {
    method Cancel (line 87) | func (s *stop) Cancel() {
  function newStop (line 25) | func newStop(ctx context.Context) *stop {
  function NewCtx (line 47) | func NewCtx(ctx context.Context, ops Options) (*Client, error) {
  method SendError (line 96) | func (c *Client) SendError() {

FILE: src/crypt/crypt.go
  function New (line 17) | func New(passphrase []byte, usersalt []byte) (key []byte, salt []byte, e...
  function Encrypt (line 37) | func Encrypt(plaintext []byte, key []byte) (encrypted []byte, err error) {
  function Decrypt (line 59) | func Decrypt(encrypted []byte, key []byte) (plaintext []byte, err error) {
  function NewArgon2 (line 79) | func NewArgon2(passphrase []byte, usersalt []byte) (aead cipher.AEAD, sa...
  function EncryptChaCha (line 100) | func EncryptChaCha(plaintext []byte, aead cipher.AEAD) (encrypted []byte...
  function DecryptChaCha (line 113) | func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (plaintext []b...

FILE: src/crypt/crypt_test.go
  function BenchmarkEncrypt (line 10) | func BenchmarkEncrypt(b *testing.B) {
  function BenchmarkDecrypt (line 17) | func BenchmarkDecrypt(b *testing.B) {
  function BenchmarkNewPbkdf2 (line 27) | func BenchmarkNewPbkdf2(b *testing.B) {
  function BenchmarkNewArgon2 (line 34) | func BenchmarkNewArgon2(b *testing.B) {
  function BenchmarkEncryptChaCha (line 41) | func BenchmarkEncryptChaCha(b *testing.B) {
  function BenchmarkDecryptChaCha (line 48) | func BenchmarkDecryptChaCha(b *testing.B) {
  function TestEncryption (line 58) | func TestEncryption(t *testing.T) {
  function TestEncryptionChaCha (line 89) | func TestEncryptionChaCha(t *testing.T) {

FILE: src/diskusage/diskusage.go
  type DiskUsage (line 11) | type DiskUsage struct
    method Free (line 27) | func (du *DiskUsage) Free() uint64 {
    method Available (line 32) | func (du *DiskUsage) Available() uint64 {
    method Size (line 37) | func (du *DiskUsage) Size() uint64 {
    method Used (line 42) | func (du *DiskUsage) Used() uint64 {
    method Usage (line 47) | func (du *DiskUsage) Usage() float32 {
  function NewDiskUsage (line 17) | func NewDiskUsage(volumePath string) *DiskUsage {

FILE: src/diskusage/diskusage_test.go
  function TestNewDiskUsage (line 10) | func TestNewDiskUsage(t *testing.T) {

FILE: src/diskusage/diskusage_windows.go
  type DiskUsage (line 9) | type DiskUsage struct
    method Free (line 33) | func (du *DiskUsage) Free() uint64 {
    method Available (line 38) | func (du *DiskUsage) Available() uint64 {
    method Size (line 43) | func (du *DiskUsage) Size() uint64 {
    method Used (line 48) | func (du *DiskUsage) Used() uint64 {
    method Usage (line 53) | func (du *DiskUsage) Usage() float32 {
  function NewDiskUsage (line 17) | func NewDiskUsage(volumePath string) *DiskUsage {

FILE: src/install/updateversion.go
  function main (line 10) | func main() {
  function run (line 17) | func run() (err error) {
  function replaceInFile (line 44) | func replaceInFile(fname, start, end, replacement string) (err error) {
  function getStringInBetween (line 65) | func getStringInBetween(str, start, end string) (result string) {

FILE: src/message/message.go
  type Type (line 13) | type Type
  constant TypePAKE (line 16) | TypePAKE           Type = "pake"
  constant TypeExternalIP (line 17) | TypeExternalIP     Type = "externalip"
  constant TypeFinished (line 18) | TypeFinished       Type = "finished"
  constant TypeError (line 19) | TypeError          Type = "error"
  constant TypeCloseRecipient (line 20) | TypeCloseRecipient Type = "close-recipient"
  constant TypeCloseSender (line 21) | TypeCloseSender    Type = "close-sender"
  constant TypeRecipientReady (line 22) | TypeRecipientReady Type = "recipientready"
  constant TypeFileInfo (line 23) | TypeFileInfo       Type = "fileinfo"
  type Message (line 27) | type Message struct
    method String (line 35) | func (m Message) String() string {
  function Send (line 41) | func Send(c *comm.Comm, key []byte, m Message) (err error) {
  function Encode (line 51) | func Encode(key []byte, m Message) (b []byte, err error) {
  function Decode (line 67) | func Decode(key []byte, b []byte) (m Message, err error) {

FILE: src/message/message_test.go
  function TestMessage (line 18) | func TestMessage(t *testing.T) {
  function TestMessageNoPass (line 38) | func TestMessageNoPass(t *testing.T) {
  function TestSend (line 51) | func TestSend(t *testing.T) {

FILE: src/mnemonicode/mnemonicode.go
  constant base (line 31) | base = 1626
  function WordsRequired (line 41) | func WordsRequired(length int) int {
  function EncodeWordList (line 48) | func EncodeWordList(dst []string, src []byte) (result []string) {

FILE: src/mnemonicode/mnemonicode_test.go
  function TestWordsRequired (line 7) | func TestWordsRequired(t *testing.T) {
  function TestEncodeWordList (line 32) | func TestEncodeWordList(t *testing.T) {
  function TestEncodeWordListConsistency (line 100) | func TestEncodeWordListConsistency(t *testing.T) {
  function TestEncodeWordListCapacityHandling (line 118) | func TestEncodeWordListCapacityHandling(t *testing.T) {
  function TestEncodeWordListBoundaryValues (line 135) | func TestEncodeWordListBoundaryValues(t *testing.T) {

FILE: src/mnemonicode/wordlist.go
  constant WordListVersion (line 32) | WordListVersion = "0.7"
  function init (line 36) | func init() {
  constant longestWord (line 42) | longestWord = 7

FILE: src/models/constants.go
  constant TCP_BUFFER_SIZE (line 16) | TCP_BUFFER_SIZE = 1024 * 64
  function getConfigFile (line 49) | func getConfigFile(requireValidPath bool) (fname string, err error) {
  function init (line 58) | func init() {
  function lookup (line 105) | func lookup(address string) (ipaddress string, err error) {
  function localLookupIP (line 135) | func localLookupIP(address string) (ipaddress string, err error) {
  function remoteLookupIP (line 152) | func remoteLookupIP(address, dns string) (ipaddress string, err error) {

FILE: src/models/models_test.go
  function TestConstants (line 10) | func TestConstants(t *testing.T) {
  function TestPublicDNSServers (line 24) | func TestPublicDNSServers(t *testing.T) {
  function TestLocalLookupIP (line 71) | func TestLocalLookupIP(t *testing.T) {
  function TestRemoteLookupIPTimeout (line 112) | func TestRemoteLookupIPTimeout(t *testing.T) {
  function TestLookupFunction (line 128) | func TestLookupFunction(t *testing.T) {
  function TestGetConfigFile (line 170) | func TestGetConfigFile(t *testing.T) {

FILE: src/tcp/ctx.go
  type stop (line 14) | type stop struct
    method Cancel (line 35) | func (s *stop) Cancel() {
  function newStop (line 24) | func newStop(ctx context.Context) *stop {
  function RunCtx (line 43) | func RunCtx(ctx context.Context, debugLevel, host, port, password string...
  function WithCtx (line 47) | func WithCtx(ctx context.Context) serverOptsFunc {
  function Ignore (line 59) | func Ignore(err error) error {

FILE: src/tcp/defaults.go
  constant DEFAULT_LOG_LEVEL (line 6) | DEFAULT_LOG_LEVEL             = "debug"
  constant DEFAULT_ROOM_CLEANUP_INTERVAL (line 7) | DEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time.Minute
  constant DEFAULT_ROOM_TTL (line 8) | DEFAULT_ROOM_TTL              = 3 * time.Hour

FILE: src/tcp/options.go
  type serverOptsFunc (line 11) | type serverOptsFunc
  function WithBanner (line 13) | func WithBanner(banner ...string) serverOptsFunc {
  function WithLogLevel (line 22) | func WithLogLevel(level string) serverOptsFunc {
  function WithRoomCleanupInterval (line 32) | func WithRoomCleanupInterval(interval time.Duration) serverOptsFunc {
  function WithRoomTTL (line 39) | func WithRoomTTL(ttl time.Duration) serverOptsFunc {
  function containsSlice (line 46) | func containsSlice(s []string, e string) bool {

FILE: src/tcp/tcp.go
  type server (line 20) | type server struct
    method start (line 91) | func (s *server) start() (err error) {
    method run (line 119) | func (s *server) run() (err error) {
    method deleteOldRooms (line 232) | func (s *server) deleteOldRooms() {
    method clientCommunication (line 278) | func (s *server) clientCommunication(c *comm.Comm) (room string, err e...
    method deleteRoom (line 439) | func (s *server) deleteRoom(room string) {
  type roomInfo (line 36) | type roomInfo struct
  type roomMap (line 43) | type roomMap struct
  constant pingRoom (line 48) | pingRoom = "pinglkasjdlfjsaldjf"
  function newDefaultServer (line 51) | func newDefaultServer() *server {
  function RunWithOptionsAsync (line 62) | func RunWithOptionsAsync(host, port, password string, opts ...serverOpts...
  function Run (line 77) | func Run(debugLevel, host, port, password string, banner ...string) (err...
  function maskedPassword (line 82) | func maskedPassword(password string) (s string) {
  function chanFromConn (line 459) | func chanFromConn(conn net.Conn) chan []byte {
  function pipe (line 489) | func pipe(conn1 net.Conn, conn2 net.Conn) {
  function PingServer (line 514) | func PingServer(address string) (err error) {
  function ConnectToTCPServer (line 539) | func ConnectToTCPServer(address, password, room string, timelimit ...tim...

FILE: src/tcp/tcp_test.go
  function BenchmarkConnection (line 14) | func BenchmarkConnection(b *testing.B) {
  function TestTCP (line 27) | func TestTCP(t *testing.T) {
  function TestTCPctx (line 80) | func TestTCPctx(t *testing.T) {
  function TestWrongPassword (line 162) | func TestWrongPassword(t *testing.T) {
  function TestRoomIsolation (line 173) | func TestRoomIsolation(t *testing.T) {
  function TestRoomRecreationAfterTTL (line 219) | func TestRoomRecreationAfterTTL(t *testing.T) {
  function TestLargeDataTransfer (line 252) | func TestLargeDataTransfer(t *testing.T) {
  function TestServerReleasesPort (line 288) | func TestServerReleasesPort(t *testing.T) {

FILE: src/utils/ctx.go
  type ctxFile (line 20) | type ctxFile struct
    method Read (line 31) | func (c *ctxFile) Read(p []byte) (n int, err error) {
    method ReadAt (line 45) | func (c *ctxFile) ReadAt(p []byte, off int64) (n int, err error) {
    method Seek (line 59) | func (c *ctxFile) Seek(offset int64, whence int) (n int64, err error) {
  function NewCtxFile (line 26) | func NewCtxFile(ctx context.Context, f *os.File) *ctxFile {
  function HashFileCtx (line 73) | func HashFileCtx(ctx context.Context, fname string, algorithm string, sh...
  function IMOHashReader (line 164) | func IMOHashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) (...
  function IMOHashReaderFull (line 189) | func IMOHashReaderFull(sr *io.SectionReader, bar *progressbar.ProgressBa...
  function MD5HashReader (line 211) | func MD5HashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) (...
  function XXHashReader (line 232) | func XXHashReader(sr *io.SectionReader, bar *progressbar.ProgressBar) ([...
  function HighwayHashReader (line 251) | func HighwayHashReader(sr *io.SectionReader, bar *progressbar.ProgressBa...

FILE: src/utils/utils.go
  constant NbPinNumbers (line 33) | NbPinNumbers = 4
  constant NbBytesWords (line 34) | NbBytesWords = 4
  function GetConfigDir (line 37) | func GetConfigDir(requireValidPath bool) (homedir string, err error) {
  function Exists (line 63) | func Exists(name string) bool {
  function GetInput (line 73) | func GetInput(prompt string) string {
  function HashFile (line 82) | func HashFile(fname string, algorithm string, showProgress ...bool) (has...
  function HighwayHashFile (line 115) | func HighwayHashFile(fname string, doShowProgress bool) (hashHighway []b...
  function MD5HashFile (line 157) | func MD5HashFile(fname string, doShowProgress bool) (hash256 []byte, err...
  function IMOHashFile (line 195) | func IMOHashFile(fname string) (hash []byte, err error) {
  function IMOHashFileFull (line 202) | func IMOHashFileFull(fname string) (hash []byte, err error) {
  function XXHashFile (line 209) | func XXHashFile(fname string, doShowProgress bool) (hash256 []byte, err ...
  function SHA256 (line 244) | func SHA256(s string) string {
  function PublicIP (line 251) | func PublicIP() (ip string, err error) {
  function LocalIP (line 271) | func LocalIP() string {
  function GenerateRandomPin (line 285) | func GenerateRandomPin() string {
  function GetRandomName (line 300) | func GetRandomName() string {
  function ByteCountDecimal (line 309) | func ByteCountDecimal(b int64) string {
  function MissingChunks (line 325) | func MissingChunks(fname string, fsize int64, chunkSize int) (chunkRange...
  function ChunkRangesToChunks (line 400) | func ChunkRangesToChunks(chunkRanges []int64) (chunks []int64) {
  function GetLocalIPs (line 415) | func GetLocalIPs() (ips []string, err error) {
  function RandomFileName (line 435) | func RandomFileName() (fname string, err error) {
  function FindOpenPorts (line 445) | func FindOpenPorts(host string, portNumStart, numPorts int) (openPorts [...
  function init (line 466) | func init() {
  function IsLocalIP (line 485) | func IsLocalIP(ipaddress string) bool {
  function ZipDirectory (line 502) | func ZipDirectory(destination string, source string) (err error) {
  function UnzipDirectory (line 640) | func UnzipDirectory(destination string, source string) error {
  function resolveUnzipPath (line 738) | func resolveUnzipPath(destination string, entryName string) (string, err...
  function ValidFileName (line 763) | func ValidFileName(fname string) (err error) {
  constant crocRemovalFile (line 793) | crocRemovalFile = "croc-marked-files.txt"
  function MarkFileForRemoval (line 795) | func MarkFileForRemoval(fname string) {
  function RemoveMarkedFiles (line 806) | func RemoveMarkedFiles() (err error) {

FILE: src/utils/utils_test.go
  constant TCP_BUFFER_SIZE (line 20) | TCP_BUFFER_SIZE = 1024 * 64
  function bigFile (line 24) | func bigFile() {
  function BenchmarkMD5 (line 28) | func BenchmarkMD5(b *testing.B) {
  function BenchmarkXXHash (line 36) | func BenchmarkXXHash(b *testing.B) {
  function BenchmarkImoHash (line 44) | func BenchmarkImoHash(b *testing.B) {
  function BenchmarkHighwayHash (line 52) | func BenchmarkHighwayHash(b *testing.B) {
  function BenchmarkImoHashFull (line 60) | func BenchmarkImoHashFull(b *testing.B) {
  function BenchmarkSha256 (line 68) | func BenchmarkSha256(b *testing.B) {
  function BenchmarkMissingChunks (line 75) | func BenchmarkMissingChunks(b *testing.B) {
  function TestExists (line 83) | func TestExists(t *testing.T) {
  function TestMD5HashFile (line 91) | func TestMD5HashFile(t *testing.T) {
  function TestHighwayHashFile (line 101) | func TestHighwayHashFile(t *testing.T) {
  function TestIMOHashFile (line 111) | func TestIMOHashFile(t *testing.T) {
  function TestXXHashFile (line 119) | func TestXXHashFile(t *testing.T) {
  function TestSHA256 (line 129) | func TestSHA256(t *testing.T) {
  function TestByteCountDecimal (line 133) | func TestByteCountDecimal(t *testing.T) {
  function TestMissingChunks (line 139) | func TestMissingChunks(t *testing.T) {
  function TestHashFile (line 195) | func TestHashFile(t *testing.T) {
  function TestPublicIP (line 215) | func TestPublicIP(t *testing.T) {
  function TestLocalIP (line 222) | func TestLocalIP(t *testing.T) {
  function TestGetRandomName (line 228) | func TestGetRandomName(t *testing.T) {
  function intSliceSame (line 234) | func intSliceSame(a, b []int) bool {
  function TestFindOpenPorts (line 246) | func TestFindOpenPorts(t *testing.T) {
  function TestIsLocalIP (line 254) | func TestIsLocalIP(t *testing.T) {
  function TestValidFileName (line 258) | func TestValidFileName(t *testing.T) {
  function TestUnzipDirectory (line 289) | func TestUnzipDirectory(t *testing.T) {
  function TestUnzipToNonExistentDirectory (line 359) | func TestUnzipToNonExistentDirectory(t *testing.T) {
  function TestUnzipDirectoryRejectsPathTraversal (line 420) | func TestUnzipDirectoryRejectsPathTraversal(t *testing.T) {
  function TestResolveUnzipPathRejectsAbsolutePathEntry (line 445) | func TestResolveUnzipPathRejectsAbsolutePathEntry(t *testing.T) {
  function TestZipAndUnzipRoundTrip (line 457) | func TestZipAndUnzipRoundTrip(t *testing.T) {
  function createTestZipWithModTime (line 606) | func createTestZipWithModTime(zipPath string, modTime time.Time) error {
  type zipTestEntry (line 667) | type zipTestEntry struct
  function createZipWithEntries (line 672) | func createZipWithEntries(zipPath string, entries []zipTestEntry) error {
  function verifyFileContent (line 698) | func verifyFileContent(t *testing.T, filePath, expectedContent string) {
  function verifyFileModTime (line 712) | func verifyFileModTime(t *testing.T, filePath string, expectedTime time....
  function TestHashFileCtxNoCancellation (line 730) | func TestHashFileCtxNoCancellation(t *testing.T) {
  function TestHashFileCtxWithCancellation (line 856) | func TestHashFileCtxWithCancellation(t *testing.T) {
  function TestHashFileCtxEquivalence (line 946) | func TestHashFileCtxEquivalence(t *testing.T) {
  function TestHashFileCtxLargeFile (line 994) | func TestHashFileCtxLargeFile(t *testing.T) {
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (383K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 16,
    "preview": "github: schollz\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 2546,
    "preview": "name: Bug Report\ndescription: File a bug report to help us improve croc\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 28,
    "preview": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 205,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n  -"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2606,
    "preview": "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: ubu"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "chars": 1344,
    "preview": "name: Deploy Docker\n\non:\n  release:\n    types: [created]\n  workflow_dispatch:\n\njobs:\n  docker:\n    runs-on: ubuntu-24.04"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 5987,
    "preview": "name: Release\n\non:\n  release:\n    types: [created]\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  prepare"
  },
  {
    "path": ".github/workflows/stale.yml",
    "chars": 655,
    "preview": "name: Mark stale issues and pull requests\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n\njobs:\n  stale:\n    runs-on: ubuntu-2"
  },
  {
    "path": ".github/workflows/winget.yml",
    "chars": 380,
    "preview": "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-o"
  },
  {
    "path": ".gitignore",
    "chars": 554,
    "preview": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gi"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 2098,
    "preview": "project_name: croc\n\nbuild:\n  main: main.go\n  binary: croc\n  ldflags: -s -w -X main.Version=\"v{{.Version}}-{{.Date}}\"\n  e"
  },
  {
    "path": ".travis.yml",
    "chars": 646,
    "preview": "language: go\n\ngo:\n  - tip\n\nenv:\n  - \"PATH=/home/travis/gopath/bin:$PATH\"\n\ninstall: true\n\nscript:\n  - env GO111MODULE=on "
  },
  {
    "path": "Dockerfile",
    "chars": 495,
    "preview": "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"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2017-2025 Zack\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 8422,
    "preview": "<p align=\"center\">\n  <img src=\"https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20"
  },
  {
    "path": "croc-entrypoint.sh",
    "chars": 103,
    "preview": "#!/bin/sh\nset -e\n\nif [ -n \"$CROC_PASS\" ]; then\n    set -- --pass \"$CROC_PASS\" \"$@\"\nfi\n\nexec /croc \"$@\"\n"
  },
  {
    "path": "croc.service",
    "chars": 201,
    "preview": "[Unit]\nDescription=croc relay\nAfter=network.target\n\n[Service]\nType=simple\nDynamicUser=yes\nCapabilityBoundingSet=CAP_NET_"
  },
  {
    "path": "go.mod",
    "chars": 1350,
    "preview": "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/readlin"
  },
  {
    "path": "go.sum",
    "chars": 13094,
    "preview": "filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:"
  },
  {
    "path": "main.go",
    "chars": 1094,
    "preview": "package main\n\n//go:generate go run src/install/updateversion.go\n//go:generate git commit -am \"bump $VERSION\"\n//go:genera"
  },
  {
    "path": "src/cli/cli.go",
    "chars": 25354,
    "preview": "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\"str"
  },
  {
    "path": "src/comm/comm.go",
    "chars": 5494,
    "preview": "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/magis"
  },
  {
    "path": "src/comm/comm_test.go",
    "chars": 2528,
    "preview": "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/lo"
  },
  {
    "path": "src/compress/compress.go",
    "chars": 1416,
    "preview": "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 r"
  },
  {
    "path": "src/compress/compress_test.go",
    "chars": 5624,
    "preview": "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"
  },
  {
    "path": "src/croc/croc.go",
    "chars": 66724,
    "preview": "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"
  },
  {
    "path": "src/croc/croc_test.go",
    "chars": 28967,
    "preview": "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\"tes"
  },
  {
    "path": "src/croc/ctx.go",
    "chars": 2481,
    "preview": "// 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/cro"
  },
  {
    "path": "src/crypt/crypt.go",
    "chars": 3467,
    "preview": "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/cry"
  },
  {
    "path": "src/crypt/crypt_test.go",
    "chars": 2701,
    "preview": "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) "
  },
  {
    "path": "src/diskusage/diskusage.go",
    "chars": 1238,
    "preview": "//go:build !windows\n// +build !windows\n\npackage diskusage\n\nimport (\n\t\"golang.org/x/sys/unix\"\n)\n\n// DiskUsage contains us"
  },
  {
    "path": "src/diskusage/diskusage_test.go",
    "chars": 373,
    "preview": "package diskusage\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nvar KB = uint64(1024)\n\nfunc TestNewDiskUsage(t *testing.T) {\n\tusage := "
  },
  {
    "path": "src/diskusage/diskusage_windows.go",
    "chars": 1295,
    "preview": "package diskusage\n\nimport (\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\ntype DiskUsage struct {\n\tfreeBytes  int64\n\ttotalBy"
  },
  {
    "path": "src/install/Makefile",
    "chars": 526,
    "preview": "# VERSION=8.X.Y make release\n\nrelease:\n\tcd ../../ && go run src/install/updateversion.go\n\tgit commit -am \"bump ${VERSION"
  },
  {
    "path": "src/install/bash_autocomplete",
    "chars": 558,
    "preview": ": ${PROG:=$(basename ${BASH_SOURCE})}\n\n_cli_bash_autocomplete() {\n  if [[ \"${COMP_WORDS[0]}\" != \"source\" ]]; then\n    lo"
  },
  {
    "path": "src/install/default.txt",
    "chars": 27284,
    "preview": "#!/bin/bash - \n#===============================================================================\n#\n#          FILE: defau"
  },
  {
    "path": "src/install/prepare-sources-tarball.sh",
    "chars": 296,
    "preview": "#!/bin/bash\ntmp=$(mktemp -d)\necho $VERSION\ngit clone -b v${VERSION} --depth 1 https://github.com/schollz/croc $tmp/croc-"
  },
  {
    "path": "src/install/updateversion.go",
    "chars": 1768,
    "preview": "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("
  },
  {
    "path": "src/install/upload-src-tarball.sh",
    "chars": 1463,
    "preview": "#!/bin/bash\nVERSION=$(cat ./src/cli/cli.go | grep 'Version = \"v' | sed 's/[^0-9.]*\\([0-9.]*\\).*/\\1/')\necho $VERSION\n\n# C"
  },
  {
    "path": "src/install/zsh_autocomplete",
    "chars": 488,
    "preview": "#compdef $PROG\n\n_cli_zsh_autocomplete() {\n\n  local -a opts\n  local cur\n  cur=${words[-1]}\n  if [[ \"$cur\" == \"-\"* ]]; the"
  },
  {
    "path": "src/message/message.go",
    "chars": 1855,
    "preview": "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/co"
  },
  {
    "path": "src/message/message_test.go",
    "chars": 2366,
    "preview": "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\"git"
  },
  {
    "path": "src/mnemonicode/mnemonicode.go",
    "chars": 2953,
    "preview": "// From GitHub version/fork maintained by Stephen Paul Weber available at:\n// https://github.com/singpolyma/mnemonicode\n"
  },
  {
    "path": "src/mnemonicode/mnemonicode_test.go",
    "chars": 3617,
    "preview": "package mnemonicode\n\nimport (\n\t\"testing\"\n)\n\nfunc TestWordsRequired(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n"
  },
  {
    "path": "src/mnemonicode/wordlist.go",
    "chars": 17776,
    "preview": "// From GitHub version/fork maintained by Stephen Paul Weber available at:\n// https://github.com/singpolyma/mnemonicode\n"
  },
  {
    "path": "src/models/constants.go",
    "chars": 4239,
    "preview": "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 \""
  },
  {
    "path": "src/models/models_test.go",
    "chars": 3721,
    "preview": "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 "
  },
  {
    "path": "src/tcp/ctx.go",
    "chars": 1457,
    "preview": "// 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"
  },
  {
    "path": "src/tcp/defaults.go",
    "chars": 176,
    "preview": "package tcp\n\nimport \"time\"\n\nconst (\n\tDEFAULT_LOG_LEVEL             = \"debug\"\n\tDEFAULT_ROOM_CLEANUP_INTERVAL = 10 * time."
  },
  {
    "path": "src/tcp/options.go",
    "chars": 999,
    "preview": "package tcp\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// TODO: maybe export from logger library?\nvar availableLogLevels = []string{\"in"
  },
  {
    "path": "src/tcp/tcp.go",
    "chars": 14163,
    "preview": "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\"g"
  },
  {
    "path": "src/tcp/tcp_test.go",
    "chars": 7935,
    "preview": "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/stret"
  },
  {
    "path": "src/utils/ctx.go",
    "chars": 6803,
    "preview": "// 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."
  },
  {
    "path": "src/utils/utils.go",
    "chars": 20726,
    "preview": "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\""
  },
  {
    "path": "src/utils/utils_test.go",
    "chars": 32208,
    "preview": "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\"s"
  }
]

About this extraction

This page contains the full source code of the schollz/croc GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (335.9 KB), approximately 104.8k tokens, and a symbol index with 299 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!