summaryrefslogtreecommitdiffhomepage
path: root/util/qrcodes/qrcodes_linux.go
blob: 474e231e23affe0fb9b59d8dd5a26e99eb95e634 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux && !ts_omit_qrcodes

package qrcodes

import (
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"syscall"
	"unsafe"

	"github.com/mattn/go-isatty"
	"golang.org/x/sys/unix"
)

func detectFormat(w io.Writer, inverse bool) (format Format, _ error) {
	var zero Format

	// Almost every terminal supports UTF-8, but the Linux
	// console may have partial or no support, which is
	// especially painful inside VMs. See tailscale/tailscale#12935.
	format = FormatSmall

	// Is the locale (LC_CTYPE) set to UTF-8?
	locale, err := locale()
	if err != nil {
		return FormatASCII, fmt.Errorf("QR: %w", err)
	}
	const utf8 = ".UTF-8"
	if !strings.HasSuffix(locale["LC_CTYPE"], utf8) &&
		!strings.HasSuffix(locale["LANG"], utf8) {
		return FormatASCII, nil
	}

	// Are we printing to a terminal?
	f, ok := w.(*os.File)
	if !ok {
		return format, nil
	}
	if !isatty.IsTerminal(f.Fd()) {
		return format, nil
	}
	fd := f.Fd()

	// On a Linux console, check that the current keyboard
	// is in Unicode mode. See unicode_start(1).
	const K_UNICODE = 0x03
	kbMode, err := ioctlGetKBMode(fd)
	if err != nil {
		if errors.Is(err, syscall.ENOTTY) {
			return format, nil
		}
		return zero, err
	}
	if kbMode != K_UNICODE {
		return FormatASCII, nil
	}

	// On a raw Linux console, detect whether the block
	// characters are available in the current font by
	// consulting the Unicode-to-font mapping.
	unimap, err := ioctlGetUniMap(fd)
	if err != nil {
		return zero, err
	}
	if _, ok := unimap['█']; ok {
		format = FormatLarge
	}
	if _, ok := unimap['▀']; ok && inverse {
		format = FormatSmall
	}
	if _, ok := unimap['▄']; ok && !inverse {
		format = FormatSmall
	}

	return format, nil
}

func locale() (map[string]string, error) {
	locale := map[string]string{
		"LANG":     os.Getenv("LANG"),
		"LC_CTYPE": os.Getenv("LC_CTYPE"),
	}

	cmd := exec.Command("locale")
	out, err := cmd.Output()
	if err != nil {
		if errors.Is(err, exec.ErrNotFound) {
			return locale, nil
		}
		return nil, fmt.Errorf("locale error: %w", err)
	}

	for line := range strings.SplitSeq(string(out), "\n") {
		if line == "" {
			continue
		}
		k, v, found := strings.Cut(line, "=")
		if !found {
			continue
		}
		v, err := strconv.Unquote(v)
		if err != nil {
			continue
		}
		locale[k] = v
	}
	return locale, nil
}

func ioctlGetKBMode(fd uintptr) (int, error) {
	const KDGKBMODE = 0x4b44
	mode, err := unix.IoctlGetInt(int(fd), KDGKBMODE)
	if err != nil {
		return 0, fmt.Errorf("keyboard mode error: %w", err)
	}
	return mode, nil
}

func ioctlGetUniMap(fd uintptr) (map[rune]int, error) {
	const GIO_UNIMAP = 0x4B66 // get unicode-to-font mapping from kernel
	var ud struct {
		Count   uint16
		Entries uintptr // pointer to unipair array
	}
	type unipair struct {
		Unicode uint16 // Unicode value
		FontPos uint16 // Font position in the console font
	}

	// First, get the number of entries:
	_, _, errno := unix.Syscall(unix.SYS_IOCTL, fd, GIO_UNIMAP, uintptr(unsafe.Pointer(&ud)))
	if errno != 0 && !errors.Is(errno, syscall.ENOMEM) {
		return nil, fmt.Errorf("unicode mapping error: %w", errno)
	}

	// Then allocate enough space and get the entries themselves:
	if ud.Count == 0 {
		return nil, nil
	}
	entries := make([]unipair, ud.Count)
	ud.Entries = uintptr(unsafe.Pointer(&entries[0]))
	_, _, errno = unix.Syscall(unix.SYS_IOCTL, fd, GIO_UNIMAP, uintptr(unsafe.Pointer(&ud)))
	if errno != 0 {
		return nil, fmt.Errorf("unicode mapping error: %w", errno)
	}

	unimap := make(map[rune]int)
	for _, e := range entries {
		unimap[rune(e.Unicode)] = int(e.FontPos)
	}
	return unimap, nil
}