Add cli/go-gh/v2 and cli/safeexec vendor dependencies

Required for authenticating with GitHub's API using the token
stored by the gh CLI.
This commit is contained in:
Jesse Duffield
2026-04-01 09:13:55 +02:00
committed by Stefan Haller
parent 4cd72b09a2
commit 678a2a90cb
25 changed files with 1166 additions and 724 deletions
+5 -3
View File
@@ -10,6 +10,7 @@ require (
github.com/adrg/xdg v0.4.0
github.com/atotto/clipboard v0.1.4
github.com/aybabtme/humanlog v0.4.1
github.com/cli/go-gh/v2 v2.13.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.24
github.com/gdamore/tcell/v2 v2.13.8
@@ -35,7 +36,7 @@ require (
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250905083220-c50528f08304
github.com/stretchr/testify v1.11.1
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sync v0.20.0
golang.org/x/sys v0.42.0
@@ -46,6 +47,7 @@ require (
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/cli/safeexec v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
@@ -57,8 +59,8 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.34.1 // indirect
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
+13 -7
View File
@@ -11,6 +11,10 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cli/go-gh/v2 v2.13.0 h1:jEHZu/VPVoIJkciK3pzZd3rbT8J90swsK5Ui4ewH1ys=
github.com/cli/go-gh/v2 v2.13.0/go.mod h1:Us/NbQ8VNM0fdaILgoXSz6PKkV5PWaEzkJdc9vR2geM=
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -72,13 +76,14 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
@@ -125,8 +130,9 @@ github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xz
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -152,13 +158,13 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+70
View File
@@ -0,0 +1,70 @@
package set
var exists = struct{}{}
type stringSet struct {
v []string
m map[string]struct{}
}
func NewStringSet() *stringSet {
s := &stringSet{}
s.m = make(map[string]struct{})
s.v = []string{}
return s
}
func (s *stringSet) Add(value string) {
if s.Contains(value) {
return
}
s.m[value] = exists
s.v = append(s.v, value)
}
func (s *stringSet) AddValues(values []string) {
for _, v := range values {
s.Add(v)
}
}
func (s *stringSet) Remove(value string) {
if !s.Contains(value) {
return
}
delete(s.m, value)
s.v = sliceWithout(s.v, value)
}
func sliceWithout(s []string, v string) []string {
idx := -1
for i, item := range s {
if item == v {
idx = i
break
}
}
if idx < 0 {
return s
}
return append(s[:idx], s[idx+1:]...)
}
func (s *stringSet) RemoveValues(values []string) {
for _, v := range values {
s.Remove(v)
}
}
func (s *stringSet) Contains(value string) bool {
_, c := s.m[value]
return c
}
func (s *stringSet) Len() int {
return len(s.m)
}
func (s *stringSet) ToSlice() []string {
return s.v
}
+210
View File
@@ -0,0 +1,210 @@
// Package yamlmap is a wrapper of gopkg.in/yaml.v3 for interacting
// with yaml data as if it were a map.
package yamlmap
import (
"errors"
"gopkg.in/yaml.v3"
)
const (
modified = "modifed"
)
type Map struct {
*yaml.Node
}
var ErrNotFound = errors.New("not found")
var ErrInvalidYaml = errors.New("invalid yaml")
var ErrInvalidFormat = errors.New("invalid format")
func StringValue(value string) *Map {
return &Map{&yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: value,
}}
}
func MapValue() *Map {
return &Map{&yaml.Node{
Kind: yaml.MappingNode,
Tag: "!!map",
}}
}
func NullValue() *Map {
return &Map{&yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!null",
}}
}
func Unmarshal(data []byte) (*Map, error) {
var root yaml.Node
err := yaml.Unmarshal(data, &root)
if err != nil {
return nil, ErrInvalidYaml
}
if len(root.Content) == 0 {
return MapValue(), nil
}
if root.Content[0].Kind != yaml.MappingNode {
return nil, ErrInvalidFormat
}
return &Map{root.Content[0]}, nil
}
func Marshal(m *Map) ([]byte, error) {
return yaml.Marshal(m.Node)
}
func (m *Map) AddEntry(key string, value *Map) {
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: key,
}
m.Content = append(m.Content, keyNode, value.Node)
m.SetModified()
}
func (m *Map) Empty() bool {
return len(m.Content) == 0
}
func (m *Map) FindEntry(key string) (*Map, error) {
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
// When iterating over the content slice we only want to compare the keys of the yamlMap.
for i, v := range m.Content {
if i%2 != 0 {
continue
}
if v.Value == key {
if i+1 < len(m.Content) {
return &Map{m.Content[i+1]}, nil
}
}
}
return nil, ErrNotFound
}
func (m *Map) Keys() []string {
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
// When iterating over the content slice we only want to select the keys of the yamlMap.
keys := []string{}
for i, v := range m.Content {
if i%2 != 0 {
continue
}
keys = append(keys, v.Value)
}
return keys
}
func (m *Map) RemoveEntry(key string) error {
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
// When iterating over the content slice we only want to compare the keys of the yamlMap.
// If we find they key to remove, remove the key and its value from the content slice.
found, skipNext := false, false
newContent := []*yaml.Node{}
for i, v := range m.Content {
if skipNext {
skipNext = false
continue
}
if i%2 != 0 || v.Value != key {
newContent = append(newContent, v)
} else {
found = true
skipNext = true
m.SetModified()
}
}
if !found {
return ErrNotFound
}
m.Content = newContent
return nil
}
func (m *Map) SetEntry(key string, value *Map) {
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
// When iterating over the content slice we only want to compare the keys of the yamlMap.
// If we find they key to set, set the next item in the content slice to the new value.
m.SetModified()
for i, v := range m.Content {
if i%2 != 0 || v.Value != key {
continue
}
if v.Value == key {
if i+1 < len(m.Content) {
m.Content[i+1] = value.Node
return
}
}
}
m.AddEntry(key, value)
}
// SetModified marks the map as modified.
//
// Note: This is a hack to introduce the concept of modified/unmodified
// on top of gopkg.in/yaml.v3. This works by setting the Value property
// of a MappingNode to a specific value and then later checking if the
// node's Value property is that specific value. When a MappingNode gets
// output as a string the Value property is not used, thus changing it
// has no impact for our purposes.
func (m *Map) SetModified() {
// Can not mark a non-mapping node as modified
if m.Node.Kind != yaml.MappingNode && m.Node.Tag == "!!null" {
m.Node.Kind = yaml.MappingNode
m.Node.Tag = "!!map"
}
if m.Node.Kind == yaml.MappingNode {
m.Node.Value = modified
}
}
// SetUnmodified traverses the map using BFS to set all nodes as unmodified.
func (m *Map) SetUnmodified() {
i := 0
queue := []*yaml.Node{m.Node}
for i < len(queue) {
q := queue[i]
i = i + 1
if q.Kind != yaml.MappingNode {
continue
}
q.Value = ""
queue = append(queue, q.Content...)
}
}
// IsModified traverses the map using BFS to search for any nodes that have been modified.
func (m *Map) IsModified() bool {
i := 0
queue := []*yaml.Node{m.Node}
for i < len(queue) {
q := queue[i]
i = i + 1
if q.Kind != yaml.MappingNode {
continue
}
if q.Value == modified {
return true
}
queue = append(queue, q.Content...)
}
return false
}
func (m *Map) String() string {
data, err := Marshal(m)
if err != nil {
return ""
}
return string(data)
}
+203
View File
@@ -0,0 +1,203 @@
// Package auth is a set of functions for retrieving authentication tokens
// and authenticated hosts.
package auth
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/cli/go-gh/v2/internal/set"
"github.com/cli/go-gh/v2/pkg/config"
"github.com/cli/safeexec"
)
const (
codespaces = "CODESPACES"
defaultSource = "default"
ghEnterpriseToken = "GH_ENTERPRISE_TOKEN"
ghHost = "GH_HOST"
ghToken = "GH_TOKEN"
github = "github.com"
githubEnterpriseToken = "GITHUB_ENTERPRISE_TOKEN"
githubToken = "GITHUB_TOKEN"
hostsKey = "hosts"
localhost = "github.localhost"
oauthToken = "oauth_token"
tenancyHost = "ghe.com" // TenancyHost is the domain suffix of a tenancy GitHub instance.
)
// TokenForHost retrieves an authentication token and the source of that token for the specified
// host. The source can be either an environment variable, configuration file, or the system
// keyring. In the latter case, this shells out to "gh auth token" to obtain the token.
//
// Returns "", "default" if no applicable token is found.
func TokenForHost(host string) (string, string) {
if token, source := TokenFromEnvOrConfig(host); token != "" {
return token, source
}
ghExe := os.Getenv("GH_PATH")
if ghExe == "" {
ghExe, _ = safeexec.LookPath("gh")
}
if ghExe != "" {
if token, source := tokenFromGh(ghExe, host); token != "" {
return token, source
}
}
return "", defaultSource
}
// TokenFromEnvOrConfig retrieves an authentication token from environment variables or the config
// file as fallback, but does not support reading the token from system keyring. Most consumers
// should use TokenForHost.
func TokenFromEnvOrConfig(host string) (string, string) {
cfg, _ := config.Read(nil)
return tokenForHost(cfg, host)
}
func tokenForHost(cfg *config.Config, host string) (string, string) {
normalizedHost := NormalizeHostname(host)
// This code is currently the exact opposite of IsEnterprise. However, we have chosen
// to write it separately, directly in line, because it is much clearer in the exact
// scenarios that we expect to use GH_TOKEN and GITHUB_TOKEN.
if normalizedHost == github || IsTenancy(normalizedHost) || normalizedHost == localhost {
if token := os.Getenv(ghToken); token != "" {
return token, ghToken
}
if token := os.Getenv(githubToken); token != "" {
return token, githubToken
}
} else {
if token := os.Getenv(ghEnterpriseToken); token != "" {
return token, ghEnterpriseToken
}
if token := os.Getenv(githubEnterpriseToken); token != "" {
return token, githubEnterpriseToken
}
}
// If config is nil, something has failed much earlier and it's probably
// more correct to panic because we don't expect to support anything
// where the config isn't available, but that would be a breaking change,
// so it's worth thinking about carefully, if we wanted to rework this.
if cfg == nil {
return "", defaultSource
}
token, err := cfg.Get([]string{hostsKey, normalizedHost, oauthToken})
if err != nil {
return "", defaultSource
}
return token, oauthToken
}
func tokenFromGh(path string, host string) (string, string) {
cmd := exec.Command(path, "auth", "token", "--secure-storage", "--hostname", host)
result, err := cmd.Output()
if err != nil {
return "", "gh"
}
return strings.TrimSpace(string(result)), "gh"
}
// KnownHosts retrieves a list of hosts that have corresponding
// authentication tokens, either from environment variables
// or from the configuration file.
// Returns an empty string slice if no hosts are found.
func KnownHosts() []string {
cfg, _ := config.Read(nil)
return knownHosts(cfg)
}
func knownHosts(cfg *config.Config) []string {
hosts := set.NewStringSet()
if host := os.Getenv(ghHost); host != "" {
hosts.Add(host)
}
if token, _ := tokenForHost(cfg, github); token != "" {
hosts.Add(github)
}
if cfg != nil {
keys, err := cfg.Keys([]string{hostsKey})
if err == nil {
hosts.AddValues(keys)
}
}
return hosts.ToSlice()
}
// DefaultHost retrieves an authenticated host and the source of host.
// The source can be either an environment variable or from the
// configuration file.
// Returns "github.com", "default" if no viable host is found.
func DefaultHost() (string, string) {
cfg, _ := config.Read(nil)
return defaultHost(cfg)
}
func defaultHost(cfg *config.Config) (string, string) {
if host := os.Getenv(ghHost); host != "" {
return host, ghHost
}
if cfg != nil {
keys, err := cfg.Keys([]string{hostsKey})
if err == nil && len(keys) == 1 {
return keys[0], hostsKey
}
}
return github, defaultSource
}
// IsEnterprise determines if a provided host is a GitHub Enterprise Server instance,
// rather than GitHub.com, a tenancy GitHub instance, or github.localhost.
func IsEnterprise(host string) bool {
// Note that if you are making changes here, you should also consider making the equivalent
// in tokenForHost, which is the exact opposite of this function.
normalizedHost := NormalizeHostname(host)
return normalizedHost != github && normalizedHost != localhost && !IsTenancy(normalizedHost)
}
// IsTenancy determines if a provided host is a tenancy GitHub instance,
// rather than GitHub.com or a GitHub Enterprise Server instance.
func IsTenancy(host string) bool {
normalizedHost := NormalizeHostname(host)
return strings.HasSuffix(normalizedHost, "."+tenancyHost)
}
// NormalizeHostname ensures the host matches the values used throughout
// the rest of the codebase with respect to hostnames. These are github,
// localhost, and tenancyHost.
func NormalizeHostname(host string) string {
hostname := strings.ToLower(host)
if strings.HasSuffix(hostname, "."+github) {
return github
}
if strings.HasSuffix(hostname, "."+localhost) {
return localhost
}
// This has been copied over from the cli/cli NormalizeHostname function
// to ensure compatible behaviour but we don't fully understand when or
// why it would be useful here. We can't see what harm will come of
// duplicating the logic.
if before, found := cutSuffix(hostname, "."+tenancyHost); found {
idx := strings.LastIndex(before, ".")
return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost)
}
return hostname
}
// Backport strings.CutSuffix from Go 1.20.
func cutSuffix(s, suffix string) (string, bool) {
if !strings.HasSuffix(s, suffix) {
return s, false
}
return s[:len(s)-len(suffix)], true
}
+344
View File
@@ -0,0 +1,344 @@
// Package config is a set of types for interacting with the gh configuration files.
// Note: This package is intended for use only in gh, any other use cases are subject
// to breakage and non-backwards compatible updates.
package config
import (
"errors"
"io"
"os"
"path/filepath"
"runtime"
"sync"
"github.com/cli/go-gh/v2/internal/yamlmap"
)
const (
appData = "AppData"
ghConfigDir = "GH_CONFIG_DIR"
localAppData = "LocalAppData"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgDataHome = "XDG_DATA_HOME"
xdgStateHome = "XDG_STATE_HOME"
xdgCacheHome = "XDG_CACHE_HOME"
)
var (
cfg *Config
once sync.Once
loadErr error
)
// Config is a in memory representation of the gh configuration files.
// It can be thought of as map where entries consist of a key that
// correspond to either a string value or a map value, allowing for
// multi-level maps.
type Config struct {
entries *yamlmap.Map
mu sync.RWMutex
}
// Get a string value from a Config.
// The keys argument is a sequence of key values so that nested
// entries can be retrieved. A undefined string will be returned
// if trying to retrieve a key that corresponds to a map value.
// Returns "", KeyNotFoundError if any of the keys can not be found.
func (c *Config) Get(keys []string) (string, error) {
c.mu.RLock()
defer c.mu.RUnlock()
m := c.entries
for _, key := range keys {
var err error
m, err = m.FindEntry(key)
if err != nil {
return "", &KeyNotFoundError{key}
}
}
return m.Value, nil
}
// Keys enumerates a Config's keys.
// The keys argument is a sequence of key values so that nested
// map values can be have their keys enumerated.
// Returns nil, KeyNotFoundError if any of the keys can not be found.
func (c *Config) Keys(keys []string) ([]string, error) {
c.mu.RLock()
defer c.mu.RUnlock()
m := c.entries
for _, key := range keys {
var err error
m, err = m.FindEntry(key)
if err != nil {
return nil, &KeyNotFoundError{key}
}
}
return m.Keys(), nil
}
// Remove an entry from a Config.
// The keys argument is a sequence of key values so that nested
// entries can be removed. Removing an entry that has nested
// entries removes those also.
// Returns KeyNotFoundError if any of the keys can not be found.
func (c *Config) Remove(keys []string) error {
c.mu.Lock()
defer c.mu.Unlock()
m := c.entries
for i := 0; i < len(keys)-1; i++ {
var err error
key := keys[i]
m, err = m.FindEntry(key)
if err != nil {
return &KeyNotFoundError{key}
}
}
err := m.RemoveEntry(keys[len(keys)-1])
if err != nil {
return &KeyNotFoundError{keys[len(keys)-1]}
}
return nil
}
// Set a string value in a Config.
// The keys argument is a sequence of key values so that nested
// entries can be set. If any of the keys do not exist they will
// be created. If the string value to be set is empty it will be
// represented as null not an empty string when written.
//
// var c *Config
// c.Set([]string{"key"}, "")
// Write(c) // writes `key: ` not `key: ""`
func (c *Config) Set(keys []string, value string) {
c.mu.Lock()
defer c.mu.Unlock()
m := c.entries
for i := 0; i < len(keys)-1; i++ {
key := keys[i]
entry, err := m.FindEntry(key)
if err != nil {
entry = yamlmap.MapValue()
m.AddEntry(key, entry)
}
m = entry
}
val := yamlmap.StringValue(value)
if value == "" {
val = yamlmap.NullValue()
}
m.SetEntry(keys[len(keys)-1], val)
}
func (c *Config) deepCopy() *Config {
return ReadFromString(c.entries.String())
}
// Read gh configuration files from the local file system and
// returns a Config. A copy of the fallback configuration will
// be returned when there are no configuration files to load.
// If there are no configuration files and no fallback configuration
// an empty configuration will be returned.
var Read = func(fallback *Config) (*Config, error) {
once.Do(func() {
cfg, loadErr = load(generalConfigFile(), hostsConfigFile(), fallback)
})
return cfg, loadErr
}
// ReadFromString takes a yaml string and returns a Config.
func ReadFromString(str string) *Config {
m, _ := mapFromString(str)
if m == nil {
m = yamlmap.MapValue()
}
return &Config{entries: m}
}
// Write gh configuration files to the local file system.
// It will only write gh configuration files that have been modified
// since last being read.
func Write(c *Config) error {
c.mu.Lock()
defer c.mu.Unlock()
hosts, err := c.entries.FindEntry("hosts")
if err == nil && hosts.IsModified() {
err := writeFile(hostsConfigFile(), []byte(hosts.String()))
if err != nil {
return err
}
hosts.SetUnmodified()
}
if c.entries.IsModified() {
// Hosts gets written to a different file above so remove it
// before writing and add it back in after writing.
hostsMap, hostsErr := c.entries.FindEntry("hosts")
if hostsErr == nil {
_ = c.entries.RemoveEntry("hosts")
}
err := writeFile(generalConfigFile(), []byte(c.entries.String()))
if err != nil {
return err
}
c.entries.SetUnmodified()
if hostsErr == nil {
c.entries.AddEntry("hosts", hostsMap)
}
}
return nil
}
func load(generalFilePath, hostsFilePath string, fallback *Config) (*Config, error) {
generalMap, err := mapFromFile(generalFilePath)
if err != nil && !os.IsNotExist(err) {
if errors.Is(err, yamlmap.ErrInvalidYaml) ||
errors.Is(err, yamlmap.ErrInvalidFormat) {
return nil, &InvalidConfigFileError{Path: generalFilePath, Err: err}
}
return nil, err
}
if generalMap == nil {
generalMap = yamlmap.MapValue()
}
hostsMap, err := mapFromFile(hostsFilePath)
if err != nil && !os.IsNotExist(err) {
if errors.Is(err, yamlmap.ErrInvalidYaml) ||
errors.Is(err, yamlmap.ErrInvalidFormat) {
return nil, &InvalidConfigFileError{Path: hostsFilePath, Err: err}
}
return nil, err
}
if hostsMap != nil && !hostsMap.Empty() {
generalMap.AddEntry("hosts", hostsMap)
generalMap.SetUnmodified()
}
if generalMap.Empty() && fallback != nil {
return fallback.deepCopy(), nil
}
return &Config{entries: generalMap}, nil
}
func generalConfigFile() string {
return filepath.Join(ConfigDir(), "config.yml")
}
func hostsConfigFile() string {
return filepath.Join(ConfigDir(), "hosts.yml")
}
func mapFromFile(filename string) (*yamlmap.Map, error) {
data, err := readFile(filename)
if err != nil {
return nil, err
}
return yamlmap.Unmarshal(data)
}
func mapFromString(str string) (*yamlmap.Map, error) {
return yamlmap.Unmarshal([]byte(str))
}
// ConfigDir returns the path to the configuration directory.
//
// Config path precedence: GH_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME.
func ConfigDir() string {
var path string
if a := os.Getenv(ghConfigDir); a != "" {
path = a
} else if b := os.Getenv(xdgConfigHome); b != "" {
path = filepath.Join(b, "gh")
} else if c := os.Getenv(appData); runtime.GOOS == "windows" && c != "" {
path = filepath.Join(c, "GitHub CLI")
} else {
d, _ := os.UserHomeDir()
path = filepath.Join(d, ".config", "gh")
}
return path
}
// StateDir returns the path to the state directory.
//
// State path precedence: XDG_STATE_HOME, LocalAppData (windows only), HOME.
func StateDir() string {
var path string
if a := os.Getenv(xdgStateHome); a != "" {
path = filepath.Join(a, "gh")
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
path = filepath.Join(b, "GitHub CLI")
} else {
c, _ := os.UserHomeDir()
path = filepath.Join(c, ".local", "state", "gh")
}
return path
}
// DataDir returns the path to the data directory.
//
// Data path precedence: XDG_DATA_HOME, LocalAppData (windows only), HOME.
func DataDir() string {
var path string
if a := os.Getenv(xdgDataHome); a != "" {
path = filepath.Join(a, "gh")
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
path = filepath.Join(b, "GitHub CLI")
} else {
c, _ := os.UserHomeDir()
path = filepath.Join(c, ".local", "share", "gh")
}
return path
}
// CacheDir returns the path to the cache directory.
//
// Cache path precedence: XDG_CACHE_HOME, LocalAppData (windows only), HOME, legacy gh-cli-cache.
func CacheDir() string {
if a := os.Getenv(xdgCacheHome); a != "" {
return filepath.Join(a, "gh")
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
return filepath.Join(b, "GitHub CLI")
} else if c, err := os.UserHomeDir(); err == nil {
return filepath.Join(c, ".cache", "gh")
} else {
// Note that this has a minor security issue because /tmp is world-writeable.
// As such, it is possible for other users on a shared system to overwrite cached data.
// The practical risk of this is low, but it's worth calling out as a risk.
// I've included this here for backwards compatibility but we should consider removing it.
return filepath.Join(os.TempDir(), "gh-cli-cache")
}
}
func readFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return data, nil
}
func writeFile(filename string, data []byte) (writeErr error) {
if writeErr = os.MkdirAll(filepath.Dir(filename), 0771); writeErr != nil {
return
}
var file *os.File
if file, writeErr = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600); writeErr != nil {
return
}
defer func() {
if err := file.Close(); writeErr == nil && err != nil {
writeErr = err
}
}()
_, writeErr = file.Write(data)
return
}
+32
View File
@@ -0,0 +1,32 @@
package config
import (
"fmt"
)
// InvalidConfigFileError represents an error when trying to read a config file.
type InvalidConfigFileError struct {
Path string
Err error
}
// Error allows InvalidConfigFileError to satisfy error interface.
func (e *InvalidConfigFileError) Error() string {
return fmt.Sprintf("invalid config file %s: %s", e.Path, e.Err)
}
// Unwrap allows InvalidConfigFileError to be unwrapped.
func (e *InvalidConfigFileError) Unwrap() error {
return e.Err
}
// KeyNotFoundError represents an error when trying to find a config key
// that does not exist.
type KeyNotFoundError struct {
Key string
}
// Error allows KeyNotFoundError to satisfy error interface.
func (e *KeyNotFoundError) Error() string {
return fmt.Sprintf("could not find key %q", e.Key)
}
+25
View File
@@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2020, GitHub Inc.
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.
+48
View File
@@ -0,0 +1,48 @@
# safeexec
A Go module that provides a stabler alternative to `exec.LookPath()` that:
- Avoids a Windows security risk of executing commands found in the current directory; and
- Allows executing commands found in PATH, even if they come from relative PATH entries.
This is an alternative to [`golang.org/x/sys/execabs`](https://pkg.go.dev/golang.org/x/sys/execabs).
## Usage
```go
import (
"os/exec"
"github.com/cli/safeexec"
)
func gitStatus() error {
gitBin, err := safeexec.LookPath("git")
if err != nil {
return err
}
cmd := exec.Command(gitBin, "status")
return cmd.Run()
}
```
## Background
### Windows security vulnerability with Go <= 1.18
Go 1.18 (and older) standard library has a security vulnerability when executing programs:
```go
import "os/exec"
func gitStatus() error {
// On Windows, this will result in `.\git.exe` or `.\git.bat` being executed
// if either were found in the current working directory.
cmd := exec.Command("git", "status")
return cmd.Run()
}
```
For historic reasons, Go used to implicitly [include the current directory](https://github.com/golang/go/issues/38736) in the PATH resolution on Windows. The `safeexec` package avoids searching the current directory on Windows.
### Relative PATH entries with Go 1.19+
Go 1.19 (and newer) standard library [throws an error](https://github.com/golang/go/issues/43724) if `exec.LookPath("git")` resolved to an executable relative to the current directory. This can happen on other platforms if the PATH environment variable contains relative entries, e.g. `PATH=./bin:$PATH`. The `safeexec` package allows respecting relative PATH entries as it assumes that the responsibility for keeping PATH safe lies outside of the Go program.
## TODO
Ideally, this module would also provide `exec.Command()` and `exec.CommandContext()` equivalents that delegate to the patched version of `LookPath`. However, this doesn't seem possible since `LookPath` may return an error, while `exec.Command/CommandContext()` themselves do not return an error. In the standard library, the resulting `exec.Cmd` struct stores the LookPath error in a private field, but that functionality isn't available to us.
+17
View File
@@ -0,0 +1,17 @@
//go:build !windows && go1.19
// +build !windows,go1.19
package safeexec
import (
"errors"
"os/exec"
)
func LookPath(file string) (string, error) {
path, err := exec.LookPath(file)
if errors.Is(err, exec.ErrDot) {
return path, nil
}
return path, err
}
+10
View File
@@ -0,0 +1,10 @@
//go:build !windows && !go1.19
// +build !windows,!go1.19
package safeexec
import "os/exec"
func LookPath(file string) (string, error) {
return exec.LookPath(file)
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * 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.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// 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
// OWNER 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.
// Package safeexec provides alternatives for exec package functions to avoid
// accidentally executing binaries found in the current working directory on
// Windows.
package safeexec
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
func chkStat(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if d.IsDir() {
return os.ErrPermission
}
return nil
}
func hasExt(file string) bool {
i := strings.LastIndex(file, ".")
if i < 0 {
return false
}
return strings.LastIndexAny(file, `:\/`) < i
}
func findExecutable(file string, exts []string) (string, error) {
if len(exts) == 0 {
return file, chkStat(file)
}
if hasExt(file) {
if chkStat(file) == nil {
return file, nil
}
}
for _, e := range exts {
if f := file + e; chkStat(f) == nil {
return f, nil
}
}
return "", os.ErrNotExist
}
// LookPath searches for an executable named file in the
// directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted.
// LookPath also uses PATHEXT environment variable to match
// a suitable candidate.
// The result may be an absolute path or a path relative to the current directory.
func LookPath(file string) (string, error) {
var exts []string
x := os.Getenv(`PATHEXT`)
if x != "" {
for _, e := range strings.Split(strings.ToLower(x), `;`) {
if e == "" {
continue
}
if e[0] != '.' {
e = "." + e
}
exts = append(exts, e)
}
} else {
exts = []string{".com", ".exe", ".bat", ".cmd"}
}
if strings.ContainsAny(file, `:\/`) {
if f, err := findExecutable(file, exts); err == nil {
return f, nil
} else {
return "", &exec.Error{file, err}
}
}
// https://github.com/golang/go/issues/38736
// if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
// return f, nil
// }
path := os.Getenv("path")
for _, dir := range filepath.SplitList(path) {
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
return f, nil
}
}
return "", &exec.Error{file, exec.ErrNotFound}
}
-2
View File
@@ -42,7 +42,6 @@ loop:
continue
}
var buf bytes.Buffer
for {
c, err := er.ReadByte()
if err != nil {
@@ -51,7 +50,6 @@ loop:
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
break
}
buf.Write([]byte(string(c)))
}
}
+3 -2
View File
@@ -1,6 +1,7 @@
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine
// +build darwin freebsd openbsd netbsd dragonfly
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine && !tinygo
// +build darwin freebsd openbsd netbsd dragonfly hurd
// +build !appengine
// +build !tinygo
package isatty
+3 -2
View File
@@ -1,5 +1,6 @@
//go:build appengine || js || nacl || wasm
// +build appengine js nacl wasm
//go:build (appengine || js || nacl || tinygo || wasm) && !windows
// +build appengine js nacl tinygo wasm
// +build !windows
package isatty
+2 -1
View File
@@ -1,6 +1,7 @@
//go:build (linux || aix || zos) && !appengine
//go:build (linux || aix || zos) && !appengine && !tinygo
// +build linux aix zos
// +build !appengine
// +build !tinygo
package isatty
-2
View File
@@ -1,7 +1,5 @@
package terminfo
//go:generate go run gen.go
// BoolCapName returns the bool capability name.
func BoolCapName(i int) string {
return boolCapNames[2*i]
+1 -499
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -70,14 +70,12 @@ func ColorLevelFromEnv() (ColorLevel, error) {
}
return ColorLevelHundreds, nil
}
// otherwise determine from TERM's max_colors capability
if term := os.Getenv("TERM"); term != "" {
ti, err := Load(term)
if err != nil {
return ColorLevelNone, err
}
v, ok := ti.Nums[MaxColors]
switch {
case !ok || v <= 16:
@@ -86,6 +84,5 @@ func ColorLevelFromEnv() (ColorLevel, error) {
return ColorLevelHundreds, nil
}
}
return ColorLevelBasic, nil
}
@@ -7,12 +7,11 @@ import (
const (
// maxFileLength is the max file length.
maxFileLength = 4096
// magic is the file magic for terminfo files.
magic = 0432
// magicExtended is the file magic for terminfo files with the extended number format.
magicExtended = 01036
magic = 0o432
// magicExtended is the file magic for terminfo files with the extended
// number format.
magicExtended = 0o1036
)
// header fields.
@@ -99,12 +98,12 @@ func readStrings(idx []int, buf []byte, n int) (map[int][]byte, int, error) {
type decoder struct {
buf []byte
pos int
len int
n int
}
// readBytes reads the next n bytes of buf, incrementing pos by n.
func (d *decoder) readBytes(n int) ([]byte, error) {
if d.len < d.pos+n {
if d.n < d.pos+n {
return nil, ErrUnexpectedFileEnd
}
n, d.pos = d.pos, d.pos+n
@@ -115,15 +114,12 @@ func (d *decoder) readBytes(n int) ([]byte, error) {
func (d *decoder) readInts(n, w int) ([]int, error) {
w /= 8
l := n * w
buf, err := d.readBytes(l)
if err != nil {
return nil, err
}
// align
d.pos += d.pos % 2
z := make([]int, n)
for i, j := 0, 0; i < l; i, j = i+w, j+1 {
switch w {
@@ -135,7 +131,6 @@ func (d *decoder) readInts(n, w int) ([]int, error) {
z[j] = int(buf[i+3])<<24 | int(buf[i+2])<<16 | int(buf[i+1])<<8 | int(buf[i])
}
}
return z, nil
}
@@ -145,7 +140,6 @@ func (d *decoder) readBools(n int) (map[int]bool, map[int]bool, error) {
if err != nil {
return nil, nil, err
}
// process
bools, boolsM := make(map[int]bool), make(map[int]bool)
for i, b := range buf {
@@ -154,7 +148,6 @@ func (d *decoder) readBools(n int) (map[int]bool, map[int]bool, error) {
boolsM[i] = true
}
}
return bools, boolsM, nil
}
@@ -164,7 +157,6 @@ func (d *decoder) readNums(n, w int) (map[int]int, map[int]bool, error) {
if err != nil {
return nil, nil, err
}
// process
nums, numsM := make(map[int]int), make(map[int]bool)
for i := 0; i < n; i++ {
@@ -173,7 +165,6 @@ func (d *decoder) readNums(n, w int) (map[int]int, map[int]bool, error) {
numsM[i] = true
}
}
return nums, numsM, nil
}
@@ -184,16 +175,13 @@ func (d *decoder) readStringTable(n, sz int) ([][]byte, []int, error) {
if err != nil {
return nil, nil, err
}
// read string data table
data, err := d.readBytes(sz)
if err != nil {
return nil, nil, err
}
// align
d.pos += d.pos % 2
// process
s := make([][]byte, n)
var m []int
@@ -209,7 +197,6 @@ func (d *decoder) readStringTable(n, sz int) ([][]byte, []int, error) {
}
}
}
return s, m, nil
}
@@ -220,7 +207,6 @@ func (d *decoder) readStrings(n, sz int) (map[int][]byte, map[int]bool, error) {
if err != nil {
return nil, nil, err
}
strs := make(map[int][]byte)
for k, v := range s {
if k == AcsChars {
@@ -228,39 +214,32 @@ func (d *decoder) readStrings(n, sz int) (map[int][]byte, map[int]bool, error) {
}
strs[k] = v
}
strsM := make(map[int]bool, len(m))
for _, k := range m {
strsM[k] = true
}
return strs, strsM, nil
}
// canonicalizeAscChars reorders chars to be unique, in order.
//
// see repair_ascc in ncurses-6.0/progs/dump_entry.c
// see repair_ascc in ncurses-6.3/progs/dump_entry.c
func canonicalizeAscChars(z []byte) []byte {
var c chars
var c []byte
enc := make(map[byte]byte, len(z)/2)
for i := 0; i < len(z); i += 2 {
if _, ok := enc[z[i]]; !ok {
a, b := z[i], z[i+1]
//log.Printf(">>> a: %d %c, b: %d %c", a, a, b, b)
// log.Printf(">>> a: %d %c, b: %d %c", a, a, b, b)
c, enc[a] = append(c, b), b
}
}
sort.Sort(c)
sort.Slice(c, func(i, j int) bool {
return c[i] < c[j]
})
r := make([]byte, 2*len(c))
for i := 0; i < len(c); i++ {
r[i*2], r[i*2+1] = c[i], enc[c[i]]
}
return r
}
type chars []byte
func (c chars) Len() int { return len(c) }
func (c chars) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c chars) Less(i, j int) bool { return c[i] < c[j] }
-8
View File
@@ -23,34 +23,27 @@ func Load(name string) (*Terminfo, error) {
if name == "" {
return nil, ErrEmptyTermName
}
termCache.RLock()
ti, ok := termCache.db[name]
termCache.RUnlock()
if ok {
return ti, nil
}
var checkDirs []string
// check $TERMINFO
if dir := os.Getenv("TERMINFO"); dir != "" {
checkDirs = append(checkDirs, dir)
}
// check $HOME/.terminfo
u, err := user.Current()
if err != nil {
return nil, err
}
checkDirs = append(checkDirs, path.Join(u.HomeDir, ".terminfo"))
// check $TERMINFO_DIRS
if dirs := os.Getenv("TERMINFO_DIRS"); dirs != "" {
checkDirs = append(checkDirs, strings.Split(dirs, ":")...)
}
// check fallback directories
checkDirs = append(checkDirs, "/etc/terminfo", "/lib/terminfo", "/usr/share/terminfo")
for _, dir := range checkDirs {
@@ -61,7 +54,6 @@ func Load(name string) (*Terminfo, error) {
return ti, nil
}
}
return nil, ErrDatabaseDirectoryNotFound
}
-85
View File
@@ -13,25 +13,18 @@ import (
type parametizer struct {
// z is the string to parameterize
z []byte
// pos is the current position in s.
pos int
// nest is the current nest level.
nest int
// s is the variable stack.
s stack
// skipElse keeps the state of skipping else.
skipElse bool
// buf is the result buffer.
buf *bytes.Buffer
// params are the parameters to interpolate.
params [9]interface{}
// vars are dynamic variables.
vars [26]interface{}
}
@@ -54,19 +47,15 @@ var parametizerPool = sync.Pool{
func newParametizer(z []byte) *parametizer {
p := parametizerPool.Get().(*parametizer)
p.z = z
return p
}
// reset resets the parametizer.
func (p *parametizer) reset() {
p.pos, p.nest = 0, 0
p.s.reset()
p.buf.Reset()
p.params, p.vars = [9]interface{}{}, [26]interface{}{}
parametizerPool.Put(p)
}
@@ -106,13 +95,11 @@ func (p *parametizer) scanTextFn() stateFn {
p.writeFrom(ppos)
return nil
}
if ch == '%' {
p.writeFrom(ppos)
p.pos++
return p.scanCodeFn
}
p.pos++
}
}
@@ -122,11 +109,9 @@ func (p *parametizer) scanCodeFn() stateFn {
if err != nil {
return nil
}
switch ch {
case '%':
p.buf.WriteByte('%')
case ':':
// this character is used to avoid interpreting "%-" and "%+" as operators.
// the next character is where the format really begins.
@@ -136,71 +121,52 @@ func (p *parametizer) scanCodeFn() stateFn {
return nil
}
return p.scanFormatFn
case '#', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
return p.scanFormatFn
case 'o':
p.buf.WriteString(strconv.FormatInt(int64(p.s.popInt()), 8))
case 'd':
p.buf.WriteString(strconv.Itoa(p.s.popInt()))
case 'x':
p.buf.WriteString(strconv.FormatInt(int64(p.s.popInt()), 16))
case 'X':
p.buf.WriteString(strings.ToUpper(strconv.FormatInt(int64(p.s.popInt()), 16)))
case 's':
p.buf.WriteString(p.s.popString())
case 'c':
p.buf.WriteByte(p.s.popByte())
case 'p':
p.pos++
return p.pushParamFn
case 'P':
p.pos++
return p.setDsVarFn
case 'g':
p.pos++
return p.getDsVarFn
case '\'':
p.pos++
ch, err = p.peek()
if err != nil {
return nil
}
p.s.push(ch)
// skip the '\''
p.pos++
case '{':
p.pos++
return p.pushIntfn
case 'l':
p.s.push(len(p.s.popString()))
case '+':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai + bi)
case '-':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai - bi)
case '*':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai * bi)
case '/':
bi, ai := p.s.popInt(), p.s.popInt()
if bi != 0 {
@@ -208,7 +174,6 @@ func (p *parametizer) scanCodeFn() stateFn {
} else {
p.s.push(0)
}
case 'm':
bi, ai := p.s.popInt(), p.s.popInt()
if bi != 0 {
@@ -216,101 +181,77 @@ func (p *parametizer) scanCodeFn() stateFn {
} else {
p.s.push(0)
}
case '&':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai & bi)
case '|':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai | bi)
case '^':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai ^ bi)
case '=':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai == bi)
case '>':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai > bi)
case '<':
bi, ai := p.s.popInt(), p.s.popInt()
p.s.push(ai < bi)
case 'A':
bi, ai := p.s.popBool(), p.s.popBool()
p.s.push(ai && bi)
case 'O':
bi, ai := p.s.popBool(), p.s.popBool()
p.s.push(ai || bi)
case '!':
p.s.push(!p.s.popBool())
case '~':
p.s.push(^p.s.popInt())
case 'i':
for i := range p.params[:2] {
if n, ok := p.params[i].(int); ok {
p.params[i] = n + 1
}
}
case '?', ';':
case 't':
return p.scanThenFn
case 'e':
p.skipElse = true
return p.skipTextFn
}
p.pos++
return p.scanTextFn
}
func (p *parametizer) scanFormatFn() stateFn {
// the character was already read, so no need to check the error.
ch, _ := p.peek()
// 6 should be the maximum length of a format string, for example "%:-9.9d".
f := []byte{'%', ch, 0, 0, 0, 0}
var err error
for {
p.pos++
ch, err = p.peek()
if err != nil {
return nil
}
f = append(f, ch)
switch ch {
case 'o', 'd', 'x', 'X':
fmt.Fprintf(p.buf, string(f), p.s.popInt())
break
case 's':
fmt.Fprintf(p.buf, string(f), p.s.popString())
break
case 'c':
fmt.Fprintf(p.buf, string(f), p.s.popByte())
break
}
}
p.pos++
return p.scanTextFn
}
@@ -319,16 +260,13 @@ func (p *parametizer) pushParamFn() stateFn {
if err != nil {
return nil
}
if ai := int(ch - '1'); ai >= 0 && ai < len(p.params) {
p.s.push(p.params[ai])
} else {
p.s.push(0)
}
// skip the '}'
p.pos++
return p.scanTextFn
}
@@ -337,7 +275,6 @@ func (p *parametizer) setDsVarFn() stateFn {
if err != nil {
return nil
}
if ch >= 'A' && ch <= 'Z' {
staticVars.Lock()
staticVars.vars[int(ch-'A')] = p.s.pop()
@@ -345,7 +282,6 @@ func (p *parametizer) setDsVarFn() stateFn {
} else if ch >= 'a' && ch <= 'z' {
p.vars[int(ch-'a')] = p.s.pop()
}
p.pos++
return p.scanTextFn
}
@@ -355,20 +291,16 @@ func (p *parametizer) getDsVarFn() stateFn {
if err != nil {
return nil
}
var a byte
if ch >= 'A' && ch <= 'Z' {
a = 'A'
} else if ch >= 'a' && ch <= 'z' {
a = 'a'
}
staticVars.Lock()
p.s.push(staticVars.vars[int(ch-a)])
staticVars.Unlock()
p.pos++
return p.scanTextFn
}
@@ -379,26 +311,21 @@ func (p *parametizer) pushIntfn() stateFn {
if err != nil {
return nil
}
p.pos++
if ch < '0' || ch > '9' {
p.s.push(ai)
return p.scanTextFn
}
ai = (ai * 10) + int(ch-'0')
}
}
func (p *parametizer) scanThenFn() stateFn {
p.pos++
if p.s.popBool() {
return p.scanTextFn
}
p.skipElse = false
return p.skipTextFn
}
@@ -408,17 +335,14 @@ func (p *parametizer) skipTextFn() stateFn {
if err != nil {
return nil
}
p.pos++
if ch == '%' {
break
}
}
if p.skipElse {
return p.skipElseFn
}
return p.skipThenFn
}
@@ -427,7 +351,6 @@ func (p *parametizer) skipThenFn() stateFn {
if err != nil {
return nil
}
p.pos++
switch ch {
case ';':
@@ -435,16 +358,13 @@ func (p *parametizer) skipThenFn() stateFn {
return p.scanTextFn
}
p.nest--
case '?':
p.nest++
case 'e':
if p.nest == 0 {
return p.scanTextFn
}
}
return p.skipTextFn
}
@@ -453,7 +373,6 @@ func (p *parametizer) skipElseFn() stateFn {
if err != nil {
return nil
}
p.pos++
switch ch {
case ';':
@@ -461,11 +380,9 @@ func (p *parametizer) skipElseFn() stateFn {
return p.scanTextFn
}
p.nest--
case '?':
p.nest++
}
return p.skipTextFn
}
@@ -473,13 +390,11 @@ func (p *parametizer) skipElseFn() stateFn {
func Printf(z []byte, params ...interface{}) string {
p := newParametizer(z)
defer p.reset()
// make sure we always have 9 parameters -- makes it easier
// later to skip checks and its faster
for i := 0; i < len(p.params) && i < len(params); i++ {
p.params[i] = params[i]
}
return p.exec()
}
+12 -71
View File
@@ -1,6 +1,8 @@
// Package terminfo implements reading terminfo files in pure go.
package terminfo
//go:generate go run gen.go
import (
"io"
"io/ioutil"
@@ -20,34 +22,24 @@ func (err Error) Error() string {
const (
// ErrInvalidFileSize is the invalid file size error.
ErrInvalidFileSize Error = "invalid file size"
// ErrUnexpectedFileEnd is the unexpected file end error.
ErrUnexpectedFileEnd Error = "unexpected file end"
// ErrInvalidStringTable is the invalid string table error.
ErrInvalidStringTable Error = "invalid string table"
// ErrInvalidMagic is the invalid magic error.
ErrInvalidMagic Error = "invalid magic"
// ErrInvalidHeader is the invalid header error.
ErrInvalidHeader Error = "invalid header"
// ErrInvalidNames is the invalid names error.
ErrInvalidNames Error = "invalid names"
// ErrInvalidExtendedHeader is the invalid extended header error.
ErrInvalidExtendedHeader Error = "invalid extended header"
// ErrEmptyTermName is the empty term name error.
ErrEmptyTermName Error = "empty term name"
// ErrDatabaseDirectoryNotFound is the database directory not found error.
ErrDatabaseDirectoryNotFound Error = "database directory not found"
// ErrFileNotFound is the file not found error.
ErrFileNotFound Error = "file not found"
// ErrInvalidTermProgramVersion is the invalid TERM_PROGRAM_VERSION error.
ErrInvalidTermProgramVersion Error = "invalid TERM_PROGRAM_VERSION"
)
@@ -56,43 +48,30 @@ const (
type Terminfo struct {
// File is the original source file.
File string
// Names are the provided cap names.
Names []string
// Bools are the bool capabilities.
Bools map[int]bool
// BoolsM are the missing bool capabilities.
BoolsM map[int]bool
// Nums are the num capabilities.
Nums map[int]int
// NumsM are the missing num capabilities.
NumsM map[int]bool
// Strings are the string capabilities.
Strings map[int][]byte
// StringsM are the missing string capabilities.
StringsM map[int]bool
// ExtBools are the extended bool capabilities.
ExtBools map[int]bool
// ExtBoolsNames is the map of extended bool capabilities to their index.
ExtBoolNames map[int][]byte
// ExtNums are the extended num capabilities.
ExtNums map[int]int
// ExtNumsNames is the map of extended num capabilities to their index.
ExtNumNames map[int][]byte
// ExtStrings are the extended string capabilities.
ExtStrings map[int][]byte
// ExtStringsNames is the map of extended string capabilities to their index.
ExtStringNames map[int][]byte
}
@@ -100,75 +79,63 @@ type Terminfo struct {
// Decode decodes the terminfo data contained in buf.
func Decode(buf []byte) (*Terminfo, error) {
var err error
// check max file length
if len(buf) >= maxFileLength {
return nil, ErrInvalidFileSize
}
d := &decoder{
buf: buf,
len: len(buf),
n: len(buf),
}
// read header
h, err := d.readInts(6, 16)
if err != nil {
return nil, err
}
var numWidth int
// check magic
if h[fieldMagic] == magic {
switch {
case h[fieldMagic] == magic:
numWidth = 16
} else if h[fieldMagic] == magicExtended {
case h[fieldMagic] == magicExtended:
numWidth = 32
} else {
default:
return nil, ErrInvalidMagic
}
// check header
if hasInvalidCaps(h) {
return nil, ErrInvalidHeader
}
// check remaining length
if d.len-d.pos < capLength(h) {
if d.n-d.pos < capLength(h) {
return nil, ErrUnexpectedFileEnd
}
// read names
names, err := d.readBytes(h[fieldNameSize])
if err != nil {
return nil, err
}
// check name is terminated properly
i := findNull(names, 0)
if i == -1 {
return nil, ErrInvalidNames
}
names = names[:i]
// read bool caps
bools, boolsM, err := d.readBools(h[fieldBoolCount])
if err != nil {
return nil, err
}
// read num caps
nums, numsM, err := d.readNums(h[fieldNumCount], numWidth)
if err != nil {
return nil, err
}
// read string caps
strs, strsM, err := d.readStrings(h[fieldStringCount], h[fieldTableSize])
if err != nil {
return nil, err
}
ti := &Terminfo{
Names: strings.Split(string(names), "|"),
Bools: bools,
@@ -178,57 +145,47 @@ func Decode(buf []byte) (*Terminfo, error) {
Strings: strs,
StringsM: strsM,
}
// at the end of file, so no extended caps
if d.pos >= d.len {
if d.pos >= d.n {
return ti, nil
}
// decode extended header
eh, err := d.readInts(5, 16)
if err != nil {
return nil, err
}
// check extended offset field
if hasInvalidExtOffset(eh) {
return nil, ErrInvalidExtendedHeader
}
// check extended cap lengths
if d.len-d.pos != extCapLength(eh, numWidth) {
if d.n-d.pos != extCapLength(eh, numWidth) {
return nil, ErrInvalidExtendedHeader
}
// read extended bool caps
ti.ExtBools, _, err = d.readBools(eh[fieldExtBoolCount])
if err != nil {
return nil, err
}
// read extended num caps
ti.ExtNums, _, err = d.readNums(eh[fieldExtNumCount], numWidth)
if err != nil {
return nil, err
}
// read extended string data table indexes
extIndexes, err := d.readInts(eh[fieldExtOffsetCount], 16)
if err != nil {
return nil, err
}
// read string data table
extData, err := d.readBytes(eh[fieldExtTableSize])
if err != nil {
return nil, err
}
// precautionary check that exactly at end of file
if d.pos != d.len {
if d.pos != d.n {
return nil, ErrUnexpectedFileEnd
}
var last int
// read extended string caps
ti.ExtStrings, last, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
@@ -236,28 +193,24 @@ func Decode(buf []byte) (*Terminfo, error) {
return nil, err
}
extIndexes, extData = extIndexes[eh[fieldExtStringCount]:], extData[last:]
// read extended bool names
ti.ExtBoolNames, _, err = readStrings(extIndexes, extData, eh[fieldExtBoolCount])
if err != nil {
return nil, err
}
extIndexes = extIndexes[eh[fieldExtBoolCount]:]
// read extended num names
ti.ExtNumNames, _, err = readStrings(extIndexes, extData, eh[fieldExtNumCount])
if err != nil {
return nil, err
}
extIndexes = extIndexes[eh[fieldExtNumCount]:]
// read extended string names
ti.ExtStringNames, _, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
if err != nil {
return nil, err
}
//extIndexes = extIndexes[eh[fieldExtStringCount]:]
// extIndexes = extIndexes[eh[fieldExtStringCount]:]
return ti, nil
}
@@ -279,23 +232,19 @@ func Open(dir, name string) (*Terminfo, error) {
if buf == nil {
return nil, ErrFileNotFound
}
// decode
ti, err := Decode(buf)
if err != nil {
return nil, err
}
// save original file name
ti.File = filename
// add to cache
termCache.Lock()
for _, n := range ti.Names {
termCache.db[n] = ti
}
termCache.Unlock()
return ti, nil
}
@@ -441,7 +390,6 @@ func (ti *Terminfo) Fprintf(w io.Writer, i int, v ...interface{}) {
// them for this terminal.
func (ti *Terminfo) Colorf(fg, bg int, str string) string {
maxColors := int(ti.Nums[MaxColors])
// map bright colors to lower versions if the color table only holds 8.
if maxColors == 8 {
if fg > 7 && fg < 16 {
@@ -451,7 +399,6 @@ func (ti *Terminfo) Colorf(fg, bg int, str string) string {
bg -= 8
}
}
var s string
if maxColors > fg && fg >= 0 {
s += ti.Printf(SetAForeground, fg)
@@ -480,20 +427,17 @@ func (ti *Terminfo) Goto(row, col int) string {
// most strings don't need padding, which is good news!
return io.WriteString(w, s)
}
end := strings.Index(s, ">")
if end == -1 {
// unterminated... just emit bytes unadulterated.
return io.WriteString(w, "$<"+s)
}
var c int
c, err = io.WriteString(w, s[:start])
if err != nil {
return n + c, err
}
n += c
s = s[start+2:]
val := s[:end]
s = s[end+1:]
@@ -518,13 +462,11 @@ func (ti *Terminfo) Goto(row, col int) string {
break
}
}
z, pad := ((baud/8)/unit)*ms, ti.Strings[PadChar]
b := make([]byte, len(pad)*z)
for bp := copy(b, pad); bp < len(b); bp *= 2 {
copy(b[bp:], b[:bp])
}
if (!ti.Bools[XonXoff] && baud > int(ti.Nums[PaddingBaudRate])) || mandatory {
c, err = w.Write(b)
if err != nil {
@@ -533,6 +475,5 @@ func (ti *Terminfo) Goto(row, col int) string {
n += c
}
}
return n, nil
}*/
+15 -6
View File
@@ -17,6 +17,15 @@ github.com/bahlo/generic-list-go
# github.com/buger/jsonparser v1.1.2
## explicit; go 1.13
github.com/buger/jsonparser
# github.com/cli/go-gh/v2 v2.13.0
## explicit; go 1.25.0
github.com/cli/go-gh/v2/internal/set
github.com/cli/go-gh/v2/internal/yamlmap
github.com/cli/go-gh/v2/pkg/auth
github.com/cli/go-gh/v2/pkg/config
# github.com/cli/safeexec v1.0.1
## explicit; go 1.15
github.com/cli/safeexec
# github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
## explicit
github.com/cloudfoundry/jibber_jabber
@@ -119,11 +128,11 @@ github.com/lucasb-eyer/go-colorful
## explicit; go 1.12
github.com/mailru/easyjson/buffer
github.com/mailru/easyjson/jwriter
# github.com/mattn/go-colorable v0.1.11
## explicit; go 1.13
# github.com/mattn/go-colorable v0.1.13
## explicit; go 1.15
github.com/mattn/go-colorable
# github.com/mattn/go-isatty v0.0.14
## explicit; go 1.12
# github.com/mattn/go-isatty v0.0.20
## explicit; go 1.15
github.com/mattn/go-isatty
# github.com/mgutz/str v1.2.0
## explicit
@@ -179,8 +188,8 @@ github.com/stretchr/testify/assert/yaml
# github.com/wk8/go-ordered-map/v2 v2.1.8
## explicit; go 1.18
github.com/wk8/go-ordered-map/v2
# github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
## explicit; go 1.15
# github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
## explicit; go 1.19
github.com/xo/terminfo
# golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
## explicit; go 1.20