Skip to content

Commit 8412289

Browse files
plugins/bundle: Escape reserved chars used in persisted bundle directory name
In Windows there are some reserved characters that cannot be used in the names of files and directories (eg. ?, *). If a bundle name contains these and if it's configured to be persisted, the operation will fail on Windows. This change attempts to fix this on Windows systems by escaping any encountered reserved characters before using them in the bundle persistence path. Fixes: #6915 Signed-off-by: Ashutosh Narkar <[email protected]>
1 parent 6c37125 commit 8412289

File tree

2 files changed

+168
-3
lines changed

2 files changed

+168
-3
lines changed

plugins/bundle/plugin.go

+37-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"os"
1515
"path/filepath"
1616
"reflect"
17+
"runtime"
1718
"strings"
1819
"sync"
1920
"time"
@@ -41,6 +42,8 @@ import (
4142
// successfully activate.
4243
const maxActivationRetry = 10
4344

45+
var goos = runtime.GOOS
46+
4447
// Loader defines the interface that the bundle plugin uses to control bundle
4548
// loading via HTTP, disk, etc.
4649
type Loader interface {
@@ -697,7 +700,9 @@ func (p *Plugin) configDelta(newConfig *Config) (map[string]*Source, map[string]
697700

698701
func (p *Plugin) saveBundleToDisk(name string, raw io.Reader) error {
699702

700-
bundleDir := filepath.Join(p.bundlePersistPath, name)
703+
bundleName := getNormalizedBundleName(name)
704+
705+
bundleDir := filepath.Join(p.bundlePersistPath, bundleName)
701706
bundleFile := filepath.Join(bundleDir, "bundle.tar.gz")
702707

703708
tmpFile, saveErr := saveCurrentBundleToDisk(bundleDir, raw)
@@ -723,10 +728,12 @@ func saveCurrentBundleToDisk(path string, raw io.Reader) (string, error) {
723728
}
724729

725730
func (p *Plugin) loadBundleFromDisk(path, name string, src *Source) (*bundle.Bundle, error) {
731+
bundleName := getNormalizedBundleName(name)
732+
726733
if src != nil {
727-
return bundleUtils.LoadBundleFromDiskForRegoVersion(p.manager.ParserOptions().RegoVersion, path, name, src.Signing)
734+
return bundleUtils.LoadBundleFromDiskForRegoVersion(p.manager.ParserOptions().RegoVersion, path, bundleName, src.Signing)
728735
}
729-
return bundleUtils.LoadBundleFromDiskForRegoVersion(p.manager.ParserOptions().RegoVersion, path, name, nil)
736+
return bundleUtils.LoadBundleFromDiskForRegoVersion(p.manager.ParserOptions().RegoVersion, path, bundleName, nil)
730737
}
731738

732739
func (p *Plugin) log(name string) logging.Logger {
@@ -756,6 +763,33 @@ func (p *Plugin) getBundlesCpy() map[string]*Source {
756763
return bundlesCpy
757764
}
758765

766+
// getNormalizedBundleName returns a version of the input with
767+
// invalid file and directory name characters on Windows escaped.
768+
// It returns the input as-is for non-Windows systems.
769+
func getNormalizedBundleName(name string) string {
770+
if goos != "windows" {
771+
return name
772+
}
773+
774+
sb := new(strings.Builder)
775+
for i := 0; i < len(name); i++ {
776+
if isReservedCharacter(rune(name[i])) {
777+
sb.WriteString(fmt.Sprintf("\\%c", name[i]))
778+
} else {
779+
sb.WriteByte(name[i])
780+
}
781+
}
782+
783+
return sb.String()
784+
}
785+
786+
// isReservedCharacter checks if the input is a reserved character on Windows that should not be
787+
// used in file and directory names
788+
// For details, see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions.
789+
func isReservedCharacter(r rune) bool {
790+
return r == '<' || r == '>' || r == ':' || r == '"' || r == '/' || r == '\\' || r == '|' || r == '?' || r == '*'
791+
}
792+
759793
type fileLoader struct {
760794
name string
761795
path string

plugins/bundle/plugin_test.go

+131
Original file line numberDiff line numberDiff line change
@@ -1911,6 +1911,85 @@ func TestLoadAndActivateBundlesFromDisk(t *testing.T) {
19111911
}
19121912
}
19131913

1914+
func TestLoadAndActivateBundlesFromDiskReservedChars(t *testing.T) {
1915+
1916+
ctx := context.Background()
1917+
manager := getTestManager()
1918+
1919+
dir := t.TempDir()
1920+
1921+
goos = "windows"
1922+
1923+
bundleName := "test?bundle=opa" // bundle name contains reserved characters
1924+
bundleSource := Source{
1925+
Persist: true,
1926+
}
1927+
1928+
bundles := map[string]*Source{}
1929+
bundles[bundleName] = &bundleSource
1930+
1931+
plugin := New(&Config{Bundles: bundles}, manager)
1932+
plugin.bundlePersistPath = filepath.Join(dir, ".opa")
1933+
1934+
plugin.loadAndActivateBundlesFromDisk(ctx)
1935+
1936+
// persist a bundle to disk and then load it
1937+
module := "package foo\n\ncorge=1"
1938+
1939+
b := bundle.Bundle{
1940+
Manifest: bundle.Manifest{Revision: "quickbrownfaux"},
1941+
Data: util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}}`)).(map[string]interface{}),
1942+
Modules: []bundle.ModuleFile{
1943+
{
1944+
URL: "/foo/bar.rego",
1945+
Path: "/foo/bar.rego",
1946+
Parsed: ast.MustParseModule(module),
1947+
Raw: []byte(module),
1948+
},
1949+
},
1950+
}
1951+
1952+
b.Manifest.Init()
1953+
1954+
var buf bytes.Buffer
1955+
if err := bundle.NewWriter(&buf).UseModulePath(true).Write(b); err != nil {
1956+
t.Fatal("unexpected error:", err)
1957+
}
1958+
1959+
err := plugin.saveBundleToDisk(bundleName, &buf)
1960+
if err != nil {
1961+
t.Fatalf("unexpected error %v", err)
1962+
}
1963+
1964+
plugin.loadAndActivateBundlesFromDisk(ctx)
1965+
1966+
txn := storage.NewTransactionOrDie(ctx, manager.Store)
1967+
defer manager.Store.Abort(ctx, txn)
1968+
1969+
ids, err := manager.Store.ListPolicies(ctx, txn)
1970+
if err != nil {
1971+
t.Fatal(err)
1972+
} else if len(ids) != 1 {
1973+
t.Fatal("Expected 1 policy")
1974+
}
1975+
1976+
bs, err := manager.Store.GetPolicy(ctx, txn, ids[0])
1977+
exp := []byte("package foo\n\ncorge=1")
1978+
if err != nil {
1979+
t.Fatal(err)
1980+
} else if !bytes.Equal(bs, exp) {
1981+
t.Fatalf("Bad policy content. Exp:\n%v\n\nGot:\n\n%v", string(exp), string(bs))
1982+
}
1983+
1984+
data, err := manager.Store.Read(ctx, txn, storage.Path{})
1985+
expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundles": {"test?bundle=opa": {"etag": "", "manifest": {"revision": "quickbrownfaux", "roots": [""]}}}}}`))
1986+
if err != nil {
1987+
t.Fatal(err)
1988+
} else if !reflect.DeepEqual(data, expData) {
1989+
t.Fatalf("Bad data content. Exp:\n%v\n\nGot:\n\n%v", expData, data)
1990+
}
1991+
}
1992+
19141993
func TestLoadAndActivateBundlesFromDiskV1Compatible(t *testing.T) {
19151994
type update struct {
19161995
modules map[string]string
@@ -6089,6 +6168,58 @@ func TestPluginManualTriggerWithTimeout(t *testing.T) {
60896168
}
60906169
}
60916170

6171+
func TestGetNormalizedBundleName(t *testing.T) {
6172+
cases := []struct {
6173+
input string
6174+
goos string
6175+
exp string
6176+
}{
6177+
{
6178+
input: "foo",
6179+
exp: "foo",
6180+
},
6181+
{
6182+
input: "foo=bar",
6183+
exp: "foo=bar",
6184+
goos: "windows",
6185+
},
6186+
{
6187+
input: "c:/foo",
6188+
exp: "c:/foo",
6189+
},
6190+
{
6191+
input: "c:/foo",
6192+
exp: "c\\:\\/foo",
6193+
goos: "windows",
6194+
},
6195+
{
6196+
input: "file:\"<>c:/a",
6197+
exp: "file\\:\\\"\\<\\>c\\:\\/a",
6198+
goos: "windows",
6199+
},
6200+
{
6201+
input: "|a?b*c",
6202+
exp: "\\|a\\?b\\*c",
6203+
goos: "windows",
6204+
},
6205+
{
6206+
input: "a?b=c",
6207+
exp: "a\\?b=c",
6208+
goos: "windows",
6209+
},
6210+
}
6211+
6212+
for _, tc := range cases {
6213+
t.Run(tc.input, func(t *testing.T) {
6214+
goos = tc.goos
6215+
actual := getNormalizedBundleName(tc.input)
6216+
if actual != tc.exp {
6217+
t.Fatalf("Want %v but got: %v", tc.exp, actual)
6218+
}
6219+
})
6220+
}
6221+
}
6222+
60926223
func writeTestBundleToDisk(t *testing.T, srcDir string, signed bool) bundle.Bundle {
60936224
t.Helper()
60946225

0 commit comments

Comments
 (0)