From b123ffcea43f804206d305e32533f64e44a2633b Mon Sep 17 00:00:00 2001 From: Dan Kegel Date: Tue, 30 Nov 2021 09:52:28 -0800 Subject: [PATCH] os: implement CreateTemp Until tinygo implements fastrand(), use a placeholder RNG here. --- src/os/export_test.go | 9 ++++ src/os/tempfile.go | 112 +++++++++++++++++++++++++++++++++++++++- src/os/tempfile_test.go | 79 ++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/os/export_test.go create mode 100644 src/os/tempfile_test.go diff --git a/src/os/export_test.go b/src/os/export_test.go new file mode 100644 index 00000000..7a218ddf --- /dev/null +++ b/src/os/export_test.go @@ -0,0 +1,9 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +// Export for testing. + +var ErrPatternHasSeparator = errPatternHasSeparator diff --git a/src/os/tempfile.go b/src/os/tempfile.go index 27949f7f..12db6c0b 100644 --- a/src/os/tempfile.go +++ b/src/os/tempfile.go @@ -1,5 +1,113 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package os -func CreateTemp(dir, pattern string) (*File, error) { - return nil, &PathError{"createtemp", pattern, ErrNotImplemented} +import ( + "errors" + "internal/itoa" + "sync" + "time" +) + +var minrandPreviousValue uint32 +var minrandMutex sync.Mutex + +func init() { + // Avoid getting same results on every run + now := time.Now() + seed := uint32(Getpid()) ^ uint32(now.Nanosecond()) ^ uint32(now.Unix()) + // initial state must be odd + minrandPreviousValue = seed | 1 +} + +// minrand() is a simple and rather poor placeholder for fastrand() +// TODO: provide fastrand in runtime, as go does. It is hard to implement properly elsewhere. +func minrand() uint32 { + // c++11's minstd_rand + // https://en.wikipedia.org/wiki/Linear_congruential_generator + // m=2^32, c=0, a=48271 + minrandMutex.Lock() + minrandPreviousValue *= 48271 + val := minrandPreviousValue + minrandMutex.Unlock() + return val +} + +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +func nextRandom() string { + // Discard lower four bits of minrand. + // They're not very random, and we don't need the full range here. + return itoa.Uitoa(uint(minrand() >> 4)) +} + +// CreateTemp creates a new temporary file in the directory dir, +// opens the file for reading and writing, and returns the resulting file. +// The filename is generated by taking pattern and adding a random string to the end. +// If pattern includes a "*", the random string replaces the last "*". +// If dir is the empty string, CreateTemp uses the default directory for temporary files, as returned by TempDir. +// Multiple programs or goroutines calling CreateTemp simultaneously will not choose the same file. +// The caller can use the file's Name method to find the pathname of the file. +// It is the caller's responsibility to remove the file when it is no longer needed. +func CreateTemp(dir, pattern string) (*File, error) { + if dir == "" { + dir = TempDir() + } + + prefix, suffix, err := prefixAndSuffix(pattern) + if err != nil { + return nil, &PathError{Op: "createtemp", Path: pattern, Err: err} + } + prefix = joinPath(dir, prefix) + + try := 0 + for { + name := prefix + nextRandom() + suffix + f, err := OpenFile(name, O_RDWR|O_CREATE|O_EXCL, 0600) + if IsExist(err) { + if try++; try < 10000 { + continue + } + return nil, &PathError{Op: "createtemp", Path: dir + string(PathSeparator) + prefix + "*" + suffix, Err: ErrExist} + } + return f, err + } +} + +var errPatternHasSeparator = errors.New("pattern contains path separator") + +// prefixAndSuffix splits pattern by the last wildcard "*", if applicable, +// returning prefix as the part before "*" and suffix as the part after "*". +func prefixAndSuffix(pattern string) (prefix, suffix string, err error) { + for i := 0; i < len(pattern); i++ { + if IsPathSeparator(pattern[i]) { + return "", "", errPatternHasSeparator + } + } + if pos := lastIndex(pattern, '*'); pos != -1 { + prefix, suffix = pattern[:pos], pattern[pos+1:] + } else { + prefix = pattern + } + return prefix, suffix, nil +} + +func joinPath(dir, name string) string { + if len(dir) > 0 && IsPathSeparator(dir[len(dir)-1]) { + return dir + name + } + return dir + string(PathSeparator) + name +} + +// LastIndexByte from the strings package. +func lastIndex(s string, sep byte) int { + for i := len(s) - 1; i >= 0; i-- { + if s[i] == sep { + return i + } + } + return -1 } diff --git a/src/os/tempfile_test.go b/src/os/tempfile_test.go new file mode 100644 index 00000000..ba31af82 --- /dev/null +++ b/src/os/tempfile_test.go @@ -0,0 +1,79 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os_test + +import ( + "errors" + . "os" + "path/filepath" + "strings" + "testing" +) + +func TestCreateTemp(t *testing.T) { + nonexistentDir := filepath.Join("_also_not_exists_", "_not_exists_") + f, err := CreateTemp(nonexistentDir, "foo") + if f != nil || err == nil { + t.Errorf("CreateTemp(%q, `foo`) = %v, %v", nonexistentDir, f, err) + } +} + +func TestCreateTempPattern(t *testing.T) { + tests := []struct{ pattern, prefix, suffix string }{ + {"tempfile_test", "tempfile_test", ""}, + {"tempfile_test*", "tempfile_test", ""}, + {"tempfile_test*xyz", "tempfile_test", "xyz"}, + } + for _, test := range tests { + f, err := CreateTemp("", test.pattern) + if err != nil { + t.Errorf("CreateTemp(..., %q) error: %v", test.pattern, err) + continue + } + defer Remove(f.Name()) + base := filepath.Base(f.Name()) + f.Close() + if !(strings.HasPrefix(base, test.prefix) && strings.HasSuffix(base, test.suffix)) { + t.Errorf("CreateTemp pattern %q created bad name %q; want prefix %q & suffix %q", + test.pattern, base, test.prefix, test.suffix) + } + } +} + +func TestCreateTempBadPattern(t *testing.T) { + tmpDir := TempDir() + + const sep = string(PathSeparator) + tests := []struct { + pattern string + wantErr bool + }{ + {"ioutil*test", false}, + {"tempfile_test*foo", false}, + {"tempfile_test" + sep + "foo", true}, + {"tempfile_test*" + sep + "foo", true}, + {"tempfile_test" + sep + "*foo", true}, + {sep + "tempfile_test" + sep + "*foo", true}, + {"tempfile_test*foo" + sep, true}, + } + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + tmpfile, err := CreateTemp(tmpDir, tt.pattern) + if tmpfile != nil { + defer tmpfile.Close() + } + if tt.wantErr { + if err == nil { + t.Errorf("CreateTemp(..., %#q) succeeded, expected error", tt.pattern) + } + if !errors.Is(err, ErrPatternHasSeparator) { + t.Errorf("CreateTemp(..., %#q): %v, expected ErrPatternHasSeparator", tt.pattern, err) + } + } else if err != nil { + t.Errorf("CreateTemp(..., %#q): %v", tt.pattern, err) + } + }) + } +}