Add Corteza studio UI personalization feature
This commit is contained in:
committed by
Mumbi Francis
parent
d10257cc36
commit
e4269669d8
+17
@@ -0,0 +1,17 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
lib/
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Bjørn Erik Pedersen
|
||||
|
||||
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.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
[](https://github.com/bep/godartsass/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/bep/godartsass)
|
||||
[](https://codecov.io/gh/bep/godartsass)
|
||||
[](https://godoc.org/github.com/bep/godartsass)
|
||||
|
||||
This is a Go API backed by the native [Dart Sass](https://github.com/sass/dart-sass/releases) executable running with `sass --embedded`.
|
||||
|
||||
>**Note:** The `v2.x.x` of this project targets the `v2` of the Dart Sass Embedded protocol with the `sass` exexutable in releases that can be downloaeded [here](https://github.com/sass/dart-sass/releases). For `v1` you need to import `github.com/bep/godartsass` and not `github.com/bep/godartsass/v2`.
|
||||
|
||||
The primary motivation for this project is to provide `SCSS` support to [Hugo](https://gohugo.io/). I welcome PRs with bug fixes. I will also consider adding functionality, but please raise an issue discussing it first.
|
||||
|
||||
For LibSass bindings in Go, see [GoLibSass](https://github.com/bep/golibsass).
|
||||
|
||||
```
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
patch: off
|
||||
|
||||
comment:
|
||||
require_changes: true
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package godartsass
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newConn(cmd *exec.Cmd) (_ conn, err error) {
|
||||
in, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return conn{}, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
in.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := cmd.StdoutPipe()
|
||||
stdErr := &tailBuffer{limit: 1024}
|
||||
buff := bufio.NewReader(out)
|
||||
c := conn{buff, buff, out, in, stdErr, cmd}
|
||||
cmd.Stderr = c.stdErr
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
type byteReadWriteCloser interface {
|
||||
io.ReadWriteCloser
|
||||
io.ByteReader
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
io.ByteReader
|
||||
io.Reader
|
||||
readerCloser io.Closer
|
||||
io.WriteCloser
|
||||
stdErr *tailBuffer
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// Start starts conn's Cmd.
|
||||
func (c conn) Start() error {
|
||||
err := c.cmd.Start()
|
||||
if err != nil {
|
||||
return c.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes conn's WriteCloser, ReadClosers, and waits for the command to finish.
|
||||
func (c conn) Close() error {
|
||||
|
||||
writeErr := c.WriteCloser.Close()
|
||||
readErr := c.readerCloser.Close()
|
||||
var interruptErr error
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
// See https://github.com/bep/godartsass/issues/19
|
||||
interruptErr = c.cmd.Process.Signal(os.Interrupt)
|
||||
if interruptErr == os.ErrProcessDone {
|
||||
interruptErr = nil
|
||||
}
|
||||
}
|
||||
|
||||
cmdErr := c.waitWithTimeout()
|
||||
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
|
||||
if interruptErr != nil {
|
||||
return interruptErr
|
||||
}
|
||||
|
||||
return cmdErr
|
||||
}
|
||||
|
||||
var brokenPipeRe = regexp.MustCompile("Broken pipe|pipe is being closed")
|
||||
|
||||
// dart-sass ends on itself on EOF, this is just to give it some
|
||||
// time to do so.
|
||||
func (c conn) waitWithTimeout() error {
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- c.cmd.Wait() }()
|
||||
select {
|
||||
case err := <-result:
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
if eerr.Error() == "signal: interrupt" {
|
||||
return nil
|
||||
}
|
||||
if brokenPipeRe.MatchString(c.stdErr.String()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
return err
|
||||
case <-time.After(5 * time.Second):
|
||||
return errors.New("timed out waiting for dart-sass to finish")
|
||||
}
|
||||
}
|
||||
|
||||
type tailBuffer struct {
|
||||
limit int
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *tailBuffer) Write(p []byte) (n int, err error) {
|
||||
if len(p)+b.Buffer.Len() > b.limit {
|
||||
b.Reset()
|
||||
}
|
||||
n, err = b.Buffer.Write(p)
|
||||
return
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
|
||||
* Install protobuf: https://github.com/protocolbuffers/protobuf
|
||||
* Install the Go plugin: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
* Download the correct version of the proto file: https://github.com/sass/sass/blob/main/spec/embedded_sass.proto
|
||||
* protoc --go_opt=Membedded_sass.proto=github.com/bep/godartsass/internal/embeddedsass --go_opt=paths=source_relative --go_out=. embedded_sass.proto
|
||||
Generated
Vendored
+5278
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+1003
File diff suppressed because it is too large
Load Diff
+231
@@ -0,0 +1,231 @@
|
||||
package godartsass
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/godartsass/v2/internal/embeddedsass"
|
||||
)
|
||||
|
||||
// Options configures a Transpiler.
|
||||
type Options struct {
|
||||
// The path to the Dart Sass wrapper binary, an absolute filename
|
||||
// if not in $PATH.
|
||||
// If this is not set, we will try 'dart-sass'
|
||||
// (or 'dart-sass.bat' on Windows) in the OS $PATH.
|
||||
// There may be several ways to install this, one would be to
|
||||
// download it from here: https://github.com/sass/dart-sass/releases
|
||||
DartSassEmbeddedFilename string
|
||||
|
||||
// Timeout is the duration allowed for dart sass to transpile.
|
||||
// This was added for the beta6 version of Dart Sass Protocol,
|
||||
// as running this code against the beta5 binary would hang
|
||||
// on Execute.
|
||||
Timeout time.Duration
|
||||
|
||||
// LogEventHandler will, if set, receive log events from Dart Sass,
|
||||
// e.g. @debug and @warn log statements.
|
||||
LogEventHandler func(LogEvent)
|
||||
}
|
||||
|
||||
// LogEvent is a type of log event from Dart Sass.
|
||||
type LogEventType int
|
||||
|
||||
const (
|
||||
// Usually triggered by the @warn directive.
|
||||
LogEventTypeWarning LogEventType = iota
|
||||
|
||||
// Events trigered for usage of deprecated Sass features.
|
||||
LogEventTypeDeprecated
|
||||
|
||||
// Triggered by the @debug directive.
|
||||
LogEventTypeDebug
|
||||
)
|
||||
|
||||
type LogEvent struct {
|
||||
// Type is the type of log event.
|
||||
Type LogEventType
|
||||
|
||||
// Message on the form url:line:col message.
|
||||
Message string
|
||||
}
|
||||
|
||||
func (opts *Options) init() error {
|
||||
if opts.DartSassEmbeddedFilename == "" {
|
||||
opts.DartSassEmbeddedFilename = defaultDartSassBinaryFilename
|
||||
}
|
||||
|
||||
if opts.Timeout == 0 {
|
||||
opts.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportResolver allows custom import resolution.
|
||||
//
|
||||
// CanonicalizeURL should create a canonical version of the given URL if it's
|
||||
// able to resolve it, else return an empty string.
|
||||
//
|
||||
// A canonicalized URL should include a scheme, e.g. 'file:///foo/bar.scss',
|
||||
// if applicable, see:
|
||||
//
|
||||
// https://en.wikipedia.org/wiki/File_URI_scheme
|
||||
//
|
||||
// Importers must ensure that the same canonical URL
|
||||
// always refers to the same stylesheet.
|
||||
//
|
||||
// Load loads the canonicalized URL's content.
|
||||
type ImportResolver interface {
|
||||
CanonicalizeURL(url string) (string, error)
|
||||
Load(canonicalizedURL string) (Import, error)
|
||||
}
|
||||
|
||||
type Import struct {
|
||||
// The content of the imported file.
|
||||
Content string
|
||||
|
||||
// The syntax of the imported file.
|
||||
SourceSyntax SourceSyntax
|
||||
}
|
||||
|
||||
// Args holds the arguments to Execute.
|
||||
type Args struct {
|
||||
// The input source.
|
||||
Source string
|
||||
|
||||
// The URL of the Source.
|
||||
// Leave empty if it's unknown.
|
||||
// Must include a scheme, e.g. 'file:///myproject/main.scss'
|
||||
// See https://en.wikipedia.org/wiki/File_URI_scheme
|
||||
//
|
||||
// Note: There is an open issue for this value when combined with custom
|
||||
// importers, see https://github.com/sass/dart-sass/issues/24
|
||||
URL string
|
||||
|
||||
// Defaults is SCSS.
|
||||
SourceSyntax SourceSyntax
|
||||
|
||||
// Default is EXPANDED.
|
||||
OutputStyle OutputStyle
|
||||
|
||||
// If enabled, a sourcemap will be generated and returned in Result.
|
||||
EnableSourceMap bool
|
||||
|
||||
// If enabled, sources will be embedded in the generated source map.
|
||||
SourceMapIncludeSources bool
|
||||
|
||||
// Custom resolver to use to resolve imports.
|
||||
// If set, this will be the first in the resolver chain.
|
||||
ImportResolver ImportResolver
|
||||
|
||||
// Additional file paths to uses to resolve imports.
|
||||
IncludePaths []string
|
||||
|
||||
sassOutputStyle embeddedsass.OutputStyle
|
||||
sassSourceSyntax embeddedsass.Syntax
|
||||
|
||||
// Ordered list starting with options.ImportResolver, then IncludePaths.
|
||||
sassImporters []*embeddedsass.InboundMessage_CompileRequest_Importer
|
||||
}
|
||||
|
||||
func (args *Args) init(seq uint32, opts Options) error {
|
||||
if args.OutputStyle == "" {
|
||||
args.OutputStyle = OutputStyleExpanded
|
||||
}
|
||||
if args.SourceSyntax == "" {
|
||||
args.SourceSyntax = SourceSyntaxSCSS
|
||||
}
|
||||
|
||||
v, ok := embeddedsass.OutputStyle_value[string(args.OutputStyle)]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid OutputStyle %q", args.OutputStyle)
|
||||
}
|
||||
args.sassOutputStyle = embeddedsass.OutputStyle(v)
|
||||
|
||||
v, ok = embeddedsass.Syntax_value[string(args.SourceSyntax)]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid SourceSyntax %q", args.SourceSyntax)
|
||||
}
|
||||
|
||||
args.sassSourceSyntax = embeddedsass.Syntax(v)
|
||||
|
||||
if args.ImportResolver != nil {
|
||||
args.sassImporters = []*embeddedsass.InboundMessage_CompileRequest_Importer{
|
||||
{
|
||||
Importer: &embeddedsass.InboundMessage_CompileRequest_Importer_ImporterId{
|
||||
ImporterId: seq,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if args.IncludePaths != nil {
|
||||
for _, p := range args.IncludePaths {
|
||||
args.sassImporters = append(args.sassImporters, &embeddedsass.InboundMessage_CompileRequest_Importer{Importer: &embeddedsass.InboundMessage_CompileRequest_Importer_Path{
|
||||
Path: filepath.Clean(p),
|
||||
}})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
// OutputStyle defines the style of the generated CSS.
|
||||
OutputStyle string
|
||||
|
||||
// SourceSyntax defines the syntax of the source passed in Execute.
|
||||
SourceSyntax string
|
||||
)
|
||||
|
||||
const (
|
||||
// Expanded (default) output.
|
||||
// Note that LibSASS and Ruby SASS have more output styles, and their
|
||||
// default is NESTED.
|
||||
OutputStyleExpanded OutputStyle = "EXPANDED"
|
||||
|
||||
// Compressed/minified output.
|
||||
OutputStyleCompressed OutputStyle = "COMPRESSED"
|
||||
)
|
||||
|
||||
const (
|
||||
// SCSS style source syntax (default).
|
||||
SourceSyntaxSCSS SourceSyntax = "SCSS"
|
||||
|
||||
// Indented or SASS style source syntax.
|
||||
SourceSyntaxSASS SourceSyntax = "INDENTED"
|
||||
|
||||
// Regular CSS source syntax.
|
||||
SourceSyntaxCSS SourceSyntax = "CSS"
|
||||
)
|
||||
|
||||
// ParseOutputStyle will convert s into OutputStyle.
|
||||
// Case insensitive, returns OutputStyleNested for unknown value.
|
||||
func ParseOutputStyle(s string) OutputStyle {
|
||||
switch OutputStyle(strings.ToUpper(s)) {
|
||||
case OutputStyleCompressed:
|
||||
return OutputStyleCompressed
|
||||
case OutputStyleExpanded:
|
||||
return OutputStyleExpanded
|
||||
default:
|
||||
return OutputStyleExpanded
|
||||
}
|
||||
}
|
||||
|
||||
// ParseSourceSyntax will convert s into SourceSyntax.
|
||||
// Case insensitive, returns SourceSyntaxSCSS for unknown value.
|
||||
func ParseSourceSyntax(s string) SourceSyntax {
|
||||
switch SourceSyntax(strings.ToUpper(s)) {
|
||||
case SourceSyntaxSCSS:
|
||||
return SourceSyntaxSCSS
|
||||
case SourceSyntaxSASS, "SASS":
|
||||
return SourceSyntaxSASS
|
||||
case SourceSyntaxCSS:
|
||||
return SourceSyntaxCSS
|
||||
default:
|
||||
return SourceSyntaxSCSS
|
||||
}
|
||||
}
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
// Package godartsass provides a Go API for the Dass Sass Embedded protocol.
|
||||
//
|
||||
// Use the Start function to create and start a new thread safe transpiler.
|
||||
// Close it when done.
|
||||
package godartsass
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
|
||||
"github.com/bep/godartsass/v2/internal/embeddedsass"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const defaultDartSassBinaryFilename = "sass"
|
||||
|
||||
// ErrShutdown will be returned from Execute and Close if the transpiler is or
|
||||
// is about to be shut down.
|
||||
var ErrShutdown = errors.New("connection is shut down")
|
||||
|
||||
// Start creates and starts a new SCSS transpiler that communicates with the
|
||||
// Dass Sass Embedded protocol via Stdin and Stdout.
|
||||
//
|
||||
// Closing the transpiler will shut down the process.
|
||||
//
|
||||
// Note that the Transpiler is thread safe, and the recommended way of using
|
||||
// this is to create one and use that for all the SCSS processing needed.
|
||||
func Start(opts Options) (*Transpiler, error) {
|
||||
if err := opts.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See https://github.com/golang/go/issues/38736
|
||||
bin, err := safeexec.LookPath(opts.DartSassEmbeddedFilename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.Command(bin)
|
||||
cmd.Args = append(cmd.Args, "--embedded")
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
conn, err := newConn(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := conn.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &Transpiler{
|
||||
opts: opts,
|
||||
conn: conn,
|
||||
lenBuf: make([]byte, binary.MaxVarintLen64),
|
||||
idBuf: make([]byte, binary.MaxVarintLen64),
|
||||
pending: make(map[uint32]*call),
|
||||
}
|
||||
|
||||
go t.input()
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Version returns version information about the Dart Sass frameworks used
|
||||
// in dartSassEmbeddedFilename.
|
||||
func Version(dartSassEmbeddedFilename string) (DartSassVersion, error) {
|
||||
var v DartSassVersion
|
||||
bin, err := safeexec.LookPath(dartSassEmbeddedFilename)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
||||
cmd := exec.Command(bin, "--embedded", "--version")
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(out, &v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type DartSassVersion struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
CompilerVersion string `json:"compilerVersion"`
|
||||
ImplementationVersion string `json:"implementationVersion"`
|
||||
ImplementationName string `json:"implementationName"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
// Transpiler controls transpiling of SCSS into CSS.
|
||||
type Transpiler struct {
|
||||
opts Options
|
||||
|
||||
// stdin/stdout of the Dart Sass protocol
|
||||
conn byteReadWriteCloser
|
||||
lenBuf []byte
|
||||
idBuf []byte
|
||||
msgBuf []byte
|
||||
|
||||
closing bool
|
||||
shutdown bool
|
||||
|
||||
// Protects the sending of messages to Dart Sass.
|
||||
sendMu sync.Mutex
|
||||
|
||||
mu sync.Mutex // Protects all below.
|
||||
seq uint32
|
||||
pending map[uint32]*call
|
||||
}
|
||||
|
||||
// IsShutDown checks if all pending calls have been shut down.
|
||||
// Used in tests.
|
||||
func (t *Transpiler) IsShutDown() bool {
|
||||
for _, p := range t.pending {
|
||||
if p.Error != ErrShutdown {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Result holds the result returned from Execute.
|
||||
type Result struct {
|
||||
CSS string
|
||||
SourceMap string
|
||||
}
|
||||
|
||||
// SassError is the error returned from Execute on compile errors.
|
||||
type SassError struct {
|
||||
Message string `json:"message"`
|
||||
Span struct {
|
||||
Text string `json:"text"`
|
||||
Start struct {
|
||||
Offset int `json:"offset"`
|
||||
Column int `json:"column"`
|
||||
} `json:"start"`
|
||||
End struct {
|
||||
Offset int `json:"offset"`
|
||||
Column int `json:"column"`
|
||||
} `json:"end"`
|
||||
Url string `json:"url"`
|
||||
Context string `json:"context"`
|
||||
} `json:"span"`
|
||||
}
|
||||
|
||||
func (e SassError) Error() string {
|
||||
span := e.Span
|
||||
file := path.Clean(strings.TrimPrefix(span.Url, "file:"))
|
||||
return fmt.Sprintf("file: %q, context: %q: %s", file, span.Context, e.Message)
|
||||
}
|
||||
|
||||
// Close closes the stream to the embedded Dart Sass Protocol, shutting it down.
|
||||
// If it is already shutting down, ErrShutdown is returned.
|
||||
func (t *Transpiler) Close() error {
|
||||
t.sendMu.Lock()
|
||||
defer t.sendMu.Unlock()
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.closing {
|
||||
return ErrShutdown
|
||||
}
|
||||
|
||||
t.closing = true
|
||||
err := t.conn.Close()
|
||||
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
if eerr.ExitCode() == 1 {
|
||||
// This is the expected exit code when shutting down.
|
||||
return ErrShutdown
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Execute transpiles the string Source given in Args into CSS.
|
||||
// If Dart Sass resturns a "compile failure", the error returned will be
|
||||
// of type SassError.
|
||||
func (t *Transpiler) Execute(args Args) (Result, error) {
|
||||
var result Result
|
||||
|
||||
createInboundMessage := func(seq uint32) (*embeddedsass.InboundMessage, error) {
|
||||
if err := args.init(seq, t.opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := &embeddedsass.InboundMessage_CompileRequest_{
|
||||
CompileRequest: &embeddedsass.InboundMessage_CompileRequest{
|
||||
Importers: args.sassImporters,
|
||||
Style: args.sassOutputStyle,
|
||||
Input: &embeddedsass.InboundMessage_CompileRequest_String_{
|
||||
String_: &embeddedsass.InboundMessage_CompileRequest_StringInput{
|
||||
Syntax: args.sassSourceSyntax,
|
||||
Source: args.Source,
|
||||
Url: args.URL,
|
||||
},
|
||||
},
|
||||
SourceMap: args.EnableSourceMap,
|
||||
SourceMapIncludeSources: args.SourceMapIncludeSources,
|
||||
},
|
||||
}
|
||||
|
||||
return &embeddedsass.InboundMessage{
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
call, err := t.newCall(createInboundMessage, args)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
select {
|
||||
case call = <-call.Done:
|
||||
case <-time.After(t.opts.Timeout):
|
||||
return result, errors.New("timeout waiting for Dart Sass to respond; note that this project is only compatible with the Dart Sass Binary found here: https://github.com/sass/dart-sass/releases/")
|
||||
}
|
||||
|
||||
if call.Error != nil {
|
||||
return result, call.Error
|
||||
}
|
||||
|
||||
response := call.Response
|
||||
csp := response.Message.(*embeddedsass.OutboundMessage_CompileResponse_)
|
||||
|
||||
switch resp := csp.CompileResponse.Result.(type) {
|
||||
case *embeddedsass.OutboundMessage_CompileResponse_Success:
|
||||
result.CSS = resp.Success.Css
|
||||
result.SourceMap = resp.Success.SourceMap
|
||||
case *embeddedsass.OutboundMessage_CompileResponse_Failure:
|
||||
asJson, err := json.Marshal(resp.Failure)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
var sassErr SassError
|
||||
err = json.Unmarshal(asJson, &sassErr)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
return result, sassErr
|
||||
default:
|
||||
return result, fmt.Errorf("unsupported response type: %T", resp)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t *Transpiler) getCall(id uint32) *call {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
call, found := t.pending[id]
|
||||
if !found {
|
||||
panic(fmt.Sprintf("call with ID %d not found", id))
|
||||
}
|
||||
return call
|
||||
}
|
||||
|
||||
func (t *Transpiler) input() {
|
||||
var err error
|
||||
|
||||
for err == nil {
|
||||
// The header is the length in bytes of the remaining message including the compilation ID.
|
||||
var l uint64
|
||||
|
||||
l, err = binary.ReadUvarint(t.conn)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
plen := int(l)
|
||||
if len(t.msgBuf) < plen {
|
||||
t.msgBuf = make([]byte, plen)
|
||||
}
|
||||
|
||||
buf := t.msgBuf[:plen]
|
||||
|
||||
_, err = io.ReadFull(t.conn, buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
v, n := binary.Uvarint(buf)
|
||||
if n <= 0 {
|
||||
break
|
||||
}
|
||||
compilationID := uint32(v)
|
||||
|
||||
buf = buf[n:]
|
||||
|
||||
var msg embeddedsass.OutboundMessage
|
||||
|
||||
if err = proto.Unmarshal(buf, &msg); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
switch c := msg.Message.(type) {
|
||||
case *embeddedsass.OutboundMessage_CompileResponse_:
|
||||
// Attach it to the correct pending call.
|
||||
t.mu.Lock()
|
||||
call := t.pending[compilationID]
|
||||
delete(t.pending, compilationID)
|
||||
t.mu.Unlock()
|
||||
if call == nil {
|
||||
err = fmt.Errorf("call with ID %d not found", compilationID)
|
||||
break
|
||||
}
|
||||
call.Response = &msg
|
||||
call.done()
|
||||
case *embeddedsass.OutboundMessage_CanonicalizeRequest_:
|
||||
call := t.getCall(compilationID)
|
||||
resolved, resolveErr := call.importResolver.CanonicalizeURL(c.CanonicalizeRequest.GetUrl())
|
||||
|
||||
var response *embeddedsass.InboundMessage_CanonicalizeResponse
|
||||
if resolveErr != nil {
|
||||
response = &embeddedsass.InboundMessage_CanonicalizeResponse{
|
||||
Id: c.CanonicalizeRequest.GetId(),
|
||||
Result: &embeddedsass.InboundMessage_CanonicalizeResponse_Error{
|
||||
Error: resolveErr.Error(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
var url *embeddedsass.InboundMessage_CanonicalizeResponse_Url
|
||||
if resolved != "" {
|
||||
url = &embeddedsass.InboundMessage_CanonicalizeResponse_Url{
|
||||
Url: resolved,
|
||||
}
|
||||
}
|
||||
response = &embeddedsass.InboundMessage_CanonicalizeResponse{
|
||||
Id: c.CanonicalizeRequest.GetId(),
|
||||
Result: url,
|
||||
}
|
||||
}
|
||||
|
||||
err = t.sendInboundMessage(
|
||||
compilationID,
|
||||
&embeddedsass.InboundMessage{
|
||||
Message: &embeddedsass.InboundMessage_CanonicalizeResponse_{
|
||||
CanonicalizeResponse: response,
|
||||
},
|
||||
},
|
||||
)
|
||||
case *embeddedsass.OutboundMessage_ImportRequest_:
|
||||
call := t.getCall(compilationID)
|
||||
url := c.ImportRequest.GetUrl()
|
||||
imp, loadErr := call.importResolver.Load(url)
|
||||
sourceSyntax := embeddedsass.Syntax_value[string(imp.SourceSyntax)]
|
||||
|
||||
var response *embeddedsass.InboundMessage_ImportResponse
|
||||
var sourceMapURL string
|
||||
|
||||
// Dart Sass expect a browser-accessible URL or an empty string.
|
||||
// If no URL is supplied, a `data:` URL wil be generated
|
||||
// automatically from `contents`
|
||||
if hasScheme(url) {
|
||||
sourceMapURL = url
|
||||
}
|
||||
|
||||
if loadErr != nil {
|
||||
response = &embeddedsass.InboundMessage_ImportResponse{
|
||||
Id: c.ImportRequest.GetId(),
|
||||
Result: &embeddedsass.InboundMessage_ImportResponse_Error{
|
||||
Error: loadErr.Error(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
response = &embeddedsass.InboundMessage_ImportResponse{
|
||||
Id: c.ImportRequest.GetId(),
|
||||
Result: &embeddedsass.InboundMessage_ImportResponse_Success{
|
||||
Success: &embeddedsass.InboundMessage_ImportResponse_ImportSuccess{
|
||||
Contents: imp.Content,
|
||||
SourceMapUrl: &sourceMapURL,
|
||||
Syntax: embeddedsass.Syntax(sourceSyntax),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
err = t.sendInboundMessage(
|
||||
compilationID,
|
||||
&embeddedsass.InboundMessage{
|
||||
Message: &embeddedsass.InboundMessage_ImportResponse_{
|
||||
ImportResponse: response,
|
||||
},
|
||||
},
|
||||
)
|
||||
case *embeddedsass.OutboundMessage_LogEvent_:
|
||||
if t.opts.LogEventHandler != nil {
|
||||
var logEvent LogEvent
|
||||
e := c.LogEvent
|
||||
if e.Span != nil {
|
||||
u := e.Span.Url
|
||||
if u == "" {
|
||||
u = "stdin"
|
||||
}
|
||||
u, _ = url.QueryUnescape(u)
|
||||
logEvent = LogEvent{
|
||||
Type: LogEventType(e.Type),
|
||||
Message: fmt.Sprintf("%s:%d:%d: %s", u, e.Span.Start.Line, e.Span.Start.Column, c.LogEvent.GetMessage()),
|
||||
}
|
||||
} else {
|
||||
logEvent = LogEvent{
|
||||
Type: LogEventType(e.Type),
|
||||
Message: e.GetMessage(),
|
||||
}
|
||||
}
|
||||
|
||||
t.opts.LogEventHandler(logEvent)
|
||||
|
||||
}
|
||||
|
||||
case *embeddedsass.OutboundMessage_Error:
|
||||
err = fmt.Errorf("SASS error: %s", c.Error.GetMessage())
|
||||
default:
|
||||
err = fmt.Errorf("unsupported response message type. %T", msg.Message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Terminate pending calls.
|
||||
t.sendMu.Lock()
|
||||
defer t.sendMu.Unlock()
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.shutdown = true
|
||||
isEOF := err == io.EOF || strings.Contains(err.Error(), "already closed")
|
||||
if isEOF {
|
||||
if t.closing {
|
||||
err = ErrShutdown
|
||||
} else {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
|
||||
for _, call := range t.pending {
|
||||
call.Error = err
|
||||
call.done()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transpiler) nextSeq() uint32 {
|
||||
t.seq++
|
||||
// The compilation ID 0 is reserved for `VersionRequest` and `VersionResponse`,
|
||||
// 4294967295 is reserved for error handling. This is the maximum number representable by a `uint32` so it should be safe to start over.
|
||||
if t.seq == 0 || t.seq == 4294967295 {
|
||||
t.seq = 1
|
||||
}
|
||||
return t.seq
|
||||
}
|
||||
|
||||
func (t *Transpiler) newCall(createInbound func(seq uint32) (*embeddedsass.InboundMessage, error), args Args) (*call, error) {
|
||||
t.mu.Lock()
|
||||
id := t.nextSeq()
|
||||
req, err := createInbound(id)
|
||||
if err != nil {
|
||||
t.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
call := &call{
|
||||
Request: req,
|
||||
Done: make(chan *call, 1),
|
||||
importResolver: args.ImportResolver,
|
||||
}
|
||||
|
||||
if t.shutdown || t.closing {
|
||||
t.mu.Unlock()
|
||||
call.Error = ErrShutdown
|
||||
call.done()
|
||||
return call, nil
|
||||
}
|
||||
|
||||
t.pending[id] = call
|
||||
|
||||
t.mu.Unlock()
|
||||
|
||||
switch call.Request.Message.(type) {
|
||||
case *embeddedsass.InboundMessage_CompileRequest_:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported request message type. %T", call.Request.Message)
|
||||
}
|
||||
|
||||
return call, t.sendInboundMessage(id, call.Request)
|
||||
}
|
||||
|
||||
func (t *Transpiler) sendInboundMessage(compilationID uint32, message *embeddedsass.InboundMessage) error {
|
||||
t.sendMu.Lock()
|
||||
defer t.sendMu.Unlock()
|
||||
t.mu.Lock()
|
||||
if t.closing || t.shutdown {
|
||||
t.mu.Unlock()
|
||||
return ErrShutdown
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
out, err := proto.Marshal(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %s", err)
|
||||
}
|
||||
|
||||
// Every message must begin with a varint indicating the length in bytes of
|
||||
// the remaining message including the compilation ID
|
||||
reqLen := uint64(len(out))
|
||||
compilationIDLen := binary.PutUvarint(t.idBuf, uint64(compilationID))
|
||||
headerLen := binary.PutUvarint(t.lenBuf, reqLen+uint64(compilationIDLen))
|
||||
_, err = t.conn.Write(t.lenBuf[:headerLen])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = t.conn.Write(t.idBuf[:compilationIDLen])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headerLen, err = t.conn.Write(out)
|
||||
if headerLen != len(out) {
|
||||
return errors.New("failed to write payload")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type call struct {
|
||||
Request *embeddedsass.InboundMessage
|
||||
Response *embeddedsass.OutboundMessage
|
||||
importResolver ImportResolver
|
||||
|
||||
Error error
|
||||
Done chan *call
|
||||
}
|
||||
|
||||
func (call *call) done() {
|
||||
select {
|
||||
case call.Done <- call:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func hasScheme(s string) bool {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return u.Scheme != ""
|
||||
}
|
||||
+25
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||
}
|
||||
Reference in New Issue
Block a user