From 2bb3f4747eaf9d81b7848b416a5aa4dc3e1f17f7 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Tue, 13 Apr 2021 13:28:26 +0100 Subject: [PATCH] File filters (windows). --- file.go | 43 +++++++++++++++++++++++++++++--- file_darwin.go | 4 +-- file_filter_test.go | 60 +++++++++++++++++++++++++++++++++------------ file_test.go | 4 +-- file_windows.go | 3 ++- 5 files changed, 89 insertions(+), 25 deletions(-) diff --git a/file.go b/file.go index 386a18d..97442c5 100644 --- a/file.go +++ b/file.go @@ -85,10 +85,42 @@ func (f FileFilters) apply(o *options) { o.fileFilters = append(o.fileFilters, f...) } -// macOS filters by case insensitive literal extension. -// Extract all literal extensions from all patterns. -// If those contain wildcards, or classes with more than one character, accept anything. -func (f FileFilters) darwin() []string { +// Windows' patterns are case insensitive, don't support character classes or escaping. +// First we remove character classes, then escaping. Patterns with literal wildcards are invalid (match nothing). +// The semicolon is a separator, so we replace it with the single character wildcard. +func (f FileFilters) simplify() { + for _, filter := range f { + for i, pattern := range filter.Patterns { + var escape, invalid bool + var buf strings.Builder + for _, r := range removeClasses(pattern) { + if !escape && r == '\\' { + escape = true + continue + } + if escape && (r == '*' || r == '?') { + invalid = true + break + } + if r == ';' { + r = '?' + } + buf.WriteRune(r) + escape = false + } + if invalid { + filter.Patterns[i] = "" + } else { + filter.Patterns[i] = buf.String() + } + } + } +} + +// macOS filters by "type"; the case insensitive literal extension is a good proxy. +// So we extract the extension from each pattern, remove character classes, then escaping. +// If an extension contains a wildcard, any type is accepted. +func (f FileFilters) types() []string { var res []string for _, filter := range f { for _, pattern := range filter.Patterns { @@ -114,6 +146,9 @@ func (f FileFilters) darwin() []string { return res } +// Remove character classes from pattern, assuming case insensitivity. +// Classes of one character (case insensitive) are replaced by the character. +// Others are replaced by the single character wildcard. func removeClasses(pattern string) string { var res strings.Builder for { diff --git a/file_darwin.go b/file_darwin.go index 9023358..dc73507 100644 --- a/file_darwin.go +++ b/file_darwin.go @@ -14,7 +14,7 @@ func selectFile(opts options) (string, error) { data.Operation = "chooseFolder" } else { data.Operation = "chooseFile" - data.Options.Type = opts.fileFilters.darwin() + data.Options.Type = opts.fileFilters.types() } out, err := zenutil.Run(opts.ctx, "file", data) @@ -34,7 +34,7 @@ func selectFileMutiple(opts options) ([]string, error) { data.Operation = "chooseFolder" } else { data.Operation = "chooseFile" - data.Options.Type = opts.fileFilters.darwin() + data.Options.Type = opts.fileFilters.types() } out, err := zenutil.Run(opts.ctx, "file", data) diff --git a/file_filter_test.go b/file_filter_test.go index 55c525c..cdd5786 100644 --- a/file_filter_test.go +++ b/file_filter_test.go @@ -5,27 +5,55 @@ import ( "testing" ) -func TestFileFilters_darwin(t *testing.T) { +func TestFileFilters_simplify(t *testing.T) { tests := []struct { data FileFilters want []string }{ - {FileFilters{{"", []string{`*.png`}}}, []string{`png`}}, - {FileFilters{{"", []string{`*.pn?`}}}, nil}, - {FileFilters{{"", []string{`*.pn\?`}}}, []string{`pn?`}}, - {FileFilters{{"", []string{`*.[PpNnGg]`}}}, nil}, - {FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{`PNG`}}, - {FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{`PNG`}}, - {FileFilters{{"", []string{`*.[PNG`}}}, []string{`[PNG`}}, - {FileFilters{{"", []string{`*.]PNG`}}}, []string{`]PNG`}}, - {FileFilters{{"", []string{`*.[[]PNG`}}}, []string{`[PNG`}}, - {FileFilters{{"", []string{`*.[]]PNG`}}}, []string{`]PNG`}}, - {FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{`[PNG`}}, - {FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{`]PNG`}}, + {FileFilters{{"", []string{`*.png`}}}, []string{"*.png"}}, + {FileFilters{{"", []string{`*.pn?`}}}, []string{"*.pn?"}}, + {FileFilters{{"", []string{`*.pn;`}}}, []string{"*.pn?"}}, + {FileFilters{{"", []string{`*.pn\?`}}}, []string{""}}, + {FileFilters{{"", []string{`*.[PpNnGg]`}}}, []string{"*.?"}}, + {FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"*.PNG"}}, + {FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"*.PNG"}}, + {FileFilters{{"", []string{`*.[PNG`}}}, []string{"*.[PNG"}}, + {FileFilters{{"", []string{`*.]PNG`}}}, []string{"*.]PNG"}}, + {FileFilters{{"", []string{`*.[[]PNG`}}}, []string{"*.[PNG"}}, + {FileFilters{{"", []string{`*.[]]PNG`}}}, []string{"*.]PNG"}}, + {FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{"*.[PNG"}}, + {FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{"*.]PNG"}}, } - for _, tt := range tests { - if got := tt.data.darwin(); !reflect.DeepEqual(got, tt.want) { - t.Fatalf("FileFilters.darwin(%+v) = %v, want %v", tt.data, got, tt.want) + for i, tt := range tests { + tt.data.simplify() + if got := tt.data[0].Patterns; !reflect.DeepEqual(got, tt.want) { + t.Errorf("FileFilters.simplify[%d] = %q, want %q", i, got, tt.want) + } + } +} + +func TestFileFilters_types(t *testing.T) { + tests := []struct { + data FileFilters + want []string + }{ + {FileFilters{{"", []string{`*.png`}}}, []string{"png"}}, + {FileFilters{{"", []string{`*.pn?`}}}, nil}, + {FileFilters{{"", []string{`*.pn;`}}}, []string{"pn;"}}, + {FileFilters{{"", []string{`*.pn\?`}}}, []string{"pn?"}}, + {FileFilters{{"", []string{`*.[PpNnGg]`}}}, nil}, + {FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"PNG"}}, + {FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"PNG"}}, + {FileFilters{{"", []string{`*.[PNG`}}}, []string{"[PNG"}}, + {FileFilters{{"", []string{`*.]PNG`}}}, []string{"]PNG"}}, + {FileFilters{{"", []string{`*.[[]PNG`}}}, []string{"[PNG"}}, + {FileFilters{{"", []string{`*.[]]PNG`}}}, []string{"]PNG"}}, + {FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{"[PNG"}}, + {FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{"]PNG"}}, + } + for i, tt := range tests { + if got := tt.data.types(); !reflect.DeepEqual(got, tt.want) { + t.Fatalf("FileFilters.types[%d] = %v, want %v", i, got, tt.want) } } } diff --git a/file_test.go b/file_test.go index 5ee972f..34a9c03 100644 --- a/file_test.go +++ b/file_test.go @@ -10,8 +10,8 @@ import ( "github.com/ncruces/zenity" ) -const defaultPath = "" -const defaultName = "" +const defaultPath = `` +const defaultName = `` func ExampleSelectFile() { zenity.SelectFile( diff --git a/file_windows.go b/file_windows.go index 4ee3478..cfbd28e 100644 --- a/file_windows.go +++ b/file_windows.go @@ -361,7 +361,8 @@ func initDirNameExt(filename string, name []uint16) (dir *uint16, ext *uint16) { return } -func initFilters(filters []FileFilter) []uint16 { +func initFilters(filters FileFilters) []uint16 { + filters.simplify() var res []uint16 for _, f := range filters { res = append(res, utf16.Encode([]rune(f.Name))...)