File ipp-usb-0.9.30.obscpio of Package ipp-usb

07070100000000000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001700000000ipp-usb-0.9.30/.github07070100000001000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000002100000000ipp-usb-0.9.30/.github/workflows07070100000002000081A400000000000000000000000167D72F5D000004DB000000000000000000000000000000000000003100000000ipp-usb-0.9.30/.github/workflows/auto-update.ymlname: Push new tag update to stable branch

on:
  schedule:
    - cron: '9 7 * * *'
  workflow_dispatch:
    inputs:
      workflow_choice:
        description: "Choose YAML to update"
        required: true
        default: "both"
        type: choice
        options:
          - snapcraft
          - rockcraft
          - both

jobs:
  update-yamls:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout this repo
        uses: actions/checkout@v3

      - name: Run desktop-snaps action (Snapcraft)
        if: ${{ github.event_name == 'schedule'  github.event.inputs.workflow_choice == 'snapcraft'  github.event.inputs.workflow_choice == 'both' }}
        uses: ubuntu/desktop-snaps@stable
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          repo: ${{ github.repository }}
          yaml-path: 'snap/snapcraft.yaml'

      - name: Run desktop-snaps action (Rockcraft)
        if: ${{ github.event_name == 'schedule'  github.event.inputs.workflow_choice == 'rockcraft'  github.event.inputs.workflow_choice == 'both' }}
        uses: ubuntu/desktop-snaps@stable
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          repo: ${{ github.repository }}
          yaml-path: 'rock/rockcraft.yaml'
          07070100000003000081A400000000000000000000000167D72F5D00001026000000000000000000000000000000000000003600000000ipp-usb-0.9.30/.github/workflows/registry-actions.ymlname: Pack and Publish OCI Image to Docker Registry and GitHub Packages

on:
  push:
    branches:
      - main
      - master
  workflow_dispatch:
    inputs:
      workflow_choice:
        description: "Choose Release Channel"
        required: true
        default: "edge"
        type: choice
        options:
          - edge
          - stable
          - both
  workflow_run:
    workflows: ["Push new tag update to stable branch"]
    types:
      - completed

jobs:
  build-rock:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Pack with Rockcraft
        uses: canonical/craft-actions/rockcraft-pack@main
        id: rockcraft
        with:
          path: rock

      - name: Upload Rock Artifact
        uses: actions/upload-artifact@v4
        with:
          name: ipp-usb-rock
          path: ${{ steps.rockcraft.outputs.rock }}

  publish-rock:
    needs: build-rock
    if: github.ref_name == 'main'|| github.ref_name == 'master'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Download Rock Artifact
        uses: actions/download-artifact@v4
        with:
          name: ipp-usb-rock

      - name: Install Dependencies
        run: |
          sudo snap install rockcraft --classic
          sudo snap install docker
          sudo snap install yq

      - name: Ensure Docker Daemon is Running
        run: |
          sudo systemctl start docker
          sudo systemctl enable docker
          sudo systemctl is-active --quiet docker || sudo systemctl start docker

      #- name: Log in to Docker Hub
      #  uses: docker/login-action@v3.2.0
      #   with:
      #     username: ${{ secrets.DOCKER_USERNAME }}
      #     password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Log in to GitHub Packages
        uses: docker/login-action@v3.2.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push Docker Image (Edge & Latest Channel)
        if: github.event.inputs.workflow_choice == 'edge' || github.event.inputs.workflow_choice == 'both' || github.event_name == 'push' || github.event_name == 'workflow_run'
        env:
          USERNAME: ${{ secrets.DOCKER_USERNAME }}
        run: |
          IMAGE="$(yq '.name' rock/rockcraft.yaml)"
          VERSION="$(yq '.version' rock/rockcraft.yaml)"
          ROCK="$(ls *.rock | tail -n 1)"
          # Push to Docker Hub
          sudo rockcraft.skopeo --insecure-policy copy oci-archive:"${ROCK}" docker-daemon:"${USERNAME}/${IMAGE}:${VERSION}-edge"
          docker push ${USERNAME}/${IMAGE}:${VERSION}-edge
          docker tag ${USERNAME}/${IMAGE}:${VERSION}-edge ${USERNAME}/${IMAGE}:latest
          docker push ${USERNAME}/${IMAGE}:latest
          # Push to GitHub Packages
          GITHUB_IMAGE="ghcr.io/${{ github.repository_owner }}/${IMAGE}"
          docker tag ${USERNAME}/${IMAGE}:${VERSION}-edge ${GITHUB_IMAGE}:${VERSION}-edge
          docker push ${GITHUB_IMAGE}:${VERSION}-edge
          docker tag ${GITHUB_IMAGE}:${VERSION}-edge ${GITHUB_IMAGE}:latest
          docker push ${GITHUB_IMAGE}:latest

      - name: Build and Push Docker Image (Stable Channel)
        if: github.event.inputs.workflow_choice == 'stable' || github.event.inputs.workflow_choice == 'both'
        env:
          USERNAME: ${{ secrets.DOCKER_USERNAME }}
        run: |
          IMAGE="$(yq '.name' rock/rockcraft.yaml)"
          VERSION="$(yq '.version' rock/rockcraft.yaml)"
          ROCK="$(ls *.rock | tail -n 1)"
          # Push to Docker Hub
          sudo rockcraft.skopeo --insecure-policy copy oci-archive:"${ROCK}" docker-daemon:"${USERNAME}/${IMAGE}:${VERSION}-stable"
          docker push ${USERNAME}/${IMAGE}:${VERSION}-stable
          # Push to GitHub Packages
          GITHUB_IMAGE="ghcr.io/${{ github.repository_owner }}/${IMAGE}"
          docker tag ${USERNAME}/${IMAGE}:${VERSION}-stable ${GITHUB_IMAGE}:${VERSION}-stable
          docker push ${GITHUB_IMAGE}:${VERSION}-stable
07070100000004000081A400000000000000000000000167D72F5D00000021000000000000000000000000000000000000001A00000000ipp-usb-0.9.30/.gitignoreipp-usb
tags
*.swp
*.orig
*.rock
07070100000005000081A400000000000000000000000167D72F5D0000052F000000000000000000000000000000000000001700000000ipp-usb-0.9.30/LICENSEBSD 2-Clause License

Copyright (c) 2020, Alexander Pevzner
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
07070100000006000081A400000000000000000000000167D72F5D00000321000000000000000000000000000000000000001800000000ipp-usb-0.9.30/MakefileMANDIR    = /usr/share/man/
QUIRKSDIR = /usr/share/ipp-usb/quirks
MANPAGE   = ipp-usb.8

# Merge DESTDIR and PREFIX
PREFIX := $(abspath $(DESTDIR)/$(PREFIX))
ifeq ($(PREFIX),/)
        PREFIX :=
endif

all:
	-gotags -R . > tags
	go build -ldflags "-s -w" -tags nethttpomithttp2 -mod=vendor

man:	$(MANPAGE)

$(MANPAGE): $(MANPAGE).md
	ronn --roff --manual=$@ $<

install: all
	install -s -D -t $(PREFIX)/sbin ipp-usb
	install -m 644 -D -t $(PREFIX)/lib/udev/rules.d systemd-udev/*.rules
	install -m 644 -D -t $(PREFIX)/lib/systemd/system systemd-udev/*.service
	install -m 644 -D -t $(PREFIX)/etc/ipp-usb ipp-usb.conf
	mkdir -p $(PREFIX)/$(MANDIR)/man8
	gzip <$(MANPAGE) > $(PREFIX)$(MANDIR)/man8/$(MANPAGE).gz
	install -m 644 -D -t $(PREFIX)/$(QUIRKSDIR) ipp-usb-quirks/*

test:
	go test -mod=vendor
07070100000007000081A400000000000000000000000167D72F5D0000399E000000000000000000000000000000000000001900000000ipp-usb-0.9.30/README.md# ipp-usb

![GitHub](https://img.shields.io/github/license/OpenPrinting/ipp-usb)
[![Go Report Card](https://goreportcard.com/badge/github.com/OpenPrinting/ipp-usb)](https://goreportcard.com/badge/github.com/OpenPrinting/ipp-usb)

## Introduction

[IPP-over-USB](https://www.usb.org/document-library/ipp-protocol-10)
allows using the IPP protocol, normally designed for network printers,
to be used with USB printers as well.

The idea behind this standard is simple: It allows to send HTTP
requests to the device via a USB connection, so enabling IPP, eSCL
(AirScan) and web console on devices without Ethernet or WiFi
connections.

Unfortunately, the naive implementation, which simply relays a TCP
connection to USB, does not work. It happens because closing the TCP
connection on the client side has a useful side effect of discarding
all data sent to this connection from the server side, but it does not
happen with USB connections. In the case of USB, all data not received
by the client will remain in the USB buffers, and the next time the
client connects to the device, it will receive unexpected data, left
from the previous abnormally completed request.

Actually, it is an obvious flaw in the IPP-over-USB standard, but we
have to live with it.

So the implementation, once the HTTP request is sent, must read the
entire HTTP response, which means that the implementation must
understand the HTTP protocol, and effectively implement a HTTP reverse
proxy, backed by the IPP-over-USB connection to the device.

And this is what the **ipp-usb** program actually does.

## Features in detail

* Implements HTTP proxy, backed by USB connection to IPP-over-USB device
* Full support of IPP printing, eSCL scanning, and web admin interface
* DNS-SD advertising for all supported services
* DNS-SD parameters for IPP based on IPP get-printer-attributes query
* DNS-SD parameters for eSCL based on parsing GET /eSCL/ScannerCapabilities response
* TCP port allocation for device is bound to particular device (combination of
VendorID, ProductID and device serial number), so if the user has multiple
devices, they will receive the same TCP port when connected. This allocation
is persisted on a disk
* Automatic DNS-SD name conflict resolution. The finally chosen device's
network name is persisted on a disk
* Can be started by **UDEV** or run in standalone mode
* Can share printer to other computers on a network, or use the loopback interface only
* Can generate very detailed logs for possible troubleshooting

## Under the hood

Though looks simple, ipp-usb does many non obvious things under the hood

* Client-side HTTP connections are completely decoupled from printer-side HTTP-over-USB connections
* HTTP requests are sanitized, missed headers are added
* HTTP protocol upgraded from 1.0 to 1.1, if needed
* Attempts to upgrade HTTP connection to winsock, if unwisely made by web console, are
prohibited, because it can steal USB connection for a long time
* Client HTTP requests are fairly balanced between all available 2-3 USB connections,
regardless of number and persistence of client connections
* Dropping connection by client properly handled in all cases, even in a middle of sending.
In a worst case, printer may receive truncated document, but HTTP transaction will always be
performed correctly

## Memory footprint

Being written on Go, ipp-usb has a large executable size. However, its
memory consumption is not very high. When single device is connected,
ipp-usb RSS is similar or even slightly less in comparison to ippusbxd.
And because ipp-usb handles all devices in a single process, it uses noticeably
less memory that ippusbxd, when serving 2 or more devices.

## External dependencies

This program has very few external dependencies, namely:
* `libusb` for USB access
* `libavahi-common` and `libavahi-client` for DNS-SD
* Running Avahi daemon

## Binary packages

Binary packages available for the following Linux distros:
* **Debian** (10)
* **Fedora** (29, 30, 31 and 32)
* **openSUSE** (Tumbleweed)
* **Ubuntu** (18.04, 19.04, 19.10 and 20.04)

**Linux Mint** users may use Ubuntu packages:
* Linux Mint 18.x - use packages for Ubuntu 16.04
* Linux Mint 19.x - use packages for Ubuntu 18.04

Follow this link for downloads: https://download.opensuse.org/repositories/home:/pzz/

## Not only Linux

We are glad to announce that `ipp-usb` was recently included into the
FreeBSD ports: https://www.freshports.org/print/ipp-usb/

Hope, NetBSD/OpenBSD support will be added as well, so technology
becomes not Linux-only, but UNIX-wide.

## The ipp-usb Snap

ipp-usb is also available as a Snap in the Snap Store: https://snapcraft.io/ipp-usb

Before you install the Snap, uninstall any already existing
installation of ipp-usb.

Simply install it via any GUI client for the Snap Store (Like "Ubuntu
Software") or via command line:

    sudo snap install --edge ipp-usb

Now you can connect and disconnect IPP-over-USB devices and ipp-usb
gets started by the Snap whenever needed. Also devices which are
already connected during boot, start, or update of the Snap are
considered.

You can also use

    ipp-usb status

to check the status of the running ipp-usb daemon (supported device
must be connected for the ipp-usb daemon to be running, accesses only
the ipp-usb daemon of the Snap) and

    ipp-usb check

to scan the USB for the presence of potentially supported USB devices
(7/1/4 interface protocol). This command requires access to the raw
USB and therefore on many systems root privileges are required.

The Snap is automatically updated when further development on ipp-usb
happens.

The configuration file is here:

    /var/snap/ipp-usb/common/etc/ipp-usb.conf

You can edit it and afterwards restart the Snap to use the changed
configuration.

Incompatibilities of particular devices are handled by workarounds
defined in the quirk files. You find them here:

    /var/snap/ipp-usb/common/quirks

You can add your own quirk files (but if they solve your problem,
please report an issue here, with your quirk file attached, so that
others with the same problem will get helped, too).

For quick tests you can also edit the existing files, but they will
get replaced (and so your changes lost) on the next update of the
Snap, as we are changing them on any report of further device
incompatibilities.

The log file is here

    /var/snap/ipp-usb/common/var/log

and device state files (to assure that each device appears on the same
port and with the same DNS-SD service name) are here:

    /var/snap/ipp-usb/common/var/dev

You can also build the Snap locally. This is useful when

* You want to modify ipp-usb
* You want to learn about snapping Go projects
* You want to learn about how to use UDEV from within a Snap (note that a Snap cannot install UDEV rules into the system)

To do so, run from the main directory of this source repository

    snapcraft snap

and then install the resulting Snap with

    sudo snap install --dangerous ipp-usb*.snap

An installed Snap from the Snap Store will get overwritten/replaced by your Snap.

Some technical notes about this Snap:

Snapping the Go project with one Go library taken from upstream (and
not from Ubuntu Core) was rather straight-forward. Only observation
was that the Go plugin seems not to do "make install". So I had to use
an "override-build" to manually install the auxiliary files
(ipp-usb.conf, quirk files). I also have adapted the auxiliary file
and state directories in paths.go in the "override-build" scriptlet.

The real challenge of this Snap was to trigger ipp-usb on the
appearing (and also the presence) of IPP-over-USB devices.

In the classic installation of ipp-usb (via "make install" or RPM/DEB
package installation) a UDEV rules file and a systemd service file (in
systemd-udev/) are installed, so that the system automatically triggers
the launch of ipp-usb when an appropriate device is connected or
already present. A Snap is not able to do so. It cannot install any
files into the system. It can only bring its own, static file system
and create files only in its own state directory. These locations are
not scanned for UDEV rules.

So the Snap must discover the devices without its own UDEV rules, but
it still can use UDEV. The trick is to do a generic monitoring of UDEV
events and filtering out the USB devices with IPP-over-USB interface
(7/1/4). If such a device appears, we trigger and ipp-usb launch. We
also check on startup of the Snap whether there is such a device
already and if so, we also trigger an ipp-usb launch.

ipp-usb is run, as in the classic installation, with "udev"
argument. This way it stops by itself when there is no device any more
(and we do not need to observe the disappearal events of the devices)
and it is assured that only one single instance of ipp-usb is running.

To do this with low coding effort I use the UDEV command line tool
udevadm in a shell script (snap/local/run-ipp-usb). Once it runs in
"monitor" mode to observe the UDEV events. Then we parse the output
lines to only consider the ones for a device appearing and run
"udevadm info -q property" on each device path, to get the properties
and filter the 7/1/4 interface. In the beginning we use "udevadm
trigger" to find the already passed appearal event of a device which
is already present. So the shell script is an auxiliary daemon to
start ipp-usb when needed.

## The ipp-usb Rock

### Install from GitHub Container Registry
#### Prerequisites

1. **Docker Installed**: Ensure Docker is installed on your system. You can download it from the [official Docker website](https://www.docker.com/get-started).
```sh
  sudo snap install docker
```

#### Step-by-Step Guide

You can pull the `ipp-usb` Docker image from the GitHub Container Registry.

**From GitHub Container Registry** <br>
To pull the image from the GitHub Container Registry, run the following command:
```sh
  sudo docker pull ghcr.io/openprinting/ipp-usb:latest
```

To run the container after pulling the image, use:
```sh
  sudo docker run -d --network host \
      -v /dev/bus/usb:/dev/bus/usb:ro \
      --device-cgroup-rule='c 189:* rmw' \
      --name ipp-usb \
      ghcr.io/openprinting/ipp-usb:latest
```

- `--network host`: Uses the host network, ensuring IPP-over-USB and Avahi service discovery work correctly.
- `-v /dev/bus/usb:/dev/bus/usb:ro`: Grants the container read-only access to USB devices.
- `--device-cgroup-rule='c 189:* rmw'`: Grants the container permission to manage USB devices (189:* covers USB device nodes).

To check the logs of `ipp-usb`, run:
```sh
  sudo docker logs -f ipp-usb
```

### Building and Running `ipp-usb` Locally

#### Prerequisites

**Docker Installed**: Ensure Docker is installed on your system. You can download it from the [official Docker website](https://www.docker.com/get-started) or from the Snap Store:
```sh
  sudo snap install docker
```

**Rockcraft**: Rockcraft should be installed. You can install Rockcraft using the following command:
```sh
  sudo snap install rockcraft --classic
```

#### Step-by-Step Guide

**Build the `ipp-usb` Rock Image**

The first step is to build the Rock from the `rockcraft.yaml`. This image will contain all the configurations and dependencies required to run `ipp-usb`.

Navigate to the directory containing `rockcraft.yaml`, then run:
```sh
  rockcraft pack -v
```

**Compile to Docker Image**

Once the `.rock` file is built, compile a Docker image from it using:
```sh
  sudo rockcraft.skopeo --insecure-policy copy oci-archive:<rock_image_name> docker-daemon:ipp-usb:latest
```

**Run the `ipp-usb` Docker Container**

```sh
  sudo docker run -d --network host \
      -v /dev/bus/usb:/dev/bus/usb:ro \
      --device-cgroup-rule='c 189:* rmw' \
      --name ipp-usb \
      ipp-usb:latest
```

### Accessing the Container Shell

To enter the running `ipp-usb` container and access a shell inside it, use:
```sh
  sudo docker exec -it ipp-usb bash
```
This allows you to inspect logs, debug issues, or manually run commands inside the container.

### Configuration

The `ipp-usb` container uses a configuration file located at:
```
/etc/ipp-usb.conf
```
To customize the configuration, mount a modified config file:
```sh
  sudo docker run -d --network host \
      -v /dev/bus/usb:/dev/bus/usb:ro \
      --device-cgroup-rule='c 189:* rmw' \
      -v /path/to/custom/ipp-usb.conf:/etc/ipp-usb.conf:ro \
      --name ipp-usb-container \
      ghcr.io/openprinting/ipp-usb:latest
```


## Installation from source

You will need to install the following packages (exact name depends
of your Linux distro):
* libusb development files
* libavahi-client and libavahi-common development files
* gcc
* Go compiler
* pkg-config
* git, make and so on

Building is really simple:

    git clone https://github.com/OpenPrinting/ipp-usb.git
    cd ipp-usb
    make

Then you may `make install` or just try to run `./ipp-usb` directly from
the build directory

## Avahi Notes (exposing printer to localhost)

IPP-over-USB normally exposes printer to localhost only, hence it
requires DNS-SD announces to work for localhost.

This requires Avahi 0.8.0 or newer. Older Avahi versions do not
support announcing to localhost.

Some Linux distros (for example recent Ubuntu and Fedora versions)
have their Avahi patched to support localhost, others (for example
Debian) not.

To determine if your Avahi supports localhost, run the following
command in one terminal session:
```
    avahi-publish -s test _test._tcp 1234
```
And simultaneously the following command in another terminal session
on the same machine:
```
    avahi-browse _test._tcp -r
```
If you see localhost in the avahi-browse output, like this:
```
    =     lo IPv4 test                                          _test._tcp           local
       hostname = [localhost]
       address = [127.0.0.1]
       port = [1234]
       txt = []
```
your Avahi is OK. Otherwise, update or patching is required.

So users of distros that ship a too old Avahi and without the patch
have three possibilities:
1. Update Avahi to 0.8.0 or newer
2. Apply the patch by themself, rebuild and reinstall avahi-daemon
3. Configure `ipp-usb` to run on all network interfaces, not only on loopback

If you decide to apply the patch, get it as `avahi/avahi-localhost.patch`
in this package or [download it here](https://raw.githubusercontent.com/OpenPrinting/ipp-usb/master/avahi/avahi-localhost.patch).

The third method is simple to do, just replace `interface = loopback`
with `interface = all` in the `ipp-usb.conf` file, but this has the
disadvantage of exposing your local USB-connected printer to the
entire local network, which can be an unwanted side effect, especially
in a big corporative network.
07070100000008000081A400000000000000000000000167D72F5D000007FC000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/addpdl_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * (*DNSSdTxtRecord) AddPDL() test
 */

package main

import (
	"testing"
)

var testDataAddPDL = []struct{ in, out string }{
	{
		"application/pdf",
		"application/pdf",
	},

	{
		"application/octet-stream," +
			"application/pdf,image/tiff,image/jpeg,image/urf," +
			"application/postscript,application/vnd.hp-PCL," +
			"application/vnd.hp-PCLXL,application/vnd.xpsdocument," +
			"image/pwg-raster",

		"application/octet-stream," +
			"application/pdf,image/tiff,image/jpeg,image/urf," +
			"application/postscript,application/vnd.hp-PCL," +
			"application/vnd.hp-PCLXL,application/vnd.xpsdocument," +
			"image/pwg-raster",
	},

	{
		"application/vnd.hp-PCL,application/vnd.hp-PCLXL," +
			"application/postscript,application/msword," +
			"application/pdf,image/jpeg,image/urf," +
			"image/pwg-raster," +
			"application/PCLm," +
			"application/vnd.openxmlformats-officedocument.wordprocessingml.document," +
			"application/vnd.ms-excel," +
			"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet," +
			"application/vnd.ms-powerpoint," +
			"application/vnd.openxmlformats-officedocument.presentationml.presentation," +
			"application/octet-stream",

		"application/vnd.hp-PCL,application/vnd.hp-PCLXL," +
			"application/postscript,application/msword," +
			"application/pdf,image/jpeg,image/urf," +
			"image/pwg-raster,application/PCLm," +
			"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
	},
}

// Test .INI reader
func TestAddPDL(t *testing.T) {
	for i, data := range testDataAddPDL {
		var txt DNSSdTxtRecord
		txt.AddPDL("pdl", data.in)

		if len(txt) != 1 {
			t.Errorf("test %d: unexpected (%d) number of TXT elements added",
				i+1, len(txt))
			return
		}

		if txt[0].Value != data.out {
			t.Errorf("test %d: expected %q, got %q",
				i+1, data.out, txt[0].Value)
		}
	}
}
07070100000009000081A400000000000000000000000167D72F5D00001F96000000000000000000000000000000000000001700000000ipp-usb-0.9.30/auth.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Authentication
 */

package main

import (
	"errors"
	"fmt"
	"net"
	"net/http"
	"os/user"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

// AuthUIDRule represents a single rule for client authentication
// based on client UID
type AuthUIDRule struct {
	Name    string  // @name means group, * means any
	Allowed AuthOps // Allowed operations
}

// IsUser tells if rule is a user rule
func (rule *AuthUIDRule) IsUser() bool {
	return !rule.IsGroup()
}

// IsGroup tells if rule is a group rule
func (rule *AuthUIDRule) IsGroup() bool {
	return strings.HasPrefix(rule.Name, "@")
}

// MatchUser matches rule against user name
func (rule *AuthUIDRule) MatchUser(name string) AuthOps {
	if rule.IsGroup() {
		return 0
	}

	if rule.Name == "*" || rule.Name == name {
		return rule.Allowed
	}

	return 0
}

// MatchGroup matches rule against group name
func (rule *AuthUIDRule) MatchGroup(name string) AuthOps {
	if !rule.IsGroup() {
		return 0
	}

	ruleName := rule.Name[1:] // Strip leading '@'
	if ruleName == "*" || ruleName == name {
		return rule.Allowed
	}

	return 0
}

// AuthOps is bitmask of allowed operations
type AuthOps int

// AuthOps values
const (
	AuthOpsConfig AuthOps = 1 << iota // Configuration web console
	AuthOpsFax                        // Faxing
	AuthOpsPrint                      // Printing
	AuthOpsScan                       // Scanning

	// All and None of above
	AuthOpsAll = AuthOpsConfig | AuthOpsFax | AuthOpsPrint |
		AuthOpsScan
	AuthOpsNone AuthOps = 0
)

// String returns string representation of AuthOps flags, for debugging.
func (ops AuthOps) String() string {
	if ops == 0 {
		return "none"
	}

	s := []string{}

	if ops&AuthOpsConfig != 0 {
		s = append(s, "config")
	}

	if ops&AuthOpsFax != 0 {
		s = append(s, "fax")
	}

	if ops&AuthOpsPrint != 0 {
		s = append(s, "print")
	}

	if ops&AuthOpsScan != 0 {
		s = append(s, "scan")
	}

	return strings.Join(s, ",")
}

// AuthUIDinfo is the resolved and cached UID info, for matching
type AuthUIDinfo struct {
	UsrNames []string  // User (numerical and symbolic) names
	GrpNames []string  // Group names (numerical and symbolic)
	expires  time.Time // Expiration time, for caching
}

// authUIDinfoCache contains authUIDinfo cache, indexed by UID
var (
	authUIDinfoCache     = make(map[int]*AuthUIDinfo)
	authUIDinfoCacheLock sync.Mutex
)

// authUIDinfoCacheTTL is the expiration timeout for authUIDinfoCache
const authUIDinfoCacheTTL = 2 * time.Second

// AuthUIDinfoLookup performs AuthUIDinfo lookup by UID.
func AuthUIDinfoLookup(uid int) (*AuthUIDinfo, error) {
	// UID is not known. Use "*" user/group names, as promised
	// by documentation
	if uid == -1 {
		info := &AuthUIDinfo{
			UsrNames: []string{"*"},
			GrpNames: []string{"*"},
			expires:  time.Now().Add(authUIDinfoCacheTTL),
		}

		return info, nil
	}

	// Lookup authUIDinfoCache
	authUIDinfoCacheLock.Lock()
	info := authUIDinfoCache[uid]
	authUIDinfoCacheLock.Unlock()

	if info != nil && info.expires.After(time.Now()) {
		return info, nil
	}

	// Resolve user names for matching
	// Also populates grpIDs with numeric group IDs
	usrNames := []string{strconv.Itoa(uid)}
	grpIDs := []string{}

	usr, err := user.LookupId(usrNames[0])
	if err != nil {
		return nil, err
	}

	usrNames = append(usrNames, usr.Username)
	grpIDs = append(grpIDs, usr.Gid)

	grpids, err := usr.GroupIds()
	if err != nil {
		return nil, err
	}

	grpIDs = append(grpIDs, grpids...)

	// Resolve group IDs to names
	grpNames := append([]string{}, grpIDs...)
	for _, gid := range grpIDs {
		grp, err := user.LookupGroupId(gid)
		if err != nil {
			return nil, err
		}

		grpNames = append(grpNames, grp.Name)
	}

	// Update cache
	info = &AuthUIDinfo{
		UsrNames: usrNames,
		GrpNames: grpNames,
		expires:  time.Now().Add(authUIDinfoCacheTTL),
	}

	authUIDinfoCacheLock.Lock()
	authUIDinfoCache[uid] = info
	authUIDinfoCacheLock.Unlock()

	// Return the answer
	return info, nil
}

// AuthUID returns operations allowed to client with given UID
// uid == -1 indicates that UID is not available (i.e., external
// connection)
func AuthUID(info *AuthUIDinfo) AuthOps {
	// Everything is allowed if authentication is not configured
	if Conf.ConfAuthUID == nil {
		return AuthOpsAll
	}

	// Apply rules
	allowed := AuthOpsNone

	for _, rule := range Conf.ConfAuthUID {
		if rule.IsUser() {
			for _, usr := range info.UsrNames {
				allowed |= rule.MatchUser(usr)
			}
		} else {
			for _, grp := range info.GrpNames {
				allowed |= rule.MatchGroup(grp)
			}
		}
	}

	return allowed
}

// authUIDrequiresUID tells if UID authentication really requires UID.
// UID is not required, if either authentication is not configured, or
// there is no rules with non-wildcard UID.
func authUIDrequiresUID() bool {
	for _, rule := range Conf.ConfAuthUID {
		if rule.Name != "*" && rule.Name != "@*" {
			return true
		}
	}

	return false
}

// AuthHTTPRequest performs authentication for the incoming
// HTTP request
//
// On success, status is http.StatusOK and err is nil.
// Otherwise, status is appropriate for HTTP error response,
// and err explains the reason
func AuthHTTPRequest(log *Logger,
	client, server *net.TCPAddr,
	rq *http.Request) (status int, err error) {

	// Guess the operation by URL
	post := rq.Method == "POST"
	ops := AuthOpsConfig // The default
	switch {
	case post && strings.HasPrefix(rq.URL.Path, "/ipp/print"):
		ops = AuthOpsPrint
	case post && strings.HasPrefix(rq.URL.Path, "/ipp/faxout"):
		ops = AuthOpsFax
	case strings.HasPrefix(rq.URL.Path, "/eSCL"):
		ops = AuthOpsScan
	}

	log.Debug(' ', "auth: operation requested: %s (HTTP %s %s)",
		ops, rq.Method, rq.URL)

	// Check if client and server addresses are both local
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		err = fmt.Errorf("can't get local IP addresses: %s", err)
		log.Error('!', "auth: %s", err)

		return http.StatusInternalServerError, err
	}

	clientIsLocal := client.IP.IsLoopback()
	serverIsLocal := server.IP.IsLoopback()

	for _, addr := range addrs {
		if clientIsLocal && serverIsLocal {
			// Both addresses known to be local,
			// we don't need to continue
			break
		}

		if ip, ok := addr.(*net.IPNet); ok {
			if client.IP.Equal(ip.IP) {
				clientIsLocal = true
			}

			if server.IP.Equal(ip.IP) {
				serverIsLocal = true
			}
		}
	}

	log.Debug(' ', "auth: address check:")
	log.Debug(' ', "  client-addr %s local=%v", client.IP, clientIsLocal)
	log.Debug(' ', "  server-addr %s local=%v", server.IP, serverIsLocal)

	// Do we need UID?
	uid := -1
	reason := ""

	switch {
	case !clientIsLocal || !serverIsLocal:
		reason = "non-local connection"
	case !TCPClientUIDSupported():
		reason = fmt.Sprintf("UID auth not supported on %s",
			runtime.GOOS)
	case !authUIDrequiresUID():
		reason = "auth rules don't use UID"
	}

	// Obtain UID, if we really need it
	if reason == "" {
		uid, err = TCPClientUID(client, server)
		if err != nil {
			err = fmt.Errorf("can't get client UID: %s",
				err)
			log.Error('!', "auth: %s", err)
			return http.StatusInternalServerError, err
		}

		log.Debug(' ', "auth: client UID=%d", uid)
	} else {
		log.Debug(' ', "auth: client UID=%d (%s)", uid, reason)
	}

	// Lookup UID info
	info, err := AuthUIDinfoLookup(uid)
	if err != nil {
		err = fmt.Errorf("can't resolve UID %d: %s", uid, err)
		log.Error('!', "auth: %s", err)
		return 0, err
	}

	log.Debug(' ', "auth: UID %d resolved:", uid)
	log.Debug(' ', "  user names:  %s", strings.Join(info.UsrNames, ","))
	log.Debug(' ', "  group names: %s", strings.Join(info.GrpNames, ","))

	// Authenticate
	allowed := AuthUID(info)
	log.Debug(' ', "auth: allowed operations: %s", allowed)

	if ops&allowed != AuthOpsNone {
		log.Debug(' ', "auth: access granted")
		return http.StatusOK, nil
	}

	err = errors.New("Operation not allowed. See ipp-usb.conf for details")
	log.Error('!', "auth: %s", err)

	return http.StatusForbidden, err
}
0707010000000A000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001500000000ipp-usb-0.9.30/avahi0707010000000B000081A400000000000000000000000167D72F5D00000B3A000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/avahi/avahi-localhost.patchdiff --git a/avahi-core/iface-linux.c b/avahi-core/iface-linux.c
index c6c5f77..e116c7b 100644
--- a/avahi-core/iface-linux.c
+++ b/avahi-core/iface-linux.c
@@ -104,8 +104,8 @@ static void netlink_callback(AvahiNetlink *nl, struct nlmsghdr *n, void* userdat
         hw->flags_ok =
             (ifinfomsg->ifi_flags & IFF_UP) &&
             (!m->server->config.use_iff_running || (ifinfomsg->ifi_flags & IFF_RUNNING)) &&
-            !(ifinfomsg->ifi_flags & IFF_LOOPBACK) &&
-            (ifinfomsg->ifi_flags & IFF_MULTICAST) &&
+            ((ifinfomsg->ifi_flags & IFF_LOOPBACK) ||
+             (ifinfomsg->ifi_flags & IFF_MULTICAST)) &&
             (m->server->config.allow_point_to_point || !(ifinfomsg->ifi_flags & IFF_POINTOPOINT));
 
         /* Handle interface attributes */
diff --git a/avahi-core/iface-pfroute.c b/avahi-core/iface-pfroute.c
index 9a2e953..27c3443 100644
--- a/avahi-core/iface-pfroute.c
+++ b/avahi-core/iface-pfroute.c
@@ -80,8 +80,8 @@ static void rtm_info(struct rt_msghdr *rtm, AvahiInterfaceMonitor *m)
   hw->flags_ok =
     (ifm->ifm_flags & IFF_UP) &&
     (!m->server->config.use_iff_running || (ifm->ifm_flags & IFF_RUNNING)) &&
-    !(ifm->ifm_flags & IFF_LOOPBACK) &&
-    (ifm->ifm_flags & IFF_MULTICAST) &&
+    ((ifm->ifm_flags & IFF_LOOPBACK) ||
+     (ifm->ifm_flags & IFF_MULTICAST)) &&
     (m->server->config.allow_point_to_point || !(ifm->ifm_flags & IFF_POINTOPOINT));
 
   avahi_free(hw->name);
@@ -427,8 +427,8 @@ static void if_add_interface(struct lifreq *lifreq, AvahiInterfaceMonitor *m, in
         hw->flags_ok =
             (flags & IFF_UP) &&
             (!m->server->config.use_iff_running || (flags & IFF_RUNNING)) &&
-            !(flags & IFF_LOOPBACK) &&
-            (flags & IFF_MULTICAST) &&
+            ((flags & IFF_LOOPBACK) ||
+             (flags & IFF_MULTICAST)) &&
             (m->server->config.allow_point_to_point || !(flags & IFF_POINTOPOINT));
         hw->name = avahi_strdup(lifreq->lifr_name);
         hw->mtu = mtu;
diff --git a/avahi-core/resolve-service.c b/avahi-core/resolve-service.c
index 3377a50..3311b6b 100644
--- a/avahi-core/resolve-service.c
+++ b/avahi-core/resolve-service.c
@@ -24,6 +24,7 @@
 #include <string.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <net/if.h>
 
 #include <avahi-common/domain.h>
 #include <avahi-common/timeval.h>
@@ -129,7 +130,7 @@ static void finish(AvahiSServiceResolver *r, AvahiResolverEvent event) {
                 r->service_name,
                 r->service_type,
                 r->domain_name,
-                r->srv_record->data.srv.name,
+                (r->interface == if_nametoindex("lo")) ? "localhost" : r->srv_record->data.srv.name,
                 r->address_record ? &a : NULL,
                 r->srv_record->data.srv.port,
                 r->txt_record ? r->txt_record->data.txt.string_list : NULL,

0707010000000C000081A400000000000000000000000167D72F5D000014BC000000000000000000000000000000000000001700000000ipp-usb-0.9.30/conf.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Program configuration
 */

package main

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"unicode"
)

const (
	// ConfFileName defines a name of ipp-usb configuration file
	ConfFileName = "ipp-usb.conf"
)

// Configuration represents a program configuration
type Configuration struct {
	HTTPMinPort        int            // Starting port number for HTTP to bind to
	HTTPMaxPort        int            // Ending port number for HTTP to bind to
	DNSSdEnable        bool           // Enable DNS-SD advertising
	LoopbackOnly       bool           // Use only loopback interface
	IPV6Enable         bool           // Enable IPv6 advertising
	ConfAuthUID        []*AuthUIDRule // [auth uid], parsed
	LogDevice          LogLevel       // Per-device LogLevel mask
	LogMain            LogLevel       // Main log LogLevel mask
	LogConsole         LogLevel       // Console  LogLevel mask
	LogMaxFileSize     int64          // Maximum log file size
	LogMaxBackupFiles  uint           // Count of files preserved during rotation
	LogAllPrinterAttrs bool           // Get *all* printer attrs, for logging
	ColorConsole       bool           // Enable ANSI colors on console
	Quirks             QuirksSet      // Device quirks
}

// Conf contains a global instance of program configuration
var Conf = Configuration{
	HTTPMinPort:        60000,
	HTTPMaxPort:        65535,
	DNSSdEnable:        true,
	LoopbackOnly:       true,
	IPV6Enable:         true,
	ConfAuthUID:        nil,
	LogDevice:          LogDebug,
	LogMain:            LogDebug,
	LogConsole:         LogDebug,
	LogMaxFileSize:     256 * 1024,
	LogMaxBackupFiles:  5,
	LogAllPrinterAttrs: false,
	ColorConsole:       true,
}

// ConfLoad loads the program configuration
func ConfLoad() error {
	// Obtain path to executable directory
	exepath, err := os.Executable()
	if err != nil {
		return fmt.Errorf("conf: %s", err)
	}

	exepath = filepath.Dir(exepath)

	// Build list of configuration files
	files := []string{
		filepath.Join(PathConfDir, ConfFileName),
		filepath.Join(exepath, ConfFileName),
	}

	// Load file by file
	for _, file := range files {
		err = confLoadInternal(file)
		if err != nil {
			return err
		}
	}

	// Load quirks
	quirksDirs := []string{
		PathQuirksDir,
		PathConfQuirksDir,
		filepath.Join(exepath, "ipp-usb-quirks"),
	}

	if err == nil {
		Conf.Quirks, err = LoadQuirksSet(quirksDirs...)
	}

	return err
}

// Load the program configuration -- internal version
func confLoadInternal(path string) error {
	// Open configuration file
	ini, err := OpenIniFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			err = nil
		}
		return err
	}

	defer ini.Close()

	// Extract options
	for err == nil {
		var rec *IniRecord
		rec, err = ini.Next()
		if err != nil {
			break
		}

		switch {
		case confMatchName(rec.Section, "network"):
			switch {
			case confMatchName(rec.Key, "http-min-port"):
				err = rec.LoadIPPort(&Conf.HTTPMinPort)
			case confMatchName(rec.Key, "http-max-port"):
				err = rec.LoadIPPort(&Conf.HTTPMaxPort)
			case confMatchName(rec.Key, "dns-sd"):
				err = rec.LoadNamedBool(&Conf.DNSSdEnable, "disable", "enable")
			case confMatchName(rec.Key, "interface"):
				err = rec.LoadNamedBool(&Conf.LoopbackOnly, "all", "loopback")
			case confMatchName(rec.Key, "ipv6"):
				err = rec.LoadNamedBool(&Conf.IPV6Enable, "disable", "enable")
			}

		case confMatchName(rec.Section, "auth uid"):
			err = rec.LoadAuthUIDRules(&Conf.ConfAuthUID)

		case confMatchName(rec.Section, "logging"):
			switch {
			case confMatchName(rec.Key, "device-log"):
				err = rec.LoadLogLevel(&Conf.LogDevice)
			case confMatchName(rec.Key, "main-log"):
				err = rec.LoadLogLevel(&Conf.LogMain)
			case confMatchName(rec.Key, "console-log"):
				err = rec.LoadLogLevel(&Conf.LogConsole)
			case confMatchName(rec.Key, "console-color"):
				err = rec.LoadNamedBool(&Conf.ColorConsole, "disable", "enable")
			case confMatchName(rec.Key, "max-file-size"):
				err = rec.LoadSize(&Conf.LogMaxFileSize)
			case confMatchName(rec.Key, "max-backup-files"):
				err = rec.LoadUint(&Conf.LogMaxBackupFiles)
			case confMatchName(rec.Key, "get-all-printer-attrs"):
				err = rec.LoadBool(&Conf.LogAllPrinterAttrs)
			}
		}
	}

	if err != nil && err != io.EOF {
		return err
	}

	// Validate configuration
	if Conf.HTTPMinPort >= Conf.HTTPMaxPort {
		return errors.New("http-min-port must be less that http-max-port")
	}

	return nil
}

// confMatchName tells if section or key name matches
// the pattern
//   - match is case-insensitive
//   - difference in amount of free space is ignored
//   - leading and trailing space is ignored
func confMatchName(name, pattern string) bool {
	name = strings.TrimSpace(name)
	pattern = strings.TrimSpace(pattern)

	for name != "" && pattern != "" {
		c1 := rune(name[0])
		c2 := rune(pattern[0])

		switch {
		case unicode.IsSpace(c1):
			if !unicode.IsSpace(c2) {
				return false
			}

			name = strings.TrimSpace(name)
			pattern = strings.TrimSpace(pattern)

		case c1 == c2:
			name = name[1:]
			pattern = pattern[1:]

		default:
			return false
		}
	}

	return true
}
0707010000000D000081A400000000000000000000000167D72F5D0000030E000000000000000000000000000000000000001800000000ipp-usb-0.9.30/const.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Configuration constants
 */

package main

import (
	"time"
)

const (
	// DevInitTimeout specifies how much time to wait for
	// device initialization
	DevInitTimeout = 5 * time.Second

	// DevShutdownTimeout specifies how much time to wait for
	// device graceful shutdown
	DevShutdownTimeout = 5 * time.Second

	// DevInitRetryInterval specifies the retry interval for
	// failed device initialization
	DevInitRetryInterval = 2 * time.Second

	// DNSSdRetryInterval specifies the retry interval in a case
	// of failed DNS-SD operation
	DNSSdRetryInterval = 2 * time.Second
)
0707010000000E000081A400000000000000000000000167D72F5D00000B57000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/ctrlsock.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Control socket handler
 *
 * ipp-usb runs a HTTP server on a top of the unix domain control
 * socket.
 *
 * Currently it is only used to obtain a per-device status from the
 * running daemon. Using HTTP here sounds as overkill, but taking
 * in account that it costs us virtually nothing and this mechanism
 * is well-extendable, this is a good choice
 */

package main

import (
	"log"
	"net"
	"net/http"
	"os"
	"syscall"
)

var (
	// CtrlsockAddr contains control socket address in
	// a form of the net.UnixAddr structure
	CtrlsockAddr = &net.UnixAddr{Name: PathControlSocket, Net: "unix"}

	// ctrlsockServer is a HTTP server that runs on a top of
	// the status socket
	ctrlsockServer = http.Server{
		Handler:  http.HandlerFunc(ctrlsockHandler),
		ErrorLog: log.New(Log.LineWriter(LogError, '!'), "", 0),
	}
)

// ctrlsockHandler handles HTTP requests that come over the
// control socket
func ctrlsockHandler(w http.ResponseWriter, r *http.Request) {
	Log.Debug(' ', "ctrlsock: %s %s", r.Method, r.URL)

	// Catch panics to log
	defer func() {
		v := recover()
		if v != nil {
			Log.Panic(v)
		}
	}()

	// Check request method
	if r.Method != "GET" {
		http.Error(w, r.Method+": method not supported",
			http.StatusMethodNotAllowed)
		return
	}

	// Check request path
	if r.URL.Path != "/status" {
		http.Error(w, "Not found", http.StatusNotFound)
		return
	}

	// Handle the request
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	httpNoCache(w)
	w.WriteHeader(http.StatusOK)
	w.Write(StatusFormat())
}

// CtrlsockStart starts control socket server
func CtrlsockStart() error {
	Log.Debug(' ', "ctrlsock: listening at %q", PathControlSocket)

	// Listen the socket
	os.Remove(PathControlSocket)

	listener, err := net.ListenUnix("unix", CtrlsockAddr)
	if err != nil {
		return err
	}

	// Make socket accessible to everybody. Error is ignores,
	// it's not a reason to abort ipp-usb
	os.Chmod(PathControlSocket, 0777)

	// Start HTTP server on a top of the listening socket
	go func() {
		ctrlsockServer.Serve(listener)
	}()

	return nil
}

// CtrlsockStop stops the control socket server
func CtrlsockStop() {
	Log.Debug(' ', "ctrlsock: shutdown")
	ctrlsockServer.Close()
}

// CtrlsockDial connects to the control socket of the running
// ipp-usb daemon
func CtrlsockDial() (net.Conn, error) {
	conn, err := net.DialUnix("unix", nil, CtrlsockAddr)

	if err == nil {
		return conn, err
	}

	if neterr, ok := err.(*net.OpError); ok {
		if syserr, ok := neterr.Err.(*os.SyscallError); ok {
			switch syserr.Err {
			case syscall.ECONNREFUSED, syscall.ENOENT:
				err = ErrNoIppUsb

			case syscall.EACCES, syscall.EPERM:
				err = ErrAccess
			}
		}
	}

	return conn, err
}
0707010000000F000081A400000000000000000000000167D72F5D0000086C000000000000000000000000000000000000001900000000ipp-usb-0.9.30/daemon.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Demonization
 */

package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"
	"syscall"
	"unicode"
)

// #include <unistd.h>
import "C"

// CloseStdInOutErr closes stdin/stdout/stderr handles
func CloseStdInOutErr() error {
	nul, err := syscall.Open(os.DevNull, syscall.O_RDONLY, 0644)
	if err != nil {
		return fmt.Errorf("Open %q: %s", os.DevNull, err)
	}

	defer syscall.Close(nul)

	// Note, syscall.Dup2 is not implemented on old Go
	// versions for ARM64 Linux. So we use C.dup2 as a
	// portable workaround
	C.dup2(C.int(nul), 0)
	C.dup2(C.int(nul), 1)
	C.dup2(C.int(nul), 2)

	return nil
}

// Daemon runs ipp-usb program in background
func Daemon() error {
	// Obtain path to program's executable
	exe, err := os.Executable()
	if err != nil {
		return err
	}

	// Create stdout/stderr pipes
	rstdout, wstdout, err := os.Pipe()
	if err != nil {
		return fmt.Errorf("pipe(): %s", err)
	}

	rstderr, wstderr, err := os.Pipe()
	if err != nil {
		return fmt.Errorf("pipe(): %s", err)
	}

	devnull, err := os.Open(os.DevNull)
	if err != nil {
		return fmt.Errorf("Open %q: %s", os.DevNull, err)
	}

	// Initialize process attributes
	attr := &os.ProcAttr{
		Files: []*os.File{devnull, wstdout, wstderr},
		Sys: &syscall.SysProcAttr{
			Setsid: true,
		},
	}

	// Initialize process arguments
	args := []string{}
	for _, arg := range os.Args {
		if arg != "-bg" {
			args = append(args, arg)
		}
	}

	// Start new process
	proc, err := os.StartProcess(exe, args, attr)
	if err != nil {
		return err
	}

	// Collect its initialization output
	wstdout.Close()
	wstderr.Close()

	stdout := &bytes.Buffer{}
	stderr := &bytes.Buffer{}
	io.Copy(stdout, rstdout)
	io.Copy(stderr, rstderr)

	if stdout.Len() != 0 {
		os.Stdout.Write(stdout.Bytes())
	}

	// Check for an error
	if stderr.Len() > 0 {
		s := strings.TrimFunc(stderr.String(), unicode.IsSpace)
		proc.Kill() // Just in case
		return errors.New(s)
	}

	proc.Release()

	return nil

}
07070100000010000081A400000000000000000000000167D72F5D00001BDA000000000000000000000000000000000000001900000000ipp-usb-0.9.30/device.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Device object brings all parts together
 */

package main

import (
	"context"
	"fmt"
	"net"
	"net/http"
)

// Device object brings all parts together, namely:
//   - HTTP proxy server
//   - USB-backed http.Transport
//   - DNS-SD advertiser
//
// There is one instance of Device object per USB device
type Device struct {
	UsbAddr        UsbAddr         // Device's USB address
	State          *DevState       // Persistent state
	HTTPClient     *http.Client    // HTTP client for internal queries
	HTTPProxy      *HTTPProxy      // HTTP proxy
	UsbTransport   *UsbTransport   // Backing USB transport
	DNSSdPublisher *DNSSdPublisher // DNS-SD publisher
	Log            *Logger         // Device's logger
}

// NewDevice creates new Device object
func NewDevice(desc UsbDeviceDesc) (*Device, error) {
	dev := &Device{
		UsbAddr: desc.UsbAddr,
	}

	var err error
	var info UsbDeviceInfo
	var listener net.Listener
	var ippinfo *IppPrinterInfo
	var dnssdName string
	var dnssdServices DNSSdServices
	var log *LogMessage
	var hwid string
	var quirks Quirks
	var httpstatus int
	var canPrint bool
	var canScan bool

	// Create USB transport
	dev.UsbTransport, err = NewUsbTransport(desc)
	if err != nil {
		goto ERROR
	}

	// Obtain quirks
	quirks = dev.UsbTransport.Quirks()

	// Obtain device's logger
	dev.Log = dev.UsbTransport.Log()

	// Obtain device info and derived information.
	info = dev.UsbTransport.UsbDeviceInfo()
	hwid = fmt.Sprintf("%4.4x&%4.4x", info.Vendor, info.Product)

	canPrint = info.BasicCaps&UsbIppBasicCapsPrint != 0
	canScan = info.BasicCaps&UsbIppBasicCapsScan != 0

	// Load persistent state
	dev.State = LoadDevState(info.Ident(), info.Comment())

	// Create HTTP client for local queries
	dev.HTTPClient = &http.Client{
		Transport: dev.UsbTransport,
	}

	// Create net.Listener
	listener, err = dev.State.HTTPListen()
	if err != nil {
		goto ERROR
	}

	// Configure transport for init
	dev.UsbTransport.SetTimeout(quirks.GetInitTimeout())

	// Create HTTP server
	dev.HTTPProxy = NewHTTPProxy(dev.Log, listener, dev.UsbTransport)

	// Obtain DNS-SD info for IPP
	log = dev.Log.Begin()
	defer log.Commit()

	ippinfo, httpstatus, err = IppService(log, &dnssdServices,
		dev.State.HTTPPort, info, dev.UsbTransport.Quirks(),
		dev.HTTPClient)

	if err != nil {
		dev.Log.Error('!', "IPP: %s", err)

		canRetry := httpstatus != 0 || ErrIsEOF(err)
		if canRetry && canPrint && quirks.GetInitRetryPartial() {
			dev.Log.Begin().
				Info(' ', "Printer not ready (HTTP status %d)",
					httpstatus).
				Info(' ', "Retrying due to the %q quirk",
					QuirkNmInitRetryPartial).
				Commit()

			err = ErrPartialInit
			goto ERROR
		}
	}

	log.Flush()

	if dev.UsbTransport.TimeoutExpired() {
		err = ErrInitTimedOut
		goto ERROR
	}

	// Obtain DNS-SD name
	if ippinfo != nil {
		dnssdName = ippinfo.DNSSdName
	} else {
		dnssdName = info.DNSSdName()
	}

	// Update device state, if name changed
	if dnssdName != dev.State.DNSSdName {
		dev.State.DNSSdName = dnssdName
		dev.State.DNSSdOverride = dnssdName
		dev.State.Save()
	}

	// Obtain DNS-SD info for eSCL
	httpstatus, err = EsclService(log, &dnssdServices, dev.State.HTTPPort, info,
		ippinfo, dev.HTTPClient)

	if err != nil {
		dev.Log.Error('!', "ESCL: %s", err)

		canRetry := httpstatus != 0 || ErrIsEOF(err)
		if canRetry && canScan && quirks.GetInitRetryPartial() {
			dev.Log.Begin().
				Info(' ', "Scanner not ready (HTTP status %d)",
					httpstatus).
				Info(' ', "Retrying due to the %q quirk",
					QuirkNmInitRetryPartial).
				Commit()

			err = ErrPartialInit
			goto ERROR
		}
	}

	log.Flush()

	if dev.UsbTransport.TimeoutExpired() {
		err = ErrInitTimedOut
		goto ERROR
	}

	// Update IPP service advertising for scanner presence
	if ippinfo != nil {
		if ippSvc := &dnssdServices[ippinfo.IppSvcIndex]; err == nil {
			ippSvc.Txt.Add("Scan", "T")
		} else {
			ippSvc.Txt.Add("Scan", "F")
		}
	}

	// Skip the device, if it cannot do something useful
	//
	// Some devices (so far, only HP-rebranded Samsung devices
	// known to have such a defect) offer 7/1/4 interfaces, but
	// actually provide no functionality behind these interfaces
	// and respond with `HTTP 404 Not found` to all the HTTP
	// requests sent to USB
	//
	// ipp-usb ignores such devices to let a chance for
	// legacy/proprietary drivers to work with them
	if len(dnssdServices) == 0 {
		err = ErrUnusable
		goto ERROR
	}

	// Add common TXT records:
	//   - usb_SER=VCF9192281  ; Device USB serial number
	//   - usb_HWID=0482&069d  ; Its vendor and device ID
	for i := range dnssdServices {
		svc := &dnssdServices[i]
		svc.Txt.Add("usb_SER", info.SerialNumber)
		svc.Txt.Add("usb_HWID", hwid)
	}

	// Advertise Web service. Assume it always exists
	dnssdServices.Add(DNSSdSvcInfo{Type: "_http._tcp", Port: dev.State.HTTPPort})

	// Advertise service with the following parameters:
	//   Instance: "BBPP", where BB and PP are bus and port numbers in hex
	//   Type:     "_ipp-usb._tcp"
	//
	// The purpose of this advertising is to help legacy drivers to
	// easily check for devices, handled by ipp-usb
	//
	// See the following for details:
	//     https://github.com/OpenPrinting/ipp-usb/issues/28
	dnssdServices.Add(DNSSdSvcInfo{
		Instance: fmt.Sprintf("%.2X%.2x", desc.Bus, info.PortNum),
		Type:     "_ipp-usb._tcp",
		Port:     dev.State.HTTPPort,
		Loopback: true,
	})

	// Enable handling incoming requests
	dev.UsbTransport.SetTimeout(0)
	dev.HTTPProxy.Enable()

	// Start DNS-SD publisher
	for _, svc := range dnssdServices {
		dev.Log.Debug('>', "%s: %s TXT record:", dnssdName, svc.Type)
		for _, txt := range svc.Txt {
			dev.Log.Debug(' ', "  %s=%s", txt.Key, txt.Value)
		}
	}

	if Conf.DNSSdEnable {
		dev.DNSSdPublisher = NewDNSSdPublisher(dev.Log, dev.State,
			dnssdServices)
		err = dev.DNSSdPublisher.Publish()
		if err != nil {
			goto ERROR
		}
	}

	return dev, nil

ERROR:
	if dev.HTTPProxy != nil {
		dev.HTTPProxy.Close()
	}

	if dev.UsbTransport != nil {
		reset := true
		switch err {
		case ErrUnusable, ErrPartialInit:
			reset = false
		}
		dev.UsbTransport.Close(reset)
	}

	if listener != nil {
		listener.Close()
	}

	return nil, err
}

// Shutdown gracefully shuts down the device. If provided context
// expires before the shutdown is complete, Shutdown returns the
// context's error
func (dev *Device) Shutdown(ctx context.Context) error {
	if dev.DNSSdPublisher != nil {
		dev.DNSSdPublisher.Unpublish()
		dev.DNSSdPublisher = nil
	}

	if dev.HTTPProxy != nil {
		dev.HTTPProxy.Close()
		dev.HTTPProxy = nil
	}

	if dev.UsbTransport != nil {
		return dev.UsbTransport.Shutdown(ctx)
	}

	return nil
}

// Close the Device
func (dev *Device) Close() {
	if dev.DNSSdPublisher != nil {
		dev.DNSSdPublisher.Unpublish()
		dev.DNSSdPublisher = nil
	}

	if dev.HTTPProxy != nil {
		dev.HTTPProxy.Close()
		dev.HTTPProxy = nil
	}

	if dev.UsbTransport != nil {
		dev.UsbTransport.Close(false)
		dev.UsbTransport = nil
	}
}
07070100000011000081A400000000000000000000000167D72F5D00001741000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/devstate.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Per-device persistent state
 */

package main

import (
	"bytes"
	"fmt"
	"io"
	"net"
	"os"
	"path/filepath"
	"strconv"
)

// DevState manages a per-device persistent state (such as HTTP
// port allocation etc)
type DevState struct {
	Ident         string // Device identification
	HTTPPort      int    // Allocated HTTP port
	DNSSdName     string // DNS-SD name, as reported by device
	DNSSdOverride string // DNS-SD name after collision resolution

	comment string // Comment in the state file
	path    string // Path to the disk file
}

// LoadDevState loads DevState from a disk file
//
// This function always succeeds, even in a case of file i/o errors.
// In a worst case we loose state persistence, not other functionality.
func LoadDevState(ident, comment string) *DevState {
	state := &DevState{
		Ident:   ident,
		comment: comment,
	}
	state.path = state.devStatePath()

	// Read state file
	ini, err := OpenIniFile(state.path)
	if err == nil {
		err = state.load(ini)
		ini.Close()
	}

	if err != nil && err != io.EOF {
		if !os.IsNotExist(err) {
			Log.Error('!', "STATE LOAD: %s", state.error("%s", err))
		}
	}

	return state
}

// LoadUsedPorts loads ports used by some of devices.
//
// The returned map contains one entry per used port. Value of this
// entry is a human-readable string, reasonable for logging
func LoadUsedPorts() (ports map[int]string) {
	ports = make(map[int]string)

	// Read the PathProgStateDev (normally "/var/ipp-usb/dev")
	// directory.
	var files []os.FileInfo
	var err error

	dir, err := os.Open(PathProgStateDev)
	if err == nil {
		files, err = dir.Readdir(0)
		dir.Close()
	}

	if err != nil {
		Log.Error('!', "Can't load existing ports allocation")
		Log.Error('!', "%s", err)
		return
	}

	if err != nil {
		return
	}

	// Scan found files
	for _, file := range files {
		Log.Debug(' ', "== %s", file.Name())
		if !file.Mode().IsRegular() {
			continue
		}

		path := filepath.Join(PathProgStateDev, file.Name())
		ini, err := OpenIniFile(path)
		if err != nil {
			Log.Error('!', "%s", err)
			continue
		}

		state := &DevState{}
		err = state.load(ini)
		ini.Close()

		if err != nil {
			Log.Error('!', "%s", err)
			continue
		}

		if state.HTTPPort != 0 {
			ports[state.HTTPPort] = file.Name()
		}
	}

	return
}

// load performs an actual work of loading the DevState file
func (state *DevState) load(ini *IniFile) error {
	err := ini.Lock(FileLockWait)
	if err == nil {
		defer ini.Unlock()
	}

	for err == nil {
		var rec *IniRecord
		rec, err = ini.Next()
		if err != nil {
			break
		}

		switch rec.Section {
		case "device":
			switch rec.Key {
			case "http-port":
				err = state.loadTCPPort(&state.HTTPPort, rec)
			case "dns-sd-name":
				state.DNSSdName = rec.Value
			case "dns-sd-override":
				state.DNSSdOverride = rec.Value
			}
		}

	}

	if err == io.EOF {
		err = nil
	}

	return err
}

// Load TCP port
func (state *DevState) loadTCPPort(out *int, rec *IniRecord) error {
	port, err := strconv.Atoi(rec.Value)

	if err != nil {
		err = state.error("%s", err)
	} else if port < 1 || port > 65535 {
		err = state.error("%s: out of range", rec.Key)
	}

	if err != nil {
		return err
	}

	*out = port

	return nil
}

// Save updates DevState on disk
func (state *DevState) Save() {
	os.MkdirAll(PathProgStateDev, 0755)

	var buf bytes.Buffer

	if state.comment != "" {
		fmt.Fprintf(&buf, "; %s\n", state.comment)
	}

	fmt.Fprintf(&buf, "[device]\n")
	fmt.Fprintf(&buf, "http-port       = %d\n", state.HTTPPort)
	fmt.Fprintf(&buf, "dns-sd-name     = %q\n", state.DNSSdName)
	fmt.Fprintf(&buf, "dns-sd-override = %q\n", state.DNSSdOverride)

	err := state.save(buf.Bytes())
	if err != nil {
		err = state.error("%s", err)
		Log.Error('!', "STATE SAVE: %s", err)
	}
}

// save performs an actual work of saving state file
func (state *DevState) save(data []byte) error {
	f, err := os.OpenFile(state.path,
		os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)

	if err != nil {
		return err
	}

	err = FileLock(f, FileLockWait)
	if err != nil {
		f.Close()
		return err
	}

	_, err = f.Write(data)
	FileUnlock(f)

	if err != nil {
		f.Close()
		return err
	}

	return f.Close()
}

// HTTPListen allocates HTTP port and updates persistent configuration
func (state *DevState) HTTPListen() (net.Listener, error) {
	port := state.HTTPPort

	// Check that preallocated port is within the configured range
	if !(Conf.HTTPMinPort <= port && port <= Conf.HTTPMaxPort) {
		port = 0
	}

	// Try to allocate port used before
	if port != 0 {
		listener, err := NewListener(port)
		if err == nil {
			return listener, nil
		}
	}

	// Allocate a port. Don't reuse ports allocated by other
	// devices.
	ports := LoadUsedPorts()

	for port = Conf.HTTPMinPort; port <= Conf.HTTPMaxPort; port++ {
		used := ports[port]
		if used != "" {
			Log.Info(' ', "HTTP port %d used by %s", port, used)
			continue
		}

		listener, err := NewListener(port)
		if err == nil {
			state.HTTPPort = port
			state.Save()
			return listener, nil
		}
	}

	// No success so far. Repeat allocation attempt, ignoring
	// existent allocations
	for port = Conf.HTTPMinPort; port <= Conf.HTTPMaxPort; port++ {
		listener, err := NewListener(port)
		if err == nil {
			state.HTTPPort = port
			state.Save()
			return listener, nil
		}
	}

	// Give up and return an error
	err := state.error("failed to allocate HTTP port", state.Ident)
	Log.Error('!', "STATE PORT: %s", err)

	return nil, err
}

// devStatePath returns a path to the DevState file
func (state *DevState) devStatePath() string {
	return filepath.Join(PathProgStateDev, state.Ident+".state")
}

// error creates a state-related error
func (state *DevState) error(format string, args ...interface{}) error {
	return fmt.Errorf(state.Ident+": "+format, args...)
}
07070100000012000081A400000000000000000000000167D72F5D00001E45000000000000000000000000000000000000001800000000ipp-usb-0.9.30/dnssd.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * DNS-SD publisher: system-independent stuff
 */

package main

import (
	"fmt"
	"strings"
	"sync"
	"time"
)

// DNSSdTxtItem represents a single TXT record item
type DNSSdTxtItem struct {
	Key, Value string // TXT entry: Key=Value
	URL        bool   // It's an URL, hostname must be adjusted
}

// DNSSdTxtRecord represents a TXT record
type DNSSdTxtRecord []DNSSdTxtItem

// Add adds regular (non-URL) item to DNSSdTxtRecord
func (txt *DNSSdTxtRecord) Add(key, value string) {
	*txt = append(*txt, DNSSdTxtItem{key, value, false})
}

// AddURL adds URL item to DNSSdTxtRecord
func (txt *DNSSdTxtRecord) AddURL(key, value string) {
	*txt = append(*txt, DNSSdTxtItem{key, value, true})
}

// AddPDL adds PDL list (list of supported Page Description Languages, i.e.,
// document formats) to the DNSSdTxtRecord.
//
// Sometimes the PDL list that comes from device, is too large to fit
// TXT record (key=value pair must not exceed 255 bytes). At this case
// we take only as much as possible leading entries of the device-supplied
// list in hope that firmware is smart enough to place most common PDLs
// to the beginning of the list, while more exotic entries goes to the end
func (txt *DNSSdTxtRecord) AddPDL(key, value string) {
	// How many space we have for value? Is it enough?
	max := 255 - len(key) - 1
	if max >= len(value) {
		txt.Add(key, value)
		return
	}

	// Safety check
	if max <= 0 {
		return
	}

	// Truncate the value to fit available space
	value = value[:max+1]
	i := strings.LastIndexByte(value, ',')
	if i < 0 {
		return
	}

	value = value[:i]
	txt.Add(key, value)
}

// IfNotEmpty adds item to DNSSdTxtRecord if its value is not empty
//
// It returns true if item was actually added, false otherwise
func (txt *DNSSdTxtRecord) IfNotEmpty(key, value string) bool {
	if value != "" {
		txt.Add(key, value)
		return true
	}
	return false
}

// URLIfNotEmpty works as IfNotEmpty, but for URLs
func (txt *DNSSdTxtRecord) URLIfNotEmpty(key, value string) bool {
	if value != "" {
		txt.AddURL(key, value)
		return true
	}
	return false
}

// export DNSSdTxtRecord into Avahi format
func (txt DNSSdTxtRecord) export() [][]byte {
	var exported [][]byte

	// Note, for a some strange reason, Avahi published
	// TXT record in reverse order, so compensate it here
	for i := len(txt) - 1; i >= 0; i-- {
		item := txt[i]
		exported = append(exported, []byte(item.Key+"="+item.Value))
	}

	return exported
}

// DNSSdSvcInfo represents a DNS-SD service information
type DNSSdSvcInfo struct {
	Instance string         // If not "", override common instance name
	Type     string         // Service type, i.e. "_ipp._tcp"
	SubTypes []string       // Service subtypes, if any
	Port     int            // TCP port
	Txt      DNSSdTxtRecord // TXT record
	Loopback bool           // Advertise only on loopback interface
}

// DNSSdServices represents a collection of DNS-SD services
type DNSSdServices []DNSSdSvcInfo

// Add DNSSdSvcInfo to DNSSdServices
func (services *DNSSdServices) Add(srv DNSSdSvcInfo) {
	*services = append(*services, srv)
}

// DNSSdPublisher represents a DNS-SD service publisher
// One publisher may publish multiple services unser the
// same Service Instance Name
type DNSSdPublisher struct {
	Log      *Logger        // Device's logger
	DevState *DevState      // Device persistent state
	Services DNSSdServices  // Registered services
	fin      chan struct{}  // Closed to terminate publisher goroutine
	finDone  sync.WaitGroup // To wait for goroutine termination
	sysdep   *dnssdSysdep   // System-dependent stuff
}

// DNSSdStatus represents DNS-SD publisher status
type DNSSdStatus int

const (
	// DNSSdNoStatus is used to indicate that status is
	// not known (yet)
	DNSSdNoStatus DNSSdStatus = iota

	// DNSSdCollision indicates instance name collision
	DNSSdCollision

	// DNSSdFailure indicates publisher failure with any
	// other reason that listed before
	DNSSdFailure

	// DNSSdSuccess indicates successful status
	DNSSdSuccess
)

// String returns human-readable representation of DNSSdStatus
func (status DNSSdStatus) String() string {
	switch status {
	case DNSSdNoStatus:
		return "DNSSdNoStatus"
	case DNSSdCollision:
		return "DNSSdCollision"
	case DNSSdFailure:
		return "DNSSdFailure"
	case DNSSdSuccess:
		return "DNSSdSuccess"
	}

	return fmt.Sprintf("Unknown DNSSdStatus %d", status)
}

// NewDNSSdPublisher creates new DNSSdPublisher
//
// Service instance name comes from the DevState, and if
// name changes as result of name collision resolution,
// DevState will be updated
func NewDNSSdPublisher(log *Logger,
	devstate *DevState, services DNSSdServices) *DNSSdPublisher {

	return &DNSSdPublisher{
		Log:      log,
		DevState: devstate,
		Services: services,
		fin:      make(chan struct{}),
	}
}

// Publish all services
func (publisher *DNSSdPublisher) Publish() error {
	instance := publisher.instance(0)
	publisher.sysdep = newDnssdSysdep(publisher.Log, instance,
		publisher.Services)

	publisher.Log.Info('+', "DNS-SD: %s: publishing requested", instance)

	publisher.finDone.Add(1)
	go publisher.goroutine()

	return nil
}

// Unpublish everything
func (publisher *DNSSdPublisher) Unpublish() {
	close(publisher.fin)
	publisher.finDone.Wait()

	publisher.sysdep.Halt()

	publisher.Log.Info('-', "DNS-SD: %s: removed", publisher.instance(0))
}

// Build service instance name with optional collision-resolution suffix
func (publisher *DNSSdPublisher) instance(suffix int) string {
	name := publisher.DevState.DNSSdName
	strSuffix := ""

	switch {
	// This happens when we try to resolve name conflict
	case suffix != 0:
		strSuffix = fmt.Sprintf(" (USB %d)", suffix)

	// This happens when we've just initialized or reset DNSSdOverride,
	// so append "(USB)" suffix
	case publisher.DevState.DNSSdName == publisher.DevState.DNSSdOverride:
		strSuffix = " (USB)"

	// Otherwise, DNSSdOverride contains saved conflict-resolved device name
	default:
		name = publisher.DevState.DNSSdOverride
	}

	const MaxDNSSDName = 63
	if len(name)+len(strSuffix) > MaxDNSSDName {
		name = name[:MaxDNSSDName-len(strSuffix)]
	}

	return name + strSuffix
}

// Event handling goroutine
func (publisher *DNSSdPublisher) goroutine() {
	// Catch panics to log
	defer func() {
		v := recover()
		if v != nil {
			Log.Panic(v)
		}
	}()

	defer publisher.finDone.Done()

	timer := time.NewTimer(time.Hour)
	timer.Stop()       // Not ticking now
	defer timer.Stop() // And cleanup at return

	var err error
	var suffix int

	instance := publisher.instance(0)
	for {
		fail := false

		select {
		case <-publisher.fin:
			return

		case status := <-publisher.sysdep.Chan():
			switch status {
			case DNSSdSuccess:
				publisher.Log.Info(' ', "DNS-SD: %s: published", instance)
				if instance != publisher.DevState.DNSSdOverride {
					publisher.DevState.DNSSdOverride = instance
					publisher.DevState.Save()
				}

			case DNSSdCollision:
				publisher.Log.Error(' ', "DNS-SD: %s: name collision",
					instance)
				suffix++
				fallthrough

			case DNSSdFailure:
				publisher.Log.Error(' ', "DNS-SD: %s: publishing failed",
					instance)

				fail = true
				publisher.sysdep.Halt()

			default:
				publisher.Log.Error(' ', "DNS-SD: %s: unknown event %s",
					instance, status)
			}

		case <-timer.C:
			instance = publisher.instance(suffix)
			publisher.sysdep = newDnssdSysdep(publisher.Log,
				instance, publisher.Services)

			if err != nil {
				publisher.Log.Error('!', "DNS-SD: %s: %s", instance, err)
				fail = true
			}
		}

		if fail {
			timer.Reset(DNSSdRetryInterval)
		}
	}
}
07070100000013000081A400000000000000000000000167D72F5D000026A3000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/dnssd_avahi.go// +build linux freebsd

/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * DNS-SD publisher: Avahi-based system-dependent part
 */

package main

// #cgo pkg-config: avahi-client
//
// #include <stdlib.h>
// #include <avahi-client/publish.h>
// #include <avahi-common/error.h>
// #include <avahi-common/thread-watch.h>
// #include <avahi-common/watch.h>
//
// void avahiClientCallback(AvahiClient*, AvahiClientState, void*);
// void avahiEntryGroupCallback(AvahiEntryGroup*, AvahiEntryGroupState, void*);
import "C"

import (
	"bytes"
	"errors"
	"fmt"
	"net/url"
	"sync"
	"unsafe"
)

var (
	avahiInitLock     sync.Mutex
	avahiThreadedPoll *C.AvahiThreadedPoll
	avahiClientMap    = make(map[*C.AvahiClient]*dnssdSysdep)
	avahiEgroupMap    = make(map[*C.AvahiEntryGroup]*dnssdSysdep)
)

// dnssdSysdep represents a system-dependent DNS-SD advertiser
type dnssdSysdep struct {
	log        *Logger            // Device's logger
	instance   string             // Service Instance Name
	fqdn       string             // Host's fully-qualified domain name
	client     *C.AvahiClient     // Avahi client
	egroup     *C.AvahiEntryGroup // Avahi entry group
	statusChan chan DNSSdStatus   // Status notifications channel
}

// dnssdSysdepErr implements error interface on a top of
// Avahi error codes
type dnssdSysdepErr C.int

// Error returns error string for the dnssdSysdepErr
func (err dnssdSysdepErr) Error() string {
	return "Avahi error: " + C.GoString(C.avahi_strerror(C.int(err)))
}

// newDnssdSysdep creates new dnssdSysdep instance
func newDnssdSysdep(log *Logger, instance string,
	services DNSSdServices) *dnssdSysdep {

	log.Debug(' ', "DNS-SD: %s: trying", instance)

	var err error
	var poll *C.AvahiPoll
	var rc C.int
	var proto, iface int

	sysdep := &dnssdSysdep{
		log:        log,
		instance:   instance,
		statusChan: make(chan DNSSdStatus, 10),
	}

	// Obtain index of loopback interface
	loopback, err := Loopback()
	if err != nil {
		goto ERROR // Very unlikely to happen
	}

	// Obtain AvahiPoll
	poll, err = avahiGetPoll()
	if err != nil {
		goto ERROR
	}

	// Synchronize with Avahi thread
	avahiThreadLock()
	defer avahiThreadUnlock()

	// Create Avahi client
	sysdep.client = C.avahi_client_new(
		poll,
		C.AVAHI_CLIENT_NO_FAIL,
		C.AvahiClientCallback(C.avahiClientCallback),
		nil,
		&rc,
	)

	if sysdep.client == nil {
		goto AVAHI_ERROR
	}

	avahiClientMap[sysdep.client] = sysdep

	sysdep.fqdn = C.GoString(C.avahi_client_get_host_name_fqdn(sysdep.client))
	sysdep.log.Debug(' ', "DNS-SD: FQDN: %q", sysdep.fqdn)

	// Create entry group
	sysdep.egroup = C.avahi_entry_group_new(
		sysdep.client,
		C.AvahiEntryGroupCallback(C.avahiEntryGroupCallback),
		nil,
	)

	if sysdep.egroup == nil {
		rc = C.avahi_client_errno(sysdep.client)
		goto AVAHI_ERROR
	}

	avahiEgroupMap[sysdep.egroup] = sysdep

	// Compute iface and proto, adjust fqdn
	iface = C.AVAHI_IF_UNSPEC
	if Conf.LoopbackOnly {
		iface = loopback
		old := sysdep.fqdn
		sysdep.fqdn = "localhost"
		sysdep.log.Debug(' ', "DNS-SD: FQDN: %q->%q", old, sysdep.fqdn)
	}

	proto = C.AVAHI_PROTO_UNSPEC
	if !Conf.IPV6Enable {
		proto = C.AVAHI_PROTO_INET
	}

	// Populate entry group
	for _, svc := range services {
		// Prepare TXT record
		var cTxt *C.AvahiStringList
		cTxt, err = sysdep.avahiTxtRecord(svc.Port, svc.Txt)
		if err != nil {
			goto ERROR
		}

		// Prepare C strings for service instance and type
		cSvcType := C.CString(svc.Type)

		var cInstance *C.char
		if svc.Instance != "" {
			cInstance = C.CString(svc.Instance)
		} else {
			cInstance = C.CString(instance)
		}

		// Handle loopback-only mode
		ifaceInUse := iface
		if svc.Loopback {
			ifaceInUse = loopback
		}

		// Register service type
		rc = C.avahi_entry_group_add_service_strlst(
			sysdep.egroup,
			C.AvahiIfIndex(ifaceInUse),
			C.AvahiProtocol(proto),
			0,
			cInstance,
			cSvcType,
			nil, // Domain
			nil, // Host
			C.uint16_t(svc.Port),
			cTxt,
		)

		// Register subtypes, if any
		for _, subtype := range svc.SubTypes {
			if rc != C.AVAHI_OK {
				break
			}

			sysdep.log.Debug(' ', "DNS-SD: +subtype: %q", subtype)

			cSubtype := C.CString(subtype)
			rc = C.avahi_entry_group_add_service_subtype(
				sysdep.egroup,
				C.AvahiIfIndex(ifaceInUse),
				C.AvahiProtocol(proto),
				0,
				cInstance,
				cSvcType,
				nil,
				cSubtype,
			)
			C.free(unsafe.Pointer(cSubtype))

		}

		// Release C memory
		C.free(unsafe.Pointer(cInstance))
		C.free(unsafe.Pointer(cSvcType))
		C.avahi_string_list_free(cTxt)

		// Check for Avahi error
		if rc != C.AVAHI_OK {
			goto AVAHI_ERROR
		}
	}

	// Commit changes
	rc = C.avahi_entry_group_commit(sysdep.egroup)
	if rc != C.AVAHI_OK {
		goto AVAHI_ERROR
	}

	// Create and return dnssdSysdep
	return sysdep

	// Error: cleanup and exit
AVAHI_ERROR:
	err = dnssdSysdepErr(rc)
ERROR:

	// Raise an error event
	sysdep.log.Error(' ', "DNS-SD: %s: %s", sysdep.instance, err)
	sysdep.haltLocked()

	if err == dnssdSysdepErr(C.AVAHI_ERR_COLLISION) {
		sysdep.notify(DNSSdCollision)
	} else {
		sysdep.notify(DNSSdFailure)
	}

	return sysdep
}

// Halt dnssdSysdep
//
// It cancel all activity related to the dnssdSysdep instance,
// but sysdep.Chan() remains valid, though no notifications
// will be pushed there anymore
func (sysdep *dnssdSysdep) Halt() {
	avahiThreadLock()
	sysdep.haltLocked()
	avahiThreadUnlock()
}

// Get status change notification channel
func (sysdep *dnssdSysdep) Chan() <-chan DNSSdStatus {
	return sysdep.statusChan
}

// Halt dnssdSysdep -- internal version
//
// Must be called under avahiThreadLock
// Can be used with semi-constructed dnssdSysdep
func (sysdep *dnssdSysdep) haltLocked() {
	// Free all Avahi stuff
	if sysdep.egroup != nil {
		C.avahi_entry_group_free(sysdep.egroup)
		delete(avahiEgroupMap, sysdep.egroup)
		sysdep.egroup = nil
	}

	if sysdep.client != nil {
		C.avahi_client_free(sysdep.client)
		delete(avahiClientMap, sysdep.client)
		sysdep.client = nil
	}

	// Drain status channel
	for len(sysdep.statusChan) > 0 {
		<-sysdep.statusChan
	}
}

// Push status change notification
func (sysdep *dnssdSysdep) notify(status DNSSdStatus) {
	sysdep.statusChan <- status
}

// avahiTxtRecord converts DNSSdTxtRecord to AvahiStringList
func (sysdep *dnssdSysdep) avahiTxtRecord(port int, txt DNSSdTxtRecord) (
	*C.AvahiStringList, error) {
	var buf bytes.Buffer
	var list, prev *C.AvahiStringList

	for _, t := range txt {
		buf.Reset()
		buf.WriteString(t.Key)
		buf.WriteByte('=')

		if !t.URL || sysdep.fqdn == "" {
			buf.WriteString(t.Value)
		} else {
			value := t.Value
			if parsed, err := url.Parse(value); err == nil && parsed.IsAbs() {
				parsed.Host = sysdep.fqdn
				if port != 0 {
					parsed.Host += fmt.Sprintf(":%d", port)
				}

				value = parsed.String()
			}
			buf.WriteString(value)
		}

		b := buf.Bytes()

		prev, list = list, C.avahi_string_list_add_arbitrary(
			list,
			(*C.uint8_t)(unsafe.Pointer(&b[0])),
			C.size_t(len(b)),
		)

		if list == nil {
			C.avahi_string_list_free(prev)
			return nil, ErrNoMemory
		}
	}

	return C.avahi_string_list_reverse(list), nil
}

// avahiClientCallback called by Avahi client to notify us about
// client state change
//
//export avahiClientCallback
func avahiClientCallback(client *C.AvahiClient,
	state C.AvahiClientState, _ unsafe.Pointer) {

	sysdep := avahiClientMap[client]
	if sysdep == nil {
		return
	}

	status := DNSSdNoStatus
	event := ""

	switch state {
	case C.AVAHI_CLIENT_S_REGISTERING:
		event = "AVAHI_CLIENT_S_REGISTERING"
	case C.AVAHI_CLIENT_S_RUNNING:
		event = "AVAHI_CLIENT_S_RUNNING"
	case C.AVAHI_CLIENT_S_COLLISION:
		// This is host name collision. We can't recover
		// it here, so lets consider it as DNSSdFailure
		event = "AVAHI_CLIENT_S_COLLISION"
		status = DNSSdFailure
	case C.AVAHI_CLIENT_FAILURE:
		event = "AVAHI_CLIENT_FAILURE"
		status = DNSSdFailure
	case C.AVAHI_CLIENT_CONNECTING:
		event = "AVAHI_CLIENT_CONNECTING"
	default:
		event = fmt.Sprintf("Unknown event %d", state)
	}

	sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event)
	if status != DNSSdNoStatus {
		sysdep.notify(status)
	}
}

// avahiEntryGroupCallback called by Avahi client to notify us about
// entry group state change
//
//export avahiEntryGroupCallback
func avahiEntryGroupCallback(egroup *C.AvahiEntryGroup,
	state C.AvahiEntryGroupState, _ unsafe.Pointer) {

	sysdep := avahiEgroupMap[egroup]
	if sysdep == nil {
		return
	}

	status := DNSSdNoStatus
	event := ""

	switch state {
	case C.AVAHI_ENTRY_GROUP_UNCOMMITED:
		event = "AVAHI_ENTRY_GROUP_UNCOMMITED"
	case C.AVAHI_ENTRY_GROUP_REGISTERING:
		event = "AVAHI_ENTRY_GROUP_REGISTERING"
	case C.AVAHI_ENTRY_GROUP_ESTABLISHED:
		event = "AVAHI_ENTRY_GROUP_ESTABLISHED"
		status = DNSSdSuccess
	case C.AVAHI_ENTRY_GROUP_COLLISION:
		event = "AVAHI_ENTRY_GROUP_COLLISION"
		status = DNSSdCollision
	case C.AVAHI_ENTRY_GROUP_FAILURE:
		event = "AVAHI_ENTRY_GROUP_FAILURE"
		status = DNSSdFailure
	}

	sysdep.log.Debug(' ', "DNS-SD: %s: %s", sysdep.instance, event)
	if status != DNSSdNoStatus {
		sysdep.notify(status)
	}
}

// avahiGetPoll returns pointer to AvahiPoll
// Avahi helper thread is created on demand
func avahiGetPoll() (*C.AvahiPoll, error) {
	avahiInitLock.Lock()
	defer avahiInitLock.Unlock()

	if avahiThreadedPoll == nil {
		avahiThreadedPoll = C.avahi_threaded_poll_new()
		if avahiThreadedPoll == nil {
			return nil, errors.New("initialization failed, not enough memory")
		}

		C.avahi_threaded_poll_start(avahiThreadedPoll)
	}

	return C.avahi_threaded_poll_get(avahiThreadedPoll), nil
}

// Lock Avahi thread
func avahiThreadLock() {
	C.avahi_threaded_poll_lock(avahiThreadedPoll)
}

// Unlock Avahi thread
func avahiThreadUnlock() {
	C.avahi_threaded_poll_unlock(avahiThreadedPoll)
}
07070100000014000081A400000000000000000000000167D72F5D00000418000000000000000000000000000000000000001600000000ipp-usb-0.9.30/err.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Common errors
 */

package main

import (
	"errors"
	"io"
	"net/url"
)

// Error values for ipp-usb
var (
	ErrLockIsBusy   = errors.New("Lock is busy")
	ErrNoMemory     = errors.New("Not enough memory")
	ErrShutdown     = errors.New("Shutdown requested")
	ErrBlackListed  = errors.New("Device is blacklisted")
	ErrInitTimedOut = errors.New("Device initialization timed out")
	ErrUnusable     = errors.New("Device doesn't implement print or scan service")
	ErrNoIppUsb     = errors.New("ipp-usb daemon not running")
	ErrAccess       = errors.New("Access denied")
	ErrPartialInit  = errors.New("Some parts of device not ready yet")
)

// ErrIsEOF tells if error is io.EOF, possibly wrapped by
// the Go HTTP client.
func ErrIsEOF(err error) bool {
	if urlerr, ok := err.(*url.Error); ok {
		return urlerr.Err == io.EOF
	}

	return err == io.EOF
}
07070100000015000081A400000000000000000000000167D72F5D00001B41000000000000000000000000000000000000001700000000ipp-usb-0.9.30/escl.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * ESCL service registration
 */

package main

import (
	"bytes"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"sort"
	"strings"
)

// EsclService queries eSCL ScannerCapabilities using provided
// http.Client and decodes received information into the form
// suitable for DNS-SD registration
//
// Discovered services will be added to the services collection
func EsclService(log *LogMessage, services *DNSSdServices,
	port int, usbinfo UsbDeviceInfo, ippinfo *IppPrinterInfo,
	c *http.Client) (httpstatus int, err error) {

	uri := fmt.Sprintf("http://localhost:%d/eSCL/ScannerCapabilities", port)

	decoder := newEsclCapsDecoder(ippinfo)
	svc := DNSSdSvcInfo{
		Type: "_uscan._tcp",
		Port: port,
	}

	var xmlData []byte
	var list []string

	// Query ScannerCapabilities
	resp, err := c.Get(uri)
	if err != nil {
		goto ERROR
	}

	if resp.StatusCode/100 != 2 {
		resp.Body.Close()
		httpstatus = resp.StatusCode
		err = fmt.Errorf("HTTP status: %s", resp.Status)
		goto ERROR
	}

	xmlData, err = ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		goto ERROR
	}

	log.Add(LogTraceESCL, '<', "ESCL Scanner Capabilities:")
	log.LineWriter(LogTraceESCL, '<').WriteClose(xmlData)
	log.Nl(LogTraceESCL)
	log.Flush()

	// Decode the XML
	err = decoder.decode(bytes.NewBuffer(xmlData))
	if err != nil {
		goto ERROR
	}

	if decoder.uuid == "" {
		decoder.uuid = usbinfo.UUID()
	}

	// If we have no data, assume eSCL response was invalud
	// If we miss some essential data, assume eSCL response was invalid
	switch {
	case decoder.version == "":
		err = errors.New("missed pwg:Version")
	case len(decoder.cs) == 0:
		err = errors.New("missed scan:ColorMode")
	case len(decoder.pdl) == 0:
		err = errors.New("missed pwg:DocumentFormat")
	case !(decoder.platen || decoder.adf):
		err = errors.New("missed pwg:DocumentFormat")
	}

	if err != nil {
		goto ERROR
	}

	// Build eSCL DNSSdInfo
	if decoder.duplex {
		svc.Txt.Add("duplex", "T")
	} else {
		svc.Txt.Add("duplex", "F")
	}

	switch {
	case decoder.platen && !decoder.adf:
		svc.Txt.Add("is", "platen")
	case !decoder.platen && decoder.adf:
		svc.Txt.Add("is", "adf")
	case decoder.platen && decoder.adf:
		svc.Txt.Add("is", "platen,adf")
	}

	list = []string{}
	for c := range decoder.cs {
		list = append(list, c)
	}
	sort.Strings(list)
	svc.Txt.IfNotEmpty("cs", strings.Join(list, ","))

	svc.Txt.IfNotEmpty("UUID", decoder.uuid)
	svc.Txt.URLIfNotEmpty("adminurl", decoder.adminurl)
	svc.Txt.URLIfNotEmpty("representation", decoder.representation)

	list = []string{}
	for p := range decoder.pdl {
		list = append(list, p)
	}
	sort.Strings(list)
	svc.Txt.AddPDL("pdl", strings.Join(list, ","))

	svc.Txt.Add("ty", usbinfo.ProductName)
	svc.Txt.Add("rs", "eSCL")
	svc.Txt.IfNotEmpty("vers", decoder.version)
	svc.Txt.IfNotEmpty("txtvers", "1")

	// Add to services
	services.Add(svc)

	return

	// Handle a error
ERROR:
	if !ErrIsEOF(err) {
		err = fmt.Errorf("eSCL: %s", err)
	}

	return
}

// esclCapsDecoder represents eSCL ScannerCapabilities decoder
type esclCapsDecoder struct {
	uuid           string              // Device UUID
	adminurl       string              // Admin URL
	representation string              // Icon URL
	version        string              // eSCL Version
	platen, adf    bool                // Has platen/ADF
	duplex         bool                // Has duplex
	pdl, cs        map[string]struct{} // Formats/colors
}

// newesclCapsDecoder creates new esclCapsDecoder
func newEsclCapsDecoder(ippinfo *IppPrinterInfo) *esclCapsDecoder {
	decoder := &esclCapsDecoder{
		pdl: make(map[string]struct{}),
		cs:  make(map[string]struct{}),
	}

	if ippinfo != nil {
		decoder.uuid = ippinfo.UUID
		decoder.adminurl = ippinfo.AdminURL
		decoder.representation = ippinfo.IconURL
	}

	return decoder
}

// Decode scanner capabilities
func (decoder *esclCapsDecoder) decode(in io.Reader) error {
	xmlDecoder := xml.NewDecoder(in)

	var path bytes.Buffer
	var lenStack []int

	for {
		token, err := xmlDecoder.RawToken()
		if err != nil {
			break
		}

		switch t := token.(type) {
		case xml.StartElement:
			lenStack = append(lenStack, path.Len())
			path.WriteByte('/')
			path.WriteString(t.Name.Space)
			path.WriteByte(':')
			path.WriteString(t.Name.Local)
			decoder.element(path.String())

		case xml.EndElement:
			last := len(lenStack) - 1
			path.Truncate(lenStack[last])
			lenStack = lenStack[:last]

		case xml.CharData:
			data := bytes.TrimSpace(t)
			if len(data) > 0 {
				decoder.data(path.String(), string(data))
			}
		}
	}

	return nil
}

const (
	// Relative to root
	esclPlaten          = "/scan:ScannerCapabilities/scan:Platen"
	esclAdf             = "/scan:ScannerCapabilities/scan:Adf"
	esclPlatenInputCaps = esclPlaten + "/scan:PlatenInputCaps"
	esclAdfSimplexCaps  = esclAdf + "/scan:AdfSimplexInputCaps"
	esclAdfDuplexCaps   = esclAdf + "/scan:AdfDuplexInputCaps"

	// Relative to esclPlatenInputCaps, esclAdfSimplexCaps or esclAdfDuplexCaps
	esclSettingProfile    = "/scan:SettingProfiles/scan:SettingProfile"
	esclColorMode         = esclSettingProfile + "/scan:ColorModes/scan:ColorMode"
	esclDocumentFormat    = esclSettingProfile + "/scan:DocumentFormats/pwg:DocumentFormat"
	esclDocumentFormatExt = esclSettingProfile + "/scan:DocumentFormats/scan:DocumentFormatExt"
)

// handle beginning of XML element
func (decoder *esclCapsDecoder) element(path string) {
	switch path {
	case esclPlaten:
		decoder.platen = true
	case esclAdf:
		decoder.adf = true
	case esclAdfDuplexCaps:
		decoder.duplex = true
	}
}

// handle XML element data
func (decoder *esclCapsDecoder) data(path, data string) {
	switch path {
	case "/scan:ScannerCapabilities/scan:UUID":
		uuid := UUIDNormalize(data)
		if uuid != "" && decoder.uuid == "" {
			decoder.uuid = data
		}
	case "/scan:ScannerCapabilities/scan:AdminURI":
		decoder.adminurl = data
	case "/scan:ScannerCapabilities/scan:IconURI":
		decoder.representation = data
	case "/scan:ScannerCapabilities/pwg:Version":
		decoder.version = data

	case esclPlatenInputCaps + esclColorMode,
		esclAdfSimplexCaps + esclColorMode,
		esclAdfDuplexCaps + esclColorMode:

		data = strings.ToLower(data)
		switch {
		case strings.HasPrefix(data, "rgb"):
			decoder.cs["color"] = struct{}{}
		case strings.HasPrefix(data, "grayscale"):
			decoder.cs["grayscale"] = struct{}{}
		case strings.HasPrefix(data, "blackandwhite"):
			decoder.cs["binary"] = struct{}{}
		}

	case esclPlatenInputCaps + esclDocumentFormat,
		esclAdfSimplexCaps + esclDocumentFormat,
		esclAdfDuplexCaps + esclDocumentFormat:

		decoder.pdl[data] = struct{}{}

	case esclPlatenInputCaps + esclDocumentFormatExt,
		esclAdfSimplexCaps + esclDocumentFormatExt,
		esclAdfDuplexCaps + esclDocumentFormatExt:

		decoder.pdl[data] = struct{}{}
	}
}
07070100000016000081A400000000000000000000000167D72F5D00000574000000000000000000000000000000000000001D00000000ipp-usb-0.9.30/flock_unix.go// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris

/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * File locking -- UNIX version
 */

package main

/*
#include <errno.h>
#include <sys/file.h>

static inline int do_flock (int fd, int op) {
    int rc = flock(fd, op);
    if (rc < 0) {
        rc = -errno;
    }
    return rc;
}

*/
import "C"

import (
	"os"
	"syscall"
)

// FileLockCmd represents set of possible values for the
// FileLock argument
type FileLockCmd C.int

const (
	// FileLockWait command used to lock the file; wait if it is busy
	FileLockWait = C.LOCK_EX

	// FileLockNoWait command used to lock the file without wait.
	// If file is busy it fails with ErrLockIsBusy error
	FileLockNoWait = C.LOCK_EX | C.LOCK_NB

	// FileLockUnlock command used to unlock the file
	FileLockUnlock = C.LOCK_UN
)

// FileLock manages file lock
func FileLock(file *os.File, cmd FileLockCmd) error {
	rc := C.do_flock(C.int(file.Fd()), C.int(cmd))
	if rc == 0 {
		return nil
	}

	var err error = syscall.Errno(-rc)
	switch err {
	case syscall.EACCES, syscall.EAGAIN:
		err = ErrLockIsBusy
	}

	return err
}

// FileUnlock releases file lock
func FileUnlock(file *os.File) error {
	return FileLock(file, FileLockUnlock)
}
07070100000017000081A400000000000000000000000167D72F5D0000060B000000000000000000000000000000000000001700000000ipp-usb-0.9.30/glob.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Glob-style pattern matching
 */

package main

// GlobMatch matches string against glob-style pattern.
// Pattern  may contain wildcards and has a following syntax:
//   *   - matches any sequence of characters
//   ?   - matches exactly one character
//   \ C - matches character C
//   C   - matches character C (C is not *, ? or \)
//
// It return a counter of matched non-wildcard characters, -1 if no match
func GlobMatch(str, pattern string) int {
	return globMatchInternal(str, pattern, 0)
}

// globMatchInternal does the actual work of GlobMatch() function
func globMatchInternal(str, pattern string, count int) int {
	for str != "" && pattern != "" {
		p := pattern[0]
		pattern = pattern[1:]

		switch p {
		case '*':
			for pattern != "" && pattern[0] == '*' {
				pattern = pattern[1:]
			}

			if pattern == "" {
				return count
			}

			for i := 0; i < len(str); i++ {
				c2 := globMatchInternal(str[i:], pattern, count)
				if c2 >= 0 {
					return c2
				}
			}

		case '?':
			str = str[1:]

		case '\\':
			if pattern == "" {
				return -1
			}
			p, pattern = pattern[0], pattern[1:]
			fallthrough

		default:
			if str[0] != p {
				return -1
			}
			str = str[1:]
			count++

		}
	}

	for pattern != "" && pattern[0] == '*' {
		pattern = pattern[1:]
	}

	if str == "" && pattern == "" {
		return count
	}

	return -1
}
07070100000018000081A400000000000000000000000167D72F5D0000036B000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/glob_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Tests for glob-style pattern matching
 */

package main

import (
	"testing"
)

// Test GlobMatch
func TestGlobMatch(t *testing.T) {
	testData := []struct {
		model, pattern string
		count          int
	}{
		{"test", "test", 4},
		{"test", "tes?", 3},
		{"test", "te?t", 3},
		{"test", "te??", 2},
		{"test", "te??x", -1},
		{"test", "te*", 2},
		{"test", "te**", 2},
		{"test", "*te**", 2},
		{"", "*", 0},
		{"test", "t\\est", 4},
		{"t?st", "t\\?st", 4},
	}

	for _, data := range testData {
		n := GlobMatch(data.model, data.pattern)
		if n != data.count {
			t.Errorf("matchModelName(%q,%q): expected %d got %d",
				data.model, data.pattern, data.count, n)
		}
	}
}
07070100000019000081A400000000000000000000000167D72F5D0000005E000000000000000000000000000000000000001600000000ipp-usb-0.9.30/go.modmodule github.com/OpenPrinting/ipp-usb

go 1.11

require github.com/OpenPrinting/goipp v1.1.0
0707010000001A000081A400000000000000000000000167D72F5D000000B1000000000000000000000000000000000000001600000000ipp-usb-0.9.30/go.sumgithub.com/OpenPrinting/goipp v1.1.0 h1:AK19DwnuvCaqbF6ckT2ICe/Hc1o1sVSS+UzE59z4Dx0=
github.com/OpenPrinting/goipp v1.1.0/go.mod h1:ot2iw+QF7fVLaX+55JUNlF5YSDNiXVo2LRAv21iGcQI=
0707010000001B000081A400000000000000000000000167D72F5D000019F3000000000000000000000000000000000000001700000000ipp-usb-0.9.30/http.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * HTTP proxy
 */

package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync/atomic"
)

var (
	httpSessionID int32
)

// HTTPProxy represents HTTP protocol proxy backed by the
// specified http.RoundTripper. It implements http.Handler
// interface
type HTTPProxy struct {
	log       *Logger       // Logger instance
	server    *http.Server  // HTTP server
	enable    bool          // Proxy can handle incoming requests
	transport *UsbTransport // Transport for outgoing requests
	closeWait chan struct{} // Closed at server close
}

// NewHTTPProxy creates new HTTP proxy
func NewHTTPProxy(logger *Logger,
	listener net.Listener, transport *UsbTransport) *HTTPProxy {

	proxy := &HTTPProxy{
		log:       logger,
		transport: transport,
		closeWait: make(chan struct{}),
	}

	proxy.server = &http.Server{
		Handler:  proxy,
		ErrorLog: log.New(logger.LineWriter(LogError, '!'), "", 0),
	}

	go func() {
		proxy.server.Serve(listener)
		close(proxy.closeWait)
	}()

	return proxy
}

// Close the proxy
func (proxy *HTTPProxy) Close() {
	proxy.server.Close()
	<-proxy.closeWait
}

// Enable indicates that initialization is completed and
// incoming requests can be handled
func (proxy *HTTPProxy) Enable() {
	proxy.enable = true
}

// Handle HTTP request
func (proxy *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Catch panics to log
	defer func() {
		v := recover()
		if v != nil {
			Log.Panic(v)
		}
	}()

	session := int(atomic.AddInt32(&httpSessionID, 1)-1) % 1000

	// Perform sanity checking
	if !proxy.enable {
		proxy.httpError(session, w, r, http.StatusServiceUnavailable,
			errors.New("ipp-usb is not ready for this device"))
		return
	}

	if r.Method == "CONNECT" {
		proxy.httpError(session, w, r, http.StatusMethodNotAllowed,
			errors.New("CONNECT not allowed"))
		return
	}

	if r.Header.Get("Upgrade") != "" {
		proxy.httpError(session, w, r, http.StatusServiceUnavailable,
			errors.New("Protocol upgrade is not implemented"))
		return
	}

	if r.URL.IsAbs() {
		proxy.httpError(session, w, r, http.StatusServiceUnavailable,
			errors.New("Absolute URL not allowed"))
		return
	}

	// Obtain request's client and server addresses
	var clientAddr, serverAddr *net.TCPAddr

	clientAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
	if err != nil {
		proxy.httpError(session, w, r, http.StatusInternalServerError,
			errors.New("Unable to get client address for request"))
		return
	}

	if v := r.Context().Value(http.LocalAddrContextKey); v != nil {
		if v != nil {
			serverAddr, _ = v.(*net.TCPAddr)
		}
	}

	if serverAddr == nil {
		proxy.httpError(session, w, r, http.StatusInternalServerError,
			errors.New("Unable to get server address for request"))
		return
	}

	// Authenticate
	if status, err := AuthHTTPRequest(proxy.log,
		clientAddr, serverAddr, r); err != nil {
		proxy.httpError(session, w, r, status, err)
		return
	}

	// Adjust request headers
	httpRemoveHopByHopHeaders(r.Header)

	if r.Host == "" {
		if serverAddr.IP.IsLoopback() {
			r.Host = fmt.Sprintf("localhost:%d", serverAddr.Port)
		} else {
			r.Host = serverAddr.String()
		}
	}

	r.URL.Scheme = "http"
	r.URL.Host = r.Host

	// If request is ordered to the loopback address, and r.Host is not
	// "localhost" or "localhost:port", redirect request to the localhost
	//
	// Note, IPP over USB specification requires Host: to be always
	// "localhost" or "localhost:port". Although most of the printers
	// accept any syntactically correct Host: header, some of the OKI
	// printers doesn't, and reject requests that violate this rule
	//
	// This redirection fixes compatibility with these printers for
	// clients that follow redirects (i.e., web browser and sane-airscan;
	// CUPS unfortunately doesn't follow redirects)
	if serverAddr.IP.IsLoopback() &&
		(r.Method == "GET" || r.Method == "HEAD") {

		host := strings.ToLower(r.Host)
		if host != "localhost" &&
			!strings.HasPrefix(host, "localhost:") {

			url := *r.URL
			url.Host = fmt.Sprintf("localhost:%d", serverAddr.Port)

			proxy.httpRedirect(session, w, r, http.StatusFound, &url)
			return
		}
	}

	// Send request and obtain response status and header
	resp, err := proxy.transport.RoundTripWithSession(session, r)
	if err != nil {
		proxy.httpError(session, w, r, http.StatusServiceUnavailable, err)
		return
	}

	httpRemoveHopByHopHeaders(resp.Header)
	httpCopyHeaders(w.Header(), resp.Header)
	w.WriteHeader(resp.StatusCode)

	// Obtain response body, if any
	_, err = io.Copy(w, resp.Body)

	if err != nil {
		proxy.log.HTTPError('!', session, "%s", err)
	}

	resp.Body.Close()
}

// Reject request with a error
func (proxy *HTTPProxy) httpError(session int, w http.ResponseWriter, r *http.Request,
	status int, err error) {

	proxy.log.Begin().
		HTTPRqParams(LogDebug, '>', session, r).
		HTTPRequest(LogTraceHTTP, '>', session, r).
		Commit()

	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	httpNoCache(w)
	w.WriteHeader(status)

	w.Write([]byte(err.Error()))
	w.Write([]byte("\n"))

	if err != context.Canceled {
		proxy.log.HTTPError('!', session, "%s", err.Error())
	} else {
		proxy.log.HTTPDebug(' ', session, "request canceled by impatient client")
	}
}

// Respond to request with the HTTP redirect
func (proxy *HTTPProxy) httpRedirect(session int, w http.ResponseWriter, r *http.Request,
	status int, location *url.URL) {

	proxy.log.Begin().
		HTTPRqParams(LogDebug, '>', session, r).
		HTTPRequest(LogTraceHTTP, '>', session, r).
		Commit()

	w.Header().Set("Location", location.String())
	w.WriteHeader(status)

	proxy.log.HTTPDebug(' ', session, "redirected to %s", location)
}

// Set response headers to disable cacheing
func httpNoCache(w http.ResponseWriter) {
	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Set("Pragma", "no-cache")
	w.Header().Set("Expires", "0")
}

// Remove HTTP hop-by-hop headers, RFC 7230, section 6.1
func httpRemoveHopByHopHeaders(hdr http.Header) {
	if c := hdr.Get("Connection"); c != "" {
		for _, f := range strings.Split(c, ",") {
			if f = strings.TrimSpace(f); f != "" {
				hdr.Del(f)
			}
		}
	}

	for _, c := range []string{"Connection", "Keep-Alive",
		"Proxy-Authenticate", "Proxy-Connection",
		"Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding"} {
		hdr.Del(c)
	}
}

// Copy HTTP headers
func httpCopyHeaders(dst, src http.Header) {
	for k, v := range src {
		dst[k] = v
	}
}
0707010000001C000081A400000000000000000000000167D72F5D000034DF000000000000000000000000000000000000001A00000000ipp-usb-0.9.30/inifile.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * .INI file loader
 */

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"math"
	"os"
	"strconv"
	"strings"
	"time"
)

// IniFile represents opened .INI file
type IniFile struct {
	file        *os.File      // Underlying file
	line        int           // Line in that file
	reader      *bufio.Reader // Reader on a top of file
	buf         bytes.Buffer  // Temporary buffer to speed up things
	rec         IniRecord     // Next record
	withRecType bool          // Return records of any type
}

// IniRecord represents a single .INI file record
type IniRecord struct {
	Section    string        // Section name
	Key, Value string        // Key and value
	File       string        // Origin file
	Line       int           // Line in that file
	Type       IniRecordType // Record type
}

// IniRecordType represents IniRecord type
type IniRecordType int

// Record types:
//
//	[section]       <- IniRecordSection
//	  key - value   <- IniRecordKeyVal
const (
	IniRecordSection IniRecordType = iota
	IniRecordKeyVal
)

// IniError represents an .INI file read error
type IniError struct {
	File    string // Origin file
	Line    int    // Line in that file
	Message string // Error message
}

// OpenIniFile opens the .INI file for reading
//
// If file is opened this way, (*IniFile) Next() returns
// records of IniRecordKeyVal type only
func OpenIniFile(path string) (ini *IniFile, err error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}

	ini = &IniFile{
		file:   f,
		line:   1,
		reader: bufio.NewReader(f),
		rec: IniRecord{
			File: path,
		},
	}

	return ini, nil
}

// OpenIniFileWithRecType opens the .INI file for reading
//
// If file is opened this way, (*IniFile) Next() returns
// records of any type
func OpenIniFileWithRecType(path string) (ini *IniFile, err error) {
	ini, err = OpenIniFile(path)
	if ini != nil {
		ini.withRecType = true
	}
	return
}

// Lock manages file lock on underlying disk file
func (ini *IniFile) Lock(cmd FileLockCmd) error {
	return FileLock(ini.file, cmd)
}

// Unlock releases file lock
func (ini *IniFile) Unlock() error {
	return FileUnlock(ini.file)
}

// Close the .INI file
func (ini *IniFile) Close() error {
	return ini.file.Close()
}

// Next returns next IniRecord or an error
func (ini *IniFile) Next() (*IniRecord, error) {
	for {
		// Read until next non-space character, skipping all comments
		c, err := ini.getcNonSpace()
		for err == nil && ini.iscomment(c) {
			ini.getcNl()
			c, err = ini.getcNonSpace()
		}

		if err != nil {
			return nil, err
		}

		// Parse next record
		ini.rec.Line = ini.line
		var token string
		switch c {
		case '[':
			c, token, err = ini.token(']', false)

			if err == nil && c == ']' {
				ini.rec.Section = token
			}

			ini.getcNl()
			ini.rec.Type = IniRecordSection

			if ini.withRecType {
				return &ini.rec, nil
			}

		case '=':
			ini.getcNl()
			return nil, ini.errorf("unexpected '=' character")

		default:
			ini.ungetc(c)

			c, token, err = ini.token('=', false)
			if err == nil && c == '=' {
				ini.rec.Key = token
				c, token, err = ini.token(-1, true)
				if err == nil {
					ini.rec.Value = token
					ini.rec.Type = IniRecordKeyVal
					return &ini.rec, nil
				}
			} else if err == nil {
				return nil, ini.errorf("expected '=' character")
			}
		}
	}
}

// Read next token
func (ini *IniFile) token(delimiter rune, linecont bool) (byte, string, error) {
	var accumulator, count, trailingSpace int
	var c byte
	var err error
	type prsState int
	const (
		prsSkipSpace prsState = iota
		prsBody
		prsString
		prsStringBslash
		prsStringHex
		prsStringOctal
		prsComment
	)

	// Parse the string
	state := prsSkipSpace
	ini.buf.Reset()

	for {
		c, err = ini.getc()
		if err != nil || c == '\n' {
			break
		}

		if (state == prsBody || state == prsSkipSpace) && rune(c) == delimiter {
			break
		}

		switch state {
		case prsSkipSpace:
			if ini.isspace(c) {
				break
			}

			state = prsBody
			fallthrough

		case prsBody:
			if c == '"' {
				state = prsString
			} else if ini.iscomment(c) {
				state = prsComment
			} else if c == '\\' && linecont {
				c2, _ := ini.getc()
				if c2 == '\n' {
					ini.buf.Truncate(ini.buf.Len() - trailingSpace)
					trailingSpace = 0
					state = prsSkipSpace
				} else {
					ini.ungetc(c2)
				}
			} else {
				ini.buf.WriteByte(c)
			}

			if state == prsBody {
				if ini.isspace(c) {
					trailingSpace++
				} else {
					trailingSpace = 0
				}
			} else {
				ini.buf.Truncate(ini.buf.Len() - trailingSpace)
				trailingSpace = 0
			}

		case prsString:
			if c == '\\' {
				state = prsStringBslash
			} else if c == '"' {
				state = prsBody
			} else {
				ini.buf.WriteByte(c)
			}

		case prsStringBslash:
			if c == 'x' || c == 'X' {
				state = prsStringHex
				accumulator, count = 0, 0
			} else if ini.isoctal(c) {
				state = prsStringOctal
				accumulator = ini.hex2int(c)
				count = 1
			} else {
				switch c {
				case 'a':
					c = '\a'
				case 'b':
					c = '\b'
				case 'e':
					c = '\x1b'
				case 'f':
					c = '\f'
				case 'n':
					c = '\n'
				case 'r':
					c = '\r'
				case 't':
					c = '\t'
				case 'v':
					c = '\v'
				}

				ini.buf.WriteByte(c)
				state = prsString
			}

		case prsStringHex:
			if ini.isxdigit(c) {
				if count != 2 {
					accumulator = accumulator*16 + ini.hex2int(c)
					count++
				}
			} else {
				state = prsString
				ini.ungetc(c)
			}

			if state != prsStringHex {
				ini.buf.WriteByte(c)
			}

		case prsStringOctal:
			if ini.isoctal(c) {
				accumulator = accumulator*8 + ini.hex2int(c)
				count++
				if count == 3 {
					state = prsString
				}
			} else {
				state = prsString
				ini.ungetc(c)
			}

			if state != prsStringOctal {
				ini.buf.WriteByte(c)
			}

		case prsComment:
			// Nothing to do
		}
	}

	// Remove trailing space, if any
	ini.buf.Truncate(ini.buf.Len() - trailingSpace)

	// Check for syntax error
	if state != prsSkipSpace && state != prsBody && state != prsComment {
		return 0, "", ini.errorf("unterminated string")
	}

	return c, ini.buf.String(), nil
}

// getc returns a next character from the input file
func (ini *IniFile) getc() (byte, error) {
	c, err := ini.reader.ReadByte()
	if c == '\n' {
		ini.line++
	}
	return c, err
}

// getcNonSpace returns a next non-space character from the input file
func (ini *IniFile) getcNonSpace() (byte, error) {
	for {
		c, err := ini.getc()
		if err != nil || !ini.isspace(c) {
			return c, err
		}
	}
}

// getcNl returns a next newline character, or reads until EOF or error
func (ini *IniFile) getcNl() (byte, error) {
	for {
		c, err := ini.getc()
		if err != nil || c == '\n' {
			return c, err
		}
	}
}

// ungetc pushes a character back to the input stream
// only one character can be unread this way
func (ini *IniFile) ungetc(c byte) {
	if c == '\n' {
		ini.line--
	}
	ini.reader.UnreadByte()
}

// isspace returns true, if character is whitespace
func (ini *IniFile) isspace(c byte) bool {
	switch c {
	case ' ', '\t', '\n', '\r':
		return true
	}
	return false
}

// iscomment returns true, if character is commentary
func (ini *IniFile) iscomment(c byte) bool {
	return c == ';' || c == '#'
}

// isoctal returns true for octal digit
func (ini *IniFile) isoctal(c byte) bool {
	return '0' <= c && c <= '7'
}

// isoctal returns true for hexadecimal digit
func (ini *IniFile) isxdigit(c byte) bool {
	return ('0' <= c && c <= '7') ||
		('a' <= c && c <= 'f') ||
		('A' <= c && c <= 'F')
}

// hex2int return integer value of hexadecimal character
func (ini *IniFile) hex2int(c byte) int {
	switch {
	case '0' <= c && c <= '9':
		return int(c - '0')
	case 'a' <= c && c <= 'f':
		return int(c-'a') + 10
	case 'A' <= c && c <= 'F':
		return int(c-'A') + 10
	}
	return 0
}

// errorf creates a new IniError
func (ini *IniFile) errorf(format string, args ...interface{}) *IniError {
	return &IniError{
		File:    ini.rec.File,
		Line:    ini.rec.Line,
		Message: fmt.Sprintf(format, args...),
	}
}

// LoadIPPort loads IP port value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadIPPort(out *int) error {
	port, err := strconv.Atoi(rec.Value)
	if err == nil && (port < 1 || port > 65535) {
		err = rec.errBadValue("must be in range 1...65535")
	}
	if err != nil {
		return err
	}

	*out = port
	return nil
}

// LoadBool loads boolean value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadBool(out *bool) error {
	return rec.LoadNamedBool(out, "false", "true")
}

// LoadNamedBool loads boolean value
// Names for "true" and "false" values are specified explicitly
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadNamedBool(out *bool, vFalse, vTrue string) error {
	switch rec.Value {
	case vFalse:
		*out = false
		return nil
	case vTrue:
		*out = true
		return nil
	default:
		return rec.errBadValue("must be %s or %s", vFalse, vTrue)
	}
}

// LoadLogLevel loads LogLevel value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadLogLevel(out *LogLevel) error {
	var mask LogLevel

	for _, s := range strings.Split(rec.Value, ",") {
		s = strings.TrimSpace(s)
		switch s {
		case "":
		case "error":
			mask |= LogError
		case "info":
			mask |= LogInfo | LogError
		case "debug":
			mask |= LogDebug | LogInfo | LogError
		case "trace-ipp":
			mask |= LogTraceIPP | LogDebug | LogInfo | LogError
		case "trace-escl":
			mask |= LogTraceESCL | LogDebug | LogInfo | LogError
		case "trace-http":
			mask |= LogTraceHTTP | LogDebug | LogInfo | LogError
		case "trace-usb":
			mask |= LogTraceUSB | LogDebug | LogInfo | LogError
		case "all", "trace-all":
			mask |= LogAll & ^LogTraceUSB
		default:
			return rec.errBadValue("invalid log level %q", s)
		}
	}

	*out = mask
	return nil
}

// LoadDuration loads time.Duration value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadDuration(out *time.Duration) error {
	var ms uint
	err := rec.LoadUint(&ms)
	if err == nil {
		*out = time.Millisecond * time.Duration(ms)
	}
	return err
}

// LoadSize loads size value (returned as int64)
// The syntax is following:
//
//	123  - size in bytes
//	123K - size in kilobytes, 1K == 1024
//	123M - size in megabytes, 1M == 1024K
//
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadSize(out *int64) error {
	var units uint64 = 1

	if l := len(rec.Value); l > 0 {
		switch rec.Value[l-1] {
		case 'k', 'K':
			units = 1024
		case 'm', 'M':
			units = 1024 * 1024
		}

		if units != 1 {
			rec.Value = rec.Value[:l-1]
		}
	}

	sz, err := strconv.ParseUint(rec.Value, 10, 64)
	if err != nil {
		return rec.errBadValue("%q: invalid size", rec.Value)
	}

	if sz > uint64(math.MaxInt64/units) {
		return rec.errBadValue("size too large")
	}

	*out = int64(sz * units)
	return nil
}

// LoadUint loads unsigned integer value
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadUint(out *uint) error {
	num, err := strconv.ParseUint(rec.Value, 10, 0)
	if err != nil {
		return rec.errBadValue("%q: invalid number", rec.Value)
	}

	*out = uint(num)
	return nil
}

// LoadUintRange loads unsigned integer value within the range
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadUintRange(out *uint, min, max uint) error {
	var val uint

	err := rec.LoadUint(&val)
	if err == nil && (val < min || val > max) {
		err = rec.errBadValue("must be in range %d...%d", min, max)
	}

	if err != nil {
		return err
	}

	*out = val
	return nil
}

// LoadAuthUIDRules loads AuthUIDRule-s value and appends them
// to the destination
//
// The destination remains untouched in a case of an error
func (rec *IniRecord) LoadAuthUIDRules(out *[]*AuthUIDRule) error {
	// Parse rec.Key -- it contains list of operations
	allowed := AuthOpsNone
	for _, s := range strings.Split(rec.Key, ",") {
		s = strings.TrimSpace(s)
		switch s {
		case "all":
			allowed |= AuthOpsAll
		case "config":
			allowed |= AuthOpsConfig
		case "fax":
			allowed |= AuthOpsFax
		case "print":
			allowed |= AuthOpsPrint
		case "scan":
			allowed |= AuthOpsScan
		default:
			return rec.errBadValue("invalid operation: %q", s)
		}
	}

	// Parse rec.Value -- it contains list of users
	rules := []*AuthUIDRule{}
	users := make(map[string]struct{})
	for _, s := range strings.Split(rec.Value, ",") {
		s = strings.TrimSpace(s)

		// Silently ignore empty users and groups
		if s == "" || s == "@" {
			continue
		}

		// Check for duplicates
		if _, dup := users[s]; dup {
			continue
		}

		users[s] = struct{}{}

		// Skip rules that allows nothing
		if allowed == AuthOpsNone {
			continue
		}

		// Build rules, preserving the order (just in case for now)
		rule := &AuthUIDRule{
			Name:    s,
			Allowed: allowed,
		}

		rules = append(rules, rule)
	}

	// Save results
	*out = append(*out, rules...)
	return nil
}

// errBadValue creates a "bad value" error related to the INI record
func (rec *IniRecord) errBadValue(format string, args ...interface{}) error {
	return &IniError{
		File:    rec.File,
		Line:    rec.Line,
		Message: fmt.Sprintf(rec.Key+": "+format, args...),
	}
}

// Error implements error interface for the IniError
func (err *IniError) Error() string {
	return fmt.Sprintf("%s:%d: %s", err.File, err.Line, err.Message)
}
0707010000001D000081A400000000000000000000000167D72F5D0000068A000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/inifile_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Tests for .INI reader
 */

package main

import (
	"io"
	"testing"
)

// Don't forget to update testData when ipp-ini.conf changes
var testData = []struct{ section, key, value string }{
	{"network", "http-min-port", "60000"},
	{"network", "http-max-port", "65535"},
	{"network", "dns-sd", "enable"},
	{"network", "interface", "loopback"},
	{"network", "ipv6", "enable"},
	{"logging", "device-log", "all"},
	{"logging", "main-log", "debug"},
	{"logging", "console-log", "debug"},
	{"logging", "max-file-size", "256K"},
	{"logging", "max-backup-files", "5"},
	{"logging", "console-color", "enable"},
}

// Test .INI reader
func TestIniReader(t *testing.T) {
	// Open ipp-usb.conf
	ini, err := OpenIniFile("testdata/ipp-usb.conf")
	if err != nil {
		t.Fatalf("%s", err)
	}

	defer ini.Close()

	// Read record by record
	var rec *IniRecord
	current := 0
	for err == nil {
		rec, err = ini.Next()
		if err != nil {
			break
		}

		if current >= len(testData) {
			t.Errorf("unexpected record: [%s] %s = %s", rec.Section, rec.Key, rec.Value)
		} else if rec.Section != testData[current].section ||
			rec.Key != testData[current].key ||
			rec.Value != testData[current].value {
			t.Errorf("data mismatch:")
			t.Errorf("  expected: [%s] %s = %s", testData[current].section, testData[current].key, testData[current].value)
			t.Errorf("  present:  [%s] %s = %s", rec.Section, rec.Key, rec.Value)
		} else {
			current++
		}
	}

	if err != io.EOF {
		t.Fatalf("%s", err)
	}
}
0707010000001E000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/ipp-usb-quirks0707010000001F000081A400000000000000000000000167D72F5D0000013A000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/ipp-usb-quirks/Brother.conf# ipp-usb quirks file -- quirks for Brother devices

# This label printer hangs when using more than one
# USB interface after a few minutes from startup
# until the USB device is closed and opened again
# (i.e. ipp-usb is restarted).
#
# So we use just 1 USB interface
[Brother QL-810W]
  usb-max-interfaces = 1

07070100000020000081A400000000000000000000000167D72F5D0000012A000000000000000000000000000000000000002900000000ipp-usb-0.9.30/ipp-usb-quirks/Canon.conf# ipp-usb quirks file -- quirks for Canon devices

# This device responds to the Get-Printer-Attributes request with the
# server-error-internal-error status, but otherwise works correctly
#
# So we just ignore its returned IPP status as workaround
[Canon SELPHY CP1500]
  ignore-ipp-status = true
07070100000021000081A400000000000000000000000167D72F5D000007D7000000000000000000000000000000000000002600000000ipp-usb-0.9.30/ipp-usb-quirks/HP.conf# ipp-usb quirks file -- quirks for HP devices

[HP LaserJet MFP M28-M31]
  http-connection = keep-alive

[HP OfficeJet Pro 8730]
  http-connection = close

# eSCL requests hangs on this device, if both USB interfaces are
# in use. Limiting number of interfaces to 1 makes scanning
# reliable in a cost of making scan cancellation impossible,
# as there is no second interface to send cancel request.
# (ADF scans still can be canceled between retrieval of
# subsequent pages).
[HP ScanJet Pro 4500 fn1]
  usb-max-interfaces = 1

# HP Photosmart 6520 series doesn't implement true faxing,
# but instead implements internet-based eFax,
# which makes no sense when connected via USB
# so can be safely disabled for this kind of devices.
[HP Photosmart 6520 series]
  disable-fax = true

# This device sometimes hangs when probing for fax support
# See long conversation here for details:
#   https://github.com/OpenPrinting/ipp-usb/issues/48
[HP ENVY 5530 series]
  disable-fax = true

# This device fails to initialize. This quirk helps.
#
# See le following link for details:
#   https://github.com/OpenPrinting/ipp-usb/issues/75
[HP OfficeJet Pro 8710]
  init-reset = soft

# These quirks fixes initialization of the HP HP Color LaserJet Flow X677
# and Hewlett-Packard HP Color LaserJet FlowMFP M578
#
# Probably, some other HP Enterprise series printers will need a similar
# set of quirks to work properly. However, it is hard to figure out the
# common match pattern to detect them all.
#
# See the following link for details:
#   https://github.com/OpenPrinting/ipp-usb/issues/64
[*HP Color LaserJet Flow*]
  init-timeout = 20s
  init-retry-partial = true
  zlp-recv-hack = true

# HP X677 device sometimes fails on printing large raster jobs.
# Looks like timing issue on firmware or hardware at the device
# side.
#
# See the following link for details:
#   https://github.com/OpenPrinting/ipp-usb/issues/95
[*HP Color LaserJet Flow X677*]
  usb-send-delay-threshold = 2048
  usb-send-delay = 0.2ms
07070100000022000081A400000000000000000000000167D72F5D000002AE000000000000000000000000000000000000002A00000000ipp-usb-0.9.30/ipp-usb-quirks/Pantum.conf# ipp-usb quirks file -- quirks for Pantum devices

# Some Pantum devices (Pantum M7300FDW known to have this bug)
# encode IPP messages improperly.
#
# With this option, ipp-usb will recode IPP responses, so that
# CUPS will accept it.
#
# Note, it still doesn't solve compatibility issues, if device
# is connected over network, not over USB. Either CUPS patch is
# required or user needs to install Pantum proprietary driver
[Pantum*]
  buggy-ipp-responses = sanitize

# This device pretends it has a fax, but actually fax unit is missed.
# Attempt to query it's printer-attributes sometimes times out, so
# it is better to disable it.
[Pantum BM5100ADN series]
  disable-fax = true
07070100000023000081A400000000000000000000000167D72F5D00000069000000000000000000000000000000000000002500000000ipp-usb-0.9.30/ipp-usb-quirks/READMEThis directory contains a collection of quirks files for various
devices.

See `man ipp-usb` for details
07070100000024000081A400000000000000000000000167D72F5D00000114000000000000000000000000000000000000002D00000000ipp-usb-0.9.30/ipp-usb-quirks/blacklist.conf# ipp-usb quirks file -- blacklisted devices

# This device has IPP-over-USB interfaces, but responds HTTP 404 Not found
# status to all requests
[HP Inc. HP Laser MFP 135a]
  blacklist = true

# And this device has the same problem
[HP Inc. HP Laser 107a]
  blacklist = true
07070100000025000081A400000000000000000000000167D72F5D00000065000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/ipp-usb-quirks/default.conf# ipp-usb quirks file -- defaults

[*]
  # Drop Connection: header by default
  http-connection = ""
07070100000026000081A400000000000000000000000167D72F5D00004B91000000000000000000000000000000000000001900000000ipp-usb-0.9.30/ipp-usb.8.\" generated with Ronn-NG/v0.10.1
.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
.TH "IPP\-USB" "8" "March 2025" "" "ipp-usb.8"
.SH "NAME"
\fBipp\-usb\fR \- Daemon for IPP over USB printer support
.SH "DESCRIPTION"
\fBipp\-usb\fR daemon enables driver\-less printing and scanning on USB\-only AirPrint\-compatible printers and MFPs\.
.P
It works by connecting to the device by USB using IPP\-over\-USB protocol, and exposing the device to the network, including DNS\-SD (ZeroConf) advertising\.
.P
IPP printing, eSCL scanning and web console are fully supported\.
.SH "SYNOPSIS"
.SS "Usage:"
\fBipp\-usb mode [options]\fR
.SS "Modes are:"
.TP
\fBstandalone\fR
run forever, automatically discover IPP\-over\-USB devices and serve them all
.TP
\fBudev\fR
like standalone, but exit when last IPP\-over\-USB device is disconnected
.TP
\fBdebug\fR
logs duplicated on console, \-bg option is ignored
.TP
\fBcheck\fR
check configuration and exit\. It also prints a list of all connected devices
.TP
\fBstatus\fR
print status of the running \fBipp\-usb\fR daemon, including information of all connected devices
.SS "Options are"
.TP
\fB\-bg\fR
run in background (ignored in debug mode)
.SH "NETWORKING"
Essentially, \fBipp\-usb\fR makes printer or scanner accessible from the network, converting network\-side HTTP operations to the USB operations\.
.P
By default, \fBipp\-usb\fR exposes device only to the loopback interface, using the \fBlocalhost\fR address (both \fB127\.0\.0\.1\fR and \fB::1\fR, for IPv4 and IPv6, respectively)\. TCP ports are allocated automatically, and allocation is persisted in the association with the particular device, so the next time the device is plugged on, it will get the same port\. The default port range for TCP ports allocation is \fB60000\-65535\fR\.
.P
This default behavior can be changed, using configuration file\. See \fBCONFIGURATION\fR section below for details\.
.P
If you decide to publish your device to the real network, the following things should be taken into consideration:
.IP "1." 4
Your \fBprivate\fR device will become \fBpublic\fR and it will become accessible by other computers from the network
.IP "2." 4
Firewall rules needs to be updated appropriately\. The \fBipp\-usb\fR daemon will not do it automatically by itself
.IP "3." 4
IPP over USB specification explicitly require that the \fBHost\fR field in the HTTP request is set to \fBlocalhost\fR or \fBlocalhost:port\fR\. If device is accessed from the real network, \fBHost\fR header will reflect the real network address\. Most of devices allow it, but some are more restrictive and will not work in this configuration\.
.IP "" 0
.SH "DNS\-SD (AVAHI INTEGRATION)"
IPP over USB is intended to be used with the automatic device discovery, and for this purpose \fBipp\-usb\fR advertises all devices it handles, using DNS\-SD protocol\. On Linux, DNS\-SD is handled with a help of Avahi daemon\.
.P
DNS\-SD advertising can be disabled via configuration file\. Also, if Avahi is not installed or not running, \fBipp\-usb\fR will still work correctly, although DNS\-SD advertising will not work\.
.P
For every device the following services will be advertised:
.TS
allbox;
l l l.
Instance	Type	Subtypes
Device name	_ipp\._tcp	_universal\._sub\._ipp\._tcp
Device name	_printer\._tcp	\~
Device name	_uscan\._tcp	\~
Device name	_http\._tcp	\~
BBPP	_ipp\-usb\._tcp	\~
.TE
.P
Notes:
.IP "\(bu" 4
\fBDevice name\fR is the name under which device appears in the list of available devices, for example, in the printing dialog (it is DNS\-SD device name, in another words), and for most of devices will match the device's model name\. It is appended with the \fB" (USB)"\fR suffix, so if device is connected via network and via USB simultaneously, these two connections can be easily distinguished\. If there are two devices with the same name connected simultaneously, the suffix becomes \fB" (USB NNN)"\fR, with NNN number unique for each device, for disambiguation\. In another words, the single \fB"Kyocera ECOSYS M2040dn"\fR device will be listed as \fB"Kyocera ECOSYS M2040dn (USB)"\fR, and two such a devices will be listed as \fB"Kyocera ECOSYS M2040dn (USB 1)"\fR and \fB"Kyocera ECOSYS M2040dn (USB 2)"\fR
.IP "\(bu" 4
\fB_ipp\._tcp\fR and \fB_printer\._tcp\fR are only advertises for printer devices and MFPs
.IP "\(bu" 4
\fB_uscan\._tcp\fR is only advertised for scanner devices and MFPs
.IP "\(bu" 4
for the \fB_ipp\._tcp\fR service, the \fB_universal\._sub\._ipp\._tcp\fR subtype is also advertised for iOS compatibility
.IP "\(bu" 4
\fB_printer\._tcp\fR is advertised with TCP port set to 0\. Other services are advertised with the actual port number
.IP "\(bu" 4
\fB_http\._tcp\fR is device web\-console\. It is always advertises in assumption it is always exist
.IP "\(bu" 4
\fBBBPP\fR, used for the \fB_ipp\-usb\._tcp\fR service, is the USB bus (BB) and port (PP) numbers in hex\. The purpose of this advertising is to help CUPS and other possible "clients" to guess which devices are handled by the \fBipp\-usb\fR service, to avoid possible conflicts with the legacy USB drivers\.
.IP "" 0
.SH "Matching DNS\-SD announcements against local USB bus"
Client software may need to match local devices exposed by \fBipp\-usb\fR with those found on the local USB bus\. This can be useful, for example, to avoid offering USB devices in the print or scan dialog that cannot be used because they are already in use by \fBipp\-usb\fR\.
.P
As a rough guideline, clients might consider USB devices that offer interfaces with Class=7, SubClass=1, and Protocol=4 as belonging to \fBipp\-usb\fR\. However, this is not a precise method\. Some devices may not properly implement the IPP over USB protocol and could be blacklisted in the \fBipp\-usb\fR configuration for this reason\. Additionally, some devices may be automatically recognized by \fBipp\-usb\fR as incompatible and thus skipped\. It's also possible that \fBipp\-usb\fR is disabled on the system entirely\.
.P
A more reliable approach is to directly query the running \fBipp\-usb\fR instance to determine which devices it actually handles\.
.P
To facilitate the matching of devices that \fBipp\-usb\fR manages and announces via DNS\-SD against local devices found on the USB bus, \fBipp\-usb\fR adds two TXT records to each \fB_ipp\._tcp\fR, \fB_printer\._tcp\fR, and \fB_uscan\._tcp\fR services it announces:
.IP "\(bu" 4
usb_SER=VCF9192281 \- USB serial number for the device
.IP "\(bu" 4
usb_HWID=0482&069d \- USB vendor and hardware ID for the device in hex
.IP "" 0
.P
Please note that when matching devices discovered via DNS\-SD with USB devices, it is important to only consider DNS\-SD advertisements from IP addresses that are either loopback addresses (127\.0\.0\.1 or ::1) or belong to a local interface\.
.SH "CONFIGURATION"
\fBipp\-usb\fR searched for its configuration file in two places:
.IP "1." 4
\fB/etc/ipp\-usb/ipp\-usb\.conf\fR
.IP "2." 4
\fBipp\-usb\.conf\fR in the directory where executable file is located
.IP "" 0
.P
Configuration file syntax is very similar to \.INI files syntax\. It consist of named sections, and each section contains a set of named variables\. Comments are started from # or ; characters and continues until end of line:
.IP "" 4
.nf
# This is a comment
[section 1]
variable 1 = value 1  ; and another comment
variable 2 = value 2
.fi
.IP "" 0
.SS "Network parameters"
Network parameters are all in the \fB[network]\fR section:
.IP "" 4
.nf
[network]
  # TCP ports for HTTP will be automatically allocated in the
  # following range
  http\-min\-port = 60000
  http\-max\-port = 65535

  # Enable or disable DNS\-SD advertisement
  dns\-sd = enable      # enable | disable

  # Network interface to use\. Set to `all` if you want to expose you
  # printer to the local network\. This way you can share your printer
  # with other computers in the network, as well as with iOS and
  # Android devices\.
  interface = loopback # all | loopback

  # Enable or disable IPv6
  ipv6 = enable        # enable | disable
.fi
.IP "" 0
.SS "Authentication"
By default, \fBipp\-usb\fR exposes locally connected USB printer to all users of the system\.
.P
Though this is reasonable behavior in most cases, when computer and printer are both in personal use, for bigger installation this approach can be too simple and primitive\.
.P
\fBipp\-usb\fR provides a mechanism, which allows to control local clients access based on UID the client program runs under\.
.P
Please note, this mechanism will not work for remote connections (disabled by default but supported)\. Authentication of remote users requires some different mechanism, which is under consideration but is not yet implemented\.
.P
Note also, this mechanism may or may not work in containerized installation (i\.e\., snap, flatpak and similar)\. The container namespace may be isolated from the system and/or user's namespaces, so even for local clients the UID as seen by the \fBipp\-usb\fR may be different from the system\-wide UID\.
.P
Authentication parameters are all in the [auth uid] section:
.IP "" 4
.nf
# Local user authentication by UID/GID
[auth uid]
  # Syntax:
  #     operations = users
  #
  # Operations are comma\-separated list of following operations:
  #     all    \- all operations
  #     config \- configuration web\-console
  #     fax    \- faxing
  #     print  \- printing
  #     scan   \- scanning
  #
  # Users have the following suntax:
  #     user   \- user name
  #     @group \- all users that belongs to the group
  #
  # Users and groups may be specified either by names or by
  # numbers\. "*" means any
  #
  # Note, if user/group is not known in the context of request
  # (for example, in the case of non\-local network connection),
  # "*" used for matching, which will only match wildcard
  # rules\.
  #
  # User/group names are resolved at the moment of request
  # processing (and cached for a couple of seconds), so running
  # daemon will see changes to the /etc/passwd and /etc/group
  #
  # Examples:
  #     fax, print = lp, @lp   # Allow CUPS to do its work
  #     scan       = *         # Allow any user to scan
  #     config     = @wheel    # Only wheel group members can do that
  all = *
.fi
.IP "" 0
.SS "Logging configuration"
Logging parameters are all in the \fB[logging]\fR section:
.IP "" 4
.nf
[logging]
  # device\-log  \- what logs are generated per device
  # main\-log    \- what common logs are generated
  # console\-log \- what of generated logs goes to console
  #
  # parameter contains a comma\-separated list of
  # the following keywords:
  #   error     \- error messages
  #   info      \- informative messages
  #   debug     \- debug messages
  #   trace\-ipp, trace\-escl, trace\-http \- very detailed
  #               per\-protocol traces
  #   trace\-usb \- hex dump of all USB traffic
  #   all       \- all logs
  #   trace\-all \- alias to all
  #
  # Note, trace\-* implies debug, debug implies info, info implies
  # error
  device\-log    = all
  main\-log      = debug
  console\-log   = debug

  # Log rotation parameters:
  #   log\-file\-size    \- max log file before rotation\. Use suffix
  #                      M for megabytes or K for kilobytes
  #   log\-backup\-files \- how many backup files to preserve during
  #                      rotation
  #
  max\-file\-size    = 256K
  max\-backup\-files = 5

  # Enable or disable ANSI colors on console
  console\-color = enable # enable | disable

  # ipp\-usb queries IPP printer attributes at the initialization time
  # for its own purposes and writes received attributes to the log\.
  # By default, only necessary attributes are requested from device\.
  #
  # If this parameter is set to true, all printer attributes will
  # be requested\. Normally, it only affects the logging\. However,
  # some enterprise\-level HP printers returns such huge amount of
  # data and do it so slowly, so it can cause initialization timeout\.
  # This is why this feature is not enabled by default
  get\-all\-printer\-attrs = false # false | true
.fi
.IP "" 0
.SS "Quirks"
Some devices, due to their firmware bugs, require special handling, called device\-specific \fBquirks\fR\. \fBipp\-usb\fR loads quirks from the \fB/usr/share/ipp\-usb/quirks/*\.conf\fR files and from the \fB/etc/ipp\-usb/quirks/*\.conf\fR files\. The \fB/etc/ipp\-usb/quirks\fR directory is for system quirks overrides or admin changes\. These files have \.INI\-file syntax with the content that looks like this:
.IP "" 4
.nf
[HP LaserJet MFP M28\-M31]
  http\-connection = keep\-alive

[HP OfficeJet Pro 8730]
  http\-connection = close

[HP Inc\. HP Laser MFP 135a]
  blacklist = true

# Default configuration
[*]
  http\-connection = ""
.fi
.IP "" 0
.P
For each discovered device, its model name is matched against sections of the quirks files\. Section names may contain glob\-style wildcards: \fB*\fR that matches any sequence of characters and \fB?\fR , that matches any single character\. To match one of these characters (\fB*\fR and \fB?\fR) literally, use backslash as escape\.
.P
Note, the simplest way to guess the exact model name for the particular device is to use \fBipp\-usb check\fR command, which prints a list of all connected devices\.
.P
All matching sections from all quirks files are taken in consideration, and applied in priority order\. Priority is computed using the following algorithm:
.IP "\(bu" 4
When matching model name against section name, amount of non\-wildcard matched characters is counted, and the longer match wins
.IP "\(bu" 4
Otherwise, section loaded first wins\. Files are loaded in alphabetical order, sections read sequentially
.IP "" 0
.P
If some parameter exist in multiple sections, used its value from the most priority section
.P
The following parameters are defined:
.IP "\(bu" 4
\fBblacklist = true | false\fR
.br
If \fBtrue\fR, the matching device is ignored by the \fBipp\-usb\fR
.IP "\(bu" 4
\fBbuggy\-ipp\-responses = reject | allow | sanitize\fR
.br
Some devices send buggy (malformed) IPP responses that violate IPP specification\. \fBipp\-usb\fR may \fBreject\fR these responses (so \fBipp\-usb\fR initialization will fail), \fBallow\fR them (\fBipp\-usb\fR initialization will succeed, but CUPS needs to accept them as well) or \fBsanitize\fR them (fix IPP specs violations)\.
.IP "\(bu" 4
\fBdisable\-fax = true | false\fR
.br
If \fBtrue\fR, the matching device's fax capability is ignored\.
.IP "\(bu" 4
\fBhttp\-XXX = YYY\fR
.br
Set XXX header of the HTTP requests forwarded to device to YYY\. If YYY is empty string, XXX header is removed\.
.IP "\(bu" 4
\fBignore\-ipp\-status = true | false\fR
.br
If \fBtrue\fR, IPP status of IPP requests sent by the \fBipp\-usb\fR by itself will be ignored\. This quirk is useful, when device correctly handles IPP request but returned status is not reliable\. Affects only \fBipp\-usb\fR initialization\.
.IP "\(bu" 4
\fBinit\-delay = DELAY\fR
.br
Delay, between device is opened and, optionally, reset, and the first request is sent to device\.
.IP "\(bu" 4
\fBinit\-retry\-partial = true | false\fR
.br
Retry the initialization in case only part of the device's functions have been initialized, instead of continuing to operate with incomplete functionality\.
.IP
It can be useful if the device takes a long time to fully initialize\. During this period, some components may respond normally while others are still initializing\. For example, the device may quickly report its scanning capabilities shortly after startup, while its printing functionality may take several minutes to become operational\.
.IP
Some enterprise\-level HP printers are known to have this problem\.
.IP "\(bu" 4
\fBinit\-reset = none | soft | hard\fR
.br
How to reset device during initialization\. Default is \fBnone\fR
.IP "\(bu" 4
\fBinit\-timeout = DELAY\fR
.br
Timeout for HTTP requests send by the \fBipp\-usb\fR during initialization\.
.IP "\(bu" 4
\fBrequest\-delay = DELAY\fR
.br
Delay between subsequent HTTP requests, sent to device (this is not the same as \fBusb\-send\-delay\fR, which inserts delays between each subsequent USB send\-to\-device requests)\.
.IP "\(bu" 4
\fBusb\-max\-interfaces = N\fR
.br
Don't use more that N USB interfaces, even if more is available\.
.IP "\(bu" 4
\fBusb\-send\-delay = DELAY\fR
.br
Delay between low\-level USB send\-to\-device requests (this is not the same as \fBrequest\-delay\fR, which inserts delays between the whole HTTP\-level requests)\.
.IP "\(bu" 4
\fBusb\-send\-delay\-threshold = N\fR
.br
\fBusb\-send\-delay\fR only applied if USB send\-to\-device request size exceeds this threshold\.
.IP "\(bu" 4
\fBzlp\-recv\-hack = true | false\fR
.br
Some enterprise\-level HP devices, during the initialization phase (which can last several minutes), may respond with an HTTP 503 status or similar, which is expected\. However, the response body may be truncated (typically, the terminating '\en' is lost)\. In such cases, \fBipp\-usb\fR will wait indefinitely for a response to maintain synchronization with the device\.
.IP
At the same time, these devices send a zero\-length UDP packet at the end of the truncated output\. If the \fBzlp\-recv\-hack\fR quirk is enabled, when ipp\-usb receives a zero\-length packet from the USB followed by a receive timeout, it interprets this combination of events as a valid termination of the response body\. It works only at the initialization time and doesn't affect futher operations\.
.IP "\(bu" 4
\fBzlp\-send = true | false\fR
.br
Terminate outgoing transfers that a multiple of the endpoint's packet size win an extra zero length packet\.
.IP "" 0
.P
The DELAY parameter can be specified either as an unsigned integer (in milliseconds) or as a sequence of decimal numbers with an optional fraction and a unit suffix, such as "300ms," "0\.5s," or "2m30s\." Valid time units are "ns," "us" (or "µs"), "ms" "s" "m" and "h"
.P
If you found out about your device that it needs a quirk to work properly or it does not work with \fBipp\-usb\fR at all, although it provides IPP\-over\-USB interface, please report the issue at https://github\.com/OpenPrinting/ipp\-usb\. It will let us to update our collection of quirks, so helping other owners of such a device\.
.SH "FILES"
.IP "\(bu" 4
\fB/etc/ipp\-usb/ipp\-usb\.conf\fR: the daemon configuration file
.IP "\(bu" 4
\fB/var/log/ipp\-usb/main\.log\fR: the main log file
.IP "\(bu" 4
\fB/var/log/ipp\-usb/<DEVICE>\.log\fR: per\-device log files
.IP "\(bu" 4
\fB/var/ipp\-usb/dev/<DEVICE>\.state\fR: device state (HTTP port allocation, DNS\-SD name)
.IP "\(bu" 4
\fB/var/ipp\-usb/lock/ipp\-usb\.lock\fR: lock file, that helps to prevent multiple copies of daemon to run simultaneously
.IP "\(bu" 4
\fB/var/ipp\-usb/ctrl\fR: \fBipp\-usb\fR control socket\. Currently only used to obtain the per\-device status (printed by \fBipp\-usb status\fR), but its functionality may be extended in a future
.IP "\(bu" 4
\fB/usr/share/ipp\-usb/quirks/*\.conf\fR: device\-specific quirks (see above)
.IP "\(bu" 4
\fB/etc/ipp\-usb/quirks/*\.conf\fR: device\-specific quirks defined by sysadmin (see above)
.IP "" 0
.SH "COPYRIGHT"
Copyright (c) by Alexander Pevzner (pzz@apevzner\.com, pzz@pzz\.msk\.ru)
.br
All rights reserved\.
.P
This program is licensed under 2\-Clause BSD license\. See LICENSE file for details\.
.SH "SEE ALSO"
\fBcups(1)\fR
07070100000027000081A400000000000000000000000167D72F5D00004A8E000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/ipp-usb.8.mdipp-usb(8) -- Daemon for IPP over USB printer support
=====================================================

## DESCRIPTION

`ipp-usb` daemon enables driver-less printing and scanning on
USB-only AirPrint-compatible printers and MFPs.

It works by connecting to the device by USB using IPP-over-USB
protocol, and exposing the device to the network, including
DNS-SD (ZeroConf) advertising.

IPP printing, eSCL scanning and web console are fully supported.

## SYNOPSIS

### Usage:

`ipp-usb mode [options]`

### Modes are:

   * `standalone`:
     run forever, automatically discover IPP-over-USB
     devices and serve them all

   * `udev`:
     like standalone, but exit when last IPP-over-USB
     device is disconnected

   * `debug`:
     logs duplicated on console, -bg option is ignored

   * `check`:
     check configuration and exit. It also prints a list
     of all connected devices

   * `status`:
     print status of the running `ipp-usb` daemon, including information
     of all connected devices

### Options are

   * `-bg`:
     run in background (ignored in debug mode)

## NETWORKING

Essentially, `ipp-usb` makes printer or scanner accessible from the
network, converting network-side HTTP operations to the USB operations.

By default, `ipp-usb` exposes device only to the loopback interface,
using the `localhost` address (both `127.0.0.1` and `::1`, for IPv4
and IPv6, respectively). TCP ports are allocated automatically, and
allocation is persisted in the association with the particular device,
so the next time the device is plugged on, it will get the same port.
The default port range for TCP ports allocation is `60000-65535`.

This default behavior can be changed, using configuration file. See
`CONFIGURATION` section below for details.

If you decide to publish your device to the real network, the following
things should be taken into consideration:

   1. Your **private** device will become **public** and it will become
      accessible by other computers from the network
   2. Firewall rules needs to be updated appropriately. The `ipp-usb`
      daemon will not do it automatically by itself
   3. IPP over USB specification explicitly require that the
      `Host` field in the HTTP request is set to `localhost`
      or `localhost:port`. If device is accessed from the real
      network, `Host` header will reflect the real network address.
      Most of devices allow it, but some are more restrictive
      and will not work in this configuration.

## DNS-SD (AVAHI INTEGRATION)

IPP over USB is intended to be used with the automatic device discovery,
and for this purpose `ipp-usb` advertises all devices it handles, using
DNS-SD protocol. On Linux, DNS-SD is handled with a help of Avahi daemon.

DNS-SD advertising can be disabled via configuration file. Also, if Avahi
is not installed or not running, `ipp-usb` will still work correctly,
although DNS-SD advertising will not work.

For every device the following services will be advertised:

   | Instance    | Type          | Subtypes                  |
   | ----------- | ------------- | ------------------------- |
   | Device name | _ipp._tcp     | _universal._sub._ipp._tcp |
   | Device name | _printer._tcp |                           |
   | Device name | _uscan._tcp   |                           |
   | Device name | _http._tcp    |                           |
   | BBPP        | _ipp-usb._tcp |                           |


Notes:

   * `Device name` is the name under which device appears in
     the list of available devices, for example, in the printing
     dialog (it is DNS-SD device name, in another words), and for
     most of devices will match the device's model name. It
     is appended with the `" (USB)"` suffix, so if device is
     connected via network and via USB simultaneously, these
     two connections can be easily distinguished. If there
     are two devices with the same name connected simultaneously,
     the suffix becomes `" (USB NNN)"`, with NNN number unique for
     each device, for disambiguation. In another words, the single
     `"Kyocera ECOSYS M2040dn"` device will be listed as
     `"Kyocera ECOSYS M2040dn (USB)"`, and two such a devices will
     be listed as `"Kyocera ECOSYS M2040dn (USB 1)"` and
     `"Kyocera ECOSYS M2040dn (USB 2)"`
   * `_ipp._tcp` and `_printer._tcp` are only advertises for
     printer devices and MFPs
   * `_uscan._tcp` is only advertised for scanner devices and MFPs
   * for the `_ipp._tcp` service, the `_universal._sub._ipp._tcp`
     subtype is also advertised for iOS compatibility
   * `_printer._tcp` is advertised with TCP port set to 0. Other
     services are advertised with the actual port number
   * `_http._tcp` is device web-console. It is always advertises
     in assumption it is always exist
   * `BBPP`, used for the `_ipp-usb._tcp` service, is the
     USB bus (BB) and port (PP) numbers in hex. The purpose
     of this advertising is to help CUPS and other possible
     "clients" to guess which devices are handled by the
     `ipp-usb` service, to avoid possible conflicts with the
     legacy USB drivers.

## Matching DNS-SD announcements against local USB bus

Client software may need to match local devices exposed by `ipp-usb` with
those found on the local USB bus. This can be useful, for example, to
avoid offering USB devices in the print or scan dialog that cannot be
used because they are already in use by `ipp-usb`.

As a rough guideline, clients might consider USB devices that offer
interfaces with Class=7, SubClass=1, and Protocol=4 as belonging to
`ipp-usb`. However, this is not a precise method. Some devices may not
properly implement the IPP over USB protocol and could be blacklisted in
the `ipp-usb` configuration for this reason. Additionally, some devices
may be automatically recognized by `ipp-usb` as incompatible and thus
skipped. It's also possible that `ipp-usb` is disabled on the system
entirely.

A more reliable approach is to directly query the running `ipp-usb`
instance to determine which devices it actually handles.

To facilitate the matching of devices that `ipp-usb` manages and
announces via  DNS-SD against local devices found on the USB bus,
`ipp-usb` adds two TXT records to each `_ipp._tcp`, `_printer._tcp`,
and `_uscan._tcp` services it announces:

   * usb_SER=VCF9192281 - USB serial number for the device

   * usb_HWID=0482&069d - USB vendor and hardware ID for the device in hex

Please note that when matching devices discovered via DNS-SD with USB
devices, it is important to only consider DNS-SD advertisements from IP
addresses that are either loopback addresses (127.0.0.1 or ::1) or
belong to a local interface.

## CONFIGURATION

`ipp-usb` searched for its configuration file in two places:

   1. `/etc/ipp-usb/ipp-usb.conf`
   2. `ipp-usb.conf` in the directory where executable file is located

Configuration file syntax is very similar to .INI files syntax.
It consist of named sections, and each section contains a set of
named variables. Comments are started from # or ; characters and
continues until end of line:

    # This is a comment
    [section 1]
    variable 1 = value 1  ; and another comment
    variable 2 = value 2

### Network parameters

Network parameters are all in the `[network]` section:

    [network]
      # TCP ports for HTTP will be automatically allocated in the
      # following range
      http-min-port = 60000
      http-max-port = 65535

      # Enable or disable DNS-SD advertisement
      dns-sd = enable      # enable | disable

      # Network interface to use. Set to `all` if you want to expose you
      # printer to the local network. This way you can share your printer
      # with other computers in the network, as well as with iOS and
      # Android devices.
      interface = loopback # all | loopback

      # Enable or disable IPv6
      ipv6 = enable        # enable | disable

### Authentication

By default, `ipp-usb` exposes locally connected USB printer to all users
of the system.

Though this is reasonable behavior in most cases, when computer and printer
are both in personal use, for bigger installation this approach can be too
simple and primitive.

`ipp-usb` provides a mechanism, which allows to control local clients
access based on UID the client program runs under.

Please note, this mechanism will not work for remote connections (disabled
by default but supported). Authentication of remote users requires some
different mechanism, which is under consideration but is not yet implemented.

Note also, this mechanism may or may not work in containerized installation
(i.e., snap, flatpak and similar).  The container namespace may be isolated
from the system and/or user's namespaces, so even for local clients the UID
as seen by the `ipp-usb` may be different from the system-wide UID.

Authentication parameters are all in the [auth uid] section:

    # Local user authentication by UID/GID
    [auth uid]
      # Syntax:
      #     operations = users
      #
      # Operations are comma-separated list of following operations:
      #     all    - all operations
      #     config - configuration web-console
      #     fax    - faxing
      #     print  - printing
      #     scan   - scanning
      #
      # Users have the following suntax:
      #     user   - user name
      #     @group - all users that belongs to the group
      #
      # Users and groups may be specified either by names or by
      # numbers. "*" means any
      #
      # Note, if user/group is not known in the context of request
      # (for example, in the case of non-local network connection),
      # "*" used for matching, which will only match wildcard
      # rules.
      #
      # User/group names are resolved at the moment of request
      # processing (and cached for a couple of seconds), so running
      # daemon will see changes to the /etc/passwd and /etc/group
      #
      # Examples:
      #     fax, print = lp, @lp   # Allow CUPS to do its work
      #     scan       = *         # Allow any user to scan
      #     config     = @wheel    # Only wheel group members can do that
      all = *

### Logging configuration

Logging parameters are all in the `[logging]` section:

    [logging]
      # device-log  - what logs are generated per device
      # main-log    - what common logs are generated
      # console-log - what of generated logs goes to console
      #
      # parameter contains a comma-separated list of
      # the following keywords:
      #   error     - error messages
      #   info      - informative messages
      #   debug     - debug messages
      #   trace-ipp, trace-escl, trace-http - very detailed
      #               per-protocol traces
      #   trace-usb - hex dump of all USB traffic
      #   all       - all logs
      #   trace-all - alias to all
      #
      # Note, trace-* implies debug, debug implies info, info implies
      # error
      device-log    = all
      main-log      = debug
      console-log   = debug

      # Log rotation parameters:
      #   log-file-size    - max log file before rotation. Use suffix
      #                      M for megabytes or K for kilobytes
      #   log-backup-files - how many backup files to preserve during
      #                      rotation
      #
      max-file-size    = 256K
      max-backup-files = 5

      # Enable or disable ANSI colors on console
      console-color = enable # enable | disable

      # ipp-usb queries IPP printer attributes at the initialization time
      # for its own purposes and writes received attributes to the log.
      # By default, only necessary attributes are requested from device.
      #
      # If this parameter is set to true, all printer attributes will
      # be requested. Normally, it only affects the logging. However,
      # some enterprise-level HP printers returns such huge amount of
      # data and do it so slowly, so it can cause initialization timeout.
      # This is why this feature is not enabled by default
      get-all-printer-attrs = false # false | true

### Quirks

Some devices, due to their firmware bugs, require special handling,
called device-specific **quirks**. `ipp-usb` loads quirks from the
`/usr/share/ipp-usb/quirks/*.conf` files and from the `/etc/ipp-usb/quirks/*.conf`
files. The `/etc/ipp-usb/quirks` directory is for system quirks overrides or
admin changes. These files have .INI-file syntax with the content that looks like this:

    [HP LaserJet MFP M28-M31]
      http-connection = keep-alive

    [HP OfficeJet Pro 8730]
      http-connection = close

    [HP Inc. HP Laser MFP 135a]
      blacklist = true

    # Default configuration
    [*]
      http-connection = ""

For each discovered device, its model name is matched against sections of the
quirks files. Section names may contain glob-style wildcards: `*` that matches
any sequence of characters and `?` , that matches any single character. To
match one of these characters (`*` and `?`) literally, use backslash as escape.

Note, the simplest way to guess the exact model name for the particular
device is to use `ipp-usb check` command, which prints a list of all
connected devices.

All matching sections from all quirks files are taken in consideration,
and applied in priority order. Priority is computed using the following
algorithm:

* When matching model name against section name, amount of non-wildcard
matched characters is counted, and the longer match wins
* Otherwise, section loaded first wins. Files are loaded in alphabetical
order, sections read sequentially

If some parameter exist in multiple sections, used its value from the
most priority section

The following parameters are defined:

   * `blacklist = true | false`<br>
     If `true`, the matching device is ignored by the `ipp-usb`

   * `buggy-ipp-responses = reject | allow | sanitize`<br>
     Some devices send buggy (malformed) IPP responses that violate
     IPP specification. `ipp-usb` may `reject` these responses
     (so `ipp-usb` initialization will fail), `allow` them (`ipp-usb`
     initialization will succeed, but CUPS needs to accept them
     as well) or `sanitize` them (fix IPP specs violations).

   * `disable-fax = true | false`<br>
     If `true`, the matching device's fax capability is ignored.

   * `http-XXX = YYY`<br>
     Set XXX header of the HTTP requests forwarded to device to YYY.
     If YYY is empty string, XXX header is removed.

   * `ignore-ipp-status = true | false`<br>
     If `true`, IPP status of IPP requests sent by the `ipp-usb` by
     itself will be ignored. This quirk is useful, when device correctly
     handles IPP request but returned status is not reliable. Affects
     only `ipp-usb` initialization.

   * `init-delay = DELAY`<br>
     Delay, between device is opened and, optionally, reset, and the
     first request is sent to device.

   * `init-retry-partial = true | false`<br>
     Retry the initialization in case only part of the device's functions
     have been initialized, instead of continuing to operate with incomplete
     functionality.

     It can be useful if the device takes a long time to fully initialize.
     During this period, some components may respond normally while others
     are still initializing. For example, the device may quickly report its
     scanning capabilities shortly after startup, while its printing
     functionality may take several minutes to become operational.

     Some enterprise-level HP printers are known to have this problem.

   * `init-reset = none | soft | hard`<br>
     How to reset device during initialization. Default is `none`

   * `init-timeout = DELAY`<br>
     Timeout for HTTP requests send by the `ipp-usb` during initialization.

   * `request-delay = DELAY`<br>
     Delay between subsequent HTTP requests, sent to device (this is not
     the same as `usb-send-delay`, which inserts delays between each
     subsequent USB send-to-device requests).

   * `usb-max-interfaces = N`<br>
     Don't use more that N USB interfaces, even if more is available.

   * `usb-send-delay = DELAY`<br>
     Delay between low-level USB send-to-device requests (this is not
     the same as `request-delay`, which inserts delays between the
     whole HTTP-level requests).

   * `usb-send-delay-threshold = N`<br>
     `usb-send-delay` only applied if USB send-to-device request size
     exceeds this threshold.

   * `zlp-recv-hack = true | false`<br>
     Some enterprise-level HP devices, during the initialization phase
     (which can last several minutes), may respond with an HTTP 503
     status or similar, which is expected. However, the response body may
     be truncated (typically, the terminating '\n' is lost). In such
     cases, `ipp-usb` will wait indefinitely for a response to maintain
     synchronization with the device.

     At the same time, these devices send a zero-length UDP packet at the
     end of the truncated output. If the `zlp-recv-hack` quirk is enabled,
     when ipp-usb receives a zero-length packet from the USB followed by
     a receive timeout, it interprets this combination of events as a
     valid termination of the response body. It works only at the
     initialization time and doesn't affect futher operations.

   * `zlp-send = true | false`<br>
     Terminate outgoing transfers that a multiple of the endpoint's
     packet size win an extra zero length packet.

The DELAY parameter can be specified either as an unsigned integer (in
milliseconds) or as a sequence of decimal numbers with an optional
fraction and a unit suffix, such as "300ms," "0.5s," or "2m30s." Valid
time units are "ns," "us" (or "µs"), "ms" "s" "m" and "h"

If you found out about your device that it needs a quirk to work properly or it
does not work with `ipp-usb` at all, although it provides IPP-over-USB
interface, please report the issue at https://github.com/OpenPrinting/ipp-usb.
It will let us to update our collection of quirks, so helping other owners
of such a device.

## FILES

   * `/etc/ipp-usb/ipp-usb.conf`:
     the daemon configuration file

   * `/var/log/ipp-usb/main.log`:
     the main log file

   * `/var/log/ipp-usb/<DEVICE>.log`:
     per-device log files

   * `/var/ipp-usb/dev/<DEVICE>.state`:
     device state (HTTP port allocation, DNS-SD name)

   * `/var/ipp-usb/lock/ipp-usb.lock`:
     lock file, that helps to prevent multiple copies of daemon to run simultaneously

   * `/var/ipp-usb/ctrl`:
     `ipp-usb` control socket. Currently only used to obtain the
     per-device status (printed by `ipp-usb status`), but its
     functionality may be extended in a future

   * `/usr/share/ipp-usb/quirks/*.conf`: device-specific quirks (see above)

   * `/etc/ipp-usb/quirks/*.conf`: device-specific quirks defined by sysadmin (see above)

## COPYRIGHT

Copyright (c) by Alexander Pevzner (pzz@apevzner.com, pzz@pzz.msk.ru)<br/>
All rights reserved.

This program is licensed under 2-Clause BSD license. See LICENSE file for details.

## SEE ALSO

**cups(1)**

# vim:ts=8:sw=4:et
07070100000028000081A400000000000000000000000167D72F5D00000D3B000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/ipp-usb.conf# ipp-usb.conf: example configuration file

# Networking parameters
[network]
  # TCP ports for HTTP will be automatically allocated in the following range
  http-min-port = 60000
  http-max-port = 65535

  # Enable or disable DNS-SD advertisement
  dns-sd = enable      # enable | disable

  # Network interface to use. Set to `all` if you want to expose you
  # printer to the local network. This way you can share your printer
  # with other computers in the network, as well as with iOS and Android
  # devices.
  interface = loopback # all | loopback

  # Enable or disable IPv6
  ipv6 = enable        # enable | disable

# Local user authentication by UID/GID
[auth uid]
  # Syntax:
  #     operations = users
  #
  # Operations are comma-separated list of following operations:
  #     all    - all operations
  #     config - configuration web-console
  #     fax    - faxing
  #     print  - printing
  #     scan   - scanning
  #
  # Users have the following suntax:
  #     user   - user name
  #     @group - all users that belongs to the group
  #
  # Users and groups may be specified either by names or by
  # numbers. "*" means any
  #
  # Note, if user/group is not known in the context of request
  # (for example, in the case of non-local network connection),
  # "*" is used for matching, which will only match wildcard
  # rules.
  #
  # User/group names are resolved at the moment of request
  # processing (and cached for a couple of seconds), so running
  # daemon will see changes to the /etc/passwd and /etc/group
  #
  # Examples:
  #     fax, print = lp, @lp   # Allow CUPS to do its work
  #     scan       = *         # Allow any user to scan
  #     config     = @wheel    # Only wheel group members can do that
  all = *

# Logging configuration
[logging]
  # device-log  - per-device log levels
  # main-log    - main log levels
  # console-log - console log levels
  #
  # parameter contains a comma-separated list of
  # the following keywords:
  #   error     - error messages
  #   info      - informative messages
  #   debug     - debug messages
  #   trace-ipp, trace-escl, trace-http - very detailed per-protocol traces
  #   trace-usb - hex dump of all USB traffic
  #   all       - all logs
  #   trace-all - alias to all
  #
  # Note, trace-* implies debug, debug implies info, info implies error
  device-log    = all
  main-log      = debug
  console-log   = debug

  # Log rotation parameters:
  #   max-file-size    - max log file before rotation. Use suffix M
  #                      for megabytes or K for kilobytes
  #   max-backup-files - how many backup files to preserve during rotation
  #
  max-file-size    = 256K
  max-backup-files = 5

  # Enable or disable ANSI colors on console
  console-color = enable # enable | disable

  # ipp-usb queries IPP printer attributes at the initialization time
  # for its own purposes and writes received attributes to the log.
  # By default, only necessary attributes are requested from device.
  #
  # If this parameter is set to true, all printer attributes will
  # be requested. Normally, it only affects the logging. However,
  # some enterprise-level HP printers returns such huge amount of
  # data and do it so slowly, so it can cause initialization timeout.
  # This is why this feature is not enabled by default
  get-all-printer-attrs = false # false | true

# vim:ts=8:sw=2:et
07070100000029000081A400000000000000000000000167D72F5D000035B1000000000000000000000000000000000000001600000000ipp-usb-0.9.30/ipp.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP service registration
 */

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/OpenPrinting/goipp"
)

// IppPrinterInfo represents additional printer information, which
// is not included into DNS-SD TXT record, but still needed for
// other purposes
type IppPrinterInfo struct {
	DNSSdName   string // DNS-SD device name
	UUID        string // Device UUID
	AdminURL    string // Admin URL
	IconURL     string // Device icon URL
	IppSvcIndex int    // IPP DNSSdSvcInfo index within array of services
}

// IppService performs IPP Get-Printer-Attributes query using provided
// http.Client and decodes received information into the form suitable
// for DNS-SD registration
//
// Discovered services will be added to the services collection
func IppService(log *LogMessage, services *DNSSdServices,
	port int, usbinfo UsbDeviceInfo, quirks Quirks,
	c *http.Client) (ippinfo *IppPrinterInfo, httpstatus int, err error) {

	// Query printer attributes
	uri := fmt.Sprintf("ipp://localhost:%d/ipp/print", port)
	msg, httpstatus, err := ippGetPrinterAttributes(log, c, quirks, uri)
	if err != nil {
		return
	}

	// Decode IPP service info
	attrs := newIppDecoder(msg)
	ippinfo, ippSvc := attrs.decode(usbinfo)

	// Check for fax support
	canFax := false
	if usbinfo.BasicCaps&UsbIppBasicCapsFax != 0 &&
		!quirks.GetDisableFax() {
		// Note, as device lists Fax on its basic capabilities,
		// this probe most likely is not needed, but as the
		// ipp-usb version 0.9.19 and earlier used to guess
		// for fax support based on the /ipp/faxout probe,
		// not on device capabilities, lets leave it here
		// for now, just in case. Firmwares in general are
		// too buggy, I can't trust them :-(
		uri = fmt.Sprintf("ipp://localhost:%d/ipp/faxout", port)
		_, _, err2 := ippGetPrinterAttributes(log, c, quirks, uri)

		if err2 == nil {
			canFax = true
			log.Debug(' ', "IPP FaxOut service detected")
		} else {
			log.Error('!', "IPP FaxOut probe failed: %s", err2)
		}
	} else {
		log.Debug(' ', "IPP FaxOut service not in capabilities")
	}

	if canFax {
		ippSvc.Txt.Add("Fax", "T")
		ippSvc.Txt.Add("rfo", "ipp/faxout")
	} else {
		ippSvc.Txt.Add("Fax", "F")
	}

	// Construct LPD info. Per Apple spec, we MUST advertise
	// LPD with zero port, even if we don't support it
	lpdSvc := DNSSdSvcInfo{
		Type: "_printer._tcp",
		Port: 0,
		Txt:  nil,
	}

	// Pack it all together
	ippSvc.Port = port
	services.Add(lpdSvc)

	ippinfo.IppSvcIndex = len(*services)
	services.Add(ippSvc)

	return
}

// ippGetPrinterAttributes performs GetPrinterAttributes query,
// using the specified http.Client and uri
//
// If this function returns nil error, it means that:
//  1. HTTP transaction performed successfully
//  2. Received reply successfully decoded
//  3. It is not an IPP error response
//
// Otherwise, the appropriate error is generated and returned
func ippGetPrinterAttributes(log *LogMessage, c *http.Client, quirks Quirks,
	uri string) (msg *goipp.Message, httpstatus int, err error) {

	// Query printer attributes
	msg = goipp.NewRequest(goipp.DefaultVersion, goipp.OpGetPrinterAttributes, 1)
	msg.Operation.Add(goipp.MakeAttribute("attributes-charset",
		goipp.TagCharset, goipp.String("utf-8")))
	msg.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
		goipp.TagLanguage, goipp.String("en-US")))
	msg.Operation.Add(goipp.MakeAttribute("printer-uri",
		goipp.TagURI, goipp.String(uri)))

	rq := goipp.Attribute{Name: "requested-attributes"}

	if Conf.LogAllPrinterAttrs {
		rq.Values.Add(goipp.TagKeyword, goipp.String("all"))
	} else {
		rq.Values.Add(goipp.TagKeyword, goipp.String("color-supported"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("document-format-supported"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("media-size-supported"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("mopria-certified"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-device-id"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-dns-sd-name"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-icons"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-info"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-kind"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-location"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-make-and-model"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-more-info"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("printer-uuid"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("sides-supported"))
		rq.Values.Add(goipp.TagKeyword, goipp.String("urf-supported"))
	}

	msg.Operation.Add(rq)

	log.Add(LogTraceIPP, '>', "IPP request:").
		IppRequest(LogTraceIPP, '>', msg).
		Nl(LogTraceIPP).
		Flush()

	req, _ := msg.EncodeBytes()
	resp, err := c.Post(uri, goipp.ContentType, bytes.NewBuffer(req))
	if err != nil {
		if !ErrIsEOF(err) {
			err = fmt.Errorf("HTTP: %s", err)
		}
		return
	}

	defer resp.Body.Close()

	// Check HTTP status
	if resp.StatusCode/100 != 2 {
		httpstatus = resp.StatusCode
		err = fmt.Errorf("HTTP: %s", resp.Status)
		return
	}

	// Decode IPP response message
	respData, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		err = fmt.Errorf("HTTP: %s", err)
		return
	}

	opts := goipp.DecoderOptions{}
	if quirks.GetBuggyIppRsp() == QuirkBuggyIppRspAllow {
		opts.EnableWorkarounds = true
	}

	err = msg.DecodeBytesEx(respData, opts)

	if err != nil {
		log.Debug(' ', "Failed to decode IPP message: %s", err)
		log.HexDump(LogTraceIPP, ' ', respData)
		err = fmt.Errorf("IPP decode: %s", err)
		return
	}

	log.Add(LogTraceIPP, '<', "IPP response:").
		IppResponse(LogTraceIPP, '<', msg).
		Nl(LogTraceIPP).
		Flush()

	// Check response status
	if msg.Code >= 0x100 && !quirks.GetIgnoreIppStatus() {
		err = fmt.Errorf("IPP: %s", goipp.Status(msg.Code))
		return
	}

	return
}

// ippAttrs represents a collection of IPP printer attributes,
// enrolled into a map for convenient access
type ippAttrs map[string]goipp.Values

// Create new ippAttrs
func newIppDecoder(msg *goipp.Message) ippAttrs {
	attrs := make(ippAttrs)

	// Note, we move from the end of list to the beginning, so
	// in a case of duplicated attributes, first occurrence wins
	for i := len(msg.Printer) - 1; i >= 0; i-- {
		attr := msg.Printer[i]
		attrs[attr.Name] = attr.Values
	}

	return attrs
}

// Decode printer attributes and build TXT record for IPP service
//
// This is where information comes from:
//
//	DNS-SD name: "printer-dns-sd-name" with fallback to "printer-info",
//	             "printer-make-and-model" and finally to MfgAndProduct
//	             from the UsbDeviceInfo
//
//	TXT fields:
//	  air:              hardcoded as "none"
//	  mopria-certified: "mopria-certified"
//	  rp:               hardcoded as "ipp/print"
//	  kind:             "printer-kind"
//	  PaperMax:         based on decoding "media-size-supported"
//	  URF:              "urf-supported" with fallback to
//	                    URF extracted from "printer-device-id"
//	  UUID:             "printer-uuid", without "urn:uuid:" prefix
//	  Color:            "color-supported"
//	  Duplex:           search "sides-supported" for strings with
//	                    prefix "one" or "two"
//	  note:             "printer-location"
//	  qtotal:           hardcoded as "1"
//	  usb_MDL:          MDL, extracted from "printer-device-id"
//	  usb_MFG:          MFG, extracted from "printer-device-id"
//	  usb_CMD:          CMD, extracted from "printer-device-id"
//	  ty:               "printer-make-and-model"
//	  priority:         hardcoded as "50"
//	  product:          "printer-make-and-model", in round brackets
//	  pdl:              "document-format-supported"
//	  txtvers:          hardcoded as "1"
//	  adminurl:         "printer-more-info"
func (attrs ippAttrs) decode(usbinfo UsbDeviceInfo) (
	ippinfo *IppPrinterInfo, svc DNSSdSvcInfo) {

	svc = DNSSdSvcInfo{
		Type:     "_ipp._tcp",
		SubTypes: []string{"_universal._sub._ipp._tcp"},
	}

	// Obtain IppPrinterInfo
	ippinfo = &IppPrinterInfo{
		AdminURL: attrs.strSingle("printer-more-info"),
		IconURL:  attrs.strSingle("printer-icons"),
	}

	// Obtain DNSSdName
	ippinfo.DNSSdName = attrs.strSingle("printer-dns-sd-name")
	if ippinfo.DNSSdName == "" {
		ippinfo.DNSSdName = attrs.strSingle("printer-info")
	}
	if ippinfo.DNSSdName == "" {
		ippinfo.DNSSdName = attrs.strSingle("printer-make-and-model")
	}
	if ippinfo.DNSSdName == "" {
		ippinfo.DNSSdName = usbinfo.MfgAndProduct
	}

	// Obtain UUID
	ippinfo.UUID = attrs.getUUID()
	if ippinfo.UUID == "" {
		ippinfo.UUID = usbinfo.UUID()
	}

	// Obtain and parse IEEE 1284 device ID
	devid := make(map[string]string)
	for _, id := range strings.Split(attrs.strSingle("printer-device-id"), ";") {
		keyval := strings.SplitN(id, ":", 2)
		if len(keyval) == 2 {
			devid[keyval[0]] = keyval[1]
		}
	}

	svc.Txt.Add("air", "none")
	svc.Txt.IfNotEmpty("mopria-certified", attrs.strSingle("mopria-certified"))
	svc.Txt.Add("rp", "ipp/print")
	svc.Txt.Add("priority", "50")
	svc.Txt.IfNotEmpty("kind", attrs.strJoined("printer-kind"))
	svc.Txt.IfNotEmpty("PaperMax", attrs.getPaperMax())
	if !svc.Txt.IfNotEmpty("URF", attrs.strJoined("urf-supported")) {
		svc.Txt.IfNotEmpty("URF", devid["URF"])
	}
	svc.Txt.IfNotEmpty("UUID", ippinfo.UUID)
	svc.Txt.IfNotEmpty("Color", attrs.getBool("color-supported"))
	svc.Txt.IfNotEmpty("Duplex", attrs.getDuplex())
	svc.Txt.Add("note", attrs.strSingle("printer-location"))
	svc.Txt.Add("qtotal", "1")
	svc.Txt.IfNotEmpty("usb_MDL", devid["MDL"])
	svc.Txt.IfNotEmpty("usb_MFG", devid["MFG"])
	svc.Txt.IfNotEmpty("usb_CMD", devid["CMD"])
	svc.Txt.IfNotEmpty("ty", attrs.strSingle("printer-make-and-model"))
	svc.Txt.IfNotEmpty("product", attrs.strBrackets("printer-make-and-model"))
	svc.Txt.AddPDL("pdl", attrs.strJoined("document-format-supported"))
	svc.Txt.Add("txtvers", "1")
	svc.Txt.URLIfNotEmpty("adminurl", ippinfo.AdminURL)

	return
}

// getUUID returns printer UUID, or "", if UUID not available
func (attrs ippAttrs) getUUID() string {
	uuid := attrs.strSingle("printer-uuid")
	return UUIDNormalize(uuid)
}

// getDuplex returns "T" if printer supports two-sided
// printing, "F" if not and "" if it cant' tell
func (attrs ippAttrs) getDuplex() string {
	vals := attrs.getAttr(goipp.TypeString, "sides-supported")
	one, two := false, false
	for _, v := range vals {
		s := string(v.(goipp.String))
		switch {
		case strings.HasPrefix(s, "one"):
			one = true
		case strings.HasPrefix(s, "two"):
			two = true
		}
	}

	if two {
		return "T"
	}

	if one {
		return "F"
	}

	return ""
}

// getPaperMax returns max paper size, supported by printer
//
// According to Bonjour Printing Specification, Version 1.2.1,
// it can take one of following values:
//
//	"<legal-A4"
//	"legal-A4"
//	"tabloid-A3"
//	"isoC-A2"
//	">isoC-A2"
//
// If PaperMax cannot be guessed, it returns empty string
func (attrs ippAttrs) getPaperMax() string {
	// Roll over "media-size-supported", extract
	// max x-dimension and max y-dimension
	vals := attrs.getAttr(goipp.TypeCollection, "media-size-supported")
	if vals == nil {
		return ""
	}

	var xDimMax, yDimMax int

	for _, collection := range vals {
		var xDimAttr, yDimAttr goipp.Attribute
		attrs := collection.(goipp.Collection)
		for i := len(attrs) - 1; i >= 0; i-- {
			switch attrs[i].Name {
			case "x-dimension":
				xDimAttr = attrs[i]
			case "y-dimension":
				yDimAttr = attrs[i]
			}
		}

		if len(xDimAttr.Values) > 0 {
			switch dim := xDimAttr.Values[0].V.(type) {
			case goipp.Integer:
				if int(dim) > xDimMax {
					xDimMax = int(dim)
				}
			case goipp.Range:
				if int(dim.Upper) > xDimMax {
					xDimMax = int(dim.Upper)
				}
			}
		}

		if len(yDimAttr.Values) > 0 {
			switch dim := yDimAttr.Values[0].V.(type) {
			case goipp.Integer:
				if int(dim) > yDimMax {
					yDimMax = int(dim)
				}
			case goipp.Range:
				if int(dim.Upper) > yDimMax {
					yDimMax = int(dim.Upper)
				}
			}
		}
	}

	if xDimMax == 0 || yDimMax == 0 {
		return ""
	}

	// Now classify by paper size
	return PaperSize{xDimMax, yDimMax}.Classify()
}

// Get a single-string attribute.
func (attrs ippAttrs) strSingle(name string) string {
	strs := attrs.getStrings(name)
	if len(strs) == 0 {
		return ""
	}

	return strs[0]
}

// Get a multi-string attribute, represented as a comma-separated list
func (attrs ippAttrs) strJoined(name string) string {
	strs := attrs.getStrings(name)
	return strings.Join(strs, ",")
}

// Get a single string, and put it into brackets
func (attrs ippAttrs) strBrackets(name string) string {
	s := attrs.strSingle(name)
	if s != "" {
		s = "(" + s + ")"
	}
	return s
}

// Get attribute's []string value by attribute name
func (attrs ippAttrs) getStrings(name string) []string {
	vals := attrs.getAttr(goipp.TypeString, name)
	strs := make([]string, len(vals))
	for i := range vals {
		strs[i] = string(vals[i].(goipp.String))
	}

	return strs
}

// Get boolean attribute. Returns "F" or "T" if attribute is found,
// empty string otherwise.
func (attrs ippAttrs) getBool(name string) string {
	vals := attrs.getAttr(goipp.TypeBoolean, name)
	if vals == nil {
		return ""
	}
	if vals[0].(goipp.Boolean) {
		return "T"
	}
	return "F"
}

// Get attribute's value by attribute name
// Value type is checked and enforced
func (attrs ippAttrs) getAttr(t goipp.Type, name string) []goipp.Value {

	v, ok := attrs[name]
	if ok && v[0].V.Type() == t {
		var vals []goipp.Value
		for i := range v {
			vals = append(vals, v[i].V)
		}
		return vals
	}

	return nil
}
0707010000002A000081A400000000000000000000000167D72F5D0000074D000000000000000000000000000000000000001D00000000ipp-usb-0.9.30/linewriter.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * LineWriter is a helper object, implementing io.Writer interface
 * on a top of write-line callback. It is used by logger.
 */

package main

import (
	"bytes"
)

// LineWriter implements io.Write and io.Close interfaces
// It splits stream into text lines and calls a provided
// callback for each complete line.
//
// Line passed to callback is not terminated by '\n'
// character. Close flushes last incomplete line, if any
type LineWriter struct {
	Func   func([]byte) // write-line callback
	Prefix string       // Prefix prepended to each line
	buf    bytes.Buffer // buffer for incomplete lines
}

// Write implements io.Writer interface
func (lw *LineWriter) Write(text []byte) (n int, err error) {
	n = len(text)

	for len(text) > 0 {
		// Fetch next line
		var line []byte
		var unfinished bool

		if l := bytes.IndexByte(text, '\n'); l >= 0 {
			l++
			line = text[:l-1]
			text = text[l:]
		} else {
			line = text
			text = nil
			unfinished = true
		}

		// Dispatch next line
		if lw.buf.Len() == 0 {
			lw.buf.Write([]byte(lw.Prefix))
		}

		lw.buf.Write(line)

		if !unfinished {
			lw.Func(lw.buf.Bytes())
			lw.buf.Reset()
		}
	}

	return
}

// Close implements io.Closer interface
//
// Close flushes the last incomplete line from the
// internal buffer. Close is not needed, if it is
// known that there is no such a line, or if its
// presence doesn't matter (without Close its content
// will be lost)
func (lw *LineWriter) Close() error {
	if lw.buf.Len() > 0 {
		lw.Func(lw.buf.Bytes())
	}
	return nil
}

// WriteClose writes text to LineWriter and then closes it
func (lw *LineWriter) WriteClose(text []byte) {
	lw.Write(text)
	lw.Close()
}
0707010000002B000081A400000000000000000000000167D72F5D000006FC000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/listener.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * HTTP listener
 */

package main

import (
	"net"
	"strconv"
	"time"
)

// Listener wraps net.Listener
//
// Note, if IP address is not specified, go stdlib
// creates a beautiful listener, able to listen to
// IPv4 and IPv6 simultaneously. But it cannot do it,
// if IP address is given
//
// So it is much simpler to always create a broadcast listener
// and to filter incoming connection in Accept() wrapper rather
// that create separate IPv4 and IPv6 listeners and dial with
// them both
type Listener struct {
	net.Listener // Underlying net.Listener
}

// NewListener creates new listener
func NewListener(port int) (net.Listener, error) {
	// Setup network and address
	network := "tcp4"
	if Conf.IPV6Enable {
		network = "tcp"
	}

	addr := ":" + strconv.Itoa(port)

	// Create net.Listener
	nl, err := net.Listen(network, addr)
	if err != nil {
		return nil, err
	}

	// Wrap into Listener
	return Listener{nl}, nil
}

// Accept new connection
func (l Listener) Accept() (net.Conn, error) {
	for {
		// Accept new connection
		conn, err := l.Listener.Accept()
		if err != nil {
			return nil, err
		}

		// Obtain underlying net.TCPConn
		tcpconn, ok := conn.(*net.TCPConn)
		if !ok {
			// Should never happen, actually
			conn.Close()
			continue
		}

		// Reject non-loopback connections, if required
		if Conf.LoopbackOnly &&
			!tcpconn.LocalAddr().(*net.TCPAddr).IP.IsLoopback() {
			tcpconn.SetLinger(0)
			tcpconn.Close()
			continue
		}

		// Setup TCP parameters
		tcpconn.SetKeepAlive(true)
		tcpconn.SetKeepAlivePeriod(20 * time.Second)

		return tcpconn, nil
	}
}
0707010000002C000081A400000000000000000000000167D72F5D000046F0000000000000000000000000000000000000001900000000ipp-usb-0.9.30/logger.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Logging
 */

package main

import (
	"bytes"
	"compress/gzip"
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"runtime/debug"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/OpenPrinting/goipp"
)

const (
	// LogMinFileSize specifies a minimum value for the
	// max-file-size parameter
	LogMinFileSize = 16 * 1024
)

// Standard loggers
var (
	// This is the default logger
	Log = NewLogger().ToMainFile()

	// Console logger always writes to console
	Console = NewLogger().ToConsole()

	// Initlog used only on initialization time
	// It writes to Stdout or Stderr, depending
	// on log level
	InitLog = NewLogger().ToStdOutErr()
)

// LogLevel enumerates possible log levels
type LogLevel int

// LogLevel constants
const (
	LogError LogLevel = 1 << iota
	LogInfo
	LogDebug
	LogTraceIPP
	LogTraceESCL
	LogTraceHTTP
	LogTraceUSB

	LogAll      = LogError | LogInfo | LogDebug | LogTraceAll
	LogTraceAll = LogTraceIPP | LogTraceESCL | LogTraceHTTP | LogTraceUSB
)

// Adjust LogLevel mask, so more detailed log levels
// imply less detailed
func (levels *LogLevel) Adjust() {
	switch {
	case *levels&LogTraceAll != 0:
		*levels |= LogDebug | LogInfo | LogError
	case *levels&LogDebug != 0:
		*levels |= LogInfo | LogError
	case *levels&LogInfo != 0:
		*levels |= LogError
	}
}

// loggerMode enumerates possible Logger modes
type loggerMode int

const (
	loggerNoMode       loggerMode = iota // Mode not yet set; log is buffered
	loggerDiscard                        // Log goes to nowhere
	loggerConsole                        // Log goes to console
	loggerColorConsole                   // Log goes to console and uses ANSI colors
	loggerFile                           // Log goes to disk file
)

// Logger implements logging facilities
type Logger struct {
	LogMessage                 // "Root" log message
	levels     LogLevel        // Levels generated by this logger
	ccLevels   LogLevel        // Sum of Cc's levels
	paused     int32           // Logger paused, if counter > 0
	mode       loggerMode      // Logger mode
	lock       sync.Mutex      // Write lock
	path       string          // Path to log file
	cc         []*Logger       // Loggers to send carbon copy to
	out        io.Writer       // Output stream, may be *os.File
	outhook    func(io.Writer, // Output hook
		LogLevel, []byte)

	// Don't reexport these methods from the root message
	Commit, Flush, Reject struct{}
}

// NewLogger creates new logger. Logger mode is not set,
// so logs written to this logger a buffered until mode
// (and direction) is set
func NewLogger() *Logger {
	l := &Logger{
		mode:     loggerNoMode,
		levels:   LogAll,
		ccLevels: 0,
		outhook: func(w io.Writer, _ LogLevel, line []byte) {
			w.Write(line)
		},
	}

	l.LogMessage.logger = l

	return l
}

// ToNowhere redirects log to nowhere
func (l *Logger) ToNowhere() *Logger {
	l.mode = loggerDiscard
	l.out = ioutil.Discard
	return l
}

// ToConsole redirects log to console
func (l *Logger) ToConsole() *Logger {
	l.mode = loggerConsole
	l.out = os.Stdout
	return l
}

// ToColorConsole redirects log to console with ANSI colors
func (l *Logger) ToColorConsole() *Logger {
	if logIsAtty(os.Stdout) {
		l.outhook = logColorConsoleWrite
	}

	return l.ToConsole()
}

// ToStdOutErr redirects log to Stdout or Stderr, depending
// on LogLevel
func (l *Logger) ToStdOutErr() *Logger {
	l.outhook = func(out io.Writer, level LogLevel, line []byte) {
		if level == LogError {
			out = os.Stderr
		}
		out.Write(line)
	}

	return l.ToConsole()
}

// ToFile redirects log to arbitrary log file
func (l *Logger) ToFile(path string) *Logger {
	l.path = path
	l.mode = loggerFile
	l.out = nil // Will be opened on demand
	return l
}

// ToMainFile redirects log to the main log file
func (l *Logger) ToMainFile() *Logger {
	return l.ToFile(PathLogFile)
}

// ToDevFile redirects log to per-device log file
func (l *Logger) ToDevFile(info UsbDeviceInfo) *Logger {
	return l.ToFile(filepath.Join(PathLogDir, info.Ident()+".log"))
}

// Cc adds Logger to send "carbon copy" to.
func (l *Logger) Cc(to *Logger) *Logger {
	l.cc = append(l.cc, to)
	l.ccLevels |= to.levels

	return l
}

// Close the logger
func (l *Logger) Close() {
	if l.mode == loggerFile && l.out != nil {
		if file, ok := l.out.(*os.File); ok {
			file.Close()
		}
	}
}

// SetLevels set logger's log levels
func (l *Logger) SetLevels(levels LogLevel) *Logger {
	levels.Adjust()
	l.levels = levels
	return l
}

// Pause the logger. All output will be buffered,
// and flushed to destination when logger is resumed
func (l *Logger) Pause() *Logger {
	atomic.AddInt32(&l.paused, 1)
	return l
}

// Resume the logger. All buffered output will be
// flushed
func (l *Logger) Resume() *Logger {
	if atomic.AddInt32(&l.paused, -1) == 0 {
		l.LogMessage.Flush()
	}
	return l
}

// Panic writes to log a panic message, including
// call stack, and terminates a program
func (l *Logger) Panic(v interface{}) {
	l.Error('!', "panic: %v", v)
	l.Error('!', "")

	w := l.LineWriter(LogError, '!')
	w.Write(debug.Stack())
	w.Close()

	os.Exit(1)
}

// Format a time prefix
func (l *Logger) fmtTime() *logLineBuf {
	buf := logLineBufAlloc(0, 0)

	if l.mode == loggerFile {
		now := time.Now()

		year, month, day := now.Date()
		hour, min, sec := now.Clock()

		fmt.Fprintf(buf, "%2.2d-%2.2d-%4.4d %2.2d:%2.2d:%2.2d:",
			day, month, year,
			hour, min, sec)
	}

	return buf
}

// Handle log rotation
func (l *Logger) rotate() {
	// Do we need to rotate?
	file, ok := l.out.(*os.File)
	if !ok {
		return
	}

	stat, err := file.Stat()
	if err != nil || stat.Size() <= Conf.LogMaxFileSize {
		return
	}

	// Perform rotation
	if Conf.LogMaxBackupFiles > 0 {
		prevpath := ""
		for i := Conf.LogMaxBackupFiles; i > 0; i-- {
			nextpath := fmt.Sprintf("%s.%d.gz", l.path, i-1)

			if i == Conf.LogMaxBackupFiles {
				os.Remove(nextpath)
			} else {
				os.Rename(nextpath, prevpath)
			}

			prevpath = nextpath
		}

		err := l.gzip(l.path, prevpath)
		if err != nil {
			return
		}
	}

	file.Truncate(0)
}

// gzip the log file
func (l *Logger) gzip(ipath, opath string) error {
	// Open input file
	ifile, err := os.Open(ipath)
	if err != nil {
		return err
	}

	defer ifile.Close()

	// Open output file
	ofile, err := os.OpenFile(opath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
	if err != nil {
		return err
	}

	// gzip ifile->ofile
	w := gzip.NewWriter(ofile)
	_, err = io.Copy(w, ifile)
	err2 := w.Close()
	err3 := ofile.Close()

	switch {
	case err == nil && err2 != nil:
		err = err2
	case err == nil && err3 != nil:
		err = err3
	}

	// Cleanup and exit
	if err != nil {
		os.Remove(opath)
	}

	return err
}

// LogMessage represents a single (possible multi line) log
// message, which will appear in the output log atomically,
// and will be not interrupted in the middle by other log activity
type LogMessage struct {
	logger *Logger       // Underlying logger
	parent *LogMessage   // Parent message
	lines  []*logLineBuf // One buffer per line
}

// logMessagePool manages a pool of reusable LogMessages
var logMessagePool = sync.Pool{New: func() interface{} { return &LogMessage{} }}

// Begin returns a child (nested) LogMessage. Writes to this
// child message appended to the parent message
func (msg *LogMessage) Begin() *LogMessage {
	msg2 := logMessagePool.Get().(*LogMessage)
	msg2.logger = msg.logger
	msg2.parent = msg
	return msg2
}

// Add formats a next line of log message, with level and prefix char
func (msg *LogMessage) Add(level LogLevel, prefix byte,
	format string, args ...interface{}) *LogMessage {

	if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
		buf := logLineBufAlloc(level, prefix)
		fmt.Fprintf(buf, format, args...)

		msg.appendLineBuf(buf)
	}

	return msg
}

// Nl adds empty line to the log message
func (msg *LogMessage) Nl(level LogLevel) *LogMessage {
	return msg.Add(level, ' ', "")
}

// addBytes adds a next line of log message, taking slice of bytes as input
func (msg *LogMessage) addBytes(level LogLevel, prefix byte, line []byte) *LogMessage {
	if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
		buf := logLineBufAlloc(level, prefix)
		buf.Write(line)

		msg.appendLineBuf(buf)
	}

	return msg
}

// appendLineBuf appends line buffer to msg.lines
func (msg *LogMessage) appendLineBuf(buf *logLineBuf) {
	if msg.parent == nil {
		// Note, many threads may write to the root
		// message simultaneously
		msg.logger.lock.Lock()
		msg.lines = append(msg.lines, buf)
		msg.logger.lock.Unlock()

		msg.Flush()
	} else {
		msg.lines = append(msg.lines, buf)
	}
}

// Debug appends a LogDebug line to the message
func (msg *LogMessage) Debug(prefix byte, format string, args ...interface{}) *LogMessage {
	return msg.Add(LogDebug, prefix, format, args...)
}

// Info appends a LogInfo line to the message
func (msg *LogMessage) Info(prefix byte, format string, args ...interface{}) *LogMessage {
	return msg.Add(LogInfo, prefix, format, args...)
}

// Error appends a LogError line to the message
func (msg *LogMessage) Error(prefix byte, format string, args ...interface{}) *LogMessage {
	return msg.Add(LogError, prefix, format, args...)
}

// Exit appends a LogError line to the message, flushes the message and
// all its parents and terminates a program by calling os.Exit(1)
func (msg *LogMessage) Exit(prefix byte, format string, args ...interface{}) {
	if msg.logger.mode == loggerNoMode {
		msg.logger.ToConsole()
	}

	msg.Error(prefix, format, args...)
	for msg.parent != nil {
		msg.Flush()
		msg = msg.parent
	}
	os.Exit(1)
}

// Check calls msg.Exit(), if err is not nil
func (msg *LogMessage) Check(err error) {
	if err != nil {
		msg.Exit(0, "%s", err)
	}
}

// HexDump appends a HEX dump to the log message
func (msg *LogMessage) HexDump(level LogLevel, prefix byte,
	data []byte) *LogMessage {

	if (msg.logger.levels|msg.logger.ccLevels)&level == 0 {
		return msg
	}

	hex := logLineBufAlloc(0, 0)
	chr := logLineBufAlloc(0, 0)

	defer hex.free()
	defer chr.free()

	off := 0

	for len(data) > 0 {
		hex.Reset()
		chr.Reset()

		sz := len(data)
		if sz > 16 {
			sz = 16
		}

		i := 0
		for ; i < sz; i++ {
			c := data[i]
			fmt.Fprintf(hex, "%2.2x", data[i])
			if i%4 == 3 {
				hex.Write([]byte(":"))
			} else {
				hex.Write([]byte(" "))
			}

			if 0x20 <= c && c < 0x80 {
				chr.WriteByte(c)
			} else {
				chr.WriteByte('.')
			}
		}

		for ; i < 16; i++ {
			hex.WriteString("   ")
		}

		msg.Add(level, prefix, "%4.4x: %s %s", off, hex, chr)

		off += sz
		data = data[sz:]
	}

	return msg
}

// HTTPRequest dumps HTTP request (except body) to the log message
func (msg *LogMessage) HTTPRequest(level LogLevel, prefix byte,
	session int, rq *http.Request) *LogMessage {

	if (msg.logger.levels|msg.logger.ccLevels)&level == 0 {
		return msg
	}

	// Clone request, drop body
	rq = rq.WithContext(context.Background())
	rq.Body = struct{ io.ReadCloser }{http.NoBody}

	// Write it to the log
	msg.Add(level, prefix, "HTTP[%3.3d]: HTTP request header:", session)

	buf := &bytes.Buffer{}
	rq.Write(buf)

	for _, l := range bytes.Split(buf.Bytes(), []byte("\n")) {
		if sz := len(l); sz > 0 && l[sz-1] == '\r' {
			l = l[:sz-1]
		}

		msg.Add(level, prefix, "  %s", l)

		if len(l) == 0 {
			break
		}
	}

	return msg
}

// HTTPResponse dumps HTTP response (expect body) to the log message
func (msg *LogMessage) HTTPResponse(level LogLevel, prefix byte,
	session int, rsp *http.Response) *LogMessage {

	if (msg.logger.levels|msg.logger.ccLevels)&level == 0 {
		return msg
	}

	// Clone response header. Avoid rsp.Header.Clone(),
	// because Go 11 doesn't support it yet
	hdr := make(http.Header, len(rsp.Header))
	for k, v := range rsp.Header {
		hdr[k] = v
	}

	// Go stdlib strips Transfer-Encoding header, so reconstruct it
	if rsp.TransferEncoding != nil {
		hdr.Add("Transfer-Encoding",
			strings.Join(rsp.TransferEncoding, ", "))
	}

	// Write it to the log
	msg.Add(level, prefix, "HTTP[%3.3d]: HTTP response header:", session)
	msg.Add(level, prefix, "  %s %s", rsp.Proto, rsp.Status)

	keys := make([]string, 0, len(hdr))

	for k := range hdr {
		keys = append(keys, k)
	}

	sort.Strings(keys)
	for _, k := range keys {
		msg.Add(level, prefix, "  %s: %s", k, hdr.Get(k))
	}

	msg.Add(level, prefix, "  ")

	return msg
}

// HTTPRqParams dumps HTTP request parameters into the log message
func (msg *LogMessage) HTTPRqParams(level LogLevel, prefix byte,
	session int, rq *http.Request) *LogMessage {

	msg.Add(level, prefix, "HTTP[%3.3d]: %s %s", session, rq.Method, rq.URL)

	return msg
}

// HTTPRspStatus dumps HTTP response status into the log message
func (msg *LogMessage) HTTPRspStatus(level LogLevel, prefix byte,
	session int, rq *http.Request, rsp *http.Response) *LogMessage {

	msg.Add(level, prefix, "HTTP[%3.3d]: %s %s - %s",
		session, rq.Method, rq.URL, rsp.Status)

	return msg
}

// HTTPError writes HTTP error into the log message
func (msg *LogMessage) HTTPError(prefix byte,
	session int, format string, args ...interface{}) *LogMessage {

	msg.Error(prefix, "HTTP[%3.3d]: %s", session, fmt.Sprintf(format, args...))

	return msg
}

// HTTPDebug writes HTTP debug line into the log message
func (msg *LogMessage) HTTPDebug(prefix byte,
	session int, format string, args ...interface{}) *LogMessage {

	msg.Debug(prefix, "HTTP[%3.3d]: %s", session, fmt.Sprintf(format, args...))

	return msg
}

// IppRequest dumps IPP request into the log message
func (msg *LogMessage) IppRequest(level LogLevel, prefix byte,
	m *goipp.Message) *LogMessage {

	if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
		m.Print(msg.LineWriter(level, prefix), true)
	}
	return msg
}

// IppResponse dumps IPP response into the log message
func (msg *LogMessage) IppResponse(level LogLevel, prefix byte,
	m *goipp.Message) *LogMessage {

	if (msg.logger.levels|msg.logger.ccLevels)&level != 0 {
		m.Print(msg.LineWriter(level, prefix), false)
	}
	return msg
}

// LineWriter creates a LineWriter that writes to the LogMessage,
// using specified LogLevel and prefix
func (msg *LogMessage) LineWriter(level LogLevel, prefix byte) *LineWriter {
	return &LineWriter{
		Func: func(line []byte) { msg.addBytes(level, prefix, line) },
	}
}

// Commit message to the log
func (msg *LogMessage) Commit() {
	msg.Flush()
	msg.free()
}

// Flush message content to the log
//
// This is equal to committing the message and starting
// the new message, with the exception that old message
// pointer remains valid. Message logical atomicity is not
// preserved between flushes
func (msg *LogMessage) Flush() {
	// Lock the logger
	msg.logger.lock.Lock()
	defer msg.logger.lock.Unlock()

	// Ignore empty messages
	if len(msg.lines) == 0 {
		return
	}

	// If message has a parent, simply flush our content there
	if msg.parent != nil {
		msg.parent.lines = append(msg.parent.lines, msg.lines...)
		msg.lines = msg.lines[:0]

		// If our parent is root, we need to flush root as well
		if msg.parent.parent == nil {
			msg = msg.parent
		} else {
			return
		}
	}

	// Do nothing, if logger is paused
	if atomic.LoadInt32(&msg.logger.paused) != 0 {
		return
	}

	// Open log file on demand
	if msg.logger.out == nil && msg.logger.mode == loggerFile {
		os.MkdirAll(PathLogDir, 0755)
		msg.logger.out, _ = os.OpenFile(msg.logger.path,
			os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
	}

	if msg.logger.out == nil {
		return
	}

	// Rotate now
	if msg.logger.mode == loggerFile {
		msg.logger.rotate()
	}

	// Prepare to carbon-copy
	var cclist []struct {
		levels LogLevel
		msg    *LogMessage
	}

	for _, cc := range msg.logger.cc {
		cclist = append(cclist, struct {
			levels LogLevel
			msg    *LogMessage
		}{cc.levels, cc.Begin()})
	}

	// Send message content to the logger
	buf := msg.logger.fmtTime()
	defer buf.free()

	timeLen := buf.Len()
	for _, l := range msg.lines {
		l.trim()

		// Generate own output
		buf.Truncate(timeLen)
		if l.level&msg.logger.levels != 0 {
			if !l.empty() {
				if timeLen != 0 {
					buf.WriteByte(' ')
				}

				buf.Write(l.Bytes())
			}

			buf.WriteByte('\n')
			msg.logger.outhook(msg.logger.out, l.level, buf.Bytes())
		}

		// Send carbon copies
		for _, cc := range cclist {
			if (cc.levels & l.level) != 0 {
				cc.msg.addBytes(l.level, 0, l.Bytes())
			}
		}

		l.free()
	}

	// Commit carbon copies
	for _, cc := range cclist {
		cc.msg.Commit()
	}

	// Reset the message
	msg.lines = msg.lines[:0]
}

// Reject the message
func (msg *LogMessage) Reject() {
	msg.free()
}

// Return message to the logMessagePool
func (msg *LogMessage) free() {
	// Free all lines
	for _, l := range msg.lines {
		l.free()
	}

	// Reset the message and put it to the pool
	if len(msg.lines) < 16 {
		msg.lines = msg.lines[:0] // Keep memory, reset content
	} else {
		msg.lines = nil // Drop this large buffer
	}

	msg.logger = nil

	logMessagePool.Put(msg)
}

// logLineBuf represents a single log line buffer
type logLineBuf struct {
	bytes.Buffer          // Underlying buffer
	level        LogLevel // Log level the line was written on
}

// logLinePool manages a pool of reusable logLines
var logLineBufPool = sync.Pool{New: func() interface{} {
	return &logLineBuf{
		Buffer: bytes.Buffer{},
	}
}}

// logLineAlloc() allocates a logLineBuf
func logLineBufAlloc(level LogLevel, prefix byte) *logLineBuf {
	buf := logLineBufPool.Get().(*logLineBuf)
	buf.level = level
	if prefix != 0 {
		buf.Write([]byte{prefix, ' '})
	}
	return buf
}

// free returns the logLineBuf to the pool
func (buf *logLineBuf) free() {
	if buf.Cap() <= 256 {
		buf.Reset()
		logLineBufPool.Put(buf)
	}
}

// trim removes trailing spaces
func (buf *logLineBuf) trim() {
	bytes := buf.Bytes()
	var i int

loop:
	for i = len(bytes); i > 0; i-- {
		c := bytes[i-1]
		switch c {
		case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
		default:
			break loop
		}
	}
	buf.Truncate(i)
}

// empty returns true if logLineBuf is empty (no text, no prefix)
func (buf *logLineBuf) empty() bool {
	return buf.Len() == 0
}
0707010000002D000081A400000000000000000000000167D72F5D00000437000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/logger_unix.go// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris

/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Logging, system-dependent part for UNIX
 */

package main

import (
	"io"
	"os"
)

// #include <unistd.h>
import "C"

// logIsAtty returns true, if os.File refers to a terminal
func logIsAtty(file *os.File) bool {
	fd := file.Fd()
	return C.isatty(C.int(fd)) == 1
}

// logColorConsoleWrite writes a colorized line to console
func logColorConsoleWrite(out io.Writer, level LogLevel, line []byte) {
	var beg, end string

	switch {
	case (level & LogError) != 0:
		beg, end = "\033[31;1m", "\033[0m" // Red
	case (level & LogInfo) != 0:
		beg, end = "\033[32;1m", "\033[0m" // Green
	case (level & LogDebug) != 0:
		beg, end = "\033[37;1m", "\033[0m" // White
	case (level & LogTraceAll) != 0:
		beg, end = "\033[37m", "\033[0m" // Gray
	}

	out.Write([]byte(beg))
	out.Write(line)
	out.Write([]byte(end))
}
0707010000002E000081A400000000000000000000000167D72F5D00000292000000000000000000000000000000000000001B00000000ipp-usb-0.9.30/loopback.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Loopback interface index discovery
 */

package main

import (
	"errors"
	"fmt"
	"net"
)

// Loopback returns index of loopback interface
func Loopback() (int, error) {
	interfaces, err := net.Interfaces()
	if err == nil {
		for _, iface := range interfaces {
			if (iface.Flags & net.FlagLoopback) != 0 {
				return iface.Index, nil
			}
		}
	}

	if err == nil {
		err = errors.New("not found")
	}

	return 0, fmt.Errorf("Loopback discovery: %s", err)
}
0707010000002F000081A400000000000000000000000167D72F5D00001D54000000000000000000000000000000000000001700000000ipp-usb-0.9.30/main.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * The main function
 */

package main

import (
	"bytes"
	"fmt"
	"os"
	"sort"
)

const usageText = `Usage:
    %s mode [options]

Modes are:
    standalone  - run forever, automatically discover IPP-over-USB
                  devices and serve them all
    udev        - like standalone, but exit when last IPP-over-USB
                  device is disconnected
    debug       - logs duplicated on console, -bg option is
                  ignored
    check       - check configuration and exit
    status      - print ipp-usb status and exit

Options are
    -bg         - run in background (ignored in debug mode)
`

// RunMode represents the program run mode
type RunMode int

// Run modes:
//   RunStandalone - run forever, automatically discover IPP-over-USB
//                   devices and serve them all
//   RunUdev       - like RunStandalone, but exit when last IPP-over-USB
//                   device is disconnected
//   RunDebug      - logs duplicated on console, -bg option is ignored
//   RunCheck      - check configuration and exit
//   RunStatus     - print ipp-usb status and exit
const (
	RunDefault RunMode = iota
	RunStandalone
	RunUdev
	RunDebug
	RunCheck
	RunStatus
)

// String returns RunMode name
func (m RunMode) String() string {
	switch m {
	case RunDefault:
		return "default"
	case RunStandalone:
		return "standalone"
	case RunUdev:
		return "udev"
	case RunDebug:
		return "debug"
	case RunCheck:
		return "check"
	case RunStatus:
		return "status"
	}

	return fmt.Sprintf("unknown (%d)", int(m))
}

// RunParameters represents the program run parameters
type RunParameters struct {
	Mode       RunMode // Run mode
	Background bool    // Run in background
}

// usage prints detailed usage and exits
func usage() {
	fmt.Printf(usageText, os.Args[0])
	os.Exit(0)
}

// usage_error prints usage error and exits
func usageError(format string, args ...interface{}) {
	if format != "" {
		fmt.Printf(format+"\n", args...)
	}

	fmt.Printf("Try %s -h for more information\n", os.Args[0])
	os.Exit(1)
}

// parseArgv parses program parameters. In a case of usage error,
// it prints a error message and exits
func parseArgv() (params RunParameters) {
	// Catch panics to log
	defer func() {
		v := recover()
		if v != nil {
			Log.Panic(v)
		}
	}()

	// For now, default mode is debug mode. It may change in a future
	params.Mode = RunDebug

	modes := 0
	for _, arg := range os.Args[1:] {
		switch arg {
		case "-h", "-help", "--help":
			usage()
		case "standalone":
			params.Mode = RunStandalone
			modes++
		case "udev":
			params.Mode = RunUdev
			modes++
		case "debug":
			params.Mode = RunDebug
			modes++
		case "check":
			params.Mode = RunCheck
			modes++
		case "status":
			params.Mode = RunStatus
			modes++
		case "-bg":
			params.Background = true
		default:
			usageError("Invalid argument %s", arg)
		}
	}

	if modes > 1 {
		usageError("Conflicting run modes")
	}

	if params.Mode == RunDebug {
		params.Background = false
	}

	return
}

// printStatus prints status of running ipp-usb daemon, if any
func printStatus() {
	// Fetch status
	text, err := StatusRetrieve()

	if err != nil {
		InitLog.Info(0, "%s", err)
		return
	}

	// Split into lines
	text = bytes.Trim(text, "\n")
	lines := bytes.Split(text, []byte("\n"))

	// Strip empty lines at the end
	for len(lines) > 0 && len(lines[len(lines)-1]) == 0 {
		lines = lines[0 : len(lines)-1]
	}

	// Write to log, line by line
	for _, line := range lines {
		InitLog.Info(0, "%s", line)
	}
}

// The main function
func main() {
	var err error

	// Parse arguments
	params := parseArgv()

	// Load configuration file
	err = ConfLoad()
	InitLog.Check(err)

	// Setup logging
	if params.Mode != RunDebug &&
		params.Mode != RunCheck &&
		params.Mode != RunStatus {
		Console.ToNowhere()
	} else if Conf.ColorConsole {
		Console.ToColorConsole()
	}

	Log.SetLevels(Conf.LogMain)
	Console.SetLevels(Conf.LogConsole)
	Log.Cc(Console)

	// In RunCheck mode, list IPP-over-USB devices
	if params.Mode == RunCheck {
		// If we are here, configuration is OK
		InitLog.Info(0, "Configuration files: OK")

		var descs map[UsbAddr]UsbDeviceDesc
		err = UsbInit(true)
		if err == nil {
			descs, err = UsbGetIppOverUsbDeviceDescs()
		}

		if err != nil {
			InitLog.Info(0, "Can't read list of USB devices: %s", err)
		} else if descs == nil || len(descs) == 0 {
			InitLog.Info(0, "No IPP over USB devices found")
		} else {
			// Repack into the sorted list
			var list []UsbDeviceDesc
			var buf bytes.Buffer

			for _, desc := range descs {
				list = append(list, desc)
			}
			sort.Slice(list, func(i, j int) bool {
				return list[i].UsbAddr.Less(list[j].UsbAddr)
			})

			InitLog.Info(0, "IPP over USB devices:")
			InitLog.Info(0, " Num  Device              Vndr:Prod  Model")
			for i, dev := range list {
				buf.Reset()
				fmt.Fprintf(&buf, "%3d. %s", i+1, dev.UsbAddr)
				if info, err := dev.GetUsbDeviceInfo(); err == nil {
					fmt.Fprintf(&buf, "  %4.4x:%.4x  %q",
						info.Vendor, info.Product, info.MfgAndProduct)
				}

				InitLog.Info(0, " %s", buf.String())
			}
		}
	}

	// In RunStatus mode, print ipp-usb status, and we are done
	if params.Mode == RunStatus {
		printStatus()
		os.Exit(0)
	}

	// Check user privileges
	if os.Geteuid() != 0 {
		InitLog.Exit(0, "This program requires root privileges")
	}

	// If mode is "check", we are done
	if params.Mode == RunCheck {
		os.Exit(0)
	}

	// If background run is requested, it's time to fork
	if params.Background {
		err = Daemon()
		InitLog.Check(err)
		os.Exit(0)
	}

	// Prevent multiple copies of ipp-usb from being running
	// in a same time
	os.MkdirAll(PathLockDir, 0755)
	lock, err := os.OpenFile(PathLockFile,
		os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	InitLog.Check(err)
	defer lock.Close()

	err = FileLock(lock, FileLockNoWait)
	if err == ErrLockIsBusy {
		if params.Mode == RunUdev {
			// It's not an error in udev mode
			os.Exit(0)
		} else {
			InitLog.Exit(0, "ipp-usb already running")
		}
	}
	InitLog.Check(err)

	// Write to log that we are here
	if params.Mode != RunCheck && params.Mode != RunStatus {
		Log.Info(' ', "===============================")
		Log.Info(' ', "ipp-usb started in %q mode, pid=%d",
			params.Mode, os.Getpid())
		defer Log.Info(' ', "ipp-usb finished")
	}

	// Initialize USB
	err = UsbInit(false)
	InitLog.Check(err)

	// Close stdin/stdout/stderr, unless running in debug mode
	if params.Mode != RunDebug {
		err = CloseStdInOutErr()
		InitLog.Check(err)
	}

	// Run PnP manager
	for {
		exitReason := PnPStart(params.Mode == RunUdev)

		// The following race is possible here:
		// 1) last device disappears, ipp-usb is about to exit
		// 2) new device connected, new ipp-usb started
		// 3) new ipp-usp exits, because lock is still held
		//    by the old ipp-usb
		// 4) old ipp-usb finally exits
		//
		// So after releasing a lock, we rescan for IPP-over-USB
		// devices, and if something was found, we try to reacquire
		// the lock, and if it succeeds, we continue to serve
		// these devices instead of exiting
		if exitReason == PnPIdle && params.Mode == RunUdev {
			err = FileUnlock(lock)
			Log.Check(err)

			if UsbCheckIppOverUsbDevices() &&
				FileLock(lock, FileLockNoWait) == nil {
				Log.Info(' ', "New IPP-over-USB device found")
				continue
			}
		}

		break
	}
}
07070100000030000081A400000000000000000000000167D72F5D0000080E000000000000000000000000000000000000001800000000ipp-usb-0.9.30/paper.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Paper Size Classifier
 */

package main

// PaperSize represents paper size, in IPP units (1/100 mm)
type PaperSize struct {
	Width, Height int // Paper width and height
}

// Standard paper sizes
//                  US name      US inches   US mm           ISO mm
//   "legal-A4"     A, Legal     8.5 x 14    215.9 x 355.6   A4: 210 x 297
//   "tabloid-A3"   B, Tabloid   11 x 17     279.4 x 431.8   A3: 297 x 420
//   "isoC-A2"      C            17 × 22     431.8 × 558.8   A2: 420 x 594
//
// Please note, Apple in the "Bonjour Printing Specification"
// incorrectly states paper sizes as 9x14, 13x19 and 18x24 inches
var (
	PaperLegal   = PaperSize{21590, 35560}
	PaperA4      = PaperSize{21000, 29700}
	PaperTabloid = PaperSize{27940, 43180}
	PaperA3      = PaperSize{29700, 42000}
	PaperC       = PaperSize{43180, 55880}
	PaperA2      = PaperSize{42000, 59400}
)

// Less checks that p is less that p2, which means:
//   * Either p.Width or p.Height is less that p2.Width or p2.Heigh
//   * Neither of p.Width or p.Height is greater that p2.Width or p2.Heigh
func (p PaperSize) Less(p2 PaperSize) bool {
	return (p.Width < p2.Width && p.Height <= p2.Height) ||
		(p.Height < p2.Height && p.Width <= p2.Width)
}

// Classify paper size according to Apple Bonjour rules
// Returns:
//     ">isoC-A2" for paper larger that C or A2
//     "isoC-A2" for C or A2 paper
//     "tabloid-A3" for Tabloid or A3 paper
//     "legal-A4" for Legal or A4 paper
//     "<legal-A4" for paper smaller that Legal or A4
func (p PaperSize) Classify() string {
	switch {
	case PaperC.Less(p) || PaperA2.Less(p):
		return ">isoC-A2"

	case !p.Less(PaperC) || !p.Less(PaperA2):
		return "isoC-A2"

	case !p.Less(PaperTabloid) || !p.Less(PaperA3):
		return "tabloid-A3"

	case !p.Less(PaperLegal) || !p.Less(PaperA4):
		return "legal-A4"

	default:
		return "<legal-A4"
	}
}
07070100000031000081A400000000000000000000000167D72F5D000009AC000000000000000000000000000000000000001D00000000ipp-usb-0.9.30/paper_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Tests for paper.go
 */

package main

import (
	"testing"
)

var allSizes = []PaperSize{
	PaperLegal,
	PaperA4,
	PaperTabloid,
	PaperA3,
	PaperC,
	PaperA2,
}

// Compute p.Less(p2) and check answer
func testPaperSizeLess(t *testing.T, p, p2 PaperSize, answer bool) {
	rsp := p.Less(p2)
	if rsp != answer {
		t.Errorf("PaperSize{%d,%d}.Less(PaperSize{%d,%d}): %v, must be %v",
			p.Width, p.Height,
			p2.Width, p2.Height,
			rsp, answer,
		)
	}
}

// Compute p.Classify() and check answer
func testPaperSizeClassify(t *testing.T, p PaperSize, answer string) {
	rsp := p.Classify()
	if rsp != answer {
		t.Errorf("PaperSize{%d,%d}.Classify(): %v, must be %v",
			p.Width, p.Height,
			rsp, answer,
		)
	}
}

// Test (PaperSize) Less()
func TestPaperSizeLess(t *testing.T) {
	var p2 PaperSize

	for _, p := range allSizes {
		testPaperSizeLess(t, p, p, false)

		if p.Less(p) {
			t.Fail()
		}

		p2 = PaperSize{p.Width - 1, p.Height}
		testPaperSizeLess(t, p, p2, false)
		testPaperSizeLess(t, p2, p, true)

		p2 = PaperSize{p.Width, p.Height - 1}
		testPaperSizeLess(t, p, p2, false)
		testPaperSizeLess(t, p2, p, true)

		p2 = PaperSize{p.Width - 1, p.Height + 1}
		testPaperSizeLess(t, p, p2, false)
		testPaperSizeLess(t, p2, p, false)

		p2 = PaperSize{p.Width + 1, p.Height - 1}
		testPaperSizeLess(t, p, p2, false)
		testPaperSizeLess(t, p2, p, false)
	}
}

// Test (PaperSize) Classify()
func TestPaperSizeClassify(t *testing.T) {
	testPaperSizeClassify(t, PaperLegal, "legal-A4")
	testPaperSizeClassify(t, PaperA4, "legal-A4")

	testPaperSizeClassify(t, PaperTabloid, "tabloid-A3")
	testPaperSizeClassify(t, PaperA3, "tabloid-A3")

	testPaperSizeClassify(t, PaperC, "isoC-A2")
	testPaperSizeClassify(t, PaperA2, "isoC-A2")

	var sizes []PaperSize
	sizes = []PaperSize{
		{PaperA4.Width - 1, PaperA4.Height},
		{PaperA4.Width, PaperA4.Height - 1},
	}

	for _, p := range sizes {
		testPaperSizeClassify(t, p, "<legal-A4")
	}

	sizes = []PaperSize{
		{PaperC.Width + 1, PaperC.Height},
		{PaperC.Width, PaperC.Height + 1},
		{PaperA2.Width + 1, PaperA2.Height},
		{PaperA2.Width, PaperA2.Height + 1},
	}

	for _, p := range sizes {
		testPaperSizeClassify(t, p, ">isoC-A2")
	}

	// HP LaserJet MFP M28
	testPaperSizeClassify(t, PaperSize{21590, 29692}, "legal-A4")
}
07070100000032000081A400000000000000000000000167D72F5D000004DC000000000000000000000000000000000000001800000000ipp-usb-0.9.30/paths.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Common paths
 */

package main

const (
	// PathConfDir defines path to configuration directory
	PathConfDir = "/etc/ipp-usb"

	// PathConfQuirksDir defines path to quirks files in configuration directory
	PathConfQuirksDir = "/etc/ipp-usb/quirks"

	// PathQuirksDir defines path to quirks files
	PathQuirksDir = "/usr/share/ipp-usb/quirks"

	// PathProgState defines path to program state directory
	PathProgState = "/var/ipp-usb"

	// PathLockDir defines path to directory that contains lock files
	PathLockDir = PathProgState + "/lock"

	// PathLockFile defines path to lock file
	PathLockFile = PathLockDir + "/ipp-usb.lock"

	// PathControlSocket defines path to the control socket
	PathControlSocket = PathProgState + "/ctrl"

	// PathProgStateDev defines path to directory where per-device state
	// files are saved to
	PathProgStateDev = PathProgState + "/dev"

	// PathLogDir defines path to log directory
	PathLogDir = "/var/log/ipp-usb"

	// PathLogFile defines path to the main log file
	PathLogFile = PathLogDir + "/main.log"
)
07070100000033000081A400000000000000000000000167D72F5D00000FD5000000000000000000000000000000000000001600000000ipp-usb-0.9.30/pnp.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * PnP manager
 */

package main

import (
	"context"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

// PnPExitReason explains why PnP manager has exited
type PnPExitReason int

// PnPExitReason constants
const (
	PnPIdle PnPExitReason = iota // No more connected devices
	PnPTerm                      // Terminating signal received
)

// pnpRetryTime returns time of next retry of failed device initialization
func pnpRetryTime(err error) time.Time {
	if err == ErrBlackListed || err == ErrUnusable {
		// These errors are unrecoverable.
		// Forget about device for the next million hours :-)
		return time.Now().Add(time.Hour * 1e6)
	}

	return time.Now().Add(DevInitRetryInterval)
}

// pnpRetryExpired checks if device initialization retry time expired
func pnpRetryExpired(tm time.Time) bool {
	return !time.Now().Before(tm)
}

// PnPStart start PnP manager
//
// If exitWhenIdle is true, PnP manager will exit, when there is no more
// devices to serve
func PnPStart(exitWhenIdle bool) PnPExitReason {
	devices := UsbAddrList{}
	devByAddr := make(map[UsbAddr]*Device)
	retryByAddr := make(map[UsbAddr]time.Time)
	sigChan := make(chan os.Signal, 1)
	ticker := time.NewTicker(DevInitRetryInterval / 4)
	tickerRunning := true

	signal.Notify(sigChan,
		os.Signal(syscall.SIGINT),
		os.Signal(syscall.SIGTERM),
		os.Signal(syscall.SIGHUP))

	// Start control socket server
	err := CtrlsockStart()
	if err == nil {
		defer CtrlsockStop()
	}

	// Serve PnP events until terminated
loop:
	for {
		devDescs, err := UsbGetIppOverUsbDeviceDescs()

		if err == nil {
			newdevices := UsbAddrList{}
			for _, desc := range devDescs {
				newdevices.Add(desc.UsbAddr)
			}

			added, removed := devices.Diff(newdevices)
			devices = newdevices

			// Handle added devices
			for _, addr := range added {
				Log.Debug('+', "PNP %s: added", addr)
				dev, err := NewDevice(devDescs[addr])
				port := 0
				if dev != nil {
					port = dev.State.HTTPPort
				}
				StatusSet(addr, devDescs[addr], port, err)

				if err == nil {
					devByAddr[addr] = dev
				} else {
					Log.Error('!', "PNP %s: %s", addr, err)
					retryByAddr[addr] = pnpRetryTime(err)
				}
			}

			// Handle removed devices
			for _, addr := range removed {
				Log.Debug('-', "PNP %s: removed", addr)
				delete(retryByAddr, addr)
				StatusDel(addr)

				dev, ok := devByAddr[addr]
				if ok {
					dev.Close()
					delete(devByAddr, addr)
				}
			}

			// Handle devices, waiting for retry
			for addr, tm := range retryByAddr {
				if !pnpRetryExpired(tm) {
					continue
				}

				Log.Debug('+', "PNP %s: retry", addr)
				dev, err := NewDevice(devDescs[addr])
				port := 0
				if dev != nil {
					port = dev.State.HTTPPort
				}
				StatusSet(addr, devDescs[addr], port, err)

				if err == nil {
					devByAddr[addr] = dev
					delete(retryByAddr, addr)
				} else {
					Log.Error('!', "PNP %s: %s", addr, err)
					retryByAddr[addr] = pnpRetryTime(err)
				}
			}
		}

		// Handle exit when idle
		if exitWhenIdle && len(devices) == 0 {
			Log.Info(' ', "No IPP-over-USB devices present, exiting")
			return PnPIdle
		}

		// Update ticker
		switch {
		case tickerRunning && len(retryByAddr) == 0:
			ticker.Stop()
			tickerRunning = false
		case !tickerRunning && len(retryByAddr) != 0:
			ticker = time.NewTicker(DevInitRetryInterval / 4)
			tickerRunning = true
		}

		// Wait for the next event
		select {
		case <-UsbHotPlugChan:
		case <-ticker.C:
		case sig := <-sigChan:
			Log.Info(' ', "%s signal received, exiting", sig)
			break loop
		}
	}

	// Close remaining devices
	ctx, cancel := context.WithTimeout(context.Background(),
		DevShutdownTimeout)
	defer cancel()

	var done sync.WaitGroup

	for _, dev := range devByAddr {
		done.Add(1)
		go func(dev *Device) {
			dev.Shutdown(ctx)
			dev.Close()
			done.Done()
		}(dev)
	}

	done.Wait()
	return PnPTerm
}
07070100000034000081A400000000000000000000000167D72F5D0000379F000000000000000000000000000000000000001900000000ipp-usb-0.9.30/quirks.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Device-specific quirks
 */

package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
)

// Quirk represents a single quirk
type Quirk struct {
	Origin    string      // file:line of definition
	Match     string      // Match pattern
	Name      string      // Quirk name
	RawValue  string      // Quirk raw (not parsed) value
	Parsed    interface{} // Parsed Value
	LoadOrder int         // Incremented in order of loading
}

// Quirk names. Use these constants instead of literal strings,
// so compiler will catch a mistake:
const (
	QuirkNmBlacklist             = "blacklist"
	QuirkNmBuggyIppResponses     = "buggy-ipp-responses"
	QuirkNmDisableFax            = "disable-fax"
	QuirkNmIgnoreIppStatus       = "ignore-ipp-status"
	QuirkNmInitDelay             = "init-delay"
	QuirkNmInitReset             = "init-reset"
	QuirkNmInitRetryPartial      = "init-retry-partial"
	QuirkNmInitTimeout           = "init-timeout"
	QuirkNmRequestDelay          = "request-delay"
	QuirkNmUsbMaxInterfaces      = "usb-max-interfaces"
	QuirkNmUsbSendDelayThreshold = "usb-send-delay-threshold"
	QuirkNmUsbSendDelay          = "usb-send-delay"
	QuirkNmZlpRecvHack           = "zlp-recv-hack"
	QuirkNmZlpSend               = "zlp-send"
)

// quirkParse maps quirk names into appropriate parsing methods,
// which defines value syntax and resulting type.
var quirkParse = map[string]func(*Quirk) error{
	QuirkNmBlacklist:             (*Quirk).parseBool,
	QuirkNmBuggyIppResponses:     (*Quirk).parseQuirkBuggyIppRsp,
	QuirkNmDisableFax:            (*Quirk).parseBool,
	QuirkNmIgnoreIppStatus:       (*Quirk).parseBool,
	QuirkNmInitDelay:             (*Quirk).parseDuration,
	QuirkNmInitReset:             (*Quirk).parseQuirkResetMethod,
	QuirkNmInitRetryPartial:      (*Quirk).parseBool,
	QuirkNmInitTimeout:           (*Quirk).parseDuration,
	QuirkNmRequestDelay:          (*Quirk).parseDuration,
	QuirkNmUsbMaxInterfaces:      (*Quirk).parseUint,
	QuirkNmUsbSendDelayThreshold: (*Quirk).parseUint,
	QuirkNmUsbSendDelay:          (*Quirk).parseDuration,
	QuirkNmZlpRecvHack:           (*Quirk).parseBool,
	QuirkNmZlpSend:               (*Quirk).parseBool,
}

// quirkDefaultStrings contains default values for quirks, in
// a string form.
var quirkDefaultStrings = map[string]string{
	QuirkNmBlacklist:             "false",
	QuirkNmBuggyIppResponses:     "reject",
	QuirkNmDisableFax:            "false",
	QuirkNmIgnoreIppStatus:       "false",
	QuirkNmInitDelay:             "0",
	QuirkNmInitRetryPartial:      "false",
	QuirkNmInitReset:             "none",
	QuirkNmInitTimeout:           DevInitTimeout.String(),
	QuirkNmRequestDelay:          "0",
	QuirkNmUsbMaxInterfaces:      "0",
	QuirkNmUsbSendDelayThreshold: "0",
	QuirkNmUsbSendDelay:          "0",
	QuirkNmZlpRecvHack:           "false",
	QuirkNmZlpSend:               "false",
}

// quirkDefault contains default values for quirks, precompiled.
var quirkDefault = make(map[string]*Quirk)

// init populates quirkDefault using quirk values from quirkDefaultStrings.
func init() {
	for name, value := range quirkDefaultStrings {
		q := &Quirk{
			Origin:    "default",
			Match:     "*",
			Name:      name,
			RawValue:  value,
			LoadOrder: math.MaxInt32,
		}

		parse := quirkParse[name]
		err := parse(q)
		if err != nil {
			panic(err)
		}

		quirkDefault[name] = q
	}
}

// parseBool parses and saves [Quirk.RawValue] as bool.
func (q *Quirk) parseBool() error {
	switch q.RawValue {
	case "true":
		q.Parsed = true
	case "false":
		q.Parsed = false
	default:
		return fmt.Errorf("%q: must be true or false", q.RawValue)
	}

	return nil
}

// parseUind parses [Quirk.RawValue] as bool.
func (q *Quirk) parseUint() error {
	v, err := strconv.ParseUint(q.RawValue, 10, 32)
	if err != nil {
		return fmt.Errorf("%q: invalid unsigned integer", q.RawValue)
	}

	q.Parsed = uint(v)
	return nil
}

// parseDuration parses [Quirk.RawValue] as time.Duration.
func (q *Quirk) parseDuration() error {
	// Try to parse as uint. If OK, interpret it
	// as a millisecond time.
	ms, err := strconv.ParseUint(q.RawValue, 10, 32)
	if err == nil {
		q.Parsed = time.Millisecond * time.Duration(ms)
		return nil
	}

	// Try to use time.ParseDuration.
	//
	if strings.HasPrefix(q.RawValue, "+") ||
		strings.HasPrefix(q.RawValue, "-") {
		// Note, time.ParseDuration allows signed duration,
		// but we don't.
		return fmt.Errorf("%q: invalid duration", q.RawValue)
	}

	v, err := time.ParseDuration(q.RawValue)
	if err == nil && v >= 0 {
		q.Parsed = v
		return nil
	}

	return fmt.Errorf("%q: invalid duration", q.RawValue)
}

// parseQuirkBuggyIppRsp parses [Quirk.RawValue] as QuirkBuggyIppRsp.
func (q *Quirk) parseQuirkBuggyIppRsp() error {
	switch q.RawValue {
	case "allow":
		q.Parsed = QuirkBuggyIppRspAllow
	case "reject":
		q.Parsed = QuirkBuggyIppRspReject
	case "sanitize":
		q.Parsed = QuirkBuggyIppRspSanitize
	default:
		s := q.RawValue
		return fmt.Errorf("%q: must be allow, reject or sanitize", s)
	}

	return nil
}

// parseQuirkResetMethod parses [Quirk.RawValue] as QuirkResetMethod.
func (q *Quirk) parseQuirkResetMethod() error {
	switch q.RawValue {
	case "none":
		q.Parsed = QuirkResetNone
	case "soft":
		q.Parsed = QuirkResetSoft
	case "hard":
		q.Parsed = QuirkResetHard
	default:
		return fmt.Errorf("%q: must be none, soft or hard", q.RawValue)
	}

	return nil
}

// prioritize returns more prioritized Quirk, choosing between q and q2.
func (q *Quirk) prioritize(q2 *Quirk, model string) *Quirk {
	matchlen := GlobMatch(model, q.Match)
	matchlen2 := GlobMatch(model, q2.Match)

	switch {
	// Choose by match length (more specific match wins)
	case matchlen > matchlen2:
		return q
	case matchlen < matchlen2:
		return q2

	// Choose by load order (first loaded wins)
	case q.LoadOrder > q2.LoadOrder:
		return q
	}

	return q
}

// QuirkResetMethod represents how to reset a device
// during initialization
type QuirkResetMethod int

// QuirkResetUnset - reset method not specified
// QuirkResetNone  - don't reset device at all
// QuirkResetSoft  - use class-specific soft reset
// QuirkResetHard  - use USB hard reset
const (
	QuirkResetNone QuirkResetMethod = iota
	QuirkResetSoft
	QuirkResetHard
)

// String returns textual representation of QuirkResetMethod
func (m QuirkResetMethod) String() string {
	switch m {
	case QuirkResetNone:
		return "none"
	case QuirkResetSoft:
		return "soft"
	case QuirkResetHard:
		return "hard"
	}

	return fmt.Sprintf("unknown (%d)", int(m))
}

// QuirkBuggyIppRsp defines, how to handle buggy IPP responses
type QuirkBuggyIppRsp int

// QuirkBuggyIppRspReject   - ipp-usb will reject bad IPP responses
// QuirkBuggyIppRspAllow    - ipp-usb will allow bad IPP responses
// QuirkBuggyIppRspSanitize - bad ipp responses will be sanitized (fixed)
const (
	QuirkBuggyIppRspReject QuirkBuggyIppRsp = iota
	QuirkBuggyIppRspAllow
	QuirkBuggyIppRspSanitize
)

// String returns textual representation of QuirkBuggyIppRsp
func (m QuirkBuggyIppRsp) String() string {
	switch m {
	case QuirkBuggyIppRspReject:
		return "reject"
	case QuirkBuggyIppRspAllow:
		return "allow"
	case QuirkBuggyIppRspSanitize:
		return "sanitize"
	}

	return fmt.Sprintf("unknown (%d)", int(m))
}

// Quirks is the collection of Quirk-s.
type Quirks struct {
	byName      map[string]*Quirk // Quirks by name
	HTTPHeaders map[string]string // HTTP header override
}

// Get returns quirk by name.
func (quirks Quirks) Get(name string) *Quirk {
	q := quirks.byName[name]
	if q == nil {
		q = quirkDefault[name]
	}

	return q
}

// All returns all quirks in the collection. This method is
// intended mostly for diagnostic purposes (logging, dumping,
// testing and so on).
func (quirks Quirks) All() []*Quirk {
	qq := make([]*Quirk, 0, len(quirks.byName))
	for _, q := range quirks.byName {
		qq = append(qq, q)
	}

	sort.Slice(qq, func(i, j int) bool {
		return qq[i].Name < qq[j].Name
	})

	return qq
}

// GetBlacklist returns effective "blacklist" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetBlacklist() bool {
	return quirks.Get(QuirkNmBlacklist).Parsed.(bool)
}

// GetBuggyIppRsp returns effective "buggy-ipp-responses" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetBuggyIppRsp() QuirkBuggyIppRsp {
	return quirks.Get(QuirkNmBuggyIppResponses).Parsed.(QuirkBuggyIppRsp)
}

// GetDisableFax returns effective "disable-fax" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetDisableFax() bool {
	return quirks.Get(QuirkNmDisableFax).Parsed.(bool)
}

// GetIgnoreIppStatus returns effective "ignore-ipp-status" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetIgnoreIppStatus() bool {
	return quirks.Get(QuirkNmIgnoreIppStatus).Parsed.(bool)
}

// GetInitDelay returns effective "init-delay" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitDelay() time.Duration {
	return quirks.Get(QuirkNmInitDelay).Parsed.(time.Duration)
}

// GetInitRetryPartial returns effective "init-retry-partial" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitRetryPartial() bool {
	return quirks.Get(QuirkNmInitRetryPartial).Parsed.(bool)
}

// GetInitReset returns effective "init-reset" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitReset() QuirkResetMethod {
	return quirks.Get(QuirkNmInitReset).Parsed.(QuirkResetMethod)
}

// GetInitTimeout returns effective "init-timeout" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetInitTimeout() time.Duration {
	return quirks.Get(QuirkNmInitTimeout).Parsed.(time.Duration)
}

// GetRequestDelay returns effective "request-delay" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetRequestDelay() time.Duration {
	return quirks.Get(QuirkNmRequestDelay).Parsed.(time.Duration)
}

// GetUsbMaxInterfaces returns effective "usb-max-interfaces" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetUsbMaxInterfaces() uint {
	return quirks.Get(QuirkNmUsbMaxInterfaces).Parsed.(uint)
}

// GetUsbSendDelayThreshold returns effective "usb-send-delay-threshold"
// parameter taking the whole set into consideration.
func (quirks Quirks) GetUsbSendDelayThreshold() uint {
	return quirks.Get(QuirkNmUsbSendDelay).Parsed.(uint)
}

// GetUsbSendDelay returns effective "usb-send-delay" parameter
// taking the whole set into consideration.
func (quirks Quirks) GetUsbSendDelay() time.Duration {
	return quirks.Get(QuirkNmUsbSendDelay).Parsed.(time.Duration)
}

// GetZlpRecvHack returns effective "zlp-send" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetZlpRecvHack() bool {
	return quirks.Get(QuirkNmZlpRecvHack).Parsed.(bool)
}

// GetZlpSend returns effective "zlp-send" parameter,
// taking the whole set into consideration.
func (quirks Quirks) GetZlpSend() bool {
	return quirks.Get(QuirkNmZlpSend).Parsed.(bool)
}

// QuirksSet represents collection of quirks
type QuirksSet []*Quirks

// LoadQuirksSet creates new QuirksSet and loads its content from a directory
func LoadQuirksSet(paths ...string) (QuirksSet, error) {
	qset := QuirksSet{}

	for _, path := range paths {
		err := qset.readDir(path)
		if err != nil {
			return nil, err
		}
	}

	return qset, nil
}

// readDir loads all Quirks from a directory
func (qset *QuirksSet) readDir(path string) error {
	files, err := ioutil.ReadDir(path)
	if err != nil {
		if os.IsNotExist(err) {
			err = nil
		}
		return err
	}

	for _, file := range files {
		if file.Mode().IsRegular() &&
			strings.HasSuffix(file.Name(), ".conf") {
			err = qset.readFile(filepath.Join(path, file.Name()))
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// readFile reads all Quirks from a file
func (qset *QuirksSet) readFile(file string) error {
	// Open quirks file
	ini, err := OpenIniFileWithRecType(file)
	if err != nil {
		return err
	}

	defer ini.Close()

	// Load all quirks
	var quirks *Quirks
	var loadOrder int

	for err == nil {
		var rec *IniRecord
		rec, err = ini.Next()
		if err != nil {
			break
		}

		origin := fmt.Sprintf("%s:%d", rec.File, rec.Line)

		// Get Quirks structure
		if rec.Type == IniRecordSection {
			quirks = &Quirks{
				byName:      make(map[string]*Quirk),
				HTTPHeaders: make(map[string]string),
			}
			qset.Add(quirks)

			continue
		} else if quirks == nil {
			err = fmt.Errorf("%s: %q = %q out of any section",
				origin, rec.Key, rec.Value)
			break
		}

		if found := quirks.byName[rec.Key]; found != nil {
			err = fmt.Errorf("%s: %q already defined at %s",
				origin, rec.Key, found.Origin)
			return err
		}

		q := &Quirk{
			Origin:    origin,
			Match:     rec.Section,
			Name:      rec.Key,
			RawValue:  rec.Value,
			LoadOrder: loadOrder,
		}

		loadOrder++

		if strings.HasPrefix(rec.Key, "http-") {
			// Canonicalize HTTP header name
			q.Name = strings.ToLower(q.Name)
			q.Parsed = q.RawValue

			hdr := http.CanonicalHeaderKey(rec.Key[5:])
			quirks.HTTPHeaders[hdr] = q.RawValue
		} else {
			parse := quirkParse[rec.Key]
			if parse == nil {
				// Ignore unknown keys, it may be due to
				// downgrade of the ipp-usb
				continue
			}

			err := parse(q)
			if err != nil {
				err = fmt.Errorf("%s: %s", origin, err)
				return err
			}
		}

		quirks.byName[rec.Key] = q
	}

	if err == io.EOF {
		err = nil
	}

	return err
}

// Add appends Quirks to QuirksSet
func (qset *QuirksSet) Add(q *Quirks) {
	*qset = append(*qset, q)
}

// MatchByModelName returns collection of quirks, applicable for
// specific device, matched by model name.
func (qset QuirksSet) MatchByModelName(model string) Quirks {
	ret := Quirks{
		byName: make(map[string]*Quirk),
	}

	for _, quirks := range qset {
		for name, q := range quirks.byName {
			if GlobMatch(model, q.Match) >= 0 {
				q2 := ret.byName[name]
				if q2 != nil {
					q = q.prioritize(q2, model)
				}
				ret.byName[name] = q
			}
		}
	}

	return ret
}
07070100000035000081A400000000000000000000000167D72F5D00002286000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/quirks_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Tests for device-specific quirks
 */

package main

import (
	"reflect"
	"testing"
	"time"
)

// TestQuirksLookup tests lookup of various parameters
func TestQuirksLookup(t *testing.T) {
	const path = "testdata/quirks"

	// Load quirks
	qset, err := LoadQuirksSet(path)
	if err != nil {
		t.Fatalf("LoadQuirksSet(%q): %s", path, err)
	}

	// Test loaded values against expected
	type testData struct {
		model  string                   // Model name
		param  string                   // Parameter (quirk) name
		get    func(Quirks) interface{} // Lookup function
		match  string                   // Expected match
		value  interface{}              // Expected value
		origin string                   // Expected origin
	}

	tests := []testData{
		// Default values for unknown device
		{
			model: "Unknown Device",
			param: QuirkNmBlacklist,
			get: func(quirks Quirks) interface{} {
				return quirks.GetBlacklist()
			},
			match:  "*",
			value:  false,
			origin: "testdata/quirks/default.conf:4",
		},

		{
			model: "Unknown Device",
			param: QuirkNmBuggyIppResponses,
			get: func(quirks Quirks) interface{} {
				return quirks.GetBuggyIppRsp()
			},
			match:  "*",
			value:  QuirkBuggyIppRspReject,
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmDisableFax,
			get: func(quirks Quirks) interface{} {
				return quirks.GetDisableFax()
			},
			match:  "*",
			value:  false,
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmIgnoreIppStatus,
			get: func(quirks Quirks) interface{} {
				return quirks.GetIgnoreIppStatus()
			},
			match:  "*",
			value:  false,
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmInitDelay,
			get: func(quirks Quirks) interface{} {
				return quirks.GetInitDelay()
			},
			match:  "*",
			value:  time.Duration(0),
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmInitRetryPartial,
			get: func(quirks Quirks) interface{} {
				return quirks.GetInitRetryPartial()
			},
			match:  "*",
			value:  false,
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmInitReset,
			get: func(quirks Quirks) interface{} {
				return quirks.GetInitReset()
			},
			match:  "*",
			value:  QuirkResetNone,
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmInitTimeout,
			get: func(quirks Quirks) interface{} {
				return quirks.GetInitTimeout()
			},
			match:  "*",
			value:  DevInitTimeout,
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmRequestDelay,
			get: func(quirks Quirks) interface{} {
				return quirks.GetRequestDelay()
			},
			match:  "*",
			value:  time.Duration(0),
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmUsbMaxInterfaces,
			get: func(quirks Quirks) interface{} {
				return quirks.GetUsbMaxInterfaces()
			},
			match:  "*",
			value:  uint(0),
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmZlpRecvHack,
			get: func(quirks Quirks) interface{} {
				return quirks.GetZlpRecvHack()
			},
			match:  "*",
			value:  false,
			origin: "default",
		},

		{
			model: "Unknown Device",
			param: QuirkNmZlpSend,
			get: func(quirks Quirks) interface{} {
				return quirks.GetZlpSend()
			},
			match:  "*",
			value:  false,
			origin: "default",
		},

		// Quirks for some known devices
		{
			model: "HP ScanJet Pro 4500 fn1",
			param: QuirkNmUsbMaxInterfaces,
			get: func(quirks Quirks) interface{} {
				return quirks.GetUsbMaxInterfaces()
			},
			match:  "HP ScanJet Pro 4500 fn1",
			value:  uint(1),
			origin: "testdata/quirks/HP.conf:16",
		},

		{
			model: "HP ScanJet Pro 4500 fn1",
			param: QuirkNmRequestDelay,
			get: func(quirks Quirks) interface{} {
				return quirks.GetRequestDelay()
			},
			match:  "*",
			value:  time.Duration(0),
			origin: "default",
		},

		{
			// Here we test that more specific 'http-connection'
			// for the particular model overrides less specific
			// default value.
			model: "HP OfficeJet Pro 8730",
			param: "http-connection",
			get: func(quirks Quirks) interface{} {
				q := quirks.Get("http-connection")
				return q.Parsed
			},
			match:  "HP OfficeJet Pro 8730",
			value:  "close",
			origin: "testdata/quirks/HP.conf:7",
		},
	}

	for _, test := range tests {
		quirks := qset.MatchByModelName(test.model)
		q := quirks.Get(test.param)
		v := test.get(quirks)

		if !reflect.DeepEqual(v, test.value) {
			t.Errorf("model: %q, param: %q: value mismatch\n"+
				"expected: %s(%v)\n"+
				"present:  %s(%v)",
				test.model, test.param,
				reflect.TypeOf(test.value), test.value,
				reflect.TypeOf(v), v)
		}

		if q.Match != test.match {
			t.Errorf("model: %q, param: %q: match mismatch\n"+
				"expected: %q\n"+
				"present:  %q",
				test.model, test.param, test.match, q.Match)
		}

		if q.Origin != test.origin {
			t.Errorf("model: %q, param: %q: origin mismatch\n"+
				"expected: %q\n"+
				"present:  %q",
				test.model, test.param, test.origin, q.Origin)
		}
	}
}

// TestQuirksParsers tests parsers for quirks
func TestQuirksParsers(t *testing.T) {
	type testData struct {
		parser func(*Quirk) error // Parser to test
		input  string             // Input string
		value  interface{}        // Expected output value
		err    string             // Or expected error
	}

	tests := []testData{
		// parseBool
		{
			parser: (*Quirk).parseBool,
			input:  "true",
			value:  true,
		},

		{
			parser: (*Quirk).parseBool,
			input:  "false",
			value:  false,
		},

		{
			parser: (*Quirk).parseBool,
			input:  "invalid",
			err:    `"invalid": must be true or false`,
		},

		// parseQuirkBuggyIppRsp
		{
			parser: (*Quirk).parseQuirkBuggyIppRsp,
			input:  "allow",
			value:  QuirkBuggyIppRspAllow,
		},

		{
			parser: (*Quirk).parseQuirkBuggyIppRsp,
			input:  "reject",
			value:  QuirkBuggyIppRspReject,
		},

		{
			parser: (*Quirk).parseQuirkBuggyIppRsp,
			input:  "sanitize",
			value:  QuirkBuggyIppRspSanitize,
		},

		{
			parser: (*Quirk).parseQuirkBuggyIppRsp,
			input:  "invalid",
			err:    `"invalid": must be allow, reject or sanitize`,
		},

		// parseDuration
		{
			parser: (*Quirk).parseDuration,
			input:  "0",
			value:  time.Duration(0),
		},

		{
			parser: (*Quirk).parseDuration,
			input:  "0s",
			value:  time.Duration(0),
		},

		{
			parser: (*Quirk).parseDuration,
			input:  "12345",
			value:  12345 * time.Millisecond,
		},

		{
			parser: (*Quirk).parseDuration,
			input:  "1h2m3s",
			value: time.Hour +
				2*time.Minute +
				3*time.Second,
		},

		{
			parser: (*Quirk).parseDuration,
			input:  "0.5s",
			value:  time.Second / 2,
		},

		{
			parser: (*Quirk).parseDuration,
			input:  "+0s",
			err:    `"+0s": invalid duration`,
		},

		{
			parser: (*Quirk).parseDuration,
			input:  "-0s",
			err:    `"-0s": invalid duration`,
		},

		{
			parser: (*Quirk).parseDuration,
			input:  "hello",
			err:    `"hello": invalid duration`,
		},

		// parseQuirkResetMethod
		{
			parser: (*Quirk).parseQuirkResetMethod,
			input:  "none",
			value:  QuirkResetNone,
		},

		{
			parser: (*Quirk).parseQuirkResetMethod,
			input:  "soft",
			value:  QuirkResetSoft,
		},

		{
			parser: (*Quirk).parseQuirkResetMethod,
			input:  "hard",
			value:  QuirkResetHard,
		},

		{
			parser: (*Quirk).parseQuirkResetMethod,
			input:  "invalid",
			err:    `"invalid": must be none, soft or hard`,
		},

		// parseUint
		{
			parser: (*Quirk).parseUint,
			input:  "0",
			value:  uint(0),
		},

		{
			parser: (*Quirk).parseUint,
			input:  "12345",
			value:  uint(12345),
		},

		{
			parser: (*Quirk).parseUint,
			input:  "hello",
			err:    `"hello": invalid unsigned integer`,
		},
	}

	for _, test := range tests {
		q := Quirk{
			RawValue: test.input,
		}

		err := test.parser(&q)
		errstr := ""
		if err != nil {
			errstr = err.Error()
		}

		if errstr != test.err {
			t.Errorf("error mismatch:\n"+
				"expected: %s\n"+
				"present:  %s",
				test.err, errstr)

			continue
		}

		if q.Parsed != test.value {
			t.Errorf("value mismatch:\n"+
				"expected: %s(%v)\n"+
				"present:  %s(%v)",
				reflect.TypeOf(test.value), test.value,
				reflect.TypeOf(q.Parsed), q.Parsed)
		}
	}
}

// TestQuirksSetLoad tests LoadQuirksSet
func TestQuirksSetLoad(t *testing.T) {
	const path = "testdata/quirks"
	const badPath = path + "-not-exist"

	// Try non-existent directory
	_, err := LoadQuirksSet(badPath)
	if err != nil {
		t.Fatalf("LoadQuirksSet(%q): %s", badPath, err)
	}

	// Try test data
	_, err = LoadQuirksSet(path)
	if err != nil {
		t.Fatalf("LoadQuirksSet(%q): %s", path, err)
	}
}
07070100000036000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001400000000ipp-usb-0.9.30/rock07070100000037000081A400000000000000000000000167D72F5D00000E1E000000000000000000000000000000000000002300000000ipp-usb-0.9.30/rock/rockcraft.yamlname: ipp-usb
base: ubuntu@24.04
version: "latest"
summary: IPP-over-USB - Driverless IPP printing on USB-connected printers
description: |
  ipp-usb is a daemon that enables driverless IPP printing on USB-connected
  printers. It emulates an IPP network printer, providing full access to the
  physical printer: Printing, scanning, fax out, and the admin web interface.

license: Apache-2.0
adopt-info: ipp-usb

platforms:
  amd64:
  arm64:
  armhf:

services:
  dbus:
    command: /scripts/run-dbus.sh
    override: replace
    on-failure: restart
    startup: enabled

  ipp-usb:
    command: /scripts/run-ipp-usb.sh
    startup: enabled
    override: replace
    on-failure: restart
    after: [dbus]

parts:
  goipp:
    plugin: go
    source: https://github.com/OpenPrinting/goipp.git
    source-type: git
    source-tag: v1.1.0
    source-depth: 1
# ext:updatesnap
#   version-format:
#     lower-than: '2'
#     no-9x-revisions: true
    build-packages:
      - golang-go
    override-prime: ""

  ipp-usb:
    plugin: go
    source: https://github.com/OpenPrinting/ipp-usb.git
    source-type: git
    source-tag: 0.9.29
    source-depth: 1
# ext:updatesnap
#   version-format:
#     lower-than: '1'
#     no-9x-revisions: true
    override-build: |
      set -eux
      craftctl default
      mkdir -p ${CRAFT_PART_INSTALL}/usr/sbin
      mv ${CRAFT_PART_INSTALL}/bin/ipp-usb ${CRAFT_PART_INSTALL}/usr/sbin/
      mkdir -p ${CRAFT_PART_INSTALL}/etc
      cp ipp-usb.conf ${CRAFT_PART_INSTALL}/etc
      mkdir -p ${CRAFT_PART_INSTALL}/usr/share/ipp-usb/quirks
      cp ipp-usb-quirks/* ${CRAFT_PART_INSTALL}/usr/share/ipp-usb/quirks/
    build-packages:
      - golang-go
      - libavahi-client-dev
      - libavahi-common-dev
      - libavahi-compat-libdnssd-dev
      - libdbus-1-dev
      - ronn
    stage-packages:
      - libavahi-client3
      - libavahi-common3
    prime:
      - etc
      - -etc/init.d
      - usr/sbin
      - -usr/sbin/systemd-hwdb
      - usr/lib
      - usr/share/ipp-usb
    after: [goipp, libusb]

  libusb:
    plugin: autotools
    source: https://github.com/libusb/libusb.git
    source-type: git
    source-tag: 'v1.0.27'  
    source-depth: 1
# ext:updatesnap
#   version-format:
#     lower-than: '2'
#     no-9x-revisions: true
    autotools-configure-parameters:
      - --disable-udev
      - --prefix=/usr    
    build-packages:
      - build-essential
      - autoconf
      - automake
      - libtool
      - pkg-config
      - git
    stage:
      - usr/lib/*
      - usr/include/*
    prime:
      - usr/lib/*
      - usr/include/*
    override-build: |
      set -e
      craftctl default
      make -j$CRAFT_PARALLEL_BUILD_COUNT
      make install DESTDIR=$CRAFT_PART_INSTALL

  avahi-daemon:
    plugin: nil
    build-packages: 
      - avahi-daemon
    overlay-packages:
      - avahi-utils
      - libnss-mdns
      - mdns-scan
      - dbus
      - libavahi-client3
      - libavahi-common3
    override-build: |
      craftctl default
      mkdir -p "${CRAFT_PART_INSTALL}/usr/share/dbus-1/"
      cp -r /usr/share/dbus-1/* "${CRAFT_PART_INSTALL}/usr/share/dbus-1/"

  scripts:
    plugin: dump
    source: scripts/
    organize:
      run-ipp-usb.sh: scripts/run-ipp-usb.sh
      run-dbus.sh: scripts/run-dbus.sh
    override-prime: |
      set -eux
      craftctl default
      if [ -f "$CRAFT_PRIME/scripts/run-ipp-usb.sh" ]; then
        chmod +x "$CRAFT_PRIME/scripts/run-ipp-usb.sh"
      fi
      if [ -f "$CRAFT_PRIME/scripts/run-dbus.sh" ]; then
        chmod +x "$CRAFT_PRIME/scripts/run-dbus.sh"
      fi
    after: [ipp-usb, avahi-daemon]
07070100000038000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/rock/scripts07070100000039000081A400000000000000000000000167D72F5D00000459000000000000000000000000000000000000002800000000ipp-usb-0.9.30/rock/scripts/run-dbus.sh#!/bin/sh
set -eux

echo "Creating system users..."

# Check if users exist before attempting to create them
if ! id -u systemd-resolve >/dev/null 2>&1; then
    useradd --system --no-create-home --shell /usr/sbin/nologin systemd-resolve
fi

if ! id -u systemd-network >/dev/null 2>&1; then
    useradd --system --no-create-home --shell /usr/sbin/nologin systemd-network
fi

echo "Ensuring necessary directories exist..."

# Ensure /run/dbus exists with correct permissions
if [ ! -d /run/dbus ]; then
    mkdir -p /run/dbus
    chmod 755 /run/dbus
    chown root:root /run/dbus
fi

echo "Starting dbus service..."

# Start dbus and verify it's running
service dbus start
if ! pgrep -x "dbus-daemon" >/dev/null; then
    echo "Failed to start dbus-daemon!" >&2
    exit 1
fi

echo "Starting avahi-daemon..."

# Start avahi-daemon and ensure it's running
avahi-daemon --daemonize --no-drop-root
if ! pgrep -x "avahi-daemon" >/dev/null; then
    echo "Failed to start avahi-daemon!" >&2
    exit 1
fi

echo "Services started successfully."

# Keep the container alive using a foreground process
exec sleep infinity
0707010000003A000081A400000000000000000000000167D72F5D0000023A000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/rock/scripts/run-ipp-usb.sh#!/bin/sh

#set -e -x

# Create needed directories (ignore errors)
mkdir -p /etc/ipp-usb || :
mkdir -p /var/log/ipp-usb || :
mkdir -p /var/lock || :
mkdir -p /var/dev || :
mkdir -p /usr/share/ipp-usb/quirks || :

# Put config files in place (do not overwrite existing user config)
yes no | cp -i /usr/share/ipp-usb/quirks/* /etc/ipp-usb/quirks >/dev/null 2>&1 || :
if [ ! -f /etc/ipp-usb/ipp-usb.conf ]; then
    cp /usr/share/ipp-usb/ipp-usb.conf /etc/ipp-usb/ >/dev/null 2>&1 || :
fi

# Run ipp-usb with the provided command-line arguments
exec /usr/sbin/ipp-usb "$@"
0707010000003B000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001400000000ipp-usb-0.9.30/snap0707010000003C000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001A00000000ipp-usb-0.9.30/snap/local0707010000003D000081ED00000000000000000000000167D72F5D000002E7000000000000000000000000000000000000002600000000ipp-usb-0.9.30/snap/local/run-ipp-usb#!/bin/sh

#set -e -x

# Create needed directories
# Ignore errors
mkdir -p $SNAP_COMMON/etc || :
mkdir -p $SNAP_COMMON/var/log || :
mkdir -p $SNAP_COMMON/var/lock || :
mkdir -p $SNAP_COMMON/var/dev || :
mkdir -p $SNAP_COMMON/quirks || :

# Put config files in place
#
# Do not overwrite files and ignore errors, to not reset user configuration
# when running as root and to not have ugly error messages when running as
# non-root.
yes no | cp -i $SNAP/usr/share/ipp-usb/quirks/* $SNAP_COMMON/quirks >/dev/null 2>&1 || :
if [ ! -f $SNAP_COMMON/etc/ipp-usb.conf ]; then
    cp $SNAP/etc/ipp-usb.conf $SNAP_COMMON/etc/ >/dev/null 2>&1 || :
fi

# Run ipp-usb with the command line arguments with which we were called
exec $SNAP/sbin/ipp-usb "$@"
0707010000003E000081ED00000000000000000000000167D72F5D000006AD000000000000000000000000000000000000002D00000000ipp-usb-0.9.30/snap/local/run-ipp-usb-server#!/bin/sh

#set -e -x

# Create needed directories
mkdir -p $SNAP_COMMON/etc
mkdir -p $SNAP_COMMON/var/log
mkdir -p $SNAP_COMMON/var/lock
mkdir -p $SNAP_COMMON/var/dev
mkdir -p $SNAP_COMMON/quirks

# Put config files in place
cp $SNAP/usr/share/ipp-usb/quirks/* $SNAP_COMMON/quirks
if [ ! -f $SNAP_COMMON/etc/ipp-usb.conf ]; then
    cp $SNAP/etc/ipp-usb.conf $SNAP_COMMON/etc/
fi

# Monitor appearing/disappearing of USB devices
udevadm monitor -k -s usb | while read START OP DEV REST; do
    START_IPP_USB=0
    if test "$START" = "KERNEL"; then
        # First lines of "udevadm monitor" output, check for already plugged
	# devices. Consider only IPP-over-USB devices (interface 7/1/4)
        if [ `udevadm trigger -v -n --subsystem-match=usb --property-match=ID_USB_INTERFACES='*:070104:*' | wc -l` -gt 0 ]; then
	    # IPP-over-USB device already connected
	    START_IPP_USB=1
	fi
    elif test "$OP" = "add"; then
        # New device got added
        if [ -z $DEV ]; then
	    # Missing device path
	    continue
	else
	    # Does the device support IPP-over-USB (interface 7/1/4)?
	    # Retry 5 times as sometimes the ID_USB_INTERFACES property is not
	    # immediately set
            for i in 1 2 3 4 5; do
	        # Give some time for ID_USB_INTERFACES property to appear
	        sleep 0.02
		# Check ID_USB_INTERFACE for 7/1/4 interface
                if udevadm info -q property -p $DEV | grep -q ID_USB_INTERFACES=.*:070104:.*; then
		    # IPP-over-USB device got connected now
       	            START_IPP_USB=1
	            break
                fi
            done
	fi
    fi
    if [ $START_IPP_USB = 1 ]; then
        # Start ipp-usb
        $SNAP/sbin/ipp-usb udev
    fi
done
0707010000003F000081A400000000000000000000000167D72F5D00000B6C000000000000000000000000000000000000002300000000ipp-usb-0.9.30/snap/snapcraft.yamlname: ipp-usb
base: core22
version: git
summary: IPP-over-USB - Driverless IPP printing on USB-connected printers
description: |
  ipp-usb is a daemon to allow driverless IPP printing on USB-connected
  printers.It emulates an IPP network printer on the local machine, giving
  full access to the physical printer: Printing, scanning, fax out, and
  the admin web interface.

grade: stable
confinement: strict
adopt-info: ipp-usb
# Only build on the architectures supported
architectures:
  - build-on: amd64
  - build-on: arm64
  - build-on: armhf

apps:
  ipp-usb-server:
    command: scripts/run-ipp-usb-server
    daemon: simple
    plugs: [avahi-control, network, network-bind, raw-usb, hardware-observe]
  ipp-usb:
    command: scripts/run-ipp-usb
    plugs: [raw-usb, hardware-observe]

parts:
  goipp:
    plugin: go
    source: https://github.com/OpenPrinting/goipp.git
    source-type: git
    source-tag: v1.1.0
    source-depth: 1
# ext:updatesnap
#   version-format:
#     lower-than: '2'
#     no-9x-revisions: true
    build-packages:
      - golang-go
    override-prime: ""

  ipp-usb:
    plugin: go
    source: .
    source-type: git
    source-tag: 0.9.29
    source-depth: 1
# ext:updatesnap
#   version-format:
#     lower-than: '1'
#     no-9x-revisions: true
    override-build: |
      set -eux
      # Correct hard-coded paths in paths.go
      # Not only the config file ipp-usb.conf will be put into a user-editable
      # space but also the quirks file, so that the user can add and debug
      # quirks
      perl -p -i -e 's:/etc/:/var/snap/ipp-usb/common/etc/:' paths.go
      perl -p -i -e 's:/var/ipp-usb:/var/snap/ipp-usb/common/var:' paths.go
      perl -p -i -e 's:/usr/share/ipp-usb/quirks:/var/snap/hplip-printer-app/common/quirks:' paths.go
      perl -p -i -e 's:/var/log/ipp-usb:/var/snap/ipp-usb/common/var/log:' paths.go
      # Build the executable
      craftctl default
      # Place the executable in /sbin, it's a system daemon
      mv ../install/bin ../install/sbin
      # Install the config file and the quirk files
      mkdir -p ../install/etc
      cp ipp-usb.conf ../install/etc
      mkdir -p ../install/usr/share/ipp-usb/quirks
      cp ipp-usb-quirks/* ../install/usr/share/ipp-usb/quirks
    build-packages:
      - golang-go
      - libavahi-client-dev
      - libavahi-common-dev
      - libusb-1.0-0-dev
      - ronn
      - perl-base
    stage-packages:
      - libavahi-client3
      - libavahi-common3
      - libusb-1.0-0
      - udev
    prime:
      - etc
      - -etc/init.d
      - -etc/udev
      - sbin
      - -sbin/systemd-hwdb
      - lib
      - -lib/modprobe.d
      - -lib/systemd
      - -lib/udev      
      - usr/lib
      - -usr/lib/tmpfiles.d
      - usr/share/ipp-usb
    after: [goipp]

  scripts:
    plugin: dump
    source: snap/local/
    organize:
      run-ipp-usb*: scripts/
    prime:
      - scripts/
    after: [ipp-usb]
    07070100000040000081A400000000000000000000000167D72F5D00000BAD000000000000000000000000000000000000001900000000ipp-usb-0.9.30/status.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * ipp-usb status support
 */

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"sort"
	"strconv"
	"sync"
)

// statusOfDevice represents a status of the particular device
type statusOfDevice struct {
	desc     UsbDeviceDesc // Device descriptor
	init     error         // Initialization error, nil if none
	HTTPPort int           // Assigned http port for the device
}

var (
	// statusTable maintains a per-device status,
	// indexed by the UsbAddr
	statusTable = make(map[UsbAddr]*statusOfDevice)

	// statusLock protects access to the statusTable
	statusLock sync.RWMutex
)

// StatusRetrieve connects to the running ipp-usb daemon, retrieves
// its status and returns retrieved status as a printable text
func StatusRetrieve() ([]byte, error) {
	t := &http.Transport{
		Dial: func(network, addr string) (net.Conn, error) {
			return CtrlsockDial()
		},
	}

	c := &http.Client{
		Transport: t,
	}

	rsp, err := c.Get("http://localhost/status")
	if err != nil {
		return nil, err
	}

	defer rsp.Body.Close()

	return ioutil.ReadAll(rsp.Body)
}

// StatusFormat formats ipp-usb status as a text
func StatusFormat() []byte {
	buf := &bytes.Buffer{}

	// Lock the statusTable
	statusLock.RLock()
	defer statusLock.RUnlock()

	// Dump ipp-usb daemon status. If we are here, we are
	// definitely running :-)
	buf.WriteString("ipp-usb daemon: running\n")

	// Sort devices by address
	devs := make([]*statusOfDevice, len(statusTable))

	i := 0
	for _, status := range statusTable {
		devs[i] = status
		i++
	}

	sort.Slice(devs, func(i, j int) bool {
		return devs[i].desc.UsbAddr.Less(devs[j].desc.UsbAddr)
	})

	// Format per-device status
	buf.WriteString("ipp-usb devices:")
	if len(statusTable) == 0 {
		buf.WriteString(" not found\n")
	} else {
		buf.WriteString("\n")
		fmt.Fprintf(buf, " Num  Device              Vndr:Prod  Port  Model\n")
		for i, status := range devs {
			info, _ := status.desc.GetUsbDeviceInfo()

			s := "-"
			if status.HTTPPort != 0 {
				s = strconv.Itoa(status.HTTPPort)
			}

			fmt.Fprintf(buf, " %3d. %s  %4.4x:%.4x  %-5s %q\n",
				i+1, status.desc.UsbAddr,
				info.Vendor, info.Product, s,
				info.MfgAndProduct)

			s = "OK"
			if status.init != nil {
				s = devs[i].init.Error()
			}

			fmt.Fprintf(buf, "      status: %s\n", s)
		}
	}

	return buf.Bytes()
}

// StatusSet adds device to the status table or updates status
// of the already known device
func StatusSet(addr UsbAddr, desc UsbDeviceDesc, HTTPPort int, init error) {
	statusLock.Lock()
	statusTable[addr] = &statusOfDevice{
		desc:     desc,
		init:     init,
		HTTPPort: HTTPPort,
	}
	statusLock.Unlock()
}

// StatusDel deletes device from the status table
func StatusDel(addr UsbAddr) {
	statusLock.Lock()
	delete(statusTable, addr)
	statusLock.Unlock()
}
07070100000041000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/systemd-udev07070100000042000081A400000000000000000000000167D72F5D00000275000000000000000000000000000000000000002D00000000ipp-usb-0.9.30/systemd-udev/71-ipp-usb.rules# Standard IPP over USB devices, with Class/SubClass/Protocol = 7/1/4
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:070104:*", OWNER="root", GROUP="lp", MODE="0664", TAG+="systemd", ENV{SYSTEMD_WANTS}+="ipp-usb.service"

# Non-standard HP devices with 255/9/1 combination
# Tested with following devices:
#   HP LaserJet MFP M426fdn
#   HP ColorLaserJet MFP M278-M281
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_VENDOR_ID}=="03f0", ENV{ID_USB_INTERFACES}=="*:ff0901:*", OWNER="root", GROUP="lp", MODE="0664", TAG+="systemd", ENV{SYSTEMD_WANTS}+="ipp-usb.service"
07070100000043000081A400000000000000000000000167D72F5D000000CF000000000000000000000000000000000000002C00000000ipp-usb-0.9.30/systemd-udev/ipp-usb.service[Unit]
Description=Daemon for IPP over USB printer support
Documentation=man:ipp-usb(8)
After=cups.service avahi-daemon.service
Wants=avahi-daemon.service

[Service]
Type=simple
ExecStart=/sbin/ipp-usb udev
07070100000044000081A400000000000000000000000167D72F5D00001083000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/tcpuid_linux.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * UID discovery for TCP connection over loopback -- Linux version
 */

package main

import (
	"encoding/binary"
	"fmt"
	"net"
	"syscall"
	"unsafe"
)

// #include <linux/inet_diag.h>
// #include <linux/in.h>
// #include <linux/netlink.h>
// #include <linux/sock_diag.h>
// #include <netinet/tcp.h>
// #include <stdint.h>
// #include <sys/socket.h>
//
// typedef struct inet_diag_req_v2 inet_diag_req_v2_struct;
// typedef struct inet_diag_sockid inet_diag_sockid_struct;
// typedef struct nlmsgerr         nlmsgerr_struct;
// typedef struct inet_diag_msg    inet_diag_msg_struct;
//
// typedef struct {
//     struct nlmsghdr         hdr;
//     struct inet_diag_req_v2 data;
// } sock_diag_request;
//
import "C"

// TCPClientUIDSupported tells if TCPClientUID supported on this platform
func TCPClientUIDSupported() bool {
	return true
}

// TCPClientUID obtains UID of client process that created
// TCP connection over the loopback interface
func TCPClientUID(client, server *net.TCPAddr) (int, error) {
	// Obtain protocol family. Check for mismatch.
	clientIs4 := client.IP.To4() != nil
	serverIs4 := server.IP.To4() != nil

	if clientIs4 != serverIs4 {
		return -1, fmt.Errorf("TCPClientUID: IP4/IP6 mismatchh")
	}

	// Open NETLINK_SOCK_DIAG socket
	sock, err := sockDiagOpen()
	if err != nil {
		return -1, err
	}

	defer syscall.Close(sock)

	// Prepare request
	rq := C.sock_diag_request{}

	rq.hdr.nlmsg_len = C.uint32_t(unsafe.Sizeof(rq))
	rq.hdr.nlmsg_type = C.uint16_t(C.SOCK_DIAG_BY_FAMILY)
	rq.hdr.nlmsg_flags = C.uint16_t(C.NLM_F_REQUEST)

	if clientIs4 {
		rq.data.sdiag_family = C.AF_INET
		copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_src))[:],
			client.IP.To4())
		copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_dst))[:],
			server.IP.To4())
	} else {
		rq.data.sdiag_family = C.AF_INET6
		copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_src))[:],
			client.IP.To16())
		copy((*[16]byte)(unsafe.Pointer(&rq.data.id.idiag_dst))[:],
			server.IP.To16())
	}

	rq.data.sdiag_protocol = C.IPPROTO_TCP
	rq.data.idiag_states = 1 << C.TCP_ESTABLISHED
	rq.data.id.idiag_sport = C.uint16_t(toBE16((uint16(client.Port))))
	rq.data.id.idiag_dport = C.uint16_t(toBE16((uint16(server.Port))))
	rq.data.id.idiag_cookie[0] = C.INET_DIAG_NOCOOKIE
	rq.data.id.idiag_cookie[1] = C.INET_DIAG_NOCOOKIE

	// Send request
	rqData := (*[unsafe.Sizeof(rq)]byte)(unsafe.Pointer(&rq))
	rqAddr := &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK}
	err = syscall.Sendto(sock, rqData[:], 0, rqAddr)
	if err != nil {
		return -1, fmt.Errorf("sock_diag: sendto(): %s", err)
	}

	// Receive responses
	buf := make([]byte, syscall.Getpagesize())
	for {
		num, _, err := syscall.Recvfrom(sock, buf, 0)
		if err != nil {
			return -1, fmt.Errorf("sock_diag: recvfrom(): %s", err)
		}

		msgs, err := syscall.ParseNetlinkMessage(buf[:num])
		if err != nil {
			return -1, fmt.Errorf("sock_diag: can't parse response")
		}

		for _, msg := range msgs {
			data := unsafe.Pointer(&msg.Data[0])
			switch msg.Header.Type {
			case syscall.NLMSG_ERROR:
				rsp := (*C.nlmsgerr_struct)(data)
				err = syscall.Errno(-rsp.error)
				err = fmt.Errorf("NLMSG_ERROR: %s", err)
				return -1, err

			case uint16(C.SOCK_DIAG_BY_FAMILY):
				rsp := (*C.inet_diag_msg_struct)(data)
				return int(rsp.idiag_uid), nil
			}
		}
	}
}

// sockDiagOpen opens NETLINK_SOCK_DIAG socket
func sockDiagOpen() (int, error) {
	const stype = syscall.SOCK_DGRAM | syscall.SOCK_CLOEXEC
	const proto = int(C.NETLINK_SOCK_DIAG)

	sock, err := syscall.Socket(syscall.AF_NETLINK, stype, proto)
	if err != nil {
		return -1, fmt.Errorf("sock_diag: socket(): %s", err)
	}

	sa := &syscall.SockaddrNetlink{Family: syscall.AF_NETLINK}
	err = syscall.Bind(sock, sa)
	if err != nil {
		syscall.Close(sock)
		return -1, fmt.Errorf("sock_diag: bind(): %s", err)
	}

	return sock, nil
}

// toBE16 converts uint16 to big endian
func toBE16(in uint16) uint16 {
	var out uint16
	p := (*[2]byte)(unsafe.Pointer(&out))
	binary.BigEndian.PutUint16(p[:], in)
	return out
}
07070100000045000081A400000000000000000000000167D72F5D000003D2000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/tcpuid_other.go// +build !linux

/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * UID discovery for TCP connection over loopback -- default version
 *
 * If you've have added support for yet another platform, please don't
 * forget to update build tag at the top of this file to exclude your
 * platform
 */

package main

import (
	"net"
)

// TCPClientUIDSupported tells if TCPClientUID supported on this platform
//
// If this function returns false, TCPClientUID should never be called
func TCPClientUIDSupported() bool {
	return false
}

// TCPClientUID obtains UID of client process that created
// TCP connection over the loopback interface
func TCPClientUID(client, server *net.TCPAddr) (int, error) {
	// Note, TCPClientUID should never be called, if
	// TCPClientUIDSupported returns false
	panic("TCPClientUID not supported")
}
07070100000046000081A400000000000000000000000167D72F5D0000098D000000000000000000000000000000000000001E00000000ipp-usb-0.9.30/tcpuid_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Tests for TCPClientUID
 */

package main

import (
	"net"
	"os"
	"testing"
)

// doTestTCPClientUID performs TCPClientUID for the specified
// network and loopback address
func doTestTCPClientUID(t *testing.T, ip4 bool) {
	// Do nothing if TCPClientUID is not supported by the platform
	if !TCPClientUIDSupported() {
		return
	}

	// Log local addresses. Check that we have appropriate
	// address family support, configured in the system.
	var haveIP4, haveIP6 bool

	if ift, err := net.Interfaces(); err == nil {
		for _, ifi := range ift {
			if addrs, err := ifi.Addrs(); err == nil {
				t.Logf("%s:", ifi.Name)
				for _, addr := range addrs {
					t.Logf("  %s", addr)

					if ipnet, ok := addr.(*net.IPNet); ok {
						if ipnet.IP.To4() != nil {
							haveIP4 = true
						} else {
							haveIP6 = true
						}
					}
				}
			}
		}
	}

	// Skip incompatible address families
	if ip4 && !haveIP4 {
		return
	}

	if !ip4 && !haveIP6 {
		return
	}

	// Create loopback listener -- it gives us a port
	network := "tcp4"
	loopback := "127.0.0.1"
	if !ip4 {
		loopback = "[::1]"
		network = "tcp6"
	}

	l, err := net.Listen(network, loopback+":")
	if err != nil {
		t.Fatalf("net.Listen(%q,%q): %s", network, loopback+":", err)
	}

	defer l.Close()

	// Dial client connection
	addr := l.Addr()
	clnt, err := net.Dial("tcp", addr.String())
	if err != nil {
		t.Fatalf("net.Dial(%q,%q): %s", network, addr, err)
	}

	defer clnt.Close()

	// Accept server connection
	srv, err := l.Accept()
	if err != nil {
		t.Fatalf("net.Accept(%q,%q): %s", network, addr, err)
	}

	defer srv.Close()

	// Get and check Client UID
	uid, err := TCPClientUID(clnt.LocalAddr().(*net.TCPAddr),
		srv.LocalAddr().(*net.TCPAddr))

	if err != nil {
		t.Fatalf("TCPClientUID(%q,%q): %s",
			clnt.LocalAddr(), srv.LocalAddr(), err)
	}

	if uid != os.Getuid() {
		t.Fatalf("TCPClientUID(%q,%q): uid mismatch (expected %d, present %d)",
			clnt.LocalAddr(), srv.LocalAddr(), os.Getuid(), uid)
	}
}

// TestTCPClientUIDIp4 performs TCPClientUID test for IPv4
func TestTCPClientUIDIp4(t *testing.T) {
	doTestTCPClientUID(t, true)
}

// TestTCPClientUIDIp6 performs TCPClientUID test for IPv6
func TestTCPClientUIDIp6(t *testing.T) {
	doTestTCPClientUID(t, false)
}
07070100000047000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001800000000ipp-usb-0.9.30/testdata07070100000048000081A400000000000000000000000167D72F5D00000660000000000000000000000000000000000000002500000000ipp-usb-0.9.30/testdata/ipp-usb.conf# ipp-usb.conf: example configuration file

# Networking parameters
[network]
  # TCP ports for HTTP will be automatically allocated in the following range
  http-min-port = 60000
  http-max-port = 65535

  # Enable or disable DNS-SD advertisement
  dns-sd = enable      # enable | disable

  # Network interface to use. Set to `all` if you want to expose you
  # printer to the local network. This way you can share your printer
  # with other computers in the network, as well as with iOS and Android
  # devices.
  interface = loopback # all | loopback

  # Enable or disable IPv6
  ipv6 = enable        # enable | disable

# Logging configuration
[logging]
  # device-log  - per-device log levels
  # main-log    - main log levels
  # console-log - console log levels
  #
  # parameter contains a comma-separated list of
  # the following keywords:
  #   error     - error messages
  #   info      - informative messages
  #   debug     - debug messages
  #   trace-ipp, trace-escl, trace-http - very detailed per-protocol traces
  #   all       - all logs
  #   trace-all - alias to all
  #
  # Note, trace-* implies debug, debug implies info, info implies error
  device-log    = all
  main-log      = debug
  console-log   = debug

  # Log rotation parameters:
  #   max-file-size    - max log file before rotation. Use suffix M
  #                      for megabytes or K for kilobytes
  #   max-backup-files - how many backup files to preserve during rotation
  #
  max-file-size    = 256K
  max-backup-files = 5

  # Enable or disable ANSI colors on console
  console-color = enable # enable | disable

# vim:ts=8:sw=2:et
07070100000049000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/testdata/quirks0707010000004A000081A400000000000000000000000167D72F5D0000012A000000000000000000000000000000000000002A00000000ipp-usb-0.9.30/testdata/quirks/Canon.conf# ipp-usb quirks file -- quirks for Canon devices

# This device responds to the Get-Printer-Attributes request with the
# server-error-internal-error status, but otherwise works correctly
#
# So we just ignore its returned IPP status as workaround
[Canon SELPHY CP1500]
  ignore-ipp-status = true
0707010000004B000081A400000000000000000000000167D72F5D00000496000000000000000000000000000000000000002700000000ipp-usb-0.9.30/testdata/quirks/HP.conf# ipp-usb quirks file -- quirks for HP devices

[HP LaserJet MFP M28-M31]
  http-connection = keep-alive

[HP OfficeJet Pro 8730]
  http-connection = close

# eSCL requests hangs on this device, if both USB interfaces are
# in use. Limiting number of interfaces to 1 makes scanning
# reliable in a cost of making scan cancellation impossible,
# as there is no second interface to send cancel request.
# (ADF scans still can be canceled between retrieval of
# subsequent pages).
[HP ScanJet Pro 4500 fn1]
  usb-max-interfaces = 1

# HP Photosmart 6520 series doesn't implement true faxing,
# but instead implements internet-based eFax,
# which makes no sense when connected via USB
# so can be safely disabled for this kind of devices.
[HP Photosmart 6520 series]
  disable-fax = true

# This device sometimes hangs when probing for fax support
# See long conversation here for details:
#   https://github.com/OpenPrinting/ipp-usb/issues/48
[HP ENVY 5530 series]
  disable-fax = true

# This device fails to initialize. This quirk helps.
#
# See le following link for details:
#   https://github.com/OpenPrinting/ipp-usb/issues/75
[HP OfficeJet Pro 8710]
  init-reset = soft
0707010000004C000081A400000000000000000000000167D72F5D000002AE000000000000000000000000000000000000002B00000000ipp-usb-0.9.30/testdata/quirks/Pantum.conf# ipp-usb quirks file -- quirks for Pantum devices

# Some Pantum devices (Pantum M7300FDW known to have this bug)
# encode IPP messages improperly.
#
# With this option, ipp-usb will recode IPP responses, so that
# CUPS will accept it.
#
# Note, it still doesn't solve compatibility issues, if device
# is connected over network, not over USB. Either CUPS patch is
# required or user needs to install Pantum proprietary driver
[Pantum*]
  buggy-ipp-responses = sanitize

# This device pretends it has a fax, but actually fax unit is missed.
# Attempt to query it's printer-attributes sometimes times out, so
# it is better to disable it.
[Pantum BM5100ADN series]
  disable-fax = true
0707010000004D000081A400000000000000000000000167D72F5D00000114000000000000000000000000000000000000002E00000000ipp-usb-0.9.30/testdata/quirks/blacklist.conf# ipp-usb quirks file -- blacklisted devices

# This device has IPP-over-USB interfaces, but responds HTTP 404 Not found
# status to all requests
[HP Inc. HP Laser MFP 135a]
  blacklist = true

# And this device has the same problem
[HP Inc. HP Laser 107a]
  blacklist = true
0707010000004E000081A400000000000000000000000167D72F5D0000007A000000000000000000000000000000000000002C00000000ipp-usb-0.9.30/testdata/quirks/default.conf# ipp-usb quirks file -- defaults

[*]
  blacklist = false

  # Drop Connection: header by default
  http-connection = ""
0707010000004F000081A400000000000000000000000167D72F5D0000011F000000000000000000000000000000000000002800000000ipp-usb-0.9.30/testdata/quirks/pzz.conf# ipp-usb quirks file -- quirks for Canon devices

# This device responds to the Get-Printer-Attributes request with the
# server-error-internal-error status, but otherwise works correctly
#
# So we just ignore its returned IPP status as workaround
[Kyocera*]
  ignore-ipp-status = true
07070100000050000081A400000000000000000000000167D72F5D00001F53000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/usbcommon.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Common types for USB
 */

package main

import (
	"crypto/sha1"
	"fmt"
	"sort"
	"strings"
)

// UsbAddr represents an USB device address
type UsbAddr struct {
	Bus     int // The bus on which the device was detected
	Address int // The address of the device on the bus
}

// String returns a human-readable representation of UsbAddr
func (addr UsbAddr) String() string {
	return fmt.Sprintf("Bus %.3d Device %.3d", addr.Bus, addr.Address)
}

// Less returns true, if addr is "less" that addr2, for sorting
func (addr UsbAddr) Less(addr2 UsbAddr) bool {
	return addr.Bus < addr2.Bus ||
		(addr.Bus == addr2.Bus && addr.Address < addr2.Address)
}

// UsbAddrList represents a list of USB addresses
//
// For faster lookup and comparable logging, address list
// is always sorted in acceding order. To maintain this
// invariant, never modify list directly, and use the provided
// (*UsbAddrList) Add() function
type UsbAddrList []UsbAddr

// Add UsbAddr to UsbAddrList
func (list *UsbAddrList) Add(addr UsbAddr) {
	// Find the smallest index of address list
	// item which is greater or equal to the
	// newly inserted address
	//
	// Note, of "not found" case sort.Search()
	// returns len(*list)
	i := sort.Search(len(*list), func(n int) bool {
		return !(*list)[n].Less(addr)
	})

	// Check for duplicate
	if i < len(*list) && (*list)[i] == addr {
		return
	}

	// The simple case: all items are less
	// that newly added, so just append new
	// address to the end
	if i == len(*list) {
		*list = append(*list, addr)
		return
	}

	// Insert item in the middle
	*list = append(*list, (*list)[i])
	(*list)[i] = addr
}

// Find address in a list. Returns address index,
// if address is found, -1 otherwise
func (list UsbAddrList) Find(addr UsbAddr) int {
	i := sort.Search(len(list), func(n int) bool {
		return !list[n].Less(addr)
	})

	if i < len(list) && list[i] == addr {
		return i
	}

	return -1
}

// Diff computes a difference between two address lists,
// returning lists of elements to be added and to be removed
// to/from the list to convert it to the list2
func (list UsbAddrList) Diff(list2 UsbAddrList) (added, removed UsbAddrList) {
	// Note, there is no needs to sort added and removed
	// lists, they are already created sorted

	for _, a := range list2 {
		if list.Find(a) < 0 {
			added.Add(a)
		}
	}

	for _, a := range list {
		if list2.Find(a) < 0 {
			removed.Add(a)
		}
	}

	return
}

// UsbIfAddr represents a full "address" of the USB interface
type UsbIfAddr struct {
	UsbAddr     // Device address
	Num     int // Interface number within Config
	Alt     int // Number of alternate setting
	In, Out int // Input/output endpoint numbers
}

// String returns a human readable short representation of UsbIfAddr
func (ifaddr UsbIfAddr) String() string {
	return fmt.Sprintf("Bus %.3d Device %.3d Interface %d Alt %d",
		ifaddr.Bus,
		ifaddr.Address,
		ifaddr.Num,
		ifaddr.Alt,
	)
}

// UsbIfAddrList represents a list of USB interface addresses
type UsbIfAddrList []UsbIfAddr

// Add UsbIfAddr to UsbIfAddrList
func (list *UsbIfAddrList) Add(addr UsbIfAddr) {
	*list = append(*list, addr)
}

// UsbDeviceDesc represents an IPP-over-USB device descriptor
type UsbDeviceDesc struct {
	UsbAddr               // Device address
	Config  int           // IPP-over-USB configuration
	IfAddrs UsbIfAddrList // IPP-over-USB interfaces
	IfDescs []UsbIfDesc   // Descriptors of all interfaces
}

// GetUsbDeviceInfo obtains UsbDeviceInfo by UsbDeviceDesc
// It may fail, if device cannot be opened
func (desc UsbDeviceDesc) GetUsbDeviceInfo() (UsbDeviceInfo, error) {
	dev, err := UsbOpenDevice(desc)
	if err == nil {
		defer dev.Close()
		return dev.UsbDeviceInfo()
	}
	return UsbDeviceInfo{}, err
}

// UsbIfDesc represents an USB interface descriptor
type UsbIfDesc struct {
	Vendor   uint16 // USB Vendor ID
	Product  uint16 // USB Device ID
	Config   int    // Configuration
	IfNum    int    // Interface number
	Alt      int    // Alternate setting
	Class    int    // Class
	SubClass int    // Subclass
	Proto    int    // Protocol
}

// IsIppOverUsb check if interface is IPP over USB
//
// FIXME. The matching rules must be configurable
func (ifdesc UsbIfDesc) IsIppOverUsb() bool {
	switch {
	// The classical combination, 7/1/4
	case ifdesc.Class == 7 && ifdesc.SubClass == 1 && ifdesc.Proto == 4:
		return true

	// Some HP devices use non-standard combination, 255/9/1
	//
	// This is valid at least with the following devices:
	//   HP LaserJet MFP M426fdn
	//   HP ColorLaserJet MFP M278-M281
	case ifdesc.Vendor == 0x03f0 &&
		ifdesc.Class == 255 && ifdesc.SubClass == 9 && ifdesc.Proto == 1:
		return true
	}

	return false
}

// UsbDeviceInfo represents USB device information
type UsbDeviceInfo struct {
	// Fields, directly decoded from USB
	Vendor       uint16          // Vendor ID
	Product      uint16          // Device ID
	SerialNumber string          // Device serial number
	Manufacturer string          // Manufacturer name
	ProductName  string          // Product name
	PortNum      int             // USB port number
	BasicCaps    UsbIppBasicCaps // Device basic capabilities

	// Precomputed fields
	MfgAndProduct string // Product with Manufacturer prefix, if needed
}

// UsbIppBasicCaps represents device basic capabilities bits,
// according to the IPP-USB specification, section 4.3
type UsbIppBasicCaps int

// Basic capabilities bits, see IPP-USB specification, section 4.3
const (
	UsbIppBasicCapsPrint UsbIppBasicCaps = 1 << iota
	UsbIppBasicCapsScan
	UsbIppBasicCapsFax
	UsbIppBasicCapsOther
	UsbIppBasicCapsAnyHTTP
)

// String returns a human-readable representation of UsbAddr
func (caps UsbIppBasicCaps) String() string {
	s := []string{}

	if caps&UsbIppBasicCapsPrint != 0 {
		s = append(s, "print")
	}

	if caps&UsbIppBasicCapsScan != 0 {
		s = append(s, "scan")
	}

	if caps&UsbIppBasicCapsFax != 0 {
		s = append(s, "fax")
	}

	if caps&UsbIppBasicCapsAnyHTTP != 0 {
		s = append(s, "http")
	}

	return strings.Join(s, ",")
}

// FixUp fixes up precomputed fields
func (info *UsbDeviceInfo) FixUp() {
	mfg := strings.TrimSpace(info.Manufacturer)
	prod := strings.TrimSpace(info.ProductName)

	info.MfgAndProduct = prod
	if !strings.HasPrefix(prod, mfg) {
		info.MfgAndProduct = mfg + " " + prod
	}
}

// Ident returns device identification string, suitable as
// persistent state identifier
func (info UsbDeviceInfo) Ident() string {
	id := fmt.Sprintf("%4.4x-%4.4x-%s-%s",
		info.Vendor, info.Product, info.SerialNumber, info.MfgAndProduct)

	id = strings.Map(func(c rune) rune {
		switch {
		case '0' <= c && c <= '9':
		case 'a' <= c && c <= 'z':
		case 'A' <= c && c <= 'Z':
		case c == '-' || c == '_':
		default:
			c = '-'
		}
		return c
	}, id)
	return id
}

// DNSSdName generates device DNS-SD name in a case it is not available
// from IPP or eSCL
func (info UsbDeviceInfo) DNSSdName() string {
	return info.MfgAndProduct
}

// UUID generates device UUID in a case it is not available
// from IPP or eSCL
func (info UsbDeviceInfo) UUID() string {
	hash := sha1.New()

	// Arbitrary namespace UUID
	const namespace = "fe678de6-f422-467e-9f83-2354e26c3b41"

	hash.Write([]byte(namespace))
	hash.Write([]byte(info.Ident()))
	uuid := hash.Sum(nil)

	// UUID.Version = 5: Name-based with SHA1; see RFC4122, 4.1.3.
	uuid[6] &= 0x0f
	uuid[6] |= 0x5f

	// UUID.Variant = 0b10: see RFC4122, 4.1.1.
	uuid[8] &= 0x3F
	uuid[8] |= 0x80

	return fmt.Sprintf(
		"%.2x%.2x%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x-%.2x%.2x%.2x%.2x%.2x%.2x",
		uuid[0], uuid[1], uuid[2], uuid[3],
		uuid[4], uuid[5], uuid[6], uuid[7],
		uuid[8], uuid[9], uuid[10], uuid[11],
		uuid[12], uuid[13], uuid[14], uuid[15])
}

// Comment returns a short comment, describing a device
func (info UsbDeviceInfo) Comment() string {
	return info.MfgAndProduct + " serial=" + info.SerialNumber
}
07070100000051000081A400000000000000000000000167D72F5D00000704000000000000000000000000000000000000002100000000ipp-usb-0.9.30/usbcommon_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Tests for usbcommon.go
 */

package main

import (
	"testing"
)

// Check if two UsbAddrList are equal
func equalUsbAddrList(l1, l2 UsbAddrList) bool {
	if len(l1) != len(l2) {
		return false
	}

	for i := range l1 {
		if l1[i] != l2[i] {
			return false
		}
	}

	return true
}

// Make UsbAddrList from individual addresses
func makeUsbAddrList(addrs ...UsbAddr) UsbAddrList {
	l := UsbAddrList{}

	for _, a := range addrs {
		l.Add(a)
	}

	return l
}

// Test (*UsbAddrList)Add() against (*UsbAddrList)Find()
func TestUsbAddrListAddFind(t *testing.T) {
	a1 := UsbAddr{0, 1}
	a2 := UsbAddr{0, 2}
	a3 := UsbAddr{0, 3}

	l1 := makeUsbAddrList(a1, a2)

	if l1.Find(a1) < 0 {
		t.Fail()
	}

	if l1.Find(a2) < 0 {
		t.Fail()
	}

	if l1.Find(a3) >= 0 {
		t.Fail()
	}
}

// Test that (*UsbAddrList)Add() is commutative operation
func TestUsbAddrListAddCommutative(t *testing.T) {
	a1 := UsbAddr{0, 1}
	a2 := UsbAddr{0, 2}

	l1 := UsbAddrList{}
	l2 := UsbAddrList{}

	l1.Add(a1)
	l1.Add(a2)

	l2.Add(a2)
	l2.Add(a1)

	if !equalUsbAddrList(l1, l2) {
		t.Fail()
	}
}

// Test (*UsbAddrList) Diff()
func TestUsbAddrListDiff(t *testing.T) {
	a1 := UsbAddr{0, 1}
	a2 := UsbAddr{0, 2}
	a3 := UsbAddr{0, 3}

	l1 := makeUsbAddrList(a2, a3)
	l2 := makeUsbAddrList(a1, a3)

	added, removed := l1.Diff(l2)

	if !equalUsbAddrList(added, makeUsbAddrList(a1)) {
		t.Fail()
	}

	if !equalUsbAddrList(removed, makeUsbAddrList(a2)) {
		t.Fail()
	}

	added, removed = l2.Diff(l1)

	if !equalUsbAddrList(removed, makeUsbAddrList(a1)) {
		t.Fail()
	}

	if !equalUsbAddrList(added, makeUsbAddrList(a2)) {
		t.Fail()
	}
}
07070100000052000081A400000000000000000000000167D72F5D00005F4D000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/usbio_libusb.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * USB low-level I/O. Cgo implementation on a top of libusb
 */

package main

import (
	"context"
	"encoding/binary"
	"errors"
	"runtime"
	"sync"
	"sync/atomic"
	"time"
	"unsafe"
)

// #cgo pkg-config: libusb-1.0
// #include <libusb.h>
//
// int libusbHotplugCallback (libusb_context *ctx, libusb_device *device,
//     libusb_hotplug_event event, void *user_data);
// void libusbTransferCallback (struct libusb_transfer *transfer);
//
// typedef struct libusb_device_descriptor libusb_device_descriptor_struct;
// typedef struct libusb_config_descriptor libusb_config_descriptor_struct;
// typedef struct libusb_interface libusb_interface_struct;
// typedef struct libusb_interface_descriptor libusb_interface_descriptor_struct;
// typedef struct libusb_endpoint_descriptor libusb_endpoint_descriptor_struct;
// typedef struct libusb_transfer libusb_transfer_struct;
//
// // Note, libusb_strerror accepts enum libusb_error argument, which
// // unfortunately behaves differently depending on target OS and compiler
// // version (sometimes as C.int, sometimes as int32). Looks like cgo
// // bug. Wrapping this function into this simple wrapper should
// // fix the problem. See #18 for details
// static inline const char*
// libusb_strerror_wrapper (int code) {
//     return libusb_strerror(code);
// }
import "C"

// UsbError represents USB error
type UsbError struct {
	Func string
	Code UsbErrCode
}

// Error describes a libusb error. It implements error interface
func (err UsbError) Error() string {
	return err.Func + ": " + err.Code.String()
}

// UsbErrCode represents USB I/O error code
type UsbErrCode int

// UsbErrCode constants
const (
	UsbEIO           UsbErrCode = C.LIBUSB_ERROR_IO
	UsbEInval                   = C.LIBUSB_ERROR_INVALID_PARAM
	UsbEAccess                  = C.LIBUSB_ERROR_ACCESS
	UsbENoDev                   = C.LIBUSB_ERROR_NO_DEVICE
	UsbENotFound                = C.LIBUSB_ERROR_NOT_FOUND
	UsbEBusy                    = C.LIBUSB_ERROR_BUSY
	UsbETimeout                 = C.LIBUSB_ERROR_TIMEOUT
	UsbEOverflow                = C.LIBUSB_ERROR_OVERFLOW
	UsbEPipe                    = C.LIBUSB_ERROR_PIPE
	UsbEIntr                    = C.LIBUSB_ERROR_INTERRUPTED
	UsbENomem                   = C.LIBUSB_ERROR_NO_MEM
	UsbENotSupported            = C.LIBUSB_ERROR_NOT_SUPPORTED
	UsbEOther                   = C.LIBUSB_ERROR_OTHER
)

// String returns string representation of error code
func (err UsbErrCode) String() string {
	return C.GoString(C.libusb_strerror_wrapper(C.int(err)))
}

var (
	// libusbContextPtr keeps a pointer to libusb_context.
	// It is initialized on demand
	libusbContextPtr *C.libusb_context

	// libusbContextLock protects libusbContextPtr initialization
	// in multithreaded context
	libusbContextLock sync.Mutex

	// Nonzero, if libusbContextPtr initialized
	libusbContextOk int32

	// libusbTransferDoneMap contains a map of completion channels,
	// associated with each active libusb_transfer.
	//
	// The libusbTransferCallback uses this map to indicate transfer
	// completion
	//
	// This is required, because CGo is very restrictive in whatever
	// can be saved in pointer passed to the C side.
	libusbTransferDoneMap = make(map[*C.libusb_transfer_struct]chan struct{})

	// libusbTransferDoneLock protects multithreaded access to
	// the libusbTransferDoneMap
	libusbTransferDoneLock sync.Mutex

	// UsbHotPlugChan receives USB hotplug event notifications
	UsbHotPlugChan = make(chan struct{}, 1)
)

// UsbInit initializes low-level USB I/O
func UsbInit(nopnp bool) error {
	_, err := libusbContext(nopnp)
	return err
}

// libusbContext returns libusb_context. It
// initializes context on demand.
func libusbContext(nopnp bool) (*C.libusb_context, error) {
	if atomic.LoadInt32(&libusbContextOk) != 0 {
		return libusbContextPtr, nil
	}

	libusbContextLock.Lock()
	defer libusbContextLock.Unlock()

	// Obtain libusb_context
	rc := C.libusb_init(&libusbContextPtr)
	if rc != 0 {
		return nil, UsbError{"libusb_init", UsbErrCode(rc)}
	}

	// Subscribe to hotplug events
	if !nopnp {
		C.libusb_hotplug_register_callback(
			libusbContextPtr, // libusb_context
			C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED| // events mask
				C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
			C.LIBUSB_HOTPLUG_NO_FLAGS,  // flags
			C.LIBUSB_HOTPLUG_MATCH_ANY, // vendor_id
			C.LIBUSB_HOTPLUG_MATCH_ANY, // product_id
			C.LIBUSB_HOTPLUG_MATCH_ANY, // dev_class
			C.libusb_hotplug_callback_fn(unsafe.Pointer(C.libusbHotplugCallback)),
			nil, // callback's data
			nil, // deregister handle
		)
	}

	// Start libusb thread (required for hotplug and asynchronous I/O)
	go func() {
		runtime.LockOSThread()
		for {
			C.libusb_handle_events(libusbContextPtr)
		}
	}()

	atomic.StoreInt32(&libusbContextOk, 1)
	return libusbContextPtr, nil
}

// Called by libusb on hotplug event
//
//export libusbHotplugCallback
func libusbHotplugCallback(ctx *C.libusb_context, dev *C.libusb_device,
	event C.libusb_hotplug_event, p unsafe.Pointer) C.int {

	usbaddr := UsbAddr{
		Bus:     int(C.libusb_get_bus_number(dev)),
		Address: int(C.libusb_get_device_address(dev)),
	}

	switch event {
	case C.LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED:
		Log.Debug('+', "HOTPLUG: added %s", usbaddr)
	case C.LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT:
		Log.Debug('-', "HOTPLUG: removed %s", usbaddr)
	}

	select {
	case UsbHotPlugChan <- struct{}{}:
	default:
	}

	return 0
}

// Called by libusb on libusb_transfer completion
//
//export libusbTransferCallback
func libusbTransferCallback(xfer *C.libusb_transfer_struct) {
	// Obtain signaling channel
	libusbTransferDoneLock.Lock()
	done := libusbTransferDoneMap[xfer]
	libusbTransferDoneLock.Unlock()

	// Indicate transfer completion by closing the channel
	close(done)
}

// libusbTransferStatusDecode decodes libusb_transfer completion status.
//
// It returns either non-negative actual transfer length or error.
//
// When computing an error, it consults context.Context cancellation
// and expiration status.
func libusbTransferStatusDecode(ctx context.Context,
	xfer *C.libusb_transfer_struct) (int, error) {

	var rc C.int
	switch xfer.status {
	// Handle special cases
	case C.LIBUSB_TRANSFER_COMPLETED:
		// Successful completion. Return no error regardless
		// of the context.Context status.
		return int(xfer.actual_length), nil

	case C.LIBUSB_TRANSFER_CANCELLED:
		switch {
		case ctx.Err() != nil:
			return 0, ctx.Err()
		default:
			rc = C.LIBUSB_ERROR_IO
		}

	case C.LIBUSB_TRANSFER_TIMED_OUT:
		// There may be a race between context.Context
		// expiration and libusb timeout. Be consistent
		// in returned error.
		return 0, context.DeadlineExceeded

	// Handle other cases
	case C.LIBUSB_TRANSFER_STALL:
		rc = C.LIBUSB_ERROR_PIPE

	case C.LIBUSB_TRANSFER_OVERFLOW:
		rc = C.LIBUSB_ERROR_OVERFLOW

	case C.LIBUSB_TRANSFER_NO_DEVICE:
		rc = C.LIBUSB_ERROR_NO_DEVICE

	case C.LIBUSB_TRANSFER_ERROR:
		rc = C.LIBUSB_ERROR_IO

	default:
		rc = C.LIBUSB_ERROR_OTHER
	}

	return 0, UsbError{"libusb_submit_transfer", UsbErrCode(rc)}
}

// libusbTransferAlloc allocates a libusb_transfer.
//
// On success, it allocates a completion channel as well and adds
// it into the libusbTransferDoneMap.
func libusbTransferAlloc() (*C.libusb_transfer_struct, chan struct{}, error) {
	xfer := C.libusb_alloc_transfer(0)
	if xfer == nil {
		return nil, nil, UsbError{"libusb_alloc_transfer", UsbENomem}
	}

	doneChan := make(chan struct{})

	libusbTransferDoneLock.Lock()
	libusbTransferDoneMap[xfer] = doneChan
	libusbTransferDoneLock.Unlock()

	return xfer, doneChan, nil
}

// libusbTransferFree removed libusb_transfer from the libusbTransferDoneMap
// and releases its memory.
func libusbTransferFree(xfer *C.libusb_transfer_struct) {
	libusbTransferDoneLock.Lock()
	delete(libusbTransferDoneMap, xfer)
	libusbTransferDoneLock.Unlock()

	C.libusb_free_transfer(xfer)
}

// UsbCheckIppOverUsbDevices returns true if there are some IPP-over-USB devices
func UsbCheckIppOverUsbDevices() bool {
	descs, _ := UsbGetIppOverUsbDeviceDescs()
	return len(descs) != 0
}

// UsbGetIppOverUsbDeviceDescs return list of IPP-over-USB
// device descriptors
func UsbGetIppOverUsbDeviceDescs() (map[UsbAddr]UsbDeviceDesc, error) {
	// Obtain libusb context
	ctx, err := libusbContext(false)
	if err != nil {
		return nil, err
	}

	// Obtain list of devices
	var devlist **C.libusb_device
	cnt := C.libusb_get_device_list(ctx, &devlist)
	if cnt < 0 {
		return nil, UsbError{"libusb_get_device_list", UsbErrCode(cnt)}
	}
	defer C.libusb_free_device_list(devlist, 1)

	// Convert devlist to slice.
	// See https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices
	devs := (*[1 << 28]*C.libusb_device)(unsafe.Pointer(devlist))[:cnt:cnt]

	// Now build list of addresses
	descs := make(map[UsbAddr]UsbDeviceDesc)

	for _, dev := range devs {
		desc, err := libusbBuildUsbDeviceDesc(dev)

		// Note, ignore devices, if we don't have
		// at least 2 IPP over USB interfaces
		// (which should not happen in real life,
		// but just in case...
		if err == nil && len(desc.IfAddrs) >= 2 {
			descs[desc.UsbAddr] = desc
		}
	}

	return descs, nil
}

// libusbBuildUsbDeviceDesc builds device descriptor
func libusbBuildUsbDeviceDesc(dev *C.libusb_device) (UsbDeviceDesc, error) {
	var cDesc C.libusb_device_descriptor_struct
	var desc UsbDeviceDesc

	// Obtain device descriptor
	rc := C.libusb_get_device_descriptor(dev, &cDesc)
	if rc < 0 {
		return desc, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)}
	}

	// Decode device descriptor
	desc.Bus = int(C.libusb_get_bus_number(dev))
	desc.Address = int(C.libusb_get_device_address(dev))
	desc.Config = -1

	// Roll over configs/interfaces/alt settings/endpoins
	for cfgNum := 0; cfgNum < int(cDesc.bNumConfigurations); cfgNum++ {
		var conf *C.libusb_config_descriptor_struct
		rc = C.libusb_get_config_descriptor(dev, C.uint8_t(cfgNum), &conf)
		if rc == 0 {
			// Make sure we use the same configuration for all interfaces
			if desc.Config >= 0 && desc.Config != int(conf.bConfigurationValue) {
				continue
			}

			ifcnt := conf.bNumInterfaces
			ifaces := (*[256]C.libusb_interface_struct)(
				unsafe.Pointer(conf._interface))[:ifcnt:ifcnt]

			for _, iface := range ifaces {
				altcnt := iface.num_altsetting
				alts := (*[256]C.libusb_interface_descriptor_struct)(
					unsafe.Pointer(iface.altsetting))[:altcnt:altcnt]

				for _, alt := range alts {
					// Build and append UsbIfDesc
					ifdesc := UsbIfDesc{
						Vendor:   uint16(cDesc.idVendor),
						Product:  uint16(cDesc.idProduct),
						Config:   int(conf.bConfigurationValue),
						IfNum:    int(alt.bInterfaceNumber),
						Alt:      int(alt.bAlternateSetting),
						Class:    int(alt.bInterfaceClass),
						SubClass: int(alt.bInterfaceSubClass),
						Proto:    int(alt.bInterfaceProtocol),
					}

					desc.IfDescs = append(desc.IfDescs, ifdesc)

					// We are only interested in IPP-over-USB
					// interfaces, i.e., LIBUSB_CLASS_PRINTER,
					// SubClass 1, Protocol 4
					if ifdesc.IsIppOverUsb() {
						epnum := alt.bNumEndpoints
						endpoints := (*[256]C.libusb_endpoint_descriptor_struct)(
							unsafe.Pointer(alt.endpoint))[:epnum:epnum]

						in, out := -1, -1
						for _, ep := range endpoints {
							num := int(ep.bEndpointAddress & 0xf)
							dir := int(ep.bEndpointAddress & 0x80)
							switch dir {
							case C.LIBUSB_ENDPOINT_IN:
								if in == -1 {
									in = num
								}
							case C.LIBUSB_ENDPOINT_OUT:
								if out == -1 {
									out = num
								}
							}
						}

						// Build and append UsbIfAddr
						if in >= 0 && out >= 0 {
							desc.Config = int(conf.bConfigurationValue)
							addr := UsbIfAddr{
								UsbAddr: desc.UsbAddr,
								Num:     int(alt.bInterfaceNumber),
								Alt:     int(alt.bAlternateSetting),
								In:      in,
								Out:     out,
							}
							desc.IfAddrs.Add(addr)
						}
					}
				}
			}

			C.libusb_free_config_descriptor(conf)
		}
	}

	return desc, nil
}

// UsbDevHandle represents libusb_device_handle
type UsbDevHandle C.libusb_device_handle

// UsbOpenDevice opens device by device descriptor
func UsbOpenDevice(desc UsbDeviceDesc) (*UsbDevHandle, error) {
	// Obtain libusb context
	ctx, err := libusbContext(false)
	if err != nil {
		return nil, err
	}

	// Obtain list of devices
	var devlist **C.libusb_device
	cnt := C.libusb_get_device_list(ctx, &devlist)
	if cnt < 0 {
		return nil, UsbError{"libusb_get_device_list", UsbErrCode(cnt)}
	}
	defer C.libusb_free_device_list(devlist, 1)

	// Convert devlist to slice.
	devs := (*[1 << 28]*C.libusb_device)(unsafe.Pointer(devlist))[:cnt:cnt]

	// Find and open a device
	for _, dev := range devs {
		bus := int(C.libusb_get_bus_number(dev))
		address := int(C.libusb_get_device_address(dev))

		if desc.Bus == bus && desc.Address == address {
			// Open device
			var devhandle *C.libusb_device_handle
			rc := C.libusb_open(dev, &devhandle)
			if rc < 0 {
				return nil, UsbError{"libusb_open", UsbErrCode(rc)}
			}

			return (*UsbDevHandle)(devhandle), nil
		}
	}

	return nil, UsbError{"libusb_get_device_list", UsbENotFound}
}

// Configure prepares the device for further work:
//   - set proper USB configuration
//   - detach kernel driver
func (devhandle *UsbDevHandle) Configure(desc UsbDeviceDesc) error {
	// Detach kernel driver
	err := (*UsbDevHandle)(devhandle).detachKernelDriver()
	if err != nil {
		return err
	}

	// Set configuration
	rc := C.libusb_set_configuration(
		(*C.libusb_device_handle)(devhandle), C.int(desc.Config))

	if rc < 0 {
		return UsbError{"libusb_set_configuration", UsbErrCode(rc)}
	}

	// Printer may require some time to switch configuration
	time.Sleep(time.Second / 4)

	return nil
}

// detachKernelDriver detaches kernel driver from all interfaces
// of current configuration
func (devhandle *UsbDevHandle) detachKernelDriver() error {
	C.libusb_set_auto_detach_kernel_driver(
		(*C.libusb_device_handle)(devhandle), 1)

	ifnums, err := devhandle.currentInterfaces()
	if err != nil {
		return err
	}

	for _, ifnum := range ifnums {
		rc := C.libusb_detach_kernel_driver(
			(*C.libusb_device_handle)(devhandle), C.int(ifnum))
		if rc == C.LIBUSB_ERROR_NOT_FOUND {
			rc = 0
		}

		if rc < 0 {
			return UsbError{"libusb_detach_kernel_driver", UsbErrCode(rc)}
		}
	}

	return nil
}

// libusbCurrentInterfaces builds list of interfaces in current configuration
func (devhandle *UsbDevHandle) currentInterfaces() ([]int, error) {
	dev := C.libusb_get_device((*C.libusb_device_handle)(devhandle))

	// Obtain device descriptor
	var cDesc C.libusb_device_descriptor_struct
	rc := C.libusb_get_device_descriptor(dev, &cDesc)
	if rc < 0 {
		return nil, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)}
	}

	// Get current configuration
	var config C.int
	rc = C.libusb_get_configuration((*C.libusb_device_handle)(devhandle), &config)
	if rc < 0 {
		return nil, UsbError{"libusb_get_configuration", UsbErrCode(rc)}
	}

	// Get configuration descriptor
	var conf *C.libusb_config_descriptor_struct

	for cfgNum := 0; cfgNum < int(cDesc.bNumConfigurations); cfgNum++ {
		rc = C.libusb_get_config_descriptor(dev, C.uint8_t(cfgNum), &conf)
		if rc < 0 {
			return nil, UsbError{"libusb_get_configuration", UsbErrCode(rc)}
		}

		if conf.bConfigurationValue == C.uint8_t(config) {
			break
		}

		C.libusb_free_config_descriptor(conf)
		conf = nil
	}

	if conf == nil {
		return nil, errors.New("libusb: unable to find current configuration in device descriptor")
	}

	defer C.libusb_free_config_descriptor(conf)

	// Build list of interface numbers
	ifcnt := conf.bNumInterfaces
	ifaces := (*[256]C.libusb_interface_struct)(
		unsafe.Pointer(conf._interface))[:ifcnt:ifcnt]
	ifnumbers := make([]int, 0, ifcnt)

	for _, iface := range ifaces {
		alt := iface.altsetting

		ifnumbers = append(ifnumbers, int(alt.bInterfaceNumber))
	}

	return ifnumbers, nil
}

// Close a device
func (devhandle *UsbDevHandle) Close() {
	C.libusb_close((*C.libusb_device_handle)(devhandle))
}

// Reset a device
func (devhandle *UsbDevHandle) Reset() {
	C.libusb_reset_device((*C.libusb_device_handle)(devhandle))
}

// UsbDeviceInfo returns UsbDeviceInfo for the device
func (devhandle *UsbDevHandle) UsbDeviceInfo() (UsbDeviceInfo, error) {
	dev := C.libusb_get_device((*C.libusb_device_handle)(devhandle))

	var cDesc C.libusb_device_descriptor_struct
	var info UsbDeviceInfo

	// Obtain device descriptor
	rc := C.libusb_get_device_descriptor(dev, &cDesc)
	if rc < 0 {
		return info, UsbError{"libusb_get_device_descriptor", UsbErrCode(rc)}
	}

	// Decode device descriptor
	info.Vendor = uint16(cDesc.idVendor)
	info.Product = uint16(cDesc.idProduct)
	info.BasicCaps = devhandle.usbIppBasicCaps()

	buf := make([]byte, 256)

	strings := []struct {
		idx C.uint8_t
		str *string
	}{
		{cDesc.iManufacturer, &info.Manufacturer},
		{cDesc.iProduct, &info.ProductName},
		{cDesc.iSerialNumber, &info.SerialNumber},
	}

	for _, s := range strings {
		rc := C.libusb_get_string_descriptor_ascii(
			(*C.libusb_device_handle)(devhandle),
			s.idx,
			(*C.uchar)(unsafe.Pointer(&buf[0])),
			C.int(len(buf)),
		)

		if rc > 0 {
			*s.str = string(buf[:rc])
		}
	}

	info.PortNum = int(C.libusb_get_port_number(dev))

	info.FixUp()

	return info, nil
}

// usbIppBasicCaps reads and decodes printer's
// Class-specific Device Info Descriptor to obtain device
// capabilities; see IPP USB specification, section 4.3 for details
//
// This function never fails. In a case of errors, it fall backs
// to the reasonable default
func (devhandle *UsbDevHandle) usbIppBasicCaps() (caps UsbIppBasicCaps) {
	// Safe default
	caps = UsbIppBasicCapsPrint |
		UsbIppBasicCapsScan |
		UsbIppBasicCapsFax |
		UsbIppBasicCapsAnyHTTP

	// Buffer length
	const bufLen = 256

	// Obtain class-specific Device Info Descriptor
	// See IPP USB specification, section 4.3 for details
	buf := make([]byte, bufLen)
	rc := C.libusb_get_descriptor(
		(*C.libusb_device_handle)(devhandle),
		0x21, 0,
		(*C.uchar)(unsafe.Pointer(&buf[0])),
		bufLen)

	if rc < 0 {
		// Some devices doesn't properly return class-specific
		// device descriptor, so ignore an error
		return
	}

	if rc < 10 {
		// Malformed response, fall back to default
		return
	}

	// Decode basic capabilities bits
	bits := binary.LittleEndian.Uint16(buf[6:8])
	if bits == 0 {
		// Paranoia. If no caps, return default
		return
	}

	return UsbIppBasicCaps(bits)
}

// OpenUsbInterface opens an interface
func (devhandle *UsbDevHandle) OpenUsbInterface(addr UsbIfAddr,
	quirks Quirks) (*UsbInterface, error) {

	// Claim the interface
	rc := C.libusb_claim_interface(
		(*C.libusb_device_handle)(devhandle),
		C.int(addr.Num),
	)
	if rc < 0 {
		return nil, UsbError{"libusb_claim_interface", UsbErrCode(rc)}
	}

	// Activate alternate setting
	rc = C.libusb_set_interface_alt_setting(
		(*C.libusb_device_handle)(devhandle),
		C.int(addr.Num),
		C.int(addr.Alt),
	)

	if rc < 0 {
		C.libusb_release_interface(
			(*C.libusb_device_handle)(devhandle),
			C.int(addr.Num),
		)
		return nil, UsbError{"libusb_set_interface_alt_setting", UsbErrCode(rc)}
	}

	return &UsbInterface{
		devhandle: devhandle,
		addr:      addr,
		quirks:    quirks,
	}, nil
}

// UsbInterface represents IPP-over-USB interface
type UsbInterface struct {
	devhandle *UsbDevHandle // Device handle
	addr      UsbIfAddr     // Interface address
	quirks    Quirks        // Device quirks
}

// Close the interface
func (iface *UsbInterface) Close() {
	C.libusb_release_interface(
		(*C.libusb_device_handle)(iface.devhandle),
		C.int(iface.addr.Num),
	)
}

// SoftReset performs interface soft reset, using class-specific
// SOFT_RESET request
//
// This code was inspired by CUPS, and the original comment follows:
//
//	This soft reset is specific to the printer device class and is much less
//	invasive than the general USB reset libusb_reset_device(). Especially it
//	does never happen that the USB addressing and configuration changes. What
//	is actually done is that all buffers get flushed and the bulk IN and OUT
//	pipes get reset to their default states. This clears all stall conditions.
//	See http://cholla.mmto.org/computers/linux/usb/usbprint11.
func (iface *UsbInterface) SoftReset() error {
	rc := C.libusb_control_transfer(
		(*C.libusb_device_handle)(iface.devhandle),
		C.LIBUSB_REQUEST_TYPE_CLASS|
			C.LIBUSB_ENDPOINT_OUT|
			C.LIBUSB_RECIPIENT_OTHER,
		2, 0, C.ushort(iface.addr.Num), nil, 0, 5000)

	if rc < 0 {
		rc = C.libusb_control_transfer(
			(*C.libusb_device_handle)(iface.devhandle),
			C.LIBUSB_REQUEST_TYPE_CLASS|
				C.LIBUSB_ENDPOINT_OUT|
				C.LIBUSB_RECIPIENT_INTERFACE,
			2, 0, C.ushort(iface.addr.Num), nil, 0, 5000)
	}

	if rc < 0 {
		return UsbError{"libusb_control_transfer", UsbErrCode(rc)}
	}

	return nil
}

// Send data to interface. Returns count of bytes actually transmitted
// and error, if any
func (iface *UsbInterface) Send(ctx context.Context,
	data []byte) (n int, err error) {

	// Don't even bother to send, if context already expired
	if ctx.Err() != nil {
		return 0, ctx.Err()
	}

	// Allocate a libusb_transfer.
	xfer, doneChan, err := libusbTransferAlloc()
	if err != nil {
		return
	}

	defer libusbTransferFree(xfer)

	// Setup bulk transfer
	C.libusb_fill_bulk_transfer(
		xfer,
		(*C.libusb_device_handle)(iface.devhandle),
		C.uint8_t(iface.addr.Out|C.LIBUSB_ENDPOINT_OUT),
		(*C.uchar)(unsafe.Pointer(&data[0])),
		C.int(len(data)),
		C.libusb_transfer_cb_fn(unsafe.Pointer(C.libusbTransferCallback)),
		nil,
		0,
	)

	if iface.quirks.GetZlpSend() {
		xfer.flags |= C.LIBUSB_TRANSFER_ADD_ZERO_PACKET
	}

	// Submit transfer
	rc := C.libusb_submit_transfer(xfer)
	if rc < 0 {
		return 0, UsbError{"libusb_submit_transfer", UsbErrCode(rc)}
	}

	// Wait for completion
	select {
	case <-ctx.Done():
		C.libusb_cancel_transfer(xfer)
	case <-doneChan:
	}

	<-doneChan
	n, err = libusbTransferStatusDecode(ctx, xfer)

	// Introduce inter-URB send delay, if configured
	if delay := iface.quirks.GetUsbSendDelay(); delay != 0 {
		threshold := int(iface.quirks.GetUsbSendDelayThreshold())
		if n > threshold {
			time.Sleep(delay)
		}
	}

	return
}

// Recv data from interface. Returns count of bytes actually transmitted
// and error, if any
//
// Note, if data size is not 512-byte aligned, and device has more data,
// that fits the provided buffer, LIBUSB_ERROR_OVERFLOW error may occur
func (iface *UsbInterface) Recv(ctx context.Context,
	data []byte) (n int, err error) {

	// Don't even bother to recv, if context already expired
	if ctx.Err() != nil {
		return 0, ctx.Err()
	}

	// Some versions of Linux kernel don't allow bulk transfers to
	// be larger that 16kb per URB, and libusb uses some smart-ass
	// mechanism to avoid this limitation.
	//
	// This mechanism seems not to work very reliable on Raspberry Pi
	// (see #3 for details). So just limit bulk reads to 16kb
	const MaxBulkRead = 16384
	if len(data) > MaxBulkRead {
		data = data[0:MaxBulkRead]
	}

	// Allocate a libusb_transfer.
	xfer, doneChan, err := libusbTransferAlloc()
	if err != nil {
		return
	}

	defer libusbTransferFree(xfer)

	// Setup bulk transfer
	C.libusb_fill_bulk_transfer(
		xfer,
		(*C.libusb_device_handle)(iface.devhandle),
		C.uint8_t(iface.addr.In|C.LIBUSB_ENDPOINT_IN),
		(*C.uchar)(unsafe.Pointer(&data[0])),
		C.int(len(data)),
		C.libusb_transfer_cb_fn(unsafe.Pointer(C.libusbTransferCallback)),
		nil,
		0,
	)

	// Submit transfer
	rc := C.libusb_submit_transfer(xfer)
	if rc < 0 {
		return 0, UsbError{"libusb_submit_transfer", UsbErrCode(rc)}
	}

	C.libusb_interrupt_event_handler(libusbContextPtr)

	// Wait for completion
	select {
	case <-ctx.Done():
		C.libusb_cancel_transfer(xfer)
	case <-doneChan:
	}

	<-doneChan
	n, err = libusbTransferStatusDecode(ctx, xfer)

	return
}

// ClearHalt clears "halted" condition of either input or output endpoint
func (iface *UsbInterface) ClearHalt(in bool) error {
	var ep C.uint8_t

	if in {
		ep = C.uint8_t(iface.addr.In | C.LIBUSB_ENDPOINT_IN)
	} else {
		ep = C.uint8_t(iface.addr.Out | C.LIBUSB_ENDPOINT_OUT)
	}

	rc := C.libusb_clear_halt(
		(*C.libusb_device_handle)(iface.devhandle),
		ep)

	if rc < 0 {
		return UsbError{"libusb_clear_halt", UsbErrCode(rc)}
	}

	return nil
}
07070100000053000081A400000000000000000000000167D72F5D0000644E000000000000000000000000000000000000001F00000000ipp-usb-0.9.30/usbtransport.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * USB transport for HTTP
 */

package main

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"net/http"
	"os"
	"sort"
	"strconv"
	"sync/atomic"
	"time"

	"github.com/OpenPrinting/goipp"
)

// UsbTransport implements HTTP transport functionality over USB
type UsbTransport struct {
	addr           UsbAddr       // Device address
	info           UsbDeviceInfo // USB device info
	log            *Logger       // Device's own logger
	dev            *UsbDevHandle // Underlying USB device
	connPool       chan *usbConn // Pool of idle connections
	connList       []*usbConn    // List of all connections
	connReleased   chan struct{} // Signalled when connection released
	shutdown       chan struct{} // Closed by Shutdown()
	connstate      *usbConnState // Connections state tracker
	quirks         Quirks        // Device quirks
	timeout        time.Duration // Timeout for requests (0 is none)
	timeoutExpired uint32        // Atomic non-zero, if timeout expired
}

// NewUsbTransport creates new http.RoundTripper backed by IPP-over-USB
func NewUsbTransport(desc UsbDeviceDesc) (*UsbTransport, error) {
	// Open the device
	dev, err := UsbOpenDevice(desc)
	if err != nil {
		return nil, err
	}

	// Create UsbTransport
	transport := &UsbTransport{
		addr:         desc.UsbAddr,
		log:          NewLogger(),
		dev:          dev,
		connReleased: make(chan struct{}, 1),
		shutdown:     make(chan struct{}),
	}

	// Obtain device info
	transport.info, err = dev.UsbDeviceInfo()
	if err != nil {
		dev.Close()
		return nil, err
	}

	transport.log.Cc(Console)
	transport.log.ToDevFile(transport.info)
	transport.log.SetLevels(Conf.LogDevice)

	// Setup quirks
	transport.quirks = Conf.Quirks.MatchByModelName(
		transport.info.MfgAndProduct)

	// Write device info to the log
	log := transport.log.Begin().
		Nl(LogDebug).
		Debug(' ', "===============================").
		Info('+', "%s: opened %s", transport.addr, transport.info.ProductName).
		Debug(' ', "Device info:").
		Debug(' ', "  USB Port:      %d", transport.info.PortNum).
		Debug(' ', "  Ident:         %s", transport.info.Ident()).
		Debug(' ', "  Manufacturer:  %s", transport.info.Manufacturer).
		Debug(' ', "  Product:       %s", transport.info.ProductName).
		Debug(' ', "  SerialNumber:  %s", transport.info.SerialNumber).
		Debug(' ', "  MfgAndProduct: %s", transport.info.MfgAndProduct).
		Debug(' ', "  BasicCaps:     %s", transport.info.BasicCaps).
		Nl(LogDebug)

	transport.dumpQuirks(log)
	log.Nl(LogDebug)

	transport.dumpUSBparams(log)
	log.Nl(LogDebug)

	log.Debug(' ', "USB interfaces:")
	log.Debug(' ', "  Config Interface Alt Class SubClass Proto")
	for _, ifdesc := range desc.IfDescs {
		prefix := byte(' ')
		if ifdesc.IsIppOverUsb() {
			prefix = '*'
		}

		log.Debug(prefix,
			"     %-3d     %-3d    %-3d %-3d    %-3d     %-3d",
			ifdesc.Config, ifdesc.IfNum,
			ifdesc.Alt, ifdesc.Class, ifdesc.SubClass, ifdesc.Proto)
	}
	log.Nl(LogDebug)
	log.Commit()

	var maxconn uint

	// Check for blacklisted device
	if transport.quirks.GetBlacklist() {
		err = ErrBlackListed
		goto ERROR
	}

	// Hard-reset the device, if needed
	if transport.quirks.GetInitReset() == QuirkResetHard {
		transport.log.Debug(' ', "Doing USB HARD RESET")
		dev.Reset()
	}

	// Configure the device
	err = dev.Configure(desc)
	if err != nil {
		goto ERROR
	}

	// Open connections
	maxconn = transport.quirks.GetUsbMaxInterfaces()
	if maxconn == 0 {
		maxconn = math.MaxUint32
	}

	for i, ifaddr := range desc.IfAddrs {
		var conn *usbConn
		conn, err = transport.openUsbConn(i, ifaddr, transport.quirks)
		if err != nil {
			goto ERROR
		}

		transport.connList = append(transport.connList, conn)

		maxconn--
		if maxconn == 0 {
			break
		}
	}

	transport.connPool = make(chan *usbConn, len(transport.connList))
	transport.connstate = newUsbConnState(len(desc.IfAddrs))

	for _, conn := range transport.connList {
		transport.connPool <- conn
	}

	return transport, nil

	// Error: cleanup and exit
ERROR:
	for _, conn := range transport.connList {
		conn.destroy()
	}

	dev.Close()
	return nil, err
}

// Dump quirks to the UsbTransport's log
func (transport *UsbTransport) dumpQuirks(log *LogMessage) {
	log.Debug(' ', "Device quirks:")

	prevMatch := ""
	for _, q := range transport.quirks.All() {
		val := q.RawValue
		if _, isStr := q.Parsed.(string); isStr {
			val = strconv.Quote(val)
		}

		if q.Match != prevMatch {
			prevMatch = q.Match
			log.Debug(' ', "  [%s]", q.Match)
		}

		log.Debug(' ', "    ; (%s)", q.Origin)
		log.Debug(' ', "    %s = %s", q.Name, val)
	}
}

// Dump USB stack parameters to the UsbTransport's log
func (transport *UsbTransport) dumpUSBparams(log *LogMessage) {
	const usbParamsDir = "/sys/module/usbcore/parameters"

	// Obtain list of parameter names (file names)
	dir, err := os.Open(usbParamsDir)
	if err != nil {
		return
	}

	files, err := dir.Readdirnames(-1)
	dir.Close()
	if err != nil {
		return
	}

	sort.Strings(files)
	if len(files) == 0 {
		return
	}

	// Compute max width of parameter names
	wid := 0
	for _, file := range files {
		if wid < len(file) {
			wid = len(file)
		}
	}

	wid++

	// Write the table
	log.Debug(' ', "USB stack parameters")

	for _, file := range files {
		p, _ := ioutil.ReadFile(usbParamsDir + "/" + file)
		if p == nil {
			p = []byte("-")
		} else {
			p = bytes.TrimSpace(p)
		}

		log.Debug(' ', "  %*s  %s", -wid, file+":", p)
	}
}

// Get count of connections still in use
func (transport *UsbTransport) connInUse() int {
	return cap(transport.connPool) - len(transport.connPool)
}

// SetTimeout sets the timeout for all subsequent requests.
//
// This is useful only at initialization time and if some requests
// were failed due to timeout, device reset is required, because
// at this case synchronization with device will probably be lost.
//
// A zero value for t means no timeout
func (transport *UsbTransport) SetTimeout(t time.Duration) {
	transport.timeout = t
}

// TimeoutExpired returns true if one or more of the preceding HTTP request
// has failed due to timeout.
func (transport *UsbTransport) TimeoutExpired() bool {
	return atomic.LoadUint32(&transport.timeoutExpired) != 0
}

// closeShutdownChan closes the transport.shutdown, which effectively
// disables connections allocation (usbConnGet will return ErrShutdown)
//
// This function can be safely called multiple times (only the first
// call closes the channel)
//
// Note, this function cannot be called simultaneously from
// different threads. However, it's not a problem, because it
// is only called from (*UsbTransport) Shutdown() and
// (*UsbTransport) Close(), and both of these functions are
// only called from the PnP thread context.
func (transport *UsbTransport) closeShutdownChan() {
	select {
	case <-transport.shutdown:
		// Channel already closed
	default:
		close(transport.shutdown)
	}
}

// Shutdown gracefully shuts down the transport. If provided
// context expires before shutdown completion, Shutdown
// returns the Context's error
func (transport *UsbTransport) Shutdown(ctx context.Context) error {
	transport.closeShutdownChan()

	for {
		n := transport.connInUse()
		if n == 0 {
			break
		}

		transport.log.Info('-', "%s: shutdown: %d connections still in use",
			transport.addr, n)

		select {
		case <-transport.connReleased:
		case <-ctx.Done():
			transport.log.Error('-', "%s: %s: shutdown timeout expired",
				transport.addr, transport.info.ProductName)
			return ctx.Err()
		}
	}

	return nil
}

// Close the transport
func (transport *UsbTransport) Close(reset bool) {
	// Reset the device, if required
	if transport.connInUse() > 0 || reset {
		transport.log.Info('-', "%s: resetting %s",
			transport.addr, transport.info.ProductName)
		transport.dev.Reset()
	}

	// Wait until all connections become inactive
	transport.Shutdown(context.Background())

	// Destroy all connections and close the USB device
	for _, conn := range transport.connList {
		conn.destroy()
	}

	transport.dev.Close()
	transport.log.Info('-', "%s: closed %s",
		transport.addr, transport.info.ProductName)
}

// Log returns device's own logger
func (transport *UsbTransport) Log() *Logger {
	return transport.log
}

// UsbDeviceInfo returns USB device information for the device
// behind the transport
func (transport *UsbTransport) UsbDeviceInfo() UsbDeviceInfo {
	return transport.info
}

// Quirks returns device's quirks
func (transport *UsbTransport) Quirks() Quirks {
	return transport.quirks
}

// RoundTrip implements http.RoundTripper interface
func (transport *UsbTransport) RoundTrip(r *http.Request) (
	*http.Response, error) {
	session := int(atomic.AddInt32(&httpSessionID, 1)-1) % 1000

	return transport.RoundTripWithSession(session, r)
}

// RoundTripWithSession executes a single HTTP transaction, returning
// a Response for the provided Request. Session number, for logging,
// provided as a separate parameter
func (transport *UsbTransport) RoundTripWithSession(session int,
	rq *http.Request) (*http.Response, error) {

	// Log the request
	transport.log.HTTPRqParams(LogDebug, '>', session, rq)

	// Prevent request from being canceled from outside
	// We cannot do it on USB: closing USB connection
	// doesn't drain buffered data that server is
	// about to send to client
	outreq := rq.WithContext(context.Background())
	outreq.Cancel = nil

	// Remove Expect: 100-continue, if any
	outreq.Header.Del("Expect")

	// Apply quirks
	for name, value := range transport.quirks.HTTPHeaders {
		if value != "" {
			outreq.Header.Set(name, value)
		} else {
			outreq.Header.Del(name)
		}
	}

	// Don't let Go's stdlib to add Connection: close header
	// automatically
	outreq.Close = false

	// Add User-Agent, if missed. It is just cosmetic
	if _, found := outreq.Header["User-Agent"]; !found {
		outreq.Header["User-Agent"] = []string{"ipp-usb"}
	}

	// Wrap request body
	if outreq.Body != nil {
		outreq.Body = &usbRequestBodyWrapper{
			log:     transport.log,
			session: session,
			body:    outreq.Body,
		}
	}

	// Prepare to correctly handle HTTP transaction, in a case
	// client drops request in a middle of reading body
	switch {
	case outreq.ContentLength <= 0:
		// Nothing to do
		if outreq.ContentLength < 0 {
			transport.log.HTTPDebug('>', session,
				"body is chunked, sending as is")
		} else {
			transport.log.HTTPDebug('>', session,
				"body is empty, sending as is")
		}

	case outreq.ContentLength < 16384:
		// Body is small, prefetch it before sending to USB
		buf := &bytes.Buffer{}
		_, err := io.CopyN(buf, outreq.Body, outreq.ContentLength)
		if err != nil {
			return nil, err
		}

		outreq.Body.Close()
		outreq.Body = ioutil.NopCloser(buf)

		transport.log.HTTPDebug('>', session,
			"body is small (%d bytes), prefetched before sending",
			buf.Len())

	default:
		// Force chunked encoding, so if client drops request,
		// we still be able to correctly handle HTTP transaction
		transport.log.HTTPDebug('>', session,
			"body is large (%d bytes), sending as chunked",
			outreq.ContentLength)

		outreq.ContentLength = -1
	}

	// Log request details
	transport.log.Begin().
		HTTPRequest(LogTraceHTTP, '>', session, outreq).
		Commit()

	// Allocate USB connection
	conn, err := transport.usbConnGet(rq.Context())
	if err != nil {
		return nil, err
	}

	transport.log.HTTPDebug(' ', session, "connection %d allocated", conn.index)

	// Make an inter-request (or initial) delay, if needed
	if delay := conn.delayUntil.Sub(time.Now()); delay > 0 {
		transport.log.HTTPDebug(' ', session, "Pausing for %s", delay)
		time.Sleep(delay)
	}

	// Set read/write Context. This effectively sets request timeout.
	//
	// This is important that context is is set after inter-request
	// or initial delay is already done, so we don't need to bother
	// with adjusting the timeout.
	//
	// The context cancel function is called from many places and
	// not always used, so for simplicity I'd better initialize it
	// to the dummy function rather that to compare it with nil
	// every time it is called.
	rwctx := context.Background()
	cleanupCtx := context.CancelFunc(func() {})

	if transport.timeout != 0 {
		rwctx, cleanupCtx = context.WithTimeout(rwctx,
			transport.timeout)
	}

	conn.setRWCtx(rwctx)

	// Send request and receive a response
	err = outreq.Write(conn)
	if err != nil {
		transport.log.HTTPError('!', session, "%s", err)
		conn.put()
		cleanupCtx()
		return nil, err
	}

	resp, err := http.ReadResponse(conn.reader, outreq)
	if err != nil {
		// If the latest conn.Read has returned io.EOF, the only
		// reason it could happen is that the zlp-recv-hack
		// quirk has triggered.
		//
		// The stdlib HTTP stack will wrap the io.EOF error into
		// its own error message. Here we force error condition
		// back to io.EOF so it cleanly can be detected and handled
		// by the initialization retry logic at the upper level
		if conn.EOFSeen() {
			err = io.EOF
		}

		transport.log.HTTPError('!', session, "%s", err)
		conn.put()
		cleanupCtx()
		return nil, err
	}

	// Wrap response body
	resp.Body = &usbResponseBodyWrapper{
		log:        transport.log,
		session:    session,
		body:       resp.Body,
		conn:       conn,
		cleanupCtx: cleanupCtx,
	}

	// Optionally sanitize IPP response
	if transport.quirks.GetBuggyIppRsp() == QuirkBuggyIppRspSanitize &&
		resp.Header.Get("Content-Type") == "application/ipp" {
		transport.sanitizeIppResponse(session, resp)
	}

	// Log the response
	if resp != nil {
		transport.log.Begin().
			HTTPRspStatus(LogDebug, '<', session, outreq, resp).
			HTTPResponse(LogTraceHTTP, '<', session, resp).
			Commit()
	}

	return resp, nil
}

// sanitizeIppResponse attempts to sanitize IPP response from device
func (transport *UsbTransport) sanitizeIppResponse(session int,
	resp *http.Response) {
	// Try to prefetch IPP part of message
	buf := &bytes.Buffer{}
	buf2 := &bytes.Buffer{}

	tee := io.TeeReader(resp.Body, buf)
	msg := goipp.Message{}
	err := msg.DecodeEx(tee, goipp.DecoderOptions{EnableWorkarounds: true})
	if err != nil {
		transport.log.HTTPDebug(' ', session,
			"IPP sanitize: decode: %s", err)
		goto REPLACE
	}

	// If backup copy decodes without any options, no need to sanitize
	if msg2 := (goipp.Message{}); msg2.DecodeBytes(buf.Bytes()) == nil {
		transport.log.HTTPDebug(' ', session,
			"IPP sanitize: not needed")
		goto REPLACE
	}

	// Re-encode the message correctly
	err = msg.Encode(buf2)
	if err != nil {
		transport.log.HTTPDebug(' ', session,
			"IPP sanitize: encode: %s", err)
		goto REPLACE
	}

	// Replace buffer, adjust resp.ContentLength
	if resp.ContentLength != -1 {
		resp.ContentLength += int64(buf2.Len() - buf.Len())

		resp.Header.Set("Content-Length",
			strconv.FormatInt(resp.ContentLength, 10))

		transport.log.HTTPDebug(' ', session,
			"IPP sanitize: %d bytes replaced with %d",
			buf.Len(), buf2.Len())
	}

	buf = buf2

	// Replace consumed part of message with re-coded or
	// saved backup copy
REPLACE:
	wrap := resp.Body.(*usbResponseBodyWrapper)
	wrap.preBody = buf
}

// usbRequestBodyWrapper wraps http.Request.Body, adding
// data path instrumentation
type usbRequestBodyWrapper struct {
	log     *Logger       // Device's logger
	session int           // HTTP session, for logging
	count   int           // Total count of received bytes
	body    io.ReadCloser // Request.body
	drained bool          // EOF or error has been seen
}

// Read from usbRequestBodyWrapper
func (wrap *usbRequestBodyWrapper) Read(buf []byte) (int, error) {
	n, err := wrap.body.Read(buf)
	wrap.count += n

	if err != nil {
		wrap.log.HTTPDebug('>', wrap.session,
			"request body: got %d bytes; %s", wrap.count, err)
		err = io.EOF
		wrap.drained = true
	}

	return n, err
}

// Close usbRequestBodyWrapper
func (wrap *usbRequestBodyWrapper) Close() error {
	if !wrap.drained {
		wrap.log.HTTPDebug('>', wrap.session,
			"request body: got %d bytes; closed", wrap.count)
	}

	return wrap.body.Close()
}

// usbResponseBodyWrapper wraps http.Response.Body and guarantees
// that connection will be always drained before closed
type usbResponseBodyWrapper struct {
	log        *Logger            // Device's logger
	session    int                // HTTP session, for logging
	preBody    *bytes.Buffer      // Data inserted before body, if not nil
	body       io.ReadCloser      // Response.body
	conn       *usbConn           // Underlying USB connection
	count      int                // Total count of received bytes
	drained    bool               // EOF or error has been seen
	cleanupCtx context.CancelFunc // Cancel function for I/O Context
}

// Read from usbResponseBodyWrapper
func (wrap *usbResponseBodyWrapper) Read(buf []byte) (int, error) {
	if wrap.preBody != nil && wrap.preBody.Len() > 0 {
		return wrap.preBody.Read(buf)
	}

	n, err := wrap.body.Read(buf)
	wrap.count += n

	if err != nil {
		wrap.log.HTTPDebug('<', wrap.session,
			"response body: got %d bytes; %s", wrap.count, err)
		wrap.drained = true
	}
	return n, err
}

// Close usbResponseBodyWrapper
func (wrap *usbResponseBodyWrapper) Close() error {
	// If EOF or error seen, we can close synchronously
	if wrap.drained {
		wrap.cleanup()
		return nil
	}

	// Otherwise, we need to drain USB connection
	wrap.log.HTTPDebug('<', wrap.session, "client has gone; draining response from USB")
	go func() {
		defer func() {
			v := recover()
			if v != nil {
				Log.Panic(v)
			}
		}()

		io.Copy(ioutil.Discard, wrap.body)
		wrap.cleanup()
	}()

	return nil
}

// cleanup performs the final cleanup of the usbResponseBodyWrapper
// after use.
func (wrap *usbResponseBodyWrapper) cleanup() {
	wrap.body.Close()
	wrap.conn.put()

	// Cleanup I/O context.Context, if any
	if wrap.cleanupCtx != nil {
		wrap.cleanupCtx()
	}

	wrap.log.HTTPDebug('<', wrap.session, "done with response body")
}

// usbConn implements an USB connection
type usbConn struct {
	transport     *UsbTransport   // Transport that owns the connection
	index         int             // Connection index (for logging)
	iface         *UsbInterface   // Underlying interface
	reader        *bufio.Reader   // For http.ReadResponse
	rwctx         context.Context // For usbConn.Read and usbConn.Write
	delayUntil    time.Time       // Delay till this time before next request
	delayInterval time.Duration   // Pause between requests
	cntRecv       int             // Total bytes received
	cntSent       int             // Total bytes sent
	eofSeen       bool            // Last usbConn.Read has returned io.EOF
}

// Open usbConn
func (transport *UsbTransport) openUsbConn(
	index int, ifaddr UsbIfAddr, quirks Quirks) (*usbConn, error) {

	dev := transport.dev

	transport.log.Debug(' ', "USB[%d]: open: %s", index, ifaddr)

	// Initialize connection structure
	conn := &usbConn{
		transport:     transport,
		index:         index,
		delayUntil:    time.Now().Add(quirks.GetInitDelay()),
		delayInterval: quirks.GetRequestDelay(),
	}

	conn.reader = bufio.NewReader(conn)

	// Obtain interface
	var err error
	conn.iface, err = dev.OpenUsbInterface(ifaddr, quirks)
	if err != nil {
		goto ERROR
	}

	// Soft-reset interface, if needed
	if quirks.GetInitReset() == QuirkResetSoft {
		transport.log.Debug(' ', "USB[%d]: doing SOFT_RESET", index)
		err = conn.iface.SoftReset()
		if err != nil {
			// Don't treat it too seriously
			transport.log.Info('?', "USB[%d]: SOFT_RESET: %s", index, err)
		}
	}

	return conn, nil

	// Error: cleanup and exit
ERROR:
	transport.log.Error('!', "USB[%d]: %s", index, err)
	if conn.iface != nil {
		conn.iface.Close()
	}

	return nil, err
}

// setRWCtx sets context.Context for subsequent Read and Write operations
func (conn *usbConn) setRWCtx(ctx context.Context) {
	conn.rwctx = ctx
}

// Read from USB
func (conn *usbConn) Read(b []byte) (int, error) {
	conn.transport.connstate.beginRead(conn)
	defer conn.transport.connstate.doneRead(conn)

	// Drop conn.eofSeenn flag
	conn.eofSeen = false

	// Note, to avoid LIBUSB_TRANSFER_OVERFLOW errors
	// from libusb, input buffer size must always
	// be aligned by 1024 bytes for USB 3.0, 512 bytes
	// for USB 2.0, so 1024 bytes alignment is safe for
	// both
	//
	// However if caller requests less that 1024 bytes, we
	// can't align here simply by shrinking the buffer,
	// because it will result a zero-size buffer. At
	// this case we assume caller knows what it is
	// doing (actually bufio never behaves this way)
	if n := len(b); n >= 1024 {
		n &= ^1023
		b = b[0:n]
	}

	// zlp-recv-hack handling
	zlpRecvHack := conn.transport.quirks.GetZlpRecvHack()
	zlpRecv := false

	// Setup deadline
	backoff := time.Millisecond * 10
	for {
		n, err := conn.iface.Recv(conn.rwctx, b)
		conn.cntRecv += n

		conn.transport.log.Add(LogTraceHTTP, '<',
			"USB[%d]: read: wanted %d got %d total %d",
			conn.index, len(b), n, conn.cntRecv)

		conn.transport.log.HexDump(LogTraceUSB, '<', b[:n])

		if err != nil {
			conn.transport.log.Error('!',
				"USB[%d]: recv: %s", conn.index, err)

			if err == context.DeadlineExceeded {
				// If we've got read timeout preceded
				// by the zero-length packet, interpret
				// is as body EOF condition
				if zlpRecvHack && zlpRecv {
					conn.eofSeen = true
					return 0, io.EOF
				}

				atomic.StoreUint32(
					&conn.transport.timeoutExpired, 1)
			}
		}

		if n != 0 || err != nil {
			return n, err
		}

		zlpRecv = true
		conn.transport.log.Debug(' ',
			"USB[%d]: zero-size read", conn.index)

		time.Sleep(backoff)
		backoff += backoff / 4 // The same as backoff *= 1.25
		if backoff > time.Millisecond*1000 {
			backoff = time.Millisecond * 1000
		}
	}
}

// Write to USB
func (conn *usbConn) Write(b []byte) (int, error) {
	conn.transport.connstate.beginWrite(conn)
	defer conn.transport.connstate.doneWrite(conn)

	n, err := conn.iface.Send(conn.rwctx, b)
	conn.cntSent += n

	conn.transport.log.Add(LogTraceHTTP, '>',
		"USB[%d]: write: wanted %d sent %d total %d",
		conn.index, len(b), n, conn.cntSent)

	conn.transport.log.HexDump(LogTraceUSB, '>', b[:n])

	if err != nil {
		conn.transport.log.Error('!',
			"USB[%d]: send: %s", conn.index, err)

		if err == context.DeadlineExceeded {
			atomic.StoreUint32(
				&conn.transport.timeoutExpired, 1)
		}
	}

	return n, err
}

// EOFSeen reports of the latest usbConn.Read has returned io.EOF
func (conn *usbConn) EOFSeen() bool {
	return conn.eofSeen
}

// Allocate a connection
func (transport *UsbTransport) usbConnGet(ctx context.Context) (*usbConn, error) {
	select {
	case <-transport.shutdown:
		return nil, ErrShutdown
	case <-ctx.Done():
		return nil, ctx.Err()
	case conn := <-transport.connPool:
		transport.connstate.gotConn(conn)
		transport.log.Debug(' ', "USB[%d]: connection allocated, %s",
			conn.index, transport.connstate)

		return conn, nil
	}
}

// Release the connection
func (conn *usbConn) put() {
	transport := conn.transport

	conn.reader.Reset(conn)
	conn.delayUntil = time.Now().Add(conn.delayInterval)
	conn.cntRecv = 0
	conn.cntSent = 0

	transport.connstate.putConn(conn)
	transport.log.Debug(' ', "USB[%d]: connection released, %s",
		conn.index, transport.connstate)

	transport.connPool <- conn

	select {
	case transport.connReleased <- struct{}{}:
	default:
	}
}

// Destroy USB connection
func (conn *usbConn) destroy() {
	conn.transport.log.Debug(' ', "USB[%d]: closed", conn.index)
	conn.iface.Close()
}

// usbConnState tracks connections state, for logging
type usbConnState struct {
	alloc []int32 // Per-connection "allocated" flag
	read  []int32 // Per-connection "reading" flag
	write []int32 // Per-connection "writing" flag
}

// newUsbConnState creates a new usbConnState for given
// number of connections
func newUsbConnState(cnt int) *usbConnState {
	return &usbConnState{
		alloc: make([]int32, cnt),
		read:  make([]int32, cnt),
		write: make([]int32, cnt),
	}
}

// gotConn notifies usbConnState, that connection is allocated
func (state *usbConnState) gotConn(conn *usbConn) {
	atomic.AddInt32(&state.alloc[conn.index], 1)
}

// putConn notifies usbConnState, that connection is released
func (state *usbConnState) putConn(conn *usbConn) {
	atomic.AddInt32(&state.alloc[conn.index], -1)
}

// beginRead notifies usbConnState, that read is started
func (state *usbConnState) beginRead(conn *usbConn) {
	atomic.AddInt32(&state.read[conn.index], 1)
}

// doneRead notifies usbConnState, that read is done
func (state *usbConnState) doneRead(conn *usbConn) {
	atomic.AddInt32(&state.read[conn.index], -1)
}

// beginWrite notifies usbConnState, that write is started
func (state *usbConnState) beginWrite(conn *usbConn) {
	atomic.AddInt32(&state.write[conn.index], 1)
}

// doneWrite notifies usbConnState, that write is done
func (state *usbConnState) doneWrite(conn *usbConn) {
	atomic.AddInt32(&state.write[conn.index], -1)
}

// String returns a string, representing connections state
func (state *usbConnState) String() string {
	buf := make([]byte, 0, 64)
	used := 0

	for i := range state.alloc {
		a := atomic.LoadInt32(&state.alloc[i])
		r := atomic.LoadInt32(&state.read[i])
		w := atomic.LoadInt32(&state.write[i])

		if len(buf) != 0 {
			buf = append(buf, ' ')
		}

		if a|r|w == 0 {
			buf = append(buf, '-', '-', '-')
		} else {
			used++

			if a != 0 {
				buf = append(buf, 'a')
			} else {
				buf = append(buf, '-')
			}

			if r != 0 {
				buf = append(buf, 'r')
			} else {
				buf = append(buf, '-')
			}

			if w != 0 {
				buf = append(buf, 'w')
			} else {
				buf = append(buf, '-')
			}
		}
	}

	return fmt.Sprintf("%d in use: %s", used, buf)
}
07070100000054000081A400000000000000000000000167D72F5D00000429000000000000000000000000000000000000001700000000ipp-usb-0.9.30/uuid.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * UUID normalizer
 */

package main

import (
	"bytes"
)

// UUIDNormalize parses an UUID and then reformats it into
// the standard form (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
//
// If input is not a valid UUID, it returns an empty string
// Many standard formats of UUIDs are recognized
func UUIDNormalize(uuid string) string {
	var buf [32]byte
	var cnt int

	in := bytes.ToLower([]byte(uuid))

	if bytes.HasPrefix(in, []byte("urn:")) {
		in = in[4:]
	}

	if bytes.HasPrefix(in, []byte("uuid:")) {
		in = in[5:]
	}

	for len(in) != 0 {
		c := in[0]
		in = in[1:]

		if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' {
			if cnt == 32 {
				return ""
			}

			buf[cnt] = c
			cnt++
		}
	}

	if cnt != 32 {
		return ""
	}

	return string(buf[0:8]) + "-" +
		string(buf[8:12]) + "-" +
		string(buf[12:16]) + "-" +
		string(buf[16:20]) + "-" +
		string(buf[20:32])
}
07070100000055000081A400000000000000000000000167D72F5D0000046E000000000000000000000000000000000000001C00000000ipp-usb-0.9.30/uuid_test.go/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * UUID normalizer test
 */

package main

import (
	"testing"
)

// Don't forget to update testData when ipp-ini.conf changes
var testDataUUID = []struct{ in, out string }{
	{"01234567-89ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
	{"01234567-89ab-cdef-0123-456789abcde", ""},
	{"01234567-89ab-cdef-0123-456789abcdef0", ""},
	{"urn:01234567-89ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
	{"urn:uuid:01234567-89ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
	{"0123456789abcdef0123456789abcdef", "01234567-89ab-cdef-0123-456789abcdef"},
	{"{0123456789abcdef0123456789abcdef}", "01234567-89ab-cdef-0123-456789abcdef"},
}

// Test .INI reader
func TestUUIDNormalize(t *testing.T) {
	for _, data := range testDataUUID {
		uuid := UUIDNormalize(data.in)
		if uuid != data.out {
			t.Errorf("UUIDNormalize(%q): expected %q, got %q", data.in, data.out, uuid)
		}
	}
}
07070100000056000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000001600000000ipp-usb-0.9.30/vendor07070100000057000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000002100000000ipp-usb-0.9.30/vendor/github.com07070100000058000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000002E00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting07070100000059000041ED00000000000000000000000267D72F5D00000000000000000000000000000000000000000000003400000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp0707010000005A000081A400000000000000000000000167D72F5D00000013000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/.gitignoreipp-usb
tags
*.swp
0707010000005B000081A400000000000000000000000167D72F5D0000052F000000000000000000000000000000000000003C00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/LICENSEBSD 2-Clause License

Copyright (c) 2020, Alexander Pevzner
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0707010000005C000081A400000000000000000000000167D72F5D00000034000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/Makefileall:
	-gotags -R . > tags
	go build

test:
	go test
0707010000005D000081A400000000000000000000000167D72F5D0000035A000000000000000000000000000000000000003E00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/README.md# goipp

[![godoc.org](https://godoc.org/github.com/OpenPrinting/goipp?status.svg)](http://godoc.org/github.com/OpenPrinting/goipp)
![GitHub](https://img.shields.io/github/license/OpenPrinting/goipp)
[![Go Report Card](https://goreportcard.com/badge/github.com/OpenPrinting/goipp)](https://goreportcard.com/report/github.com/OpenPrinting/goipp)

The goipp library is fairly complete implementation of IPP core protocol in
pure Go. Essentially, it is  IPP messages parser/composer. Transport is
not implemented here, because Go standard library has an excellent built-in
HTTP client, and it doesn't make a lot of sense to wrap it here.

High-level requests, like "print a file" are also not implemented, only the
low-level stuff.

All documentation is on godoc.org -- follow the link above. Pull requests
are welcomed, assuming they don't break existing API.
0707010000005E000081A400000000000000000000000167D72F5D0000001D000000000000000000000000000000000000004000000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/_config.ymltheme: jekyll-theme-architect0707010000005F000081A400000000000000000000000167D72F5D00000811000000000000000000000000000000000000003C00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/attr.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Message attributes
 */

package goipp

import (
	"fmt"
)

// Attributes represents a slice of attributes
type Attributes []Attribute

// Add Attribute to Attributes
func (attrs *Attributes) Add(attr Attribute) {
	*attrs = append(*attrs, attr)
}

// Equal checks that attrs and attrs2 are equal
func (attrs Attributes) Equal(attrs2 Attributes) bool {
	if len(attrs) != len(attrs2) {
		return false
	}

	for i, attr := range attrs {
		attr2 := attrs2[i]
		if !attr.Equal(attr2) {
			return false
		}
	}

	return true
}

// Attribute represents a single attribute, which consist of
// the Name and one or more Values
type Attribute struct {
	Name   string // Attribute name
	Values Values // Slice of values
}

// MakeAttribute makes Attribute with single value
func MakeAttribute(name string, tag Tag, value Value) Attribute {
	attr := Attribute{Name: name}
	attr.Values.Add(tag, value)
	return attr
}

// Equal checks that Attribute is equal to another Attribute
// (i.e., names are the same and values are equal)
func (a Attribute) Equal(a2 Attribute) bool {
	return a.Name == a2.Name && a.Values.Equal(a2.Values)
}

// Unpack attribute value from its wire representation
func (a *Attribute) unpack(tag Tag, value []byte) error {
	var err error
	var val Value

	switch tag.Type() {
	case TypeVoid, TypeCollection:
		val = Void{}

	case TypeInteger:
		val = Integer(0)

	case TypeBoolean:
		val = Boolean(false)

	case TypeString:
		val = String("")

	case TypeDateTime:
		val = Time{}

	case TypeResolution:
		val = Resolution{}

	case TypeRange:
		val = Range{}

	case TypeTextWithLang:
		val = TextWithLang{}

	case TypeBinary:
		val = Binary(nil)

	default:
		panic(fmt.Sprintf("(Attribute) uppack(): tag=%s type=%s", tag, tag.Type()))
	}

	val, err = val.decode(value)

	if err == nil {
		a.Values.Add(tag, val)
	} else {
		err = fmt.Errorf("%s: %s", tag, err)
	}

	return err
}
07070100000060000081A400000000000000000000000167D72F5D0000019D000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/const.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Various constants
 */

package goipp

const (
	// ContentType is the HTTP content type for IPP messages
	ContentType = "application/ipp"

	// msgPrintIndent used for indentation by message pretty-printer
	msgPrintIndent = "    "
)
07070100000061000081A400000000000000000000000167D72F5D00002614000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/decoder.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP Message decoder
 */

package goipp

import (
	"encoding/binary"
	"errors"
	"fmt"
	"io"
)

// DecoderOptions represents message decoder options
type DecoderOptions struct {
	// EnableWorkarounds, if set to true, enables various workarounds
	// for decoding IPP messages that violate IPP protocol specification
	//
	// Currently it includes the following workarounds:
	// * Pantum M7300FDW violates collection encoding rules.
	//   Instead of using TagMemberName, it uses named attributes
	//   within the collection
	//
	// The list of implemented workarounds may grow in the
	// future
	EnableWorkarounds bool
}

// messageDecoder represents Message decoder
type messageDecoder struct {
	in  io.Reader      // Input stream
	off int            // Offset of last read
	cnt int            // Count of read bytes
	opt DecoderOptions // Options
}

// Decode the message
func (md *messageDecoder) decode(m *Message) error {
	// Wire format:
	//
	//   2 bytes:  Version
	//   2 bytes:  Code (Operation or Status)
	//   4 bytes:  RequestID
	//   variable: attributes
	//   1 byte:   TagEnd

	// Parse message header
	var err error
	m.Version, err = md.decodeVersion()
	if err == nil {
		m.Code, err = md.decodeCode()
	}
	if err == nil {
		m.RequestID, err = md.decodeU32()
	}

	// Now parse attributes
	done := false
	var group *Attributes
	var attr Attribute
	var prev *Attribute

	for err == nil && !done {
		var tag Tag
		tag, err = md.decodeTag()

		if err != nil {
			break
		}

		if tag.IsDelimiter() {
			prev = nil
		}

		if tag.IsGroup() {
			m.Groups.Add(Group{tag, nil})
		}

		switch tag {
		case TagZero:
			err = errors.New("Invalid tag 0")
		case TagEnd:
			done = true

		case TagOperationGroup:
			group = &m.Operation
		case TagJobGroup:
			group = &m.Job
		case TagPrinterGroup:
			group = &m.Printer
		case TagUnsupportedGroup:
			group = &m.Unsupported
		case TagSubscriptionGroup:
			group = &m.Subscription
		case TagEventNotificationGroup:
			group = &m.EventNotification
		case TagResourceGroup:
			group = &m.Resource
		case TagDocumentGroup:
			group = &m.Document
		case TagSystemGroup:
			group = &m.System
		case TagFuture11Group:
			group = &m.Future11
		case TagFuture12Group:
			group = &m.Future12
		case TagFuture13Group:
			group = &m.Future13
		case TagFuture14Group:
			group = &m.Future14
		case TagFuture15Group:
			group = &m.Future15

		default:
			// Decode attribute
			if tag == TagMemberName || tag == TagEndCollection {
				err = fmt.Errorf("Unexpected tag %s", tag)
			} else {
				attr, err = md.decodeAttribute(tag)
			}

			if err == nil && tag == TagBeginCollection {
				attr.Values[0].V, err = md.decodeCollection()
			}

			// If everything is OK, save attribute
			switch {
			case err != nil:
			case attr.Name == "":
				if prev != nil {
					prev.Values.Add(attr.Values[0].T, attr.Values[0].V)

					// Append value to the last Attribute of the
					// last Group in the m.Groups
					//
					// Note, if we are here, this last Attribute definitely exists,
					// because:
					//   * prev != nil
					//   * prev is set when new named attribute is added
					//   * prev is reset when delimiter tag is encountered
					gLast := &m.Groups[len(m.Groups)-1]
					aLast := &gLast.Attrs[len(gLast.Attrs)-1]
					aLast.Values.Add(attr.Values[0].T, attr.Values[0].V)
				} else {
					err = errors.New("Additional value without preceding attribute")
				}
			case group != nil:
				group.Add(attr)
				prev = &(*group)[len(*group)-1]
				m.Groups[len(m.Groups)-1].Add(attr)
			default:
				err = errors.New("Attribute without a group")
			}
		}
	}

	if err != nil {
		err = fmt.Errorf("%s at 0x%x", err, md.off)
	}

	return err
}

// Decode a Collection
//
// Collection is like a nested object - an attribute which value is a sequence
// of named attributes. Collections can be nested.
//
// Wire format:
//   ATTR: Tag = TagBeginCollection,            - the outer attribute that
//         Name = "name", value - ignored         contains the collection
//
//   ATTR: Tag = TagMemberName, name = "",      - member name  \
//         value - string, name of the next                     |
//         member                                               | repeated for
//                                                              | each member
//   ATTR: Tag = any attribute tag, name = "",  - repeated for  |
//         value = member value                   multi-value  /
//                                                members
//
//   ATTR: Tag = TagEndCollection, name = "",
//         value - ignored
//
// The format looks a bit baroque, but please note that it was added
// in the IPP 2.0. For IPP 1.x collection looks like a single multi-value
// TagBeginCollection attribute (attributes without names considered
// next value for the previously defined named attributes) and so
// 1.x parser silently ignores collections and doesn't get confused
// with them.
func (md *messageDecoder) decodeCollection() (Collection, error) {
	collection := make(Collection, 0)

	memberName := ""

	for {
		tag, err := md.decodeTag()
		if err != nil {
			return nil, err
		}

		// Delimiter cannot be inside a collection
		if tag.IsDelimiter() {
			err = fmt.Errorf("Collection: unexpected tag %s", tag)
			return nil, err
		}

		// Check for TagMemberName without the subsequent value attribute
		if (tag == TagMemberName || tag == TagEndCollection) && memberName != "" {
			err = fmt.Errorf("Collection: unexpected %s, expected value tag", tag)
			return nil, err
		}

		// Fetch next attribute
		attr, err := md.decodeAttribute(tag)
		if err != nil {
			return nil, err
		}

		// Process next attribute
		switch tag {
		case TagEndCollection:
			return collection, nil

		case TagMemberName:
			memberName = string(attr.Values[0].V.(String))
			if memberName == "" {
				err = fmt.Errorf("Collection: %s value is empty", tag)
				return nil, err
			}

		case TagBeginCollection:
			// Decode nested collection
			attr.Values[0].V, err = md.decodeCollection()
			if err != nil {
				return nil, err
			}
			fallthrough

		default:
			if md.opt.EnableWorkarounds &&
				memberName == "" && attr.Name != "" {
				// Workaround for: Pantum M7300FDW
				//
				// This device violates collection encoding rules.
				// Instead of using TagMemberName, it uses named
				// attributes within the collection
				memberName = attr.Name
			}

			if memberName != "" {
				attr.Name = memberName
				collection = append(collection, attr)
				memberName = ""
			} else if len(collection) > 0 {
				l := len(collection)
				collection[l-1].Values.Add(tag, attr.Values[0].V)
			} else {
				// We've got a value without preceding TagMemberName
				err = fmt.Errorf("Collection: unexpected %s, expected %s", tag, TagMemberName)
				return nil, err
			}
		}
	}
}

// Decode a tag
func (md *messageDecoder) decodeTag() (Tag, error) {
	t, err := md.decodeU8()

	return Tag(t), err
}

// Decode a Version
func (md *messageDecoder) decodeVersion() (Version, error) {
	code, err := md.decodeU16()
	return Version(code), err
}

// Decode a Code
func (md *messageDecoder) decodeCode() (Code, error) {
	code, err := md.decodeU16()
	return Code(code), err
}

// Decode a single attribute
//
// Wire format:
//   1   byte:   Tag
//   2+N bytes:  Name length (2 bytes) + name string
//   2+N bytes:  Value length (2 bytes) + value bytes
//
// For the extended tag format, Tag is encoded as TagExtension and
// 4 bytes of the actual tag value prepended to the value bytes
func (md *messageDecoder) decodeAttribute(tag Tag) (Attribute, error) {
	var attr Attribute
	var value []byte
	var err error

	// Obtain attribute name and raw value
	attr.Name, err = md.decodeString()
	if err != nil {
		goto ERROR
	}

	value, err = md.decodeBytes()
	if err != nil {
		goto ERROR
	}

	// Handle TagExtension
	if tag == TagExtension {
		if len(value) < 4 {
			err = errors.New("Extension tag truncated")
			goto ERROR
		}

		t := binary.BigEndian.Uint32(value[:4])
		value = value[4:]

		if t > 0x7fffffff {
			err = errors.New("Extension tag out of range")
			goto ERROR
		}

		tag = Tag(t)
	}

	// Unpack value
	err = attr.unpack(tag, value)
	if err != nil {
		goto ERROR
	}

	return attr, nil

	// Return a error
ERROR:
	return Attribute{}, err
}

// Decode a 8-bit integer
func (md *messageDecoder) decodeU8() (uint8, error) {
	buf := make([]byte, 1)
	err := md.read(buf)
	return buf[0], err
}

// Decode a 16-bit integer
func (md *messageDecoder) decodeU16() (uint16, error) {
	buf := make([]byte, 2)
	err := md.read(buf)
	return binary.BigEndian.Uint16(buf[:]), err
}

// Decode a 32-bit integer
func (md *messageDecoder) decodeU32() (uint32, error) {
	buf := make([]byte, 4)
	err := md.read(buf)
	return binary.BigEndian.Uint32(buf[:]), err
}

// Decode sequence of bytes
func (md *messageDecoder) decodeBytes() ([]byte, error) {
	length, err := md.decodeU16()
	if err != nil {
		return nil, err
	}

	data := make([]byte, length)
	err = md.read(data)
	if err != nil {
		return nil, err
	}

	return data, nil
}

// Decode string
func (md *messageDecoder) decodeString() (string, error) {
	data, err := md.decodeBytes()
	if err != nil {
		return "", err
	}

	return string(data), nil
}

// Read a piece of raw data from input stream
func (md *messageDecoder) read(data []byte) error {
	md.off = md.cnt

	for len(data) > 0 {
		n, err := md.in.Read(data)
		if n > 0 {
			md.cnt += n
			data = data[n:]
		} else {
			md.off = md.cnt
			if err == nil || err == io.EOF {
				err = errors.New("Message truncated")
			}
			return err
		}

	}

	return nil
}
07070100000062000081A400000000000000000000000167D72F5D00001240000000000000000000000000000000000000003B00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/doc.go/* Go IPP - IPP core protocol implementation in pure Go
/*
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Package documentation
*/

/*
Package goipp implements IPP core protocol, as defined by RFC 8010

It doesn't implement high-level operations, such as "print a document",
"cancel print job" and so on. It's scope is limited to proper generation
and parsing of IPP requests and responses.

    IPP protocol uses the following simple model:
    1. Send a request
    2. Receive a response

Request and response both has a similar format, represented here
by type Message, with the only difference, that Code field of
that Message is the Operation code in request and Status code
in response. So most of operations are common for request and
response messages

# Example (Get-Printer-Attributes):
    package main

    import (
	    "bytes"
	    "net/http"
	    "os"

	    "github.com/OpenPrinting/goipp"
    )

    const uri = "http://192.168.1.102:631"

    // Build IPP OpGetPrinterAttributes request
    func makeRequest() ([]byte, error) {
	    m := goipp.NewRequest(goipp.DefaultVersion, goipp.OpGetPrinterAttributes, 1)
	    m.Operation.Add(goipp.MakeAttribute("attributes-charset",
		    goipp.TagCharset, goipp.String("utf-8")))
	    m.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
		    goipp.TagLanguage, goipp.String("en-US")))
	    m.Operation.Add(goipp.MakeAttribute("printer-uri",
		    goipp.TagURI, goipp.String(uri)))
	    m.Operation.Add(goipp.MakeAttribute("requested-attributes",
		    goipp.TagKeyword, goipp.String("all")))

	    return m.EncodeBytes()
    }

    // Check that there is no error
    func check(err error) {
	    if err != nil {
		    panic(err)
	    }
    }

    func main() {
	    request, err := makeRequest()
	    check(err)

	    resp, err := http.Post(uri, goipp.ContentType, bytes.NewBuffer(request))
	    check(err)

	    var respMsg goipp.Message

	    err = respMsg.Decode(resp.Body)
	    check(err)

	    respMsg.Print(os.Stdout, false)
    }

# Example (Print PDF file):

    package main

    import (
	    "bytes"
	    "errors"
	    "fmt"
	    "io"
	    "net/http"
	    "os"

	    "github.com/OpenPrinting/goipp"
    )

    const (
	    PrinterURL = "http://192.168.1.102:631/ipp/print"
	    TestPage   = "onepage-a4.pdf"
    )

    type T int

    // checkErr checks for an error. If err != nil, it prints error
    // message and exits
    func checkErr(err error, format string, args ...interface{}) {
	    if err != nil {
		    msg := fmt.Sprintf(format, args...)
		    fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
		    os.Exit(1)
	    }
    }

    // ExamplePrintPDF demo
    func main() {
	    // Build and encode IPP request
	    req := goipp.NewRequest(goipp.DefaultVersion, goipp.OpPrintJob, 1)
	    req.Operation.Add(goipp.MakeAttribute("attributes-charset",
		    goipp.TagCharset, goipp.String("utf-8")))
	    req.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
		    goipp.TagLanguage, goipp.String("en-US")))
	    req.Operation.Add(goipp.MakeAttribute("printer-uri",
		    goipp.TagURI, goipp.String(PrinterURL)))
	    req.Operation.Add(goipp.MakeAttribute("requesting-user-name",
		    goipp.TagName, goipp.String("John Doe")))
	    req.Operation.Add(goipp.MakeAttribute("job-name",
		    goipp.TagName, goipp.String("job name")))
	    req.Operation.Add(goipp.MakeAttribute("document-format",
		    goipp.TagMimeType, goipp.String("application/pdf")))

	    payload, err := req.EncodeBytes()
	    checkErr(err, "IPP encode")

	    // Open document file
	    file, err := os.Open(TestPage)
	    checkErr(err, "Open document file")

	    defer file.Close()

	    // Build HTTP request
	    body := io.MultiReader(bytes.NewBuffer(payload), file)

	    httpReq, err := http.NewRequest(http.MethodPost, PrinterURL, body)
	    checkErr(err, "HTTP")

	    httpReq.Header.Set("content-type", goipp.ContentType)
	    httpReq.Header.Set("accept", goipp.ContentType)
	    httpReq.Header.Set("accept-encoding", "gzip, deflate, identity")

	    // Execute HTTP request
	    httpRsp, err := http.DefaultClient.Do(httpReq)
	    if httpRsp != nil {
		    defer httpRsp.Body.Close()
	    }

	    checkErr(err, "HTTP")

	    if httpRsp.StatusCode/100 != 2 {
		    checkErr(errors.New(httpRsp.Status), "HTTP")
	    }

	    // Decode IPP response
	    rsp := &goipp.Message{}
	    err = rsp.Decode(httpRsp.Body)
	    checkErr(err, "IPP decode")

	    if goipp.Status(rsp.Code) != goipp.StatusOk {
		    err = errors.New(goipp.Status(rsp.Code).String())
		    checkErr(err, "IPP")
	    }
    }

*/
package goipp
07070100000063000081A400000000000000000000000167D72F5D00001334000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/encoder.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP Message encoder
 */

package goipp

import (
	"errors"
	"fmt"
	"io"
	"math"
)

// Type messageEncoder represents Message encoder
type messageEncoder struct {
	out io.Writer // Output stream
}

// Encode the message
func (me *messageEncoder) encode(m *Message) error {
	// Wire format:
	//
	//   2 bytes:  Version
	//   2 bytes:  Code (Operation or Status)
	//   4 bytes:  RequestID
	//   variable: attributes
	//   1 byte:   TagEnd

	// Encode message header
	var err error
	err = me.encodeU16(uint16(m.Version))
	if err == nil {
		err = me.encodeU16(uint16(m.Code))
	}
	if err == nil {
		err = me.encodeU32(uint32(m.RequestID))
	}

	// Encode attributes
	for _, grp := range m.attrGroups() {
		err = me.encodeTag(grp.Tag)
		if err == nil {
			for _, attr := range grp.Attrs {
				if attr.Name == "" {
					err = errors.New("Attribute without name")
				} else {
					err = me.encodeAttr(attr, true)
				}
			}
		}

		if err != nil {
			break
		}
	}

	if err == nil {
		err = me.encodeTag(TagEnd)
	}

	return err
}

// Encode attribute
func (me *messageEncoder) encodeAttr(attr Attribute, checkTag bool) error {
	// Wire format
	//     1 byte:   Tag
	//     2 bytes:  len(Name)
	//     variable: name
	//     2 bytes:  len(Value)
	//     variable  Value
	//
	// And each additional value comes as attribute
	// without name
	if len(attr.Values) == 0 {
		return errors.New("Attribute without value")
	}

	name := attr.Name
	for _, val := range attr.Values {
		tag := val.T

		if checkTag {
			if tag.IsDelimiter() || tag == TagMemberName || tag == TagEndCollection {
				return fmt.Errorf("Tag %s cannot be used with value", tag)
			}

			if uint(tag)&0x80000000 != 0 {
				return fmt.Errorf("Tag %s exceeds extension tag range", tag)
			}
		}

		var err error
		if tag >= 0x100 {
			err = me.encodeTag(TagExtension)
		} else {
			err = me.encodeTag(tag)
		}

		if err != nil {
			return err
		}

		err = me.encodeName(name)
		if err != nil {
			return err
		}

		err = me.encodeValue(val.T, val.V)
		if err != nil {
			return err
		}

		name = "" // Each additional value comes without name
	}

	return nil
}

// Encode 8-bit integer
func (me *messageEncoder) encodeU8(v uint8) error {
	return me.write([]byte{v})
}

// Encode 16-bit integer
func (me *messageEncoder) encodeU16(v uint16) error {
	return me.write([]byte{byte(v >> 8), byte(v)})
}

// Encode 32-bit integer
func (me *messageEncoder) encodeU32(v uint32) error {
	return me.write([]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}

// Encode Tag
func (me *messageEncoder) encodeTag(tag Tag) error {
	return me.encodeU8(byte(tag))
}

// Encode Attribute name
func (me *messageEncoder) encodeName(name string) error {
	if len(name) > math.MaxInt16 {
		return fmt.Errorf("Attribute name exceeds %d bytes",
			math.MaxInt16)
	}

	err := me.encodeU16(uint16(len(name)))
	if err == nil {
		err = me.write([]byte(name))
	}

	return err
}

// Encode Attribute value
func (me *messageEncoder) encodeValue(tag Tag, v Value) error {
	// Check Value type vs the Tag
	tagType := tag.Type()
	if tagType == TypeVoid {
		v = Void{} // Ignore supplied value
	} else if tagType != v.Type() {
		return fmt.Errorf("Tag %s: %s value required, %s present",
			tag, tagType, v.Type())
	}

	// Encode the value
	//
	// If tag >= 0x100, tag is replaced with TagExtension, and actual
	// tag value prepended to the data bytes. See RFC 8010, 3.5.2 for
	// details
	data, err := v.encode()
	if err != nil {
		return err
	}

	valueLen := len(data)
	if tag >= 0x100 {
		valueLen += 4 // Prepend extension tag value to the data
	}

	if valueLen > math.MaxInt16 {
		return fmt.Errorf("Attribute value exceeds %d bytes",
			math.MaxInt16)
	}

	err = me.encodeU16(uint16(valueLen))
	if err == nil && tag >= 0x100 {
		err = me.encodeU32(uint32(tag))
	}
	if err == nil {
		err = me.write(data)
	}

	// Handle collection
	if collection, ok := v.(Collection); ok {
		return me.encodeCollection(tag, collection)
	}

	return err
}

// Encode collection
func (me *messageEncoder) encodeCollection(tag Tag, collection Collection) error {
	for _, attr := range collection {
		if attr.Name == "" {
			return errors.New("Collection member without name")
		}

		attrName := MakeAttribute("", TagMemberName, String(attr.Name))

		err := me.encodeAttr(attrName, false)
		if err == nil {
			err = me.encodeAttr(Attribute{Name: "", Values: attr.Values}, true)
		}

		if err != nil {
			return err
		}
	}

	return me.encodeAttr(MakeAttribute("", TagEndCollection, Void{}), false)
}

// Write a piece of raw data to output stream
func (me *messageEncoder) write(data []byte) error {
	for len(data) > 0 {
		n, err := me.out.Write(data)
		if err != nil {
			return err
		}
		data = data[n:]
	}

	return nil
}
07070100000064000081A400000000000000000000000167D72F5D0000002E000000000000000000000000000000000000003B00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/go.modmodule github.com/OpenPrinting/goipp

go 1.11
07070100000065000081A400000000000000000000000167D72F5D000004B4000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/group.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Groups of attributes
 */

package goipp

// Group represents a group of attributes.
//
// Since 1.1.0
type Group struct {
	Tag   Tag        // Group tag
	Attrs Attributes // Group attributes
}

// Groups represents a sequence of groups
//
// The primary purpose of this type is to represent
// messages with repeated groups with the same group tag
//
// See Message type documentation for more details
//
// Since 1.1.0
type Groups []Group

// Add Attribute to the Group
func (g *Group) Add(attr Attribute) {
	g.Attrs.Add(attr)
}

// Equal checks that groups g and g2 are equal
func (g Group) Equal(g2 Group) bool {
	return g.Tag == g2.Tag && g.Attrs.Equal(g2.Attrs)
}

// Add Group to Groups
func (groups *Groups) Add(g Group) {
	*groups = append(*groups, g)
}

// Equal checks that groups and groups2 are equal
func (groups Groups) Equal(groups2 Groups) bool {
	if len(groups) != len(groups2) {
		return false
	}

	for i, g := range groups {
		g2 := groups2[i]
		if !g.Equal(g2) {
			return false
		}
	}

	return true
}
07070100000066000081A400000000000000000000000167D72F5D00000352000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/index.md# goipp

[![godoc.org](https://godoc.org/github.com/OpenPrinting/goipp?status.svg)](http://godoc.org/github.com/OpenPrinting/goipp)
![GitHub](https://img.shields.io/github/license/OpenPrinting/goipp)

The goipp library is fairly complete implementation of IPP core protocol in
pure Go. Essentially, it is  IPP messages parser/composer. Transport is
not implemented here, because Go standard library has an excellent built-in
HTTP client, and it doesn't make a lot of sense to wrap it here.

High-level requests, like "print a file" are also not implemented, only the
low-level stuff.

All documentation is on godoc.org -- follow the link above. Pull requests
are welcomed, assuming they don't break existing API.

For more information and software downloads, please visit the
[Project's page at GitHub](https://github.com/OpenPrinting/sane-airscan)

07070100000067000081A400000000000000000000000167D72F5D00001E50000000000000000000000000000000000000003F00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/message.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP protocol messages
 */

package goipp

import (
	"bytes"
	"fmt"
	"io"
)

// Code represents Op(operation) or Status codes
type Code uint16

// Version represents a protocol version. It consist
// of Major and Minor version codes, packed into a single
// 16-bit word
type Version uint16

// DefaultVersion is the default IPP version (2.0 for now)
const DefaultVersion Version = 0x0200

// MakeVersion makes version from major and minor parts
func MakeVersion(major, minor uint8) Version {
	return Version(major)<<8 | Version(minor)
}

// Major returns a major part of version
func (v Version) Major() uint8 {
	return uint8(v >> 8)
}

// Minor returns a minor part of version
func (v Version) Minor() uint8 {
	return uint8(v)
}

// String() converts version to string (i.e., "2.0")
func (v Version) String() string {
	return fmt.Sprintf("%d.%d", v.Major(), v.Minor())
}

// Message represents a single IPP message, which may be either
// client request or server response
type Message struct {
	// Common header
	Version   Version // Protocol version
	Code      Code    // Operation for request, status for response
	RequestID uint32  // Set in request, returned in response

	// Groups of Attributes
	//
	// This field allows to represent messages with repeated
	// groups of attributes with the same group tag. The most
	// noticeable use case is the Get-Jobs response which uses
	// multiple Job groups, one per returned job. See RFC 8011,
	// 4.2.6.2. for more details
	//
	// See also the following discussions which explain the demand
	// to implement this interface:
	//   https://github.com/OpenPrinting/goipp/issues/2
	//   https://github.com/OpenPrinting/goipp/pull/3
	//
	// With respect to backward compatibility, the following
	// behavior is implemented here:
	//   1. (*Message).Decode() fills both Groups and named per-group
	//      fields (i.e., Operation, Job etc)
	//   2. (*Message).Encode() and (*Message) Print, if Groups != nil,
	//      uses Groups and ignores  named per-group fields. Otherwise,
	//      named fields are used as in 1.0.0
	//   3. (*Message) Equal(), for each message uses Groups if
	//      it is not nil or named per-group fields otherwise.
	//      In another words, Equal() compares messages as if
	//      they were encoded
	//
	// Since 1.1.0
	Groups Groups

	// Attributes, by group
	Operation         Attributes // Operation attributes
	Job               Attributes // Job attributes
	Printer           Attributes // Printer attributes
	Unsupported       Attributes // Unsupported attributes
	Subscription      Attributes // Subscription attributes
	EventNotification Attributes // Event Notification attributes
	Resource          Attributes // Resource attributes
	Document          Attributes // Document attributes
	System            Attributes // System attributes
	Future11          Attributes // \
	Future12          Attributes //  \
	Future13          Attributes //   | Reserved for future extensions
	Future14          Attributes //  /
	Future15          Attributes // /
}

// NewRequest creates a new request message
//
// Use DefaultVersion as a first argument, if you don't
// have any specific needs
func NewRequest(v Version, op Op, id uint32) *Message {
	return &Message{
		Version:   v,
		Code:      Code(op),
		RequestID: id,
	}
}

// NewResponse creates a new response message
//
// Use DefaultVersion as a first argument, if you don't
func NewResponse(v Version, status Status, id uint32) *Message {
	return &Message{
		Version:   v,
		Code:      Code(status),
		RequestID: id,
	}
}

// Equal checks that two messages are equal
func (m Message) Equal(m2 Message) bool {
	if m.Version != m2.Version ||
		m.Code != m2.Code ||
		m.RequestID != m2.RequestID {
		return false
	}

	groups := m.attrGroups()
	groups2 := m2.attrGroups()

	return groups.Equal(groups2)
}

// Reset the message into initial state
func (m *Message) Reset() {
	*m = Message{}
}

// Encode message
func (m *Message) Encode(out io.Writer) error {
	me := messageEncoder{
		out: out,
	}

	return me.encode(m)
}

// EncodeBytes encodes message to byte slice
func (m *Message) EncodeBytes() ([]byte, error) {
	var buf bytes.Buffer

	err := m.Encode(&buf)
	return buf.Bytes(), err
}

// Decode reads message from io.Reader
func (m *Message) Decode(in io.Reader) error {
	return m.DecodeEx(in, DecoderOptions{})
}

// DecodeEx reads message from io.Reader
//
// It is extended version of the Decode method, with additional
// DecoderOptions parameter
func (m *Message) DecodeEx(in io.Reader, opt DecoderOptions) error {
	md := messageDecoder{
		in:  in,
		opt: opt,
	}

	m.Reset()
	return md.decode(m)
}

// DecodeBytes decodes message from byte slice
func (m *Message) DecodeBytes(data []byte) error {
	return m.Decode(bytes.NewBuffer(data))
}

// DecodeBytesEx decodes message from byte slice
//
// It is extended version of the DecodeBytes method, with additional
// DecoderOptions parameter
func (m *Message) DecodeBytesEx(data []byte, opt DecoderOptions) error {
	return m.DecodeEx(bytes.NewBuffer(data), opt)
}

// Print pretty-prints the message. The 'request' parameter affects
// interpretation of Message.Code: it is interpreted either
// as Op or as Status
func (m *Message) Print(out io.Writer, request bool) {
	out.Write([]byte("{\n"))

	fmt.Fprintf(out, msgPrintIndent+"VERSION %s\n", m.Version)

	if request {
		fmt.Fprintf(out, msgPrintIndent+"OPERATION %s\n", Op(m.Code))
	} else {
		fmt.Fprintf(out, msgPrintIndent+"STATUS %s\n", Status(m.Code))
	}

	for _, grp := range m.attrGroups() {
		fmt.Fprintf(out, "\n"+msgPrintIndent+"GROUP %s\n", grp.Tag)
		for _, attr := range grp.Attrs {
			m.printAttribute(out, attr, 1)
			out.Write([]byte("\n"))
		}
	}

	out.Write([]byte("}\n"))
}

// Pretty-print an attribute. Handles Collection attributes
// recursively
func (m *Message) printAttribute(out io.Writer, attr Attribute, indent int) {
	m.printIndent(out, indent)
	fmt.Fprintf(out, "ATTR %q", attr.Name)

	tag := TagZero
	for _, val := range attr.Values {
		if val.T != tag {
			fmt.Fprintf(out, " %s:", val.T)
			tag = val.T
		}

		if collection, ok := val.V.(Collection); ok {
			out.Write([]byte(" {\n"))
			for _, attr2 := range collection {
				m.printAttribute(out, attr2, indent+1)
				out.Write([]byte("\n"))
			}
			m.printIndent(out, indent)
			out.Write([]byte("}"))
		} else {
			fmt.Fprintf(out, " %s", val.V)
		}
	}
}

// Print indentation
func (m *Message) printIndent(out io.Writer, indent int) {
	for i := 0; i < indent; i++ {
		out.Write([]byte(msgPrintIndent))
	}
}

// Get attributes by group. Groups with nil Attributes are skipped,
// but groups with non-nil are not, even if len(Attributes) == 0
//
// This is a helper function for message encoder and pretty-printer
func (m *Message) attrGroups() Groups {
	// If m.Groups is set, use it
	if m.Groups != nil {
		return m.Groups
	}

	// Initialize slice of groups
	groups := Groups{
		{TagOperationGroup, m.Operation},
		{TagJobGroup, m.Job},
		{TagPrinterGroup, m.Printer},
		{TagUnsupportedGroup, m.Unsupported},
		{TagSubscriptionGroup, m.Subscription},
		{TagEventNotificationGroup, m.EventNotification},
		{TagResourceGroup, m.Resource},
		{TagDocumentGroup, m.Document},
		{TagSystemGroup, m.System},
		{TagFuture11Group, m.Future11},
		{TagFuture12Group, m.Future12},
		{TagFuture13Group, m.Future13},
		{TagFuture14Group, m.Future14},
		{TagFuture15Group, m.Future15},
	}

	// Skip all empty groups
	out := 0
	for in := 0; in < len(groups); in++ {
		if groups[in].Attrs != nil {
			groups[out] = groups[in]
			out++
		}
	}

	return groups[:out]
}
07070100000068000081A400000000000000000000000167D72F5D00004482000000000000000000000000000000000000003A00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/op.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP Operation Codes
 */

package goipp

import (
	"fmt"
)

// Op represents an IPP Operation Code
type Op Code

// Op codes
const (
	OpPrintJob             Op = 0x0002 // Print-Job: Print a single file
	OpPrintURI             Op = 0x0003 // Print-URI: Print a single URL
	OpValidateJob          Op = 0x0004 // Validate-Job: Validate job values prior to submission
	OpCreateJob            Op = 0x0005 // Create-Job: Create an empty print job
	OpSendDocument         Op = 0x0006 // Send-Document: Add a file to a job
	OpSendURI              Op = 0x0007 // Send-URI: Add a URL to a job
	OpCancelJob            Op = 0x0008 // Cancel-Job: Cancel a job
	OpGetJobAttributes     Op = 0x0009 // Get-Job-Attribute: Get information about a job
	OpGetJobs              Op = 0x000a // Get-Jobs: Get a list of jobs
	OpGetPrinterAttributes Op = 0x000b // Get-Printer-Attributes: Get information about a printer
	OpHoldJob              Op = 0x000c // Hold-Job: Hold a job for printing
	OpReleaseJob           Op = 0x000d // Release-Job: Release a job for printing
	OpRestartJob           Op = 0x000e // Restart-Job: Reprint a job

	OpPausePrinter               Op = 0x0010 // Pause-Printer: Stop a printer
	OpResumePrinter              Op = 0x0011 // Resume-Printer: Start a printer
	OpPurgeJobs                  Op = 0x0012 // Purge-Jobs: Delete all jobs
	OpSetPrinterAttributes       Op = 0x0013 // Set-Printer-Attributes: Set printer values
	OpSetJobAttributes           Op = 0x0014 // Set-Job-Attributes: Set job values
	OpGetPrinterSupportedValues  Op = 0x0015 // Get-Printer-Supported-Values: Get supported values
	OpCreatePrinterSubscriptions Op = 0x0016 // Create-Printer-Subscriptions: Create one or more printer subscriptions
	OpCreateJobSubscriptions     Op = 0x0017 // Create-Job-Subscriptions: Create one of more job subscriptions
	OpGetSubscriptionAttributes  Op = 0x0018 // Get-Subscription-Attributes: Get subscription information
	OpGetSubscriptions           Op = 0x0019 // Get-Subscriptions: Get list of subscriptions
	OpRenewSubscription          Op = 0x001a // Renew-Subscription: Renew a printer subscription
	OpCancelSubscription         Op = 0x001b // Cancel-Subscription: Cancel a subscription
	OpGetNotifications           Op = 0x001c // Get-Notifications: Get notification events
	OpSendNotifications          Op = 0x001d // Send-Notifications: Send notification events
	OpGetResourceAttributes      Op = 0x001e // Get-Resource-Attributes: Get resource information
	OpGetResourceData            Op = 0x001f // Get-Resource-Data: Get resource data

	OpGetResources                Op = 0x0020 // Get-Resources: Get list of resources
	OpGetPrintSupportFiles        Op = 0x0021 // Get-Printer-Support-Files: Get printer support files
	OpEnablePrinter               Op = 0x0022 // Enable-Printer: Accept new jobs for a printer
	OpDisablePrinter              Op = 0x0023 // Disable-Printer: Reject new jobs for a printer
	OpPausePrinterAfterCurrentJob Op = 0x0024 // Pause-Printer-After-Current-Job: Stop printer after the current job
	OpHoldNewJobs                 Op = 0x0025 // Hold-New-Jobs: Hold new jobs
	OpReleaseHeldNewJobs          Op = 0x0026 // Release-Held-New-Jobs: Release new jobs that were previously held
	OpDeactivatePrinter           Op = 0x0027 // Deactivate-Printer: Stop a printer and do not accept jobs
	OpActivatePrinter             Op = 0x0028 // Activate-Printer: Start a printer and accept jobs
	OpRestartPrinter              Op = 0x0029 // Restart-Printer: Restart a printer
	OpShutdownPrinter             Op = 0x002a // Shutdown-Printer: Turn a printer off
	OpStartupPrinter              Op = 0x002b // Startup-Printer: Turn a printer on
	OpReprocessJob                Op = 0x002c // Reprocess-Job: Reprint a job
	OpCancelCurrentJob            Op = 0x002d // Cancel-Current-Job: Cancel the current job
	OpSuspendCurrentJob           Op = 0x002e // Suspend-Current-Job: Suspend the current job
	OpResumeJob                   Op = 0x002f // Resume-Job: Resume the current job

	OpPromoteJob            Op = 0x0030 // Promote-Job: Promote a job to print sooner
	OpScheduleJobAfter      Op = 0x0031 // Schedule-Job-After: Schedule a job to print after another
	OpCancelDocument        Op = 0x0033 // Cancel-Document: Cancel a document
	OpGetDocumentAttributes Op = 0x0034 // Get-Document-Attributes: Get document information
	OpGetDocuments          Op = 0x0035 // Get-Documents: Get a list of documents in a job
	OpDeleteDocument        Op = 0x0036 // Delete-Document: Delete a document
	OpSetDocumentAttributes Op = 0x0037 // Set-Document-Attributes: Set document values
	OpCancelJobs            Op = 0x0038 // Cancel-Jobs: Cancel all jobs (administrative)
	OpCancelMyJobs          Op = 0x0039 // Cancel-My-Jobs: Cancel a user's jobs
	OpResubmitJob           Op = 0x003a // Resubmit-Job: Copy and reprint a job
	OpCloseJob              Op = 0x003b // Close-Job: Close a job and start printing
	OpIdentifyPrinter       Op = 0x003c // Identify-Printer: Make the printer beep, flash, or display a message for identification
	OpValidateDocument      Op = 0x003d // Validate-Document: Validate document values prior to submission
	OpAddDocumentImages     Op = 0x003e // Add-Document-Images: Add image(s) from the specified scanner source
	OpAcknowledgeDocument   Op = 0x003f // Acknowledge-Document: Acknowledge processing of a document

	OpAcknowledgeIdentifyPrinter   Op = 0x0040 // Acknowledge-Identify-Printer: Acknowledge action on an Identify-Printer request
	OpAcknowledgeJob               Op = 0x0041 // Acknowledge-Job: Acknowledge processing of a job
	OpFetchDocument                Op = 0x0042 // Fetch-Document: Fetch a document for processing
	OpFetchJob                     Op = 0x0043 // Fetch-Job: Fetch a job for processing
	OpGetOutputDeviceAttributes    Op = 0x0044 // Get-Output-Device-Attributes: Get printer information for a specific output device
	OpUpdateActiveJobs             Op = 0x0045 // Update-Active-Jobs: Update the list of active jobs that a proxy has processed
	OpDeregisterOutputDevice       Op = 0x0046 // Deregister-Output-Device: Remove an output device
	OpUpdateDocumentStatus         Op = 0x0047 // Update-Document-Status: Update document values
	OpUpdateJobStatus              Op = 0x0048 // Update-Job-Status: Update job values
	OpupdateOutputDeviceAttributes Op = 0x0049 // Update-Output-Device-Attributes: Update output device values
	OpGetNextDocumentData          Op = 0x004a // Get-Next-Document-Data: Scan more document data
	OpAllocatePrinterResources     Op = 0x004b // Allocate-Printer-Resources: Use resources for a printer
	OpCreatePrinter                Op = 0x004c // Create-Printer: Create a new service
	OpDeallocatePrinterResources   Op = 0x004d // Deallocate-Printer-Resources: Stop using resources for a printer
	OpDeletePrinter                Op = 0x004e // Delete-Printer: Delete an existing service
	OpGetPrinters                  Op = 0x004f // Get-Printers: Get a list of services

	OpShutdownOnePrinter              Op = 0x0050 // Shutdown-One-Printer: Shutdown a service
	OpStartupOnePrinter               Op = 0x0051 // Startup-One-Printer: Start a service
	OpCancelResource                  Op = 0x0052 // Cancel-Resource: Uninstall a resource
	OpCreateResource                  Op = 0x0053 // Create-Resource: Create a new (empty) resource
	OpInstallResource                 Op = 0x0054 // Install-Resource: Install a resource
	OpSendResourceData                Op = 0x0055 // Send-Resource-Data: Upload the data for a resource
	OpSetResourceAttributes           Op = 0x0056 // Set-Resource-Attributes: Set resource object  attributes
	OpCreateResourceSubscriptions     Op = 0x0057 // Create-Resource-Subscriptions: Create event subscriptions for a resource
	OpCreateSystemSubscriptions       Op = 0x0058 // Create-System-Subscriptions: Create event subscriptions for a system
	OpDisableAllPrinters              Op = 0x0059 // Disable-All-Printers: Stop accepting new jobs on all services
	OpEnableAllPrinters               Op = 0x005a // Enable-All-Printers: Start accepting new jobs on all services
	OpGetSystemAttributes             Op = 0x005b // Get-System-Attributes: Get system object attributes
	OpGetSystemSupportedValues        Op = 0x005c // Get-System-Supported-Values: Get supported values for system object attributes
	OpPauseAllPrinters                Op = 0x005d // Pause-All-Printers: Stop all services immediately
	OpPauseAllPrintersAfterCurrentJob Op = 0x005e // Pause-All-Printers-After-Current-Job: Stop all services after processing the current jobs
	OpRegisterOutputDevice            Op = 0x005f // Register-Output-Device: Register a remote service

	OpRestartSystem       Op = 0x0060 // Restart-System: Restart all services
	OpResumeAllPrinters   Op = 0x0061 // Resume-All-Printers: Start job processing on all services
	OpSetSystemAttributes Op = 0x0062 // Set-System-Attributes: Set system object attributes
	OpShutdownAllPrinters Op = 0x0063 // Shutdown-All-Printers: Shutdown all services
	OpStartupAllPrinters  Op = 0x0064 // Startup-All-Printers: Startup all services

	OpCupsGetDefault       Op = 0x4001 // CUPS-Get-Default: Get the default printer
	OpCupsGetPrinters      Op = 0x4002 // CUPS-Get-Printers: Get a list of printers and/or classes
	OpCupsAddModifyPrinter Op = 0x4003 // CUPS-Add-Modify-Printer: Add or modify a printer
	OpCupsDeletePrinter    Op = 0x4004 // CUPS-Delete-Printer: Delete a printer
	OpCupsGetClasses       Op = 0x4005 // CUPS-Get-Classes: Get a list of classes
	OpCupsAddModifyClass   Op = 0x4006 // CUPS-Add-Modify-Class: Add or modify a class
	OpCupsDeleteClass      Op = 0x4007 // CUPS-Delete-Class: Delete a class
	OpCupsAcceptJobs       Op = 0x4008 // CUPS-Accept-Jobs: Accept new jobs on a printer
	OpCupsRejectJobs       Op = 0x4009 // CUPS-Reject-Jobs: Reject new jobs on a printer
	OpCupsSetDefault       Op = 0x400a // CUPS-Set-Default: Set the default printer
	OpCupsGetDevices       Op = 0x400b // CUPS-Get-Devices: Get a list of supported devices
	OpCupsGetPpds          Op = 0x400c // CUPS-Get-PPDs: Get a list of supported drivers
	OpCupsMoveJob          Op = 0x400d // CUPS-Move-Job: Move a job to a different printer
	OpCupsAuthenticateJob  Op = 0x400e // CUPS-Authenticate-Job: Authenticate a job
	OpCupsGetPpd           Op = 0x400f // CUPS-Get-PPD: Get a PPD file

	OpCupsGetDocument        Op = 0x4027 // CUPS-Get-Document: Get a document file
	OpCupsCreateLocalPrinter Op = 0x4028 // CUPS-Create-Local-Printer: Create a local (temporary) printer

)

// String() returns a Status name, as defined by RFC 8010
func (op Op) String() string {
	if int(op) < len(opNames) {
		if s := opNames[op]; s != "" {
			return s
		}
	}

	return fmt.Sprintf("0x%4.4x", int(op))
}

var opNames = [...]string{
	OpPrintJob:                        "Print-Job",
	OpPrintURI:                        "Print-URI",
	OpValidateJob:                     "Validate-Job",
	OpCreateJob:                       "Create-Job",
	OpSendDocument:                    "Send-Document",
	OpSendURI:                         "Send-URI",
	OpCancelJob:                       "Cancel-Job",
	OpGetJobAttributes:                "Get-Job-Attribute",
	OpGetJobs:                         "Get-Jobs",
	OpGetPrinterAttributes:            "Get-Printer-Attributes",
	OpHoldJob:                         "Hold-Job",
	OpReleaseJob:                      "Release-Job",
	OpRestartJob:                      "Restart-Job",
	OpPausePrinter:                    "Pause-Printer",
	OpResumePrinter:                   "Resume-Printer",
	OpPurgeJobs:                       "Purge-Jobs",
	OpSetPrinterAttributes:            "Set-Printer-Attributes",
	OpSetJobAttributes:                "Set-Job-Attributes",
	OpGetPrinterSupportedValues:       "Get-Printer-Supported-Values",
	OpCreatePrinterSubscriptions:      "Create-Printer-Subscriptions",
	OpCreateJobSubscriptions:          "Create-Job-Subscriptions",
	OpGetSubscriptionAttributes:       "Get-Subscription-Attributes",
	OpGetSubscriptions:                "Get-Subscriptions",
	OpRenewSubscription:               "Renew-Subscription",
	OpCancelSubscription:              "Cancel-Subscription",
	OpGetNotifications:                "Get-Notifications",
	OpSendNotifications:               "Send-Notifications",
	OpGetResourceAttributes:           "Get-Resource-Attributes",
	OpGetResourceData:                 "Get-Resource-Data",
	OpGetResources:                    "Get-Resources",
	OpGetPrintSupportFiles:            "Get-Printer-Support-Files",
	OpEnablePrinter:                   "Enable-Printer",
	OpDisablePrinter:                  "Disable-Printer",
	OpPausePrinterAfterCurrentJob:     "Pause-Printer-After-Current-Job",
	OpHoldNewJobs:                     "Hold-New-Jobs",
	OpReleaseHeldNewJobs:              "Release-Held-New-Jobs",
	OpDeactivatePrinter:               "Deactivate-Printer",
	OpActivatePrinter:                 "Activate-Printer",
	OpRestartPrinter:                  "Restart-Printer",
	OpShutdownPrinter:                 "Shutdown-Printer",
	OpStartupPrinter:                  "Startup-Printer",
	OpReprocessJob:                    "Reprocess-Job",
	OpCancelCurrentJob:                "Cancel-Current-Job",
	OpSuspendCurrentJob:               "Suspend-Current-Job",
	OpResumeJob:                       "Resume-Job",
	OpPromoteJob:                      "Promote-Job",
	OpScheduleJobAfter:                "Schedule-Job-After",
	OpCancelDocument:                  "Cancel-Document",
	OpGetDocumentAttributes:           "Get-Document-Attributes",
	OpGetDocuments:                    "Get-Documents",
	OpDeleteDocument:                  "Delete-Document",
	OpSetDocumentAttributes:           "Set-Document-Attributes",
	OpCancelJobs:                      "Cancel-Jobs",
	OpCancelMyJobs:                    "Cancel-My-Jobs",
	OpResubmitJob:                     "Resubmit-Job",
	OpCloseJob:                        "Close-Job",
	OpIdentifyPrinter:                 "Identify-Printer",
	OpValidateDocument:                "Validate-Document",
	OpAddDocumentImages:               "Add-Document-Images",
	OpAcknowledgeDocument:             "Acknowledge-Document",
	OpAcknowledgeIdentifyPrinter:      "Acknowledge-Identify-Printer",
	OpAcknowledgeJob:                  "Acknowledge-Job",
	OpFetchDocument:                   "Fetch-Document",
	OpFetchJob:                        "Fetch-Job",
	OpGetOutputDeviceAttributes:       "Get-Output-Device-Attributes",
	OpUpdateActiveJobs:                "Update-Active-Jobs",
	OpDeregisterOutputDevice:          "Deregister-Output-Device",
	OpUpdateDocumentStatus:            "Update-Document-Status",
	OpUpdateJobStatus:                 "Update-Job-Status",
	OpupdateOutputDeviceAttributes:    "Update-Output-Device-Attributes",
	OpGetNextDocumentData:             "Get-Next-Document-Data",
	OpAllocatePrinterResources:        "Allocate-Printer-Resources",
	OpCreatePrinter:                   "Create-Printer",
	OpDeallocatePrinterResources:      "Deallocate-Printer-Resources",
	OpDeletePrinter:                   "Delete-Printer",
	OpGetPrinters:                     "Get-Printers",
	OpShutdownOnePrinter:              "Shutdown-One-Printer",
	OpStartupOnePrinter:               "Startup-One-Printer",
	OpCancelResource:                  "Cancel-Resource",
	OpCreateResource:                  "Create-Resource",
	OpInstallResource:                 "Install-Resource",
	OpSendResourceData:                "Send-Resource-Data",
	OpSetResourceAttributes:           "Set-Resource-Attributes",
	OpCreateResourceSubscriptions:     "Create-Resource-Subscriptions",
	OpCreateSystemSubscriptions:       "Create-System-Subscriptions",
	OpDisableAllPrinters:              "Disable-All-Printers",
	OpEnableAllPrinters:               "Enable-All-Printers",
	OpGetSystemAttributes:             "Get-System-Attributes",
	OpGetSystemSupportedValues:        "Get-System-Supported-Values",
	OpPauseAllPrinters:                "Pause-All-Printers",
	OpPauseAllPrintersAfterCurrentJob: "Pause-All-Printers-After-Current-Job",
	OpRegisterOutputDevice:            "Register-Output-Device",
	OpRestartSystem:                   "Restart-System",
	OpResumeAllPrinters:               "Resume-All-Printers",
	OpSetSystemAttributes:             "Set-System-Attributes",
	OpShutdownAllPrinters:             "Shutdown-All-Printers",
	OpStartupAllPrinters:              "Startup-All-Printers",
	OpCupsGetDefault:                  "CUPS-Get-Default",
	OpCupsGetPrinters:                 "CUPS-Get-Printers",
	OpCupsAddModifyPrinter:            "CUPS-Add-Modify-Printer",
	OpCupsDeletePrinter:               "CUPS-Delete-Printer",
	OpCupsGetClasses:                  "CUPS-Get-Classes",
	OpCupsAddModifyClass:              "CUPS-Add-Modify-Class",
	OpCupsDeleteClass:                 "CUPS-Delete-Class",
	OpCupsAcceptJobs:                  "CUPS-Accept-Jobs",
	OpCupsRejectJobs:                  "CUPS-Reject-Jobs",
	OpCupsSetDefault:                  "CUPS-Set-Default",
	OpCupsGetDevices:                  "CUPS-Get-Devices",
	OpCupsGetPpds:                     "CUPS-Get-PPDs",
	OpCupsMoveJob:                     "CUPS-Move-Job",
	OpCupsAuthenticateJob:             "CUPS-Authenticate-Job",
	OpCupsGetPpd:                      "CUPS-Get-PPD",
	OpCupsGetDocument:                 "CUPS-Get-Document",
	OpCupsCreateLocalPrinter:          "CUPS-Create-Local-Printer",
}
07070100000069000081A400000000000000000000000167D72F5D000026C6000000000000000000000000000000000000003E00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/status.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP Status Codes
 */

package goipp

import (
	"fmt"
)

// Status represents an IPP Status Code
type Status Code

// Status codes
const (
	StatusOk                              Status = 0x0000 // successful-ok
	StatusOkIgnoredOrSubstituted          Status = 0x0001 // successful-ok-ignored-or-substituted-attributes
	StatusOkConflicting                   Status = 0x0002 // successful-ok-conflicting-attributes
	StatusOkIgnoredSubscriptions          Status = 0x0003 // successful-ok-ignored-subscriptions
	StatusOkIgnoredNotifications          Status = 0x0004 // successful-ok-ignored-notifications
	StatusOkTooManyEvents                 Status = 0x0005 // successful-ok-too-many-events
	StatusOkButCancelSubscription         Status = 0x0006 // successful-ok-but-cancel-subscription
	StatusOkEventsComplete                Status = 0x0007 // successful-ok-events-complete
	StatusRedirectionOtherSite            Status = 0x0200 // redirection-other-site
	StatusCupsSeeOther                    Status = 0x0280 // cups-see-other
	StatusErrorBadRequest                 Status = 0x0400 // client-error-bad-request
	StatusErrorForbidden                  Status = 0x0401 // client-error-forbidden
	StatusErrorNotAuthenticated           Status = 0x0402 // client-error-not-authenticated
	StatusErrorNotAuthorized              Status = 0x0403 // client-error-not-authorized
	StatusErrorNotPossible                Status = 0x0404 // client-error-not-possible
	StatusErrorTimeout                    Status = 0x0405 // client-error-timeout
	StatusErrorNotFound                   Status = 0x0406 // client-error-not-found
	StatusErrorGone                       Status = 0x0407 // client-error-gone
	StatusErrorRequestEntity              Status = 0x0408 // client-error-request-entity-too-large
	StatusErrorRequestValue               Status = 0x0409 // client-error-request-value-too-long
	StatusErrorDocumentFormatNotSupported Status = 0x040a // client-error-document-format-not-supported
	StatusErrorAttributesOrValues         Status = 0x040b // client-error-attributes-or-values-not-supported
	StatusErrorURIScheme                  Status = 0x040c // client-error-uri-scheme-not-supported
	StatusErrorCharset                    Status = 0x040d // client-error-charset-not-supported
	StatusErrorConflicting                Status = 0x040e // client-error-conflicting-attributes
	StatusErrorCompressionNotSupported    Status = 0x040f // client-error-compression-not-supported
	StatusErrorCompressionError           Status = 0x0410 // client-error-compression-error
	StatusErrorDocumentFormatError        Status = 0x0411 // client-error-document-format-error
	StatusErrorDocumentAccess             Status = 0x0412 // client-error-document-access-error
	StatusErrorAttributesNotSettable      Status = 0x0413 // client-error-attributes-not-settable
	StatusErrorIgnoredAllSubscriptions    Status = 0x0414 // client-error-ignored-all-subscriptions
	StatusErrorTooManySubscriptions       Status = 0x0415 // client-error-too-many-subscriptions
	StatusErrorIgnoredAllNotifications    Status = 0x0416 // client-error-ignored-all-notifications
	StatusErrorPrintSupportFileNotFound   Status = 0x0417 // client-error-print-support-file-not-found
	StatusErrorDocumentPassword           Status = 0x0418 // client-error-document-password-error
	StatusErrorDocumentPermission         Status = 0x0419 // client-error-document-permission-error
	StatusErrorDocumentSecurity           Status = 0x041a // client-error-document-security-error
	StatusErrorDocumentUnprintable        Status = 0x041b // client-error-document-unprintable-error
	StatusErrorAccountInfoNeeded          Status = 0x041c // client-error-account-info-needed
	StatusErrorAccountClosed              Status = 0x041d // client-error-account-closed
	StatusErrorAccountLimitReached        Status = 0x041e // client-error-account-limit-reached
	StatusErrorAccountAuthorizationFailed Status = 0x041f // client-error-account-authorization-failed
	StatusErrorNotFetchable               Status = 0x0420 // client-error-not-fetchable
	StatusErrorInternal                   Status = 0x0500 // server-error-internal-error
	StatusErrorOperationNotSupported      Status = 0x0501 // server-error-operation-not-supported
	StatusErrorServiceUnavailable         Status = 0x0502 // server-error-service-unavailable
	StatusErrorVersionNotSupported        Status = 0x0503 // server-error-version-not-supported
	StatusErrorDevice                     Status = 0x0504 // server-error-device-error
	StatusErrorTemporary                  Status = 0x0505 // server-error-temporary-error
	StatusErrorNotAcceptingJobs           Status = 0x0506 // server-error-not-accepting-jobs
	StatusErrorBusy                       Status = 0x0507 // server-error-busy
	StatusErrorJobCanceled                Status = 0x0508 // server-error-job-canceled
	StatusErrorMultipleJobsNotSupported   Status = 0x0509 // server-error-multiple-document-jobs-not-supported
	StatusErrorPrinterIsDeactivated       Status = 0x050a // server-error-printer-is-deactivated
	StatusErrorTooManyJobs                Status = 0x050b // server-error-too-many-jobs
	StatusErrorTooManyDocuments           Status = 0x050c // server-error-too-many-documents
)

// String() returns a Status name, as defined by RFC 8010
func (status Status) String() string {
	if int(status) < len(statusNames) {
		if s := statusNames[status]; s != "" {
			return s
		}
	}

	return fmt.Sprintf("0x%4.4x", int(status))
}

var statusNames = [...]string{
	StatusOk:                              "successful-ok",
	StatusOkIgnoredOrSubstituted:          "successful-ok-ignored-or-substituted-attributes",
	StatusOkConflicting:                   "successful-ok-conflicting-attributes",
	StatusOkIgnoredSubscriptions:          "successful-ok-ignored-subscriptions",
	StatusOkIgnoredNotifications:          "successful-ok-ignored-notifications",
	StatusOkTooManyEvents:                 "successful-ok-too-many-events",
	StatusOkButCancelSubscription:         "successful-ok-but-cancel-subscription",
	StatusOkEventsComplete:                "successful-ok-events-complete",
	StatusRedirectionOtherSite:            "redirection-other-site",
	StatusCupsSeeOther:                    "cups-see-other",
	StatusErrorBadRequest:                 "client-error-bad-request",
	StatusErrorForbidden:                  "client-error-forbidden",
	StatusErrorNotAuthenticated:           "client-error-not-authenticated",
	StatusErrorNotAuthorized:              "client-error-not-authorized",
	StatusErrorNotPossible:                "client-error-not-possible",
	StatusErrorTimeout:                    "client-error-timeout",
	StatusErrorNotFound:                   "client-error-not-found",
	StatusErrorGone:                       "client-error-gone",
	StatusErrorRequestEntity:              "client-error-request-entity-too-large",
	StatusErrorRequestValue:               "client-error-request-value-too-long",
	StatusErrorDocumentFormatNotSupported: "client-error-document-format-not-supported",
	StatusErrorAttributesOrValues:         "client-error-attributes-or-values-not-supported",
	StatusErrorURIScheme:                  "client-error-uri-scheme-not-supported",
	StatusErrorCharset:                    "client-error-charset-not-supported",
	StatusErrorConflicting:                "client-error-conflicting-attributes",
	StatusErrorCompressionNotSupported:    "client-error-compression-not-supported",
	StatusErrorCompressionError:           "client-error-compression-error",
	StatusErrorDocumentFormatError:        "client-error-document-format-error",
	StatusErrorDocumentAccess:             "client-error-document-access-error",
	StatusErrorAttributesNotSettable:      "client-error-attributes-not-settable",
	StatusErrorIgnoredAllSubscriptions:    "client-error-ignored-all-subscriptions",
	StatusErrorTooManySubscriptions:       "client-error-too-many-subscriptions",
	StatusErrorIgnoredAllNotifications:    "client-error-ignored-all-notifications",
	StatusErrorPrintSupportFileNotFound:   "client-error-print-support-file-not-found",
	StatusErrorDocumentPassword:           "client-error-document-password-error",
	StatusErrorDocumentPermission:         "client-error-document-permission-error",
	StatusErrorDocumentSecurity:           "client-error-document-security-error",
	StatusErrorDocumentUnprintable:        "client-error-document-unprintable-error",
	StatusErrorAccountInfoNeeded:          "client-error-account-info-needed",
	StatusErrorAccountClosed:              "client-error-account-closed",
	StatusErrorAccountLimitReached:        "client-error-account-limit-reached",
	StatusErrorAccountAuthorizationFailed: "client-error-account-authorization-failed",
	StatusErrorNotFetchable:               "client-error-not-fetchable",
	StatusErrorInternal:                   "server-error-internal-error",
	StatusErrorOperationNotSupported:      "server-error-operation-not-supported",
	StatusErrorServiceUnavailable:         "server-error-service-unavailable",
	StatusErrorVersionNotSupported:        "server-error-version-not-supported",
	StatusErrorDevice:                     "server-error-device-error",
	StatusErrorTemporary:                  "server-error-temporary-error",
	StatusErrorNotAcceptingJobs:           "server-error-not-accepting-jobs",
	StatusErrorBusy:                       "server-error-busy",
	StatusErrorJobCanceled:                "server-error-job-canceled",
	StatusErrorMultipleJobsNotSupported:   "server-error-multiple-document-jobs-not-supported",
	StatusErrorPrinterIsDeactivated:       "server-error-printer-is-deactivated",
	StatusErrorTooManyJobs:                "server-error-too-many-jobs",
	StatusErrorTooManyDocuments:           "server-error-too-many-documents",
}
0707010000006A000081A400000000000000000000000167D72F5D00001735000000000000000000000000000000000000003B00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/tag.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP Tags
 */

package goipp

import (
	"fmt"
)

// Tag represents a tag used in a binary representation
// of the IPP message
type Tag int

// Tag values
const (
	// Delimiter tags
	TagZero                   Tag = 0x00 // Zero tag - used for separators
	TagOperationGroup         Tag = 0x01 // Operation group
	TagJobGroup               Tag = 0x02 // Job group
	TagEnd                    Tag = 0x03 // End-of-attributes
	TagPrinterGroup           Tag = 0x04 // Printer group
	TagUnsupportedGroup       Tag = 0x05 // Unsupported attributes group
	TagSubscriptionGroup      Tag = 0x06 // Subscription group
	TagEventNotificationGroup Tag = 0x07 // Event group
	TagResourceGroup          Tag = 0x08 // Resource group
	TagDocumentGroup          Tag = 0x09 // Document group
	TagSystemGroup            Tag = 0x0a // System group
	TagFuture11Group          Tag = 0x0b // Future group 11
	TagFuture12Group          Tag = 0x0c // Future group 12
	TagFuture13Group          Tag = 0x0d // Future group 13
	TagFuture14Group          Tag = 0x0e // Future group 14
	TagFuture15Group          Tag = 0x0f // Future group 15

	// Value tags
	TagUnsupportedValue Tag = 0x10 // Unsupported value
	TagDefault          Tag = 0x11 // Default value
	TagUnknown          Tag = 0x12 // Unknown value
	TagNoValue          Tag = 0x13 // No-value value
	TagNotSettable      Tag = 0x15 // Not-settable value
	TagDeleteAttr       Tag = 0x16 // Delete-attribute value
	TagAdminDefine      Tag = 0x17 // Admin-defined value
	TagInteger          Tag = 0x21 // Integer value
	TagBoolean          Tag = 0x22 // Boolean value
	TagEnum             Tag = 0x23 // Enumeration value
	TagString           Tag = 0x30 // Octet string value
	TagDateTime         Tag = 0x31 // Date/time value
	TagResolution       Tag = 0x32 // Resolution value
	TagRange            Tag = 0x33 // Range value
	TagBeginCollection  Tag = 0x34 // Beginning of collection value
	TagTextLang         Tag = 0x35 // Text-with-language value
	TagNameLang         Tag = 0x36 // Name-with-language value
	TagEndCollection    Tag = 0x37 // End of collection value
	TagText             Tag = 0x41 // Text value
	TagName             Tag = 0x42 // Name value
	TagReservedString   Tag = 0x43 // Reserved for future string value
	TagKeyword          Tag = 0x44 // Keyword value
	TagURI              Tag = 0x45 // URI value
	TagURIScheme        Tag = 0x46 // URI scheme value
	TagCharset          Tag = 0x47 // Character set value
	TagLanguage         Tag = 0x48 // Language value
	TagMimeType         Tag = 0x49 // MIME media type value
	TagMemberName       Tag = 0x4a // Collection member name value
	TagExtension        Tag = 0x7f // Extension point for 32-bit tags
)

// IsDelimiter returns true for delimiter tags
func (tag Tag) IsDelimiter() bool {
	return uint(tag) < 0x10
}

// IsGroup returns true for group tags
func (tag Tag) IsGroup() bool {
	return tag.IsDelimiter() && tag != TagZero && tag != TagEnd
}

// Type returns Type of Value that corresponds to the tag
func (tag Tag) Type() Type {
	if tag.IsDelimiter() {
		return TypeInvalid
	}

	switch tag {
	case TagInteger, TagEnum:
		return TypeInteger

	case TagBoolean:
		return TypeBoolean

	case TagUnsupportedValue, TagDefault, TagUnknown, TagNotSettable,
		TagDeleteAttr, TagAdminDefine:
		// These tags not expected to have value
		return TypeVoid

	case TagText, TagName, TagReservedString, TagKeyword, TagURI, TagURIScheme,
		TagCharset, TagLanguage, TagMimeType, TagMemberName:
		return TypeString

	case TagDateTime:
		return TypeDateTime

	case TagResolution:
		return TypeResolution

	case TagRange:
		return TypeRange

	case TagTextLang, TagNameLang:
		return TypeTextWithLang

	case TagBeginCollection:
		return TypeCollection

	case TagEndCollection:
		return TypeVoid

	default:
		return TypeBinary
	}
}

// String() returns a tag name, as defined by RFC 8010
func (tag Tag) String() string {
	if 0 <= tag && int(tag) < len(tagNames) {
		if s := tagNames[tag]; s != "" {
			return s
		}
	}

	if tag < 0x100 {
		return fmt.Sprintf("0x%2.2x", uint(tag))
	}

	return fmt.Sprintf("0x%8.8x", uint(tag))
}

var tagNames = [...]string{
	// Delimiter tags
	TagZero:                   "zero",
	TagOperationGroup:         "operation-attributes-tag",
	TagJobGroup:               "job-attributes-tag",
	TagEnd:                    "end-of-attributes-tag",
	TagPrinterGroup:           "printer-attributes-tag",
	TagUnsupportedGroup:       "unsupported-attributes-tag",
	TagSubscriptionGroup:      "subscription-attributes-tag",
	TagEventNotificationGroup: "event-notification-attributes-tag",
	TagResourceGroup:          "resource-attributes-tag",
	TagDocumentGroup:          "document-attributes-tag",
	TagSystemGroup:            "system-attributes-tag",

	// Value tags
	TagUnsupportedValue: "unsupported",
	TagDefault:          "default",
	TagUnknown:          "unknown",
	TagNoValue:          "no-value",
	TagNotSettable:      "not-settable",
	TagDeleteAttr:       "delete-attribute",
	TagAdminDefine:      "admin-define",
	TagInteger:          "integer",
	TagBoolean:          "boolean",
	TagEnum:             "enum",
	TagString:           "octetString",
	TagDateTime:         "dateTime",
	TagResolution:       "resolution",
	TagRange:            "rangeOfInteger",
	TagBeginCollection:  "collection",
	TagTextLang:         "textWithLanguage",
	TagNameLang:         "nameWithLanguage",
	TagEndCollection:    "endCollection",
	TagText:             "textWithoutLanguage",
	TagName:             "nameWithoutLanguage",
	TagKeyword:          "keyword",
	TagURI:              "uri",
	TagURIScheme:        "uriScheme",
	TagCharset:          "charset",
	TagLanguage:         "naturalLanguage",
	TagMimeType:         "mimeMediaType",
	TagMemberName:       "memberAttrName",
}
0707010000006B000081A400000000000000000000000167D72F5D000005CE000000000000000000000000000000000000003C00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/type.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Enumeration of value types
 */

package goipp

import (
	"fmt"
)

// Type enumerates all possible value types
type Type int

// Type values
const (
	TypeInvalid      Type = -1   // Invalid Value type
	TypeVoid         Type = iota // Value is Void
	TypeInteger                  // Value is Integer
	TypeBoolean                  // Value is Boolean
	TypeString                   // Value is String
	TypeDateTime                 // Value is Time
	TypeResolution               // Value is Resolution
	TypeRange                    // Value is Range
	TypeTextWithLang             // Value is TextWithLang
	TypeBinary                   // Value is Binary
	TypeCollection               // Value is Collection
)

// String converts Type to string, for debugging
func (t Type) String() string {
	if t == TypeInvalid {
		return "Invalid"
	}

	if 0 <= t && int(t) < len(typeNames) {
		if s := typeNames[t]; s != "" {
			return s
		}
	}

	return fmt.Sprintf("0x%4.4x", uint(t))
}

var typeNames = [...]string{
	TypeVoid:         "Void",
	TypeInteger:      "Integer",
	TypeBoolean:      "Boolean",
	TypeString:       "String",
	TypeDateTime:     "DateTime",
	TypeResolution:   "Resolution",
	TypeRange:        "Range",
	TypeTextWithLang: "TextWithLang",
	TypeBinary:       "Binary",
	TypeCollection:   "Collection",
}
0707010000006C000081A400000000000000000000000167D72F5D00003806000000000000000000000000000000000000003D00000000ipp-usb-0.9.30/vendor/github.com/OpenPrinting/goipp/value.go/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Values for message attributes
 */

package goipp

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"math"
	"time"
)

// Values represents a sequence of values with tags.
// Usually Values used as a "payload" of Attribute
type Values []struct {
	T Tag   // The tag
	V Value // The value
}

// Add Value to Values
func (values *Values) Add(t Tag, v Value) {
	*values = append(*values, struct {
		T Tag
		V Value
	}{t, v})
}

// String converts Values to string
func (values Values) String() string {
	if len(values) == 1 {
		return values[0].V.String()
	}

	var buf bytes.Buffer
	buf.Write([]byte("["))
	for i, v := range values {
		if i != 0 {
			buf.Write([]byte(","))
		}
		buf.Write([]byte(v.V.String()))
	}
	buf.Write([]byte("]"))

	return buf.String()
}

// Equal performs deep check of equality of two Values
func (values Values) Equal(values2 Values) bool {
	if len(values) != len(values2) {
		return false
	}

	for i, v := range values {
		v2 := values2[i]
		if v.T != v2.T || !ValueEqual(v.V, v2.V) {
			return false
		}
	}

	return true
}

// Value represents an attribute value
//
// IPP uses typed values, and type of each value is unambiguously
// defined by the attribute tag
type Value interface {
	String() string
	Type() Type
	encode() ([]byte, error)
	decode([]byte) (Value, error)
}

// ValueEqual checks if two values are equal
//
// Equality means that types and values are equal. For structured
// values, like Collection, deep comparison is performed
func ValueEqual(v1, v2 Value) bool {
	if v1.Type() != v2.Type() {
		return false
	}

	switch v1.Type() {
	case TypeDateTime:
		return v1.(Time).Equal(v2.(Time).Time)
	case TypeBinary:
		return bytes.Equal(v1.(Binary), v2.(Binary))
	case TypeCollection:
		c1 := Attributes(v1.(Collection))
		c2 := Attributes(v2.(Collection))
		return c1.Equal(c2)
	}

	return v1 == v2
}

// Void is the Value that represents "no value"
//
// Use with: TagUnsupportedValue, TagDefault, TagUnknown,
// TagNotSettable, TagDeleteAttr, TagAdminDefine
type Void struct{}

// String converts Void Value to string
func (Void) String() string { return "" }

// Type returns type of Value (TypeVoid for Void)
func (Void) Type() Type { return TypeVoid }

// Encode Void Value into wire format
func (v Void) encode() ([]byte, error) {
	return []byte{}, nil
}

// Decode Void Value from wire format
func (Void) decode([]byte) (Value, error) {
	return Void{}, nil
}

// Integer is the Value that represents 32-bit signed int
//
// Use with: TagInteger, TagEnum
type Integer int32

// String converts Integer value to string
func (v Integer) String() string { return fmt.Sprintf("%d", int32(v)) }

// Type returns type of Value (TypeInteger for Integer)
func (Integer) Type() Type { return TypeInteger }

// Encode Integer Value into wire format
func (v Integer) encode() ([]byte, error) {
	return []byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}, nil
}

// Decode Integer Value from wire format
func (Integer) decode(data []byte) (Value, error) {
	if len(data) != 4 {
		return nil, errors.New("value must be 4 bytes")
	}

	return Integer(binary.BigEndian.Uint32(data)), nil
}

// Boolean is the Value that contains true of false
//
// Use with: TagBoolean
type Boolean bool

// String converts Boolean value to string
func (v Boolean) String() string { return fmt.Sprintf("%t", bool(v)) }

// Type returns type of Value (TypeBoolean for Boolean)
func (Boolean) Type() Type { return TypeBoolean }

// Encode Boolean Value into wire format
func (v Boolean) encode() ([]byte, error) {
	if v {
		return []byte{1}, nil
	}
	return []byte{0}, nil
}

// Decode Boolean Value from wire format
func (Boolean) decode(data []byte) (Value, error) {
	if len(data) != 1 {
		return nil, errors.New("value must be 1 byte")
	}

	return Boolean(data[0] != 0), nil
}

// String is the Value that represents string of text
//
// Use with: TagText, TagName, TagReservedString, TagKeyword, TagURI,
// TagURIScheme, TagCharset, TagLanguage, TagMimeType, TagMemberName
type String string

// String converts String value to string
func (v String) String() string { return string(v) }

// Type returns type of Value (TypeString for String)
func (String) Type() Type { return TypeString }

// Encode String Value into wire format
func (v String) encode() ([]byte, error) {
	return []byte(v), nil
}

// Decode String Value from wire format
func (String) decode(data []byte) (Value, error) {
	return String(data), nil
}

// Time is the Value that represents DataTime
//
// Use with: TagTime
type Time struct{ time.Time }

// String converts Time value to string
func (v Time) String() string { return v.Time.Format(time.RFC3339) }

// Type returns type of Value (TypeDateTime for Time)
func (Time) Type() Type { return TypeDateTime }

// Encode Time Value into wire format
func (v Time) encode() ([]byte, error) {
	// From RFC2579:
	//
	//     field  octets  contents                  range
	//     -----  ------  --------                  -----
	//       1      1-2   year*                     0..65536
	//       2       3    month                     1..12
	//       3       4    day                       1..31
	//       4       5    hour                      0..23
	//       5       6    minutes                   0..59
	//       6       7    seconds                   0..60
	//                    (use 60 for leap-second)
	//       7       8    deci-seconds              0..9
	//       8       9    direction from UTC        '+' / '-'
	//       9      10    hours from UTC*           0..13
	//      10      11    minutes from UTC          0..59
	//
	//     * Notes:
	//     - the value of year is in network-byte order
	//     - daylight saving time in New Zealand is +13

	year := v.Year()
	_, zone := v.Zone()
	dir := byte('+')
	if zone < 0 {
		zone = -zone
		dir = '-'
	}

	return []byte{
		byte(year >> 8), byte(year),
		byte(v.Month()),
		byte(v.Day()),
		byte(v.Hour()),
		byte(v.Minute()),
		byte(v.Second()),
		byte(v.Nanosecond() / 100000000),
		dir,
		byte(zone / 3600),
		byte((zone / 60) % 60),
	}, nil
}

// Decode Time Value from wire format
func (Time) decode(data []byte) (Value, error) {
	// Check size
	if len(data) != 11 {
		return nil, errors.New("value must be 11 bytes")
	}

	// Validate ranges
	var err error
	switch {
	case data[2] < 1 || data[2] > 12:
		err = fmt.Errorf("bad month %d", data[2])
	case data[3] < 1 || data[3] > 31:
		err = fmt.Errorf("bad day %d", data[3])
	case data[4] > 23:
		err = fmt.Errorf("bad hours %d", data[4])
	case data[5] > 59:
		err = fmt.Errorf("bad minutes %d", data[5])
	case data[6] > 60:
		err = fmt.Errorf("bad seconds %d", data[6])
	case data[7] > 9:
		err = fmt.Errorf("bad deciseconds %d", data[7])
	case data[8] != '+' && data[8] != '-':
		return nil, errors.New("bad UTC sign")
	case data[9] > 11:
		err = fmt.Errorf("bad UTC hours %d", data[9])
	case data[10] > 59:
		err = fmt.Errorf("bad UTC minutes %d", data[10])
	}

	if err != nil {
		return Time{}, err
	}

	// Decode time zone
	tzName := fmt.Sprintf("UTC%c%d", data[8], data[9])
	if data[10] != 0 {
		tzName += fmt.Sprintf(":%d", data[10])
	}

	tzOff := 3600*int(data[9]) + 60*int(data[10])
	if data[8] == '-' {
		tzOff = -tzOff
	}

	tz := time.FixedZone(tzName, tzOff)

	// Decode time
	t := time.Date(
		int(binary.BigEndian.Uint16(data[0:2])), // year
		time.Month(data[2]),                     // month
		int(data[3]),                            // day
		int(data[4]),                            // hour
		int(data[5]),                            // min
		int(data[6]),                            // sec
		int(data[7])*100000000,                  // nsec
		tz,                                      // time zone
	)

	return Time{t}, nil
}

// Resolution is the Value that represents image resolution.
//
// Use with: TagResolution
type Resolution struct {
	Xres, Yres int   // X/Y resolutions
	Units      Units // Resolution units
}

// String converts Resolution value to string
func (v Resolution) String() string {
	return fmt.Sprintf("%dx%d%s", v.Xres, v.Yres, v.Units)
}

// Type returns type of Value (TypeResolution for Resolution)
func (Resolution) Type() Type { return TypeResolution }

// Encode Resolution Value into wire format
func (v Resolution) encode() ([]byte, error) {
	// Wire format
	//    4 bytes: Xres
	//    4 bytes: Yres
	//    1 byte:  Units

	x, y := v.Xres, v.Yres

	return []byte{
		byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x),
		byte(y >> 24), byte(y >> 16), byte(y >> 8), byte(y),
		byte(v.Units),
	}, nil
}

// Decode Resolution Value from wire format
func (Resolution) decode(data []byte) (Value, error) {
	if len(data) != 9 {
		return nil, errors.New("value must be 9 bytes")
	}

	return Resolution{
		Xres:  int(binary.BigEndian.Uint32(data[0:4])),
		Yres:  int(binary.BigEndian.Uint32(data[4:8])),
		Units: Units(data[8]),
	}, nil

}

// Units represents resolution units
type Units uint8

// Resolution units codes
const (
	UnitsDpi  Units = 3 // Dots per inch
	UnitsDpcm Units = 4 // Dots per cm
)

// String converts Units to string
func (u Units) String() string {
	switch u {
	case UnitsDpi:
		return "dpi"
	case UnitsDpcm:
		return "dpcm"
	default:
		return fmt.Sprintf("0x%2.2x", uint8(u))
	}
}

// Range is the Value that represents a range of 32-bit signed integers
//
// Use with: TagRange
type Range struct {
	Lower, Upper int // Lower/upper bounds
}

// String converts Range value to string
func (v Range) String() string {
	return fmt.Sprintf("%d-%d", v.Lower, v.Upper)
}

// Type returns type of Value (TypeRange for Range)
func (Range) Type() Type { return TypeRange }

// Encode Range Value into wire format
func (v Range) encode() ([]byte, error) {
	// Wire format
	//    4 bytes: Lower
	//    4 bytes: Upper

	l, u := v.Lower, v.Upper

	return []byte{
		byte(l >> 24), byte(l >> 16), byte(l >> 8), byte(l),
		byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u),
	}, nil
}

// Decode Range Value from wire format
func (Range) decode(data []byte) (Value, error) {
	if len(data) != 8 {
		return nil, errors.New("value must be 8 bytes")
	}

	return Range{
		Lower: int(binary.BigEndian.Uint32(data[0:4])),
		Upper: int(binary.BigEndian.Uint32(data[4:8])),
	}, nil
}

// TextWithLang is the Value that represents a combination
// of two strings:
//   * text on some natural language (i.e., "hello")
//   * name of that language (i.e., "en")
//
// Use with: TagTextLang, TagNameLang
type TextWithLang struct {
	Lang, Text string // Language and text
}

// String converts TextWithLang value to string
func (v TextWithLang) String() string { return v.Text + " [" + v.Lang + "]" }

// Type returns type of Value (TypeTextWithLang for TextWithLang)
func (TextWithLang) Type() Type { return TypeTextWithLang }

// Encode TextWithLang Value into wire format
func (v TextWithLang) encode() ([]byte, error) {
	// Wire format
	//    2 bytes:  len(Lang)
	//    variable: Lang
	//    2 bytes:  len(Text)
	//    variable: Text

	lang := []byte(v.Lang)
	text := []byte(v.Text)

	if len(lang) > math.MaxUint16 {
		return nil, fmt.Errorf("Lang exceeds %d bytes", math.MaxUint16)
	}

	if len(text) > math.MaxUint16 {
		return nil, fmt.Errorf("Text exceeds %d bytes", math.MaxUint16)
	}

	data := make([]byte, 2+2+len(lang)+len(text))
	binary.BigEndian.PutUint16(data, uint16(len(lang)))
	copy(data[2:], []byte(lang))

	data2 := data[2+len(lang):]
	binary.BigEndian.PutUint16(data2, uint16(len(text)))
	copy(data2[2:], []byte(text))

	return data, nil
}

// Decode TextWithLang Value from wire format
func (TextWithLang) decode(data []byte) (Value, error) {
	var langLen, textLen int
	var lang, text string

	// Unpack language length
	if len(data) < 2 {
		return nil, errors.New("truncated language length")
	}

	langLen = int(binary.BigEndian.Uint16(data[0:2]))
	data = data[2:]

	// Unpack language value
	if len(data) < langLen {
		return nil, errors.New("truncated language name")
	}

	lang = string(data[:langLen])
	data = data[langLen:]

	// Unpack text length
	if len(data) < 2 {
		return nil, errors.New("truncated text length")
	}

	textLen = int(binary.BigEndian.Uint16(data[0:2]))
	data = data[2:]

	// Unpack text value
	if len(data) < textLen {
		return nil, errors.New("truncated text string")
	}

	text = string(data[:textLen])
	data = data[textLen:]

	// We must have consumed all bytes at this point
	if len(data) != 0 {
		return nil, fmt.Errorf("extra %d bytes at the end of value",
			len(data))
	}

	// Return a value
	return TextWithLang{Lang: lang, Text: text}, nil
}

// Binary is the Value that represents a raw binary data
type Binary []byte

// String converts Binary value to string
func (v Binary) String() string {
	return fmt.Sprintf("%x", []byte(v))
}

// Type returns type of Value (TypeBinary for Binary)
func (Binary) Type() Type { return TypeBinary }

// Encode TextWithLang Value into wire format
func (v Binary) encode() ([]byte, error) {
	return []byte(v), nil
}

// Decode Binary Value from wire format
func (Binary) decode(data []byte) (Value, error) {
	return Binary(data), nil
}

// Collection is the Value that represents collection of attributes
//
// Use with: TagBeginCollection
type Collection Attributes

// Add Attribute to Attributes
func (v *Collection) Add(attr Attribute) {
	*v = append(*v, attr)
}

// Equal checks that two collections are equal
func (v Collection) Equal(v2 Attributes) bool {
	return Attributes(v).Equal(Attributes(v2))
}

// String converts Collection to string
func (v Collection) String() string {
	var buf bytes.Buffer
	buf.Write([]byte("{"))
	for i, attr := range v {
		if i > 0 {
			buf.Write([]byte(" "))
		}
		fmt.Fprintf(&buf, "%s=%s", attr.Name, attr.Values)
	}
	buf.Write([]byte("}"))

	return buf.String()
}

// Type returns type of Value (TypeCollection for Collection)
func (Collection) Type() Type { return TypeCollection }

// Encode Collection Value into wire format
func (Collection) encode() ([]byte, error) {
	// Note, TagBeginCollection attribute contains
	// no data, collection itself handled the different way
	return []byte{}, nil
}

// Decode Collection Value from wire format
func (Collection) decode(data []byte) (Value, error) {
	panic("internal error")
}
0707010000006D000081A400000000000000000000000167D72F5D00000045000000000000000000000000000000000000002200000000ipp-usb-0.9.30/vendor/modules.txt# github.com/OpenPrinting/goipp v1.1.0
github.com/OpenPrinting/goipp
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!827 blocks
openSUSE Build Service is sponsored by