Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions pkg/compose/envresolver_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright 2020 Docker Compose CLI authors

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 compose

import (
"strings"
"testing"
)

// FuzzEnvResolverWithCase locks the resolution semantics: the resolver is
// exactly the map when case-sensitive, and an exact match must win over the
// lower-cased fallback when case-insensitive.
func FuzzEnvResolverWithCase(f *testing.F) {
f.Add("FOO", "bar", "BAZ", "qux", "foo")
f.Add("PATH", "/bin", "HOME", "/root", "path")
f.Fuzz(func(t *testing.T, k1, v1, k2, v2, query string) {
// keep the two keys distinct after case folding so the lower-cased
// fallback has a single unambiguous winner
if strings.EqualFold(k1, k2) {
k2 += "#"
}
environment := map[string]string{k1: v1, k2: v2}
Comment on lines +31 to +36

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks to be explicitly avoiding an ... actually problematic case in the implementation; the implementation handles uppercase and lowercase correctly, but mixed case will be randomised; given, e.g.;

env := map[string]string{
    "FOO": "upper",
    "foo": "lower",
    "Foo": "mixed",
}

A direct match (on either FOO or foo, or Foo) will return a deterministic value, but a case-insensitive match will be randomised (return either upper, lower, or mixed), depending on how the Go runtime randomises the map.

The code also unconditionally allocates a new map, regardless if a direct match was found;

// variable names must be treated case-insensitively.
// Resolves in this way:
// * Return the value if its name matches with the passed name case-sensitively.
// * Otherwise, return the value if its lower-cased name matches lower-cased passed name.
// * The value is indefinite if multiple variable matches.
loweredEnvironment := make(map[string]string, len(environment))
for k, v := range environment {
loweredEnvironment[strings.ToLower(k)] = v
}
return func(s string) (string, bool) {
v, ok := environment[s]
if ok {
return v, ok
}
v, ok = loweredEnvironment[strings.ToLower(s)]
return v, ok
}

(So given that it already loops over values, it may as well just loop over values, and do a case-insensitive (try match exact UPPER or lower first, then fallback to Equalfold if desired) only if a direct match wasn't found.


// nil environment never resolves
if v, ok := envResolverWithCase(nil, true)(query); ok || v != "" {
t.Fatalf("nil environment must never resolve %q, got (%q, %t)", query, v, ok)
}

// case-sensitive: exactly the map
gotV, gotOK := envResolverWithCase(environment, false)(query)
if wantOK := query == k1 || query == k2; gotOK != wantOK || gotV != environment[query] {
t.Fatalf("case-sensitive resolver disagrees with the map for %q: got (%q, %t), want (%q, %t)",
query, gotV, gotOK, environment[query], wantOK)
}

// case-insensitive: exact match first, then lower-cased fallback
gotV, gotOK = envResolverWithCase(environment, true)(query)
wantV, wantOK := "", false
if query == k1 || query == k2 {
wantV, wantOK = environment[query], true
} else {
switch strings.ToLower(query) {
case strings.ToLower(k1):
wantV, wantOK = v1, true
case strings.ToLower(k2):
wantV, wantOK = v2, true
}
}
if gotV != wantV || gotOK != wantOK {
t.Fatalf("case-insensitive resolver for %q: got (%q, %t), want (%q, %t)",
query, gotV, gotOK, wantV, wantOK)
}
})
}