Refactor extensions.

This commit is contained in:
Nuno Cruces
2024-01-03 00:54:30 +00:00
parent fab70ddbec
commit ae850191c8
23 changed files with 491 additions and 256 deletions

95
util/fsutil/mode.go Normal file
View File

@@ -0,0 +1,95 @@
package fsutil
import (
"io/fs"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// ParseFileMode parses a file mode as returned by
// [fs.FileMode.String].
func ParseFileMode(str string) (fs.FileMode, error) {
var mode fs.FileMode
err := util.ErrorString("invalid mode: " + str)
if len(str) < 10 {
return 0, err
}
for i, c := range []byte("dalTLDpSugct?") {
if str[0] == c {
if len(str) < 10 {
return 0, err
}
mode |= 1 << uint(32-1-i)
str = str[1:]
}
}
if mode == 0 {
if str[0] != '-' {
return 0, err
}
str = str[1:]
}
if len(str) != 9 {
return 0, err
}
for i, c := range []byte("rwxrwxrwx") {
if str[i] == c {
mode |= 1 << uint(9-1-i)
}
if str[i] != '-' {
return 0, err
}
}
return mode, nil
}
// FileModeFromUnix converts a POSIX mode_t to a file mode.
func FileModeFromUnix(mode fs.FileMode) fs.FileMode {
const (
S_IFMT fs.FileMode = 0170000
S_IFIFO fs.FileMode = 0010000
S_IFCHR fs.FileMode = 0020000
S_IFDIR fs.FileMode = 0040000
S_IFBLK fs.FileMode = 0060000
S_IFREG fs.FileMode = 0100000
S_IFLNK fs.FileMode = 0120000
S_IFSOCK fs.FileMode = 0140000
)
switch mode & S_IFMT {
case S_IFDIR:
mode |= fs.ModeDir
case S_IFLNK:
mode |= fs.ModeSymlink
case S_IFBLK:
mode |= fs.ModeDevice
case S_IFCHR:
mode |= fs.ModeCharDevice | fs.ModeDevice
case S_IFIFO:
mode |= fs.ModeNamedPipe
case S_IFSOCK:
mode |= fs.ModeSocket
case S_IFREG, 0:
//
default:
mode |= fs.ModeIrregular
}
return mode &^ S_IFMT
}
// FileModeFromValue calls [FileModeFromUnix] for numeric values,
// and [ParseFileMode] for textual values.
func FileModeFromValue(val sqlite3.Value) fs.FileMode {
if n := val.Int64(); n != 0 {
return FileModeFromUnix(fs.FileMode(n))
}
mode, _ := ParseFileMode(val.Text())
return mode
}

54
util/fsutil/mode_test.go Normal file
View File

@@ -0,0 +1,54 @@
package fsutil
import (
"io/fs"
"testing"
)
func TestFileModeFromUnix(t *testing.T) {
tests := []struct {
mode fs.FileMode
want fs.FileMode
}{
{0010754, 0754 | fs.ModeNamedPipe},
{0020754, 0754 | fs.ModeCharDevice | fs.ModeDevice},
{0040754, 0754 | fs.ModeDir},
{0060754, 0754 | fs.ModeDevice},
{0100754, 0754},
{0120754, 0754 | fs.ModeSymlink},
{0140754, 0754 | fs.ModeSocket},
{0170754, 0754 | fs.ModeIrregular},
}
for _, tt := range tests {
t.Run(tt.mode.String(), func(t *testing.T) {
if got := FileModeFromUnix(tt.mode); got != tt.want {
t.Errorf("fixMode() = %o, want %o", got, tt.want)
}
})
}
}
func FuzzParseFileMode(f *testing.F) {
f.Add("---------")
f.Add("rwxrwxrwx")
f.Add("----------")
f.Add("-rwxrwxrwx")
f.Add("b")
f.Add("b---------")
f.Add("drwxrwxrwx")
f.Add("dalTLDpSugct?")
f.Add("dalTLDpSugct?---------")
f.Add("dalTLDpSugct?rwxrwxrwx")
f.Add("dalTLDpSugct?----------")
f.Fuzz(func(t *testing.T, str string) {
mode, err := ParseFileMode(str)
if err != nil {
return
}
got := mode.String()
if got != str {
t.Errorf("was %q, got %q (%o)", str, got, mode)
}
})
}

34
util/fsutil/osfs.go Normal file
View File

@@ -0,0 +1,34 @@
// Package fsutil implements file system utility functions.
package fsutil
import (
"io/fs"
"os"
)
// OSFS implements [fs.FS], [fs.StatFS], and [fs.ReadFileFS]
// using package [os].
//
// This filesystem does not respect [fs.ValidPath] rules,
// and fails [testing/fstest.TestFS]!
//
// Still, it can be a useful tool to unify implementations
// that can access either the [os] filesystem or an [fs.FS].
// It's OK to use this to open files, but you should avoid
// opening directories, resolving paths, or walking the file system.
type OSFS struct{}
// Open implements [fs.FS].
func (OSFS) Open(name string) (fs.File, error) {
return os.Open(name)
}
// ReadFileFS implements [fs.StatFS].
func (OSFS) Stat(name string) (fs.FileInfo, error) {
return os.Stat(name)
}
// ReadFile implements [fs.ReadFileFS].
func (OSFS) ReadFile(name string) ([]byte, error) {
return os.ReadFile(name)
}

60
util/ioutil/seek.go Normal file
View File

@@ -0,0 +1,60 @@
package ioutil
import (
"io"
"sync"
)
// SeekingReaderAt implements [io.ReaderAt]
// through an underlying [io.ReadSeeker].
type SeekingReaderAt struct {
l sync.Mutex
r io.ReadSeeker
}
// NewSeekingReaderAt creates a new SeekingReaderAt.
// The SeekingReaderAt takes ownership of r
// and will modify its seek offset,
// so callers should not use r after this call.
func NewSeekingReaderAt(r io.ReadSeeker) *SeekingReaderAt {
return &SeekingReaderAt{r: r}
}
// ReadAt implements [io.ReaderAt].
func (s *SeekingReaderAt) ReadAt(p []byte, off int64) (n int, _ error) {
s.l.Lock()
defer s.l.Unlock()
_, err := s.r.Seek(off, io.SeekStart)
if err != nil {
return 0, err
}
for len(p) > 0 {
i, err := s.r.Read(p)
p = p[i:]
n += i
if err != nil {
return n, err
}
}
return n, nil
}
// Size implements [SizeReaderAt].
func (s *SeekingReaderAt) Size() (int64, error) {
s.l.Lock()
defer s.l.Unlock()
return s.r.Seek(0, io.SeekEnd)
}
// ReadAt implements [io.Closer].
func (s *SeekingReaderAt) Close() error {
s.l.Lock()
defer s.l.Unlock()
if c, ok := s.r.(io.Closer); ok {
s.r = nil
return c.Close()
}
return nil
}

28
util/ioutil/seek_test.go Normal file
View File

@@ -0,0 +1,28 @@
package ioutil
import (
"strings"
"testing"
)
func TestNewSeekingReaderAt(t *testing.T) {
reader := NewSeekingReaderAt(strings.NewReader("abc"))
defer reader.Close()
n, err := reader.Size()
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("got %d", n)
}
var buf [3]byte
r, err := reader.ReadAt(buf[:], 0)
if err != nil {
t.Fatal(err)
}
if r != 3 {
t.Errorf("got %d", r)
}
}

49
util/ioutil/size.go Normal file
View File

@@ -0,0 +1,49 @@
// Package ioutil implements I/O utility functions.
package ioutil
import (
"io"
"io/fs"
"github.com/ncruces/go-sqlite3"
)
// A SizeReaderAt is a ReaderAt with a Size method.
// Use [NewSizeReaderAt] to adapt different Size interfaces.
type SizeReaderAt interface {
Size() (int64, error)
io.ReaderAt
}
// NewSizeReaderAt returns a SizeReaderAt given an io.ReaderAt
// that implements one of:
// - Size() (int64, error)
// - Size() int64
// - Len() int
// - Stat() (fs.FileInfo, error)
// - Seek(offset int64, whence int) (int64, error)
func NewSizeReaderAt(r io.ReaderAt) SizeReaderAt {
return sizer{r}
}
type sizer struct{ io.ReaderAt }
func (s sizer) Size() (int64, error) {
switch s := s.ReaderAt.(type) {
case interface{ Size() (int64, error) }:
return s.Size()
case interface{ Size() int64 }:
return s.Size(), nil
case interface{ Len() int }:
return int64(s.Len()), nil
case interface{ Stat() (fs.FileInfo, error) }:
fi, err := s.Stat()
if err != nil {
return 0, err
}
return fi.Size(), nil
case io.Seeker:
return s.Seek(0, io.SeekEnd)
}
return 0, sqlite3.IOERR_SEEK
}

87
util/ioutil/size_test.go Normal file
View File

@@ -0,0 +1,87 @@
package ioutil
import (
"io"
"os"
"path/filepath"
"strings"
"testing"
)
func TestNewSizeReaderAt(t *testing.T) {
f, err := os.Create(filepath.Join(t.TempDir(), "abc.txt"))
if err != nil {
t.Fatal(err)
}
defer f.Close()
n, err := NewSizeReaderAt(f).Size()
if err != nil {
t.Fatal(err)
}
if n != 0 {
t.Errorf("got %d", n)
}
reader := strings.NewReader("abc")
n, err = NewSizeReaderAt(reader).Size()
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("got %d", n)
}
n, err = NewSizeReaderAt(readlener{reader, reader.Len()}).Size()
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("got %d", n)
}
n, err = NewSizeReaderAt(readsizer{reader, reader.Size()}).Size()
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("got %d", n)
}
n, err = NewSizeReaderAt(readseeker{reader, reader}).Size()
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("got %d", n)
}
_, err = NewSizeReaderAt(readerat{reader}).Size()
if err == nil {
t.Error("want error")
}
}
type readlener struct {
io.ReaderAt
len int
}
func (l readlener) Len() int { return l.len }
type readsizer struct {
io.ReaderAt
size int64
}
func (l readsizer) Size() (int64, error) { return l.size, nil }
type readseeker struct {
io.ReaderAt
io.Seeker
}
type readerat struct {
io.ReaderAt
}

34
util/vtabutil/arg.go Normal file
View File

@@ -0,0 +1,34 @@
// Package ioutil implements virtual table utility functions.
package vtabutil
import "strings"
// NamedArg splits an named arg into a key and value,
// around an equals sign.
// Spaces are trimmed around both key and value.
func NamedArg(arg string) (key, val string) {
key, val, _ = strings.Cut(arg, "=")
key = strings.TrimSpace(key)
val = strings.TrimSpace(val)
return
}
// Unquote unquotes a string.
func Unquote(val string) string {
if len(val) < 2 {
return val
}
if val[0] != val[len(val)-1] {
return val
}
var old, new string
switch val[0] {
default:
return val
case '"':
old, new = `""`, `"`
case '\'':
old, new = `''`, `'`
}
return strings.ReplaceAll(val[1:len(val)-1], old, new)
}