blob: 39f731d28537604b65b10a94971da52e7d368314 [file] [edit]
// Copyright 2020-2026 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bufplugin
import (
"bytes"
"fmt"
"strings"
"sync"
"github.com/bufbuild/buf/private/bufpkg/bufparse"
"github.com/bufbuild/buf/private/pkg/cas"
"github.com/bufbuild/buf/private/pkg/syserror"
"github.com/google/uuid"
)
// Plugin presents a BSR plugin.
type Plugin interface {
// OpaqueID returns an unstructured ID that can uniquely identify a Plugin
// relative to the Workspace.
//
// An OpaqueID's structure should not be relied upon, and is not a
// globally-unique identifier. It's uniqueness property only applies to
// the lifetime of the Plugin, and only within the Workspace the Plugin
// is defined in.
//
// If two Plugins have the same Name and Args, they will have the same OpaqueID.
OpaqueID() string
// Name returns the name of the Plugin.
// - For local Plugins, this is the path to the executable binary.
// - For local Wasm Plugins, this is the path to the Wasm binary.
// - For remote Plugins, this is the FullName of the Plugin in the form
// remote/owner/name.
//
// This is never empty.
Name() string
// Args returns the arguments to invoke the Plugin.
//
// May be nil.
Args() []string
// FullName returns the full name of the Plugin.
//
// May be nil. Callers should not rely on this value being present.
// However, this is always present for remote Plugins.
//
// Use OpaqueID as an always-present identifier.
FullName() bufparse.FullName
// CommitID returns the BSR ID of the Commit.
//
// It is up to the caller to convert this to a dashless ID when necessary.
//
// May be empty, that is CommitID() == uuid.Nil may be true.
// Callers should not rely on this value being present.
//
// If FullName is nil, this will always be empty.
CommitID() uuid.UUID
// Description returns a human-readable description of the Plugin.
//
// This is used to construct descriptive error messages pointing to configured plugins.
//
// This will never be empty. If a description was not explicitly set, this falls back to
// OpaqueID.
Description() string
// Digest returns the Plugin digest for the given DigestType.
//
// Note this is *not* a cas.Digest - this is a Digest.
// cas.Digests are a lower-level type that just deal in terms of
// files and content. A Digest is a specific algorithm applied to the
// content of a Plugin.
//
// Will return an error if the Plugin is not a Wasm Plugin.
Digest(DigestType) (Digest, error)
// Data returns the bytes of the Plugin as a Wasm module.
//
// This is the raw bytes of the Wasm module in an uncompressed form.
//
// Will return an error if the Plugin is not a Wasm Plugin.
Data() ([]byte, error)
// IsWasm returns true if the Plugin is a Wasm Plugin.
//
// Plugins are either Wasm or local.
//
// A Wasm Plugin is a Plugin that is a Wasm module. Wasm Plugins are invoked
// with the wasm.Runtime. The Plugin will have Data and will be able to
// calculate Digests.
//
// Wasm Plugins will always have Data.
IsWasm() bool
// IsLocal returns true if the Plugin is a local Plugin.
//
// Plugins are either local or remote.
//
// A local Plugin is one that is built from sources from the "local context",
// such as a Workspace. Local Plugins are important for understanding what Plugins
// to push.
//
// Remote Plugins will always have FullNames.
IsLocal() bool
isPlugin()
}
// NewLocalPlugin returns a new Plugin for a local plugin.
//
// The name is the path to the executable binary.
// The args are the arguments to invoke the Plugin. These are passed to the Plugin
// as command line arguments.
func NewLocalPlugin(
name string,
args []string,
) (Plugin, error) {
return newPlugin(
"", // description
nil,
name,
args,
uuid.Nil, // commitID
false, // isWasm
true, // isLocal
nil, // getData
)
}
// NewLocalWasmPlugin returns a new Plugin for a local Wasm plugin.
//
// The pluginFullName may be nil.
// The name is the path to the Wasm plugin and must end with .wasm.
// The args are the arguments to the Wasm plugin. These are passed to the Wasm plugin
// as command line arguments.
// The getData function is called to get the bytes of the Wasm plugin.
// This is the raw bytes of the Wasm module in an uncompressed form.
func NewLocalWasmPlugin(
pluginFullName bufparse.FullName,
name string,
args []string,
getData func() ([]byte, error),
) (Plugin, error) {
return newPlugin(
"", // description
pluginFullName,
name,
args,
uuid.Nil, // commitID
true, // isWasm
true, // isLocal
getData,
)
}
// NewRemoteWasmPlugin returns a new Plugin for a remote Wasm plugin.
//
// The pluginFullName is the remote reference to the plugin.
// The args are the arguments to the remote plugin. These are passed to the remote plugin
// as command line arguments.
// The commitID is the BSR ID of the Commit.
// It is up to the caller to convert this to a dashless ID when necessary.
// The getData function is called to get the bytes of the Wasm plugin.
// This is the raw bytes of the Wasm module in an uncompressed form.
func NewRemoteWasmPlugin(
pluginFullName bufparse.FullName,
args []string,
commitID uuid.UUID,
getData func() ([]byte, error),
) (Plugin, error) {
return newPlugin(
"", // description
pluginFullName,
pluginFullName.String(),
args,
commitID,
true, // isWasm
false, // isLocal
getData,
)
}
// *** PRIVATE ***
type plugin struct {
description string
pluginFullName bufparse.FullName
name string
args []string
commitID uuid.UUID
isWasm bool
isLocal bool
getData func() ([]byte, error)
digestTypeToGetDigest map[DigestType]func() (Digest, error)
}
func newPlugin(
description string,
pluginFullName bufparse.FullName,
name string,
args []string,
commitID uuid.UUID,
isWasm bool,
isLocal bool,
getData func() ([]byte, error),
) (*plugin, error) {
if name == "" {
return nil, syserror.New("name not present when constructing a Plugin")
}
if isWasm && getData == nil {
return nil, syserror.Newf("getData not present when constructing a Wasm Plugin")
}
if !isLocal && pluginFullName == nil {
return nil, syserror.New("pluginFullName not present when constructing a remote Plugin")
}
if !isLocal && !isWasm {
return nil, syserror.New("remote non-Wasm Plugins are not supported")
}
if isLocal && commitID != uuid.Nil {
return nil, syserror.New("commitID present when constructing a local Plugin")
}
if pluginFullName == nil && commitID != uuid.Nil {
return nil, syserror.New("pluginFullName not present and commitID present when constructing a remote Plugin")
}
plugin := &plugin{
description: description,
pluginFullName: pluginFullName,
name: name,
args: args,
commitID: commitID,
isWasm: isWasm,
isLocal: isLocal,
getData: sync.OnceValues(getData),
}
plugin.digestTypeToGetDigest = newSyncOnceValueDigestTypeToGetDigestFuncForPlugin(plugin)
return plugin, nil
}
func (p *plugin) OpaqueID() string {
return strings.Join(append([]string{p.name}, p.args...), " ")
}
func (p *plugin) Name() string {
return p.name
}
func (p *plugin) Args() []string {
return p.args
}
func (p *plugin) FullName() bufparse.FullName {
return p.pluginFullName
}
func (p *plugin) CommitID() uuid.UUID {
return p.commitID
}
func (p *plugin) Description() string {
if p.description != "" {
return p.description
}
return p.OpaqueID()
}
func (p *plugin) Data() ([]byte, error) {
if !p.isWasm {
return nil, fmt.Errorf("Plugin is not a Wasm Plugin")
}
return p.getData()
}
func (p *plugin) Digest(digestType DigestType) (Digest, error) {
getDigest, ok := p.digestTypeToGetDigest[digestType]
if !ok {
return nil, syserror.Newf("DigestType %v was not in plugin.digestTypeToGetDigest", digestType)
}
return getDigest()
}
func (p *plugin) IsWasm() bool {
return p.isWasm
}
func (p *plugin) IsLocal() bool {
return p.isLocal
}
func (p *plugin) isPlugin() {}
func newSyncOnceValueDigestTypeToGetDigestFuncForPlugin(plugin *plugin) map[DigestType]func() (Digest, error) {
m := make(map[DigestType]func() (Digest, error))
for digestType := range digestTypeToString {
m[digestType] = sync.OnceValues(newGetDigestFuncForPluginAndDigestType(plugin, digestType))
}
return m
}
func newGetDigestFuncForPluginAndDigestType(plugin *plugin, digestType DigestType) func() (Digest, error) {
return func() (Digest, error) {
data, err := plugin.getData()
if err != nil {
return nil, err
}
casDigest, err := cas.NewDigestForContent(bytes.NewReader(data))
if err != nil {
return nil, err
}
return NewDigest(digestType, casDigest)
}
}