Add Corteza studio UI personalization feature

This commit is contained in:
Mumbi Francis
2023-10-31 15:52:39 +03:00
committed by Mumbi Francis
parent d10257cc36
commit e4269669d8
235 changed files with 22920 additions and 3659 deletions
+17
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,15 @@
[![Tests on Linux, MacOS and Windows](https://github.com/bep/godartsass/workflows/Test/badge.svg)](https://github.com/bep/godartsass/actions?query=workflow%3ATest)
[![Go Report Card](https://goreportcard.com/badge/github.com/bep/godartsass)](https://goreportcard.com/report/github.com/bep/godartsass)
[![codecov](https://codecov.io/gh/bep/godartsass/branch/main/graph/badge.svg?token=OWZ9RCAYWO)](https://codecov.io/gh/bep/godartsass)
[![GoDoc](https://godoc.org/github.com/bep/godartsass?status.svg)](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
View File
@@ -0,0 +1,10 @@
coverage:
status:
project:
default:
target: auto
threshold: 0.5%
patch: off
comment:
require_changes: true
+125
View File
@@ -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
}
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+231
View File
@@ -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
View File
@@ -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
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}
}