qrcodes_linux.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux && !ts_omit_qrcodes
  4. package qrcodes
  5. import (
  6. "errors"
  7. "fmt"
  8. "io"
  9. "os"
  10. "os/exec"
  11. "strconv"
  12. "strings"
  13. "syscall"
  14. "unsafe"
  15. "github.com/mattn/go-isatty"
  16. "golang.org/x/sys/unix"
  17. )
  18. func detectFormat(w io.Writer, inverse bool) (format Format, _ error) {
  19. var zero Format
  20. // Almost every terminal supports UTF-8, but the Linux
  21. // console may have partial or no support, which is
  22. // especially painful inside VMs. See tailscale/tailscale#12935.
  23. format = FormatSmall
  24. // Is the locale (LC_CTYPE) set to UTF-8?
  25. locale, err := locale()
  26. if err != nil {
  27. return FormatASCII, fmt.Errorf("QR: %w", err)
  28. }
  29. const utf8 = ".UTF-8"
  30. if !strings.HasSuffix(locale["LC_CTYPE"], utf8) &&
  31. !strings.HasSuffix(locale["LANG"], utf8) {
  32. return FormatASCII, nil
  33. }
  34. // Are we printing to a terminal?
  35. f, ok := w.(*os.File)
  36. if !ok {
  37. return format, nil
  38. }
  39. if !isatty.IsTerminal(f.Fd()) {
  40. return format, nil
  41. }
  42. fd := f.Fd()
  43. // On a Linux console, check that the current keyboard
  44. // is in Unicode mode. See unicode_start(1).
  45. const K_UNICODE = 0x03
  46. kbMode, err := ioctlGetKBMode(fd)
  47. if err != nil {
  48. if errors.Is(err, syscall.ENOTTY) {
  49. return format, nil
  50. }
  51. return zero, err
  52. }
  53. if kbMode != K_UNICODE {
  54. return FormatASCII, nil
  55. }
  56. // On a raw Linux console, detect whether the block
  57. // characters are available in the current font by
  58. // consulting the Unicode-to-font mapping.
  59. unimap, err := ioctlGetUniMap(fd)
  60. if err != nil {
  61. return zero, err
  62. }
  63. if _, ok := unimap['█']; ok {
  64. format = FormatLarge
  65. }
  66. if _, ok := unimap['▀']; ok && inverse {
  67. format = FormatSmall
  68. }
  69. if _, ok := unimap['▄']; ok && !inverse {
  70. format = FormatSmall
  71. }
  72. return format, nil
  73. }
  74. func locale() (map[string]string, error) {
  75. locale := map[string]string{
  76. "LANG": os.Getenv("LANG"),
  77. "LC_CTYPE": os.Getenv("LC_CTYPE"),
  78. }
  79. cmd := exec.Command("locale")
  80. out, err := cmd.Output()
  81. if err != nil {
  82. if errors.Is(err, exec.ErrNotFound) {
  83. return locale, nil
  84. }
  85. return nil, fmt.Errorf("locale error: %w", err)
  86. }
  87. for line := range strings.SplitSeq(string(out), "\n") {
  88. if line == "" {
  89. continue
  90. }
  91. k, v, found := strings.Cut(line, "=")
  92. if !found {
  93. continue
  94. }
  95. v, err := strconv.Unquote(v)
  96. if err != nil {
  97. continue
  98. }
  99. locale[k] = v
  100. }
  101. return locale, nil
  102. }
  103. func ioctlGetKBMode(fd uintptr) (int, error) {
  104. const KDGKBMODE = 0x4b44
  105. mode, err := unix.IoctlGetInt(int(fd), KDGKBMODE)
  106. if err != nil {
  107. return 0, fmt.Errorf("keyboard mode error: %w", err)
  108. }
  109. return mode, nil
  110. }
  111. func ioctlGetUniMap(fd uintptr) (map[rune]int, error) {
  112. const GIO_UNIMAP = 0x4B66 // get unicode-to-font mapping from kernel
  113. var ud struct {
  114. Count uint16
  115. Entries uintptr // pointer to unipair array
  116. }
  117. type unipair struct {
  118. Unicode uint16 // Unicode value
  119. FontPos uint16 // Font position in the console font
  120. }
  121. // First, get the number of entries:
  122. _, _, errno := unix.Syscall(unix.SYS_IOCTL, fd, GIO_UNIMAP, uintptr(unsafe.Pointer(&ud)))
  123. if errno != 0 && !errors.Is(errno, syscall.ENOMEM) {
  124. return nil, fmt.Errorf("unicode mapping error: %w", errno)
  125. }
  126. // Then allocate enough space and get the entries themselves:
  127. if ud.Count == 0 {
  128. return nil, nil
  129. }
  130. entries := make([]unipair, ud.Count)
  131. ud.Entries = uintptr(unsafe.Pointer(&entries[0]))
  132. _, _, errno = unix.Syscall(unix.SYS_IOCTL, fd, GIO_UNIMAP, uintptr(unsafe.Pointer(&ud)))
  133. if errno != 0 {
  134. return nil, fmt.Errorf("unicode mapping error: %w", errno)
  135. }
  136. unimap := make(map[rune]int)
  137. for _, e := range entries {
  138. unimap[rune(e.Unicode)] = int(e.FontPos)
  139. }
  140. return unimap, nil
  141. }