detached 9dcc0d9

Harden CEF file dialog edge cases

commit
9dcc0d9957106550f1c85a81aaabe13ba7bea6f5
author
brice · · 2026-05-08 05:26 UTC
parent
163e508
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +163 -43
M internal/infrastructure/cef/file_dialog.go +68 -43
@@ -29,7 +29,7 @@ type cefFileDialogRequest struct {
2929
3030 type fileDialogPresenter func(*WebView, cefFileDialogRequest, purecef.FileDialogCallback)
3131
32-const maxExpandedFolderUploadFiles = 5000
32+var maxExpandedFolderUploadFiles = 5000
3333
3434 var (
3535 decodeCEFStringList = purecef.StringListToSlice
@@ -68,7 +68,7 @@ func (h *handlerSet) OnFileDialog(
6868 logCEFFileDialog(h.wv).Debug().
6969 Int32("mode", mode).
7070 Str("title", title).
71- Str("default_file_path", defaultFilePath).
71+ Bool("has_default_file_path", strings.TrimSpace(defaultFilePath) != "").
7272 Int("accept_filter_count", len(req.AcceptFilters)).
7373 Int("accept_extension_count", len(req.AcceptExtensions)).
7474 Msg("cef: file dialog requested")
@@ -86,7 +86,7 @@ func presentGTKFileDialog(wv *WebView, req cefFileDialogRequest, callback purece
8686 logCEFFileDialog(wv).Debug().
8787 Int32("mode", req.Mode).
8888 Str("title", req.Title).
89- Str("default_file_path", req.DefaultFilePath).
89+ Bool("has_default_file_path", strings.TrimSpace(req.DefaultFilePath) != "").
9090 Msg("cef: presenting GTK file dialog")
9191 dialog, ok := newNativeFileDialog(wv, req)
9292 if !ok || dialog == nil {
@@ -116,7 +116,6 @@ func presentGTKFileDialog(wv *WebView, req cefFileDialogRequest, callback purece
116116 paths := selectedFileDialogPaths(dialog, req.Mode)
117117 logCEFFileDialog(wv).Debug().
118118 Int("path_count", len(paths)).
119- Strs("paths", paths).
120119 Msg("cef: file dialog selection captured")
121120 paths = folderUploadPaths(wv, req.Mode, paths)
122121 dispatchFileDialogResult(wv, callback, paths)
@@ -184,44 +183,69 @@ func fileDialogParentWindow(wv *WebView) *gtk.Window {
184183 return gtk.WindowNewFromInternalPtr(ancestor.GoPointer())
185184 }
186185
187-func applyDefaultDialogPath(dialog *gtk.FileChooserNative, req cefFileDialogRequest) {
188- if dialog == nil {
189- return
190- }
191- path := strings.TrimSpace(req.DefaultFilePath)
186+// defaultDialogPathInfo holds the result of parsing DefaultFilePath for use by
187+// applyDefaultDialogPath. It is a pure value type to remain unit-testable without
188+// GTK objects.
189+type defaultDialogPathInfo struct {
190+ setFolder string // path to set as current folder (empty = don't set)
191+ setName string // name to set as current name (empty = don't set)
192+ setFile string // path to set as selected file (empty = don't set)
193+}
194+
195+// parseDefaultDialogPath extracts folder/name/file settings from DefaultFilePath
196+// without any GTK dependencies. For save mode, if the path points to an existing
197+// directory, it sets that as the current folder without a current name.
198+func parseDefaultDialogPath(mode purecef.FileDialogMode, defaultFilePath string) defaultDialogPathInfo {
199+ path := strings.TrimSpace(defaultFilePath)
192200 if path == "" {
193- return
201+ return defaultDialogPathInfo{}
194202 }
195203
196- if req.Mode == purecef.FileDialogModeFileDialogSave {
197- if dir := filepath.Dir(path); dir != "." && dir != "" {
198- if folder := gio.FileNewForPath(dir); folder != nil {
199- defer unrefGObjectPointer(folder.GoPointer())
200- _, _ = dialog.SetCurrentFolder(folder)
201- }
204+ if mode == purecef.FileDialogModeFileDialogSave {
205+ if info, err := os.Stat(path); err == nil && info.IsDir() {
206+ return defaultDialogPathInfo{setFolder: path}
202207 }
203- if name := filepath.Base(path); name != "." && name != string(filepath.Separator) {
204- dialog.SetCurrentName(name)
208+ name := filepath.Base(path)
209+ if name == "." || name == string(filepath.Separator) {
210+ name = ""
205211 }
206- return
212+ info := defaultDialogPathInfo{setName: name}
213+ if dir := filepath.Dir(path); dir != "." && dir != "" {
214+ info.setFolder = dir
215+ }
216+ return info
207217 }
208218
209219 if info, err := os.Stat(path); err == nil && info.IsDir() {
210- if folder := gio.FileNewForPath(path); folder != nil {
211- defer unrefGObjectPointer(folder.GoPointer())
212- _, _ = dialog.SetCurrentFolder(folder)
213- }
214- return
220+ return defaultDialogPathInfo{setFolder: path}
215221 }
216222
223+ info := defaultDialogPathInfo{}
217224 if dir := filepath.Dir(path); dir != "." && dir != "" {
218- if folder := gio.FileNewForPath(dir); folder != nil {
225+ info.setFolder = dir
226+ }
227+ if mode != purecef.FileDialogModeFileDialogOpenFolder {
228+ info.setFile = path
229+ }
230+ return info
231+}
232+
233+func applyDefaultDialogPath(dialog *gtk.FileChooserNative, req cefFileDialogRequest) {
234+ if dialog == nil {
235+ return
236+ }
237+ dpi := parseDefaultDialogPath(req.Mode, req.DefaultFilePath)
238+ if dpi.setFolder != "" {
239+ if folder := gio.FileNewForPath(dpi.setFolder); folder != nil {
219240 defer unrefGObjectPointer(folder.GoPointer())
220241 _, _ = dialog.SetCurrentFolder(folder)
221242 }
222243 }
223- if req.Mode != purecef.FileDialogModeFileDialogOpenFolder {
224- if file := gio.FileNewForPath(path); file != nil {
244+ if dpi.setName != "" {
245+ dialog.SetCurrentName(dpi.setName)
246+ }
247+ if dpi.setFile != "" {
248+ if file := gio.FileNewForPath(dpi.setFile); file != nil {
225249 defer unrefGObjectPointer(file.GoPointer())
226250 _, _ = dialog.SetFile(file)
227251 }
@@ -358,8 +382,6 @@ func folderUploadPaths(wv *WebView, mode purecef.FileDialogMode, paths []string)
358382 info, err := os.Stat(folder)
359383 if err != nil || !info.IsDir() {
360384 logCEFFileDialog(wv).Warn().
361- Err(err).
362- Str("path", folder).
363385 Msg("cef: folder upload path expansion skipped; selected path is not a directory")
364386 return paths
365387 }
@@ -368,7 +390,7 @@ func folderUploadPaths(wv *WebView, mode purecef.FileDialogMode, paths []string)
368390 truncated := false
369391 walkErr := filepath.WalkDir(folder, func(path string, entry fs.DirEntry, err error) error {
370392 if err != nil {
371- logCEFFileDialog(wv).Debug().Err(err).Str("path", path).Msg("cef: folder upload path expansion skipped unreadable entry")
393+ logCEFFileDialog(wv).Debug().Msg("cef: folder upload path expansion skipped unreadable entry")
372394 return nil
373395 }
374396 if path == folder || entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
@@ -387,22 +409,24 @@ func folderUploadPaths(wv *WebView, mode purecef.FileDialogMode, paths []string)
387409 })
388410 if walkErr != nil {
389411 logCEFFileDialog(wv).Warn().
390- Err(walkErr).
391- Str("folder", folder).
392412 Msg("cef: folder upload descendant enumeration failed; using selected directory path")
393413 return paths
394414 }
395415 if len(files) == 0 {
396416 logCEFFileDialog(wv).Warn().
397- Str("folder", folder).
398417 Msg("cef: folder upload descendant enumeration found no regular files; using selected directory path")
399418 return paths
400419 }
401420
402- logCEFFileDialog(wv).Warn().
403- Str("folder", folder).
421+ if truncated {
422+ logCEFFileDialog(wv).Warn().
423+ Int("limit", maxExpandedFolderUploadFiles).
424+ Msg("cef: folder upload expansion truncated; canceling selection")
425+ return nil
426+ }
427+
428+ logCEFFileDialog(wv).Debug().
404429 Int("file_count", len(files)).
405- Bool("truncated", truncated).
406430 Msg("cef: folder upload expanded selected directory to descendant file paths")
407431 return files
408432 }
@@ -431,27 +455,30 @@ func dispatchFileDialogResult(wv *WebView, callback purecef.FileDialogCallback,
431455 if callback == nil {
432456 return
433457 }
458+ logger := logCEFFileDialog(wv)
434459 if wv == nil || wv.engine == nil {
460+ logger.Warn().Msg("cef: webview engine unavailable; canceling file dialog selection")
461+ continueCEFFileDialog(logger, callback)
435462 return
436463 }
437464
438465 task := cefNewTask(cefTaskFunc(func() {
439- logger := logCEFFileDialog(wv)
440466 logger.Debug().
441467 Int("path_count", len(paths)).
442- Strs("paths", paths).
443468 Int32("currently_on_ui", purecef.CurrentlyOn(purecef.ThreadIDTidUi)).
444469 Msg("cef: file dialog continuation task executing")
445470 continueCEFFileDialog(logger, callback, paths...)
446471 logger.Debug().
447472 Int("path_count", len(paths)).
448- Strs("paths", paths).
449473 Msg("cef: file dialog continuation task finished")
450474 }))
451475 if result := cefPostTask(purecef.ThreadIDTidUi, task); result != 1 {
452- logCEFFileDialog(wv).Warn().Int32("post_result", result).Msg("cef: failed to post file dialog continuation to CEF UI thread")
476+ logger.Warn().
477+ Int32("post_result", result).
478+ Msg("cef: failed to post file dialog continuation to CEF UI thread; canceling selection")
479+ continueCEFFileDialog(logger, callback)
453480 } else {
454- logCEFFileDialog(wv).Debug().Int("path_count", len(paths)).Strs("paths", paths).Msg("cef: file dialog continuation task posted")
481+ logger.Debug().Int("path_count", len(paths)).Msg("cef: file dialog continuation task posted")
455482 }
456483 }
457484
@@ -474,7 +501,6 @@ func continueCEFFileDialogWithLogs(logger *zerolog.Logger, callback purecef.File
474501 logger.Debug().
475502 Uint64("callback_ptr", uint64(callbackPtr)).
476503 Int("path_count", len(filePaths)).
477- Strs("paths", filePaths).
478504 Msg("cef: allocating file dialog CEF string list")
479505 list := newCEFStringList(filePaths...)
480506 if list == 0 {
@@ -498,7 +524,6 @@ func continueCEFFileDialogWithLogs(logger *zerolog.Logger, callback purecef.File
498524 Uint64("callback_ptr", uint64(callbackPtr)).
499525 Uint64("string_list", listValue).
500526 Int("path_count", len(filePaths)).
501- Strs("paths", filePaths).
502527 Msg("cef: invoking file dialog callback continuation")
503528 callback.Cont(list)
504529 logger.Debug().
@@ -29,7 +29,7 @@ type cefFileDialogRequest struct {
29 29
30 type fileDialogPresenter func(*WebView, cefFileDialogRequest, purecef.FileDialogCallback)30 type fileDialogPresenter func(*WebView, cefFileDialogRequest, purecef.FileDialogCallback)
31 31
32-const maxExpandedFolderUploadFiles = 500032+var maxExpandedFolderUploadFiles = 5000
33 33
34 var (34 var (
35 decodeCEFStringList = purecef.StringListToSlice35 decodeCEFStringList = purecef.StringListToSlice
@@ -68,7 +68,7 @@ func (h *handlerSet) OnFileDialog(
68 logCEFFileDialog(h.wv).Debug().68 logCEFFileDialog(h.wv).Debug().
69 Int32("mode", mode).69 Int32("mode", mode).
70 Str("title", title).70 Str("title", title).
71- Str("default_file_path", defaultFilePath).71+ Bool("has_default_file_path", strings.TrimSpace(defaultFilePath) != "").
72 Int("accept_filter_count", len(req.AcceptFilters)).72 Int("accept_filter_count", len(req.AcceptFilters)).
73 Int("accept_extension_count", len(req.AcceptExtensions)).73 Int("accept_extension_count", len(req.AcceptExtensions)).
74 Msg("cef: file dialog requested")74 Msg("cef: file dialog requested")
@@ -86,7 +86,7 @@ func presentGTKFileDialog(wv *WebView, req cefFileDialogRequest, callback purece
86 logCEFFileDialog(wv).Debug().86 logCEFFileDialog(wv).Debug().
87 Int32("mode", req.Mode).87 Int32("mode", req.Mode).
88 Str("title", req.Title).88 Str("title", req.Title).
89- Str("default_file_path", req.DefaultFilePath).89+ Bool("has_default_file_path", strings.TrimSpace(req.DefaultFilePath) != "").
90 Msg("cef: presenting GTK file dialog")90 Msg("cef: presenting GTK file dialog")
91 dialog, ok := newNativeFileDialog(wv, req)91 dialog, ok := newNativeFileDialog(wv, req)
92 if !ok || dialog == nil {92 if !ok || dialog == nil {
@@ -116,7 +116,6 @@ func presentGTKFileDialog(wv *WebView, req cefFileDialogRequest, callback purece
116 paths := selectedFileDialogPaths(dialog, req.Mode)116 paths := selectedFileDialogPaths(dialog, req.Mode)
117 logCEFFileDialog(wv).Debug().117 logCEFFileDialog(wv).Debug().
118 Int("path_count", len(paths)).118 Int("path_count", len(paths)).
119- Strs("paths", paths).
120 Msg("cef: file dialog selection captured")119 Msg("cef: file dialog selection captured")
121 paths = folderUploadPaths(wv, req.Mode, paths)120 paths = folderUploadPaths(wv, req.Mode, paths)
122 dispatchFileDialogResult(wv, callback, paths)121 dispatchFileDialogResult(wv, callback, paths)
@@ -184,44 +183,69 @@ func fileDialogParentWindow(wv *WebView) *gtk.Window {
184 return gtk.WindowNewFromInternalPtr(ancestor.GoPointer())183 return gtk.WindowNewFromInternalPtr(ancestor.GoPointer())
185 }184 }
186 185
187-func applyDefaultDialogPath(dialog *gtk.FileChooserNative, req cefFileDialogRequest) {186+// defaultDialogPathInfo holds the result of parsing DefaultFilePath for use by
188- if dialog == nil {187+// applyDefaultDialogPath. It is a pure value type to remain unit-testable without
189- return188+// GTK objects.
190- }189+type defaultDialogPathInfo struct {
191- path := strings.TrimSpace(req.DefaultFilePath)190+ setFolder string // path to set as current folder (empty = don't set)
191+ setName string // name to set as current name (empty = don't set)
192+ setFile string // path to set as selected file (empty = don't set)
193+}
194+
195+// parseDefaultDialogPath extracts folder/name/file settings from DefaultFilePath
196+// without any GTK dependencies. For save mode, if the path points to an existing
197+// directory, it sets that as the current folder without a current name.
198+func parseDefaultDialogPath(mode purecef.FileDialogMode, defaultFilePath string) defaultDialogPathInfo {
199+ path := strings.TrimSpace(defaultFilePath)
192 if path == "" {200 if path == "" {
193- return201+ return defaultDialogPathInfo{}
194 }202 }
195 203
196- if req.Mode == purecef.FileDialogModeFileDialogSave {204+ if mode == purecef.FileDialogModeFileDialogSave {
197- if dir := filepath.Dir(path); dir != "." && dir != "" {205+ if info, err := os.Stat(path); err == nil && info.IsDir() {
198- if folder := gio.FileNewForPath(dir); folder != nil {206+ return defaultDialogPathInfo{setFolder: path}
199- defer unrefGObjectPointer(folder.GoPointer())
200- _, _ = dialog.SetCurrentFolder(folder)
201- }
202 }207 }
203- if name := filepath.Base(path); name != "." && name != string(filepath.Separator) {208+ name := filepath.Base(path)
204- dialog.SetCurrentName(name)209+ if name == "." || name == string(filepath.Separator) {
210+ name = ""
205 }211 }
206- return212+ info := defaultDialogPathInfo{setName: name}
213+ if dir := filepath.Dir(path); dir != "." && dir != "" {
214+ info.setFolder = dir
215+ }
216+ return info
207 }217 }
208 218
209 if info, err := os.Stat(path); err == nil && info.IsDir() {219 if info, err := os.Stat(path); err == nil && info.IsDir() {
210- if folder := gio.FileNewForPath(path); folder != nil {220+ return defaultDialogPathInfo{setFolder: path}
211- defer unrefGObjectPointer(folder.GoPointer())
212- _, _ = dialog.SetCurrentFolder(folder)
213- }
214- return
215 }221 }
216 222
223+ info := defaultDialogPathInfo{}
217 if dir := filepath.Dir(path); dir != "." && dir != "" {224 if dir := filepath.Dir(path); dir != "." && dir != "" {
218- if folder := gio.FileNewForPath(dir); folder != nil {225+ info.setFolder = dir
226+ }
227+ if mode != purecef.FileDialogModeFileDialogOpenFolder {
228+ info.setFile = path
229+ }
230+ return info
231+}
232+
233+func applyDefaultDialogPath(dialog *gtk.FileChooserNative, req cefFileDialogRequest) {
234+ if dialog == nil {
235+ return
236+ }
237+ dpi := parseDefaultDialogPath(req.Mode, req.DefaultFilePath)
238+ if dpi.setFolder != "" {
239+ if folder := gio.FileNewForPath(dpi.setFolder); folder != nil {
219 defer unrefGObjectPointer(folder.GoPointer())240 defer unrefGObjectPointer(folder.GoPointer())
220 _, _ = dialog.SetCurrentFolder(folder)241 _, _ = dialog.SetCurrentFolder(folder)
221 }242 }
222 }243 }
223- if req.Mode != purecef.FileDialogModeFileDialogOpenFolder {244+ if dpi.setName != "" {
224- if file := gio.FileNewForPath(path); file != nil {245+ dialog.SetCurrentName(dpi.setName)
246+ }
247+ if dpi.setFile != "" {
248+ if file := gio.FileNewForPath(dpi.setFile); file != nil {
225 defer unrefGObjectPointer(file.GoPointer())249 defer unrefGObjectPointer(file.GoPointer())
226 _, _ = dialog.SetFile(file)250 _, _ = dialog.SetFile(file)
227 }251 }
@@ -358,8 +382,6 @@ func folderUploadPaths(wv *WebView, mode purecef.FileDialogMode, paths []string)
358 info, err := os.Stat(folder)382 info, err := os.Stat(folder)
359 if err != nil || !info.IsDir() {383 if err != nil || !info.IsDir() {
360 logCEFFileDialog(wv).Warn().384 logCEFFileDialog(wv).Warn().
361- Err(err).
362- Str("path", folder).
363 Msg("cef: folder upload path expansion skipped; selected path is not a directory")385 Msg("cef: folder upload path expansion skipped; selected path is not a directory")
364 return paths386 return paths
365 }387 }
@@ -368,7 +390,7 @@ func folderUploadPaths(wv *WebView, mode purecef.FileDialogMode, paths []string)
368 truncated := false390 truncated := false
369 walkErr := filepath.WalkDir(folder, func(path string, entry fs.DirEntry, err error) error {391 walkErr := filepath.WalkDir(folder, func(path string, entry fs.DirEntry, err error) error {
370 if err != nil {392 if err != nil {
371- logCEFFileDialog(wv).Debug().Err(err).Str("path", path).Msg("cef: folder upload path expansion skipped unreadable entry")393+ logCEFFileDialog(wv).Debug().Msg("cef: folder upload path expansion skipped unreadable entry")
372 return nil394 return nil
373 }395 }
374 if path == folder || entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {396 if path == folder || entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
@@ -387,22 +409,24 @@ func folderUploadPaths(wv *WebView, mode purecef.FileDialogMode, paths []string)
387 })409 })
388 if walkErr != nil {410 if walkErr != nil {
389 logCEFFileDialog(wv).Warn().411 logCEFFileDialog(wv).Warn().
390- Err(walkErr).
391- Str("folder", folder).
392 Msg("cef: folder upload descendant enumeration failed; using selected directory path")412 Msg("cef: folder upload descendant enumeration failed; using selected directory path")
393 return paths413 return paths
394 }414 }
395 if len(files) == 0 {415 if len(files) == 0 {
396 logCEFFileDialog(wv).Warn().416 logCEFFileDialog(wv).Warn().
397- Str("folder", folder).
398 Msg("cef: folder upload descendant enumeration found no regular files; using selected directory path")417 Msg("cef: folder upload descendant enumeration found no regular files; using selected directory path")
399 return paths418 return paths
400 }419 }
401 420
402- logCEFFileDialog(wv).Warn().421+ if truncated {
403- Str("folder", folder).422+ logCEFFileDialog(wv).Warn().
423+ Int("limit", maxExpandedFolderUploadFiles).
424+ Msg("cef: folder upload expansion truncated; canceling selection")
425+ return nil
426+ }
427+
428+ logCEFFileDialog(wv).Debug().
404 Int("file_count", len(files)).429 Int("file_count", len(files)).
405- Bool("truncated", truncated).
406 Msg("cef: folder upload expanded selected directory to descendant file paths")430 Msg("cef: folder upload expanded selected directory to descendant file paths")
407 return files431 return files
408 }432 }
@@ -431,27 +455,30 @@ func dispatchFileDialogResult(wv *WebView, callback purecef.FileDialogCallback,
431 if callback == nil {455 if callback == nil {
432 return456 return
433 }457 }
458+ logger := logCEFFileDialog(wv)
434 if wv == nil || wv.engine == nil {459 if wv == nil || wv.engine == nil {
460+ logger.Warn().Msg("cef: webview engine unavailable; canceling file dialog selection")
461+ continueCEFFileDialog(logger, callback)
435 return462 return
436 }463 }
437 464
438 task := cefNewTask(cefTaskFunc(func() {465 task := cefNewTask(cefTaskFunc(func() {
439- logger := logCEFFileDialog(wv)
440 logger.Debug().466 logger.Debug().
441 Int("path_count", len(paths)).467 Int("path_count", len(paths)).
442- Strs("paths", paths).
443 Int32("currently_on_ui", purecef.CurrentlyOn(purecef.ThreadIDTidUi)).468 Int32("currently_on_ui", purecef.CurrentlyOn(purecef.ThreadIDTidUi)).
444 Msg("cef: file dialog continuation task executing")469 Msg("cef: file dialog continuation task executing")
445 continueCEFFileDialog(logger, callback, paths...)470 continueCEFFileDialog(logger, callback, paths...)
446 logger.Debug().471 logger.Debug().
447 Int("path_count", len(paths)).472 Int("path_count", len(paths)).
448- Strs("paths", paths).
449 Msg("cef: file dialog continuation task finished")473 Msg("cef: file dialog continuation task finished")
450 }))474 }))
451 if result := cefPostTask(purecef.ThreadIDTidUi, task); result != 1 {475 if result := cefPostTask(purecef.ThreadIDTidUi, task); result != 1 {
452- logCEFFileDialog(wv).Warn().Int32("post_result", result).Msg("cef: failed to post file dialog continuation to CEF UI thread")476+ logger.Warn().
477+ Int32("post_result", result).
478+ Msg("cef: failed to post file dialog continuation to CEF UI thread; canceling selection")
479+ continueCEFFileDialog(logger, callback)
453 } else {480 } else {
454- logCEFFileDialog(wv).Debug().Int("path_count", len(paths)).Strs("paths", paths).Msg("cef: file dialog continuation task posted")481+ logger.Debug().Int("path_count", len(paths)).Msg("cef: file dialog continuation task posted")
455 }482 }
456 }483 }
457 484
@@ -474,7 +501,6 @@ func continueCEFFileDialogWithLogs(logger *zerolog.Logger, callback purecef.File
474 logger.Debug().501 logger.Debug().
475 Uint64("callback_ptr", uint64(callbackPtr)).502 Uint64("callback_ptr", uint64(callbackPtr)).
476 Int("path_count", len(filePaths)).503 Int("path_count", len(filePaths)).
477- Strs("paths", filePaths).
478 Msg("cef: allocating file dialog CEF string list")504 Msg("cef: allocating file dialog CEF string list")
479 list := newCEFStringList(filePaths...)505 list := newCEFStringList(filePaths...)
480 if list == 0 {506 if list == 0 {
@@ -498,7 +524,6 @@ func continueCEFFileDialogWithLogs(logger *zerolog.Logger, callback purecef.File
498 Uint64("callback_ptr", uint64(callbackPtr)).524 Uint64("callback_ptr", uint64(callbackPtr)).
499 Uint64("string_list", listValue).525 Uint64("string_list", listValue).
500 Int("path_count", len(filePaths)).526 Int("path_count", len(filePaths)).
501- Strs("paths", filePaths).
502 Msg("cef: invoking file dialog callback continuation")527 Msg("cef: invoking file dialog callback continuation")
503 callback.Cont(list)528 callback.Cont(list)
504 logger.Debug().529 logger.Debug().
M internal/infrastructure/cef/file_dialog_test.go +95 -0
@@ -2,6 +2,7 @@ package cef
22
33 import (
44 "context"
5+ "fmt"
56 "os"
67 "path/filepath"
78 "testing"
@@ -102,6 +103,100 @@ func TestFolderUploadPathsKeepsEmptyDirectoryPath(t *testing.T) {
102103 require.Equal(t, []string{dir}, got)
103104 }
104105
106+func TestParseDefaultDialogPathSaveModeWithExistingDirectory(t *testing.T) {
107+ dir := t.TempDir()
108+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, dir)
109+ require.Equal(t, dir, info.setFolder)
110+ require.Empty(t, info.setName)
111+ require.Empty(t, info.setFile)
112+}
113+
114+func TestParseDefaultDialogPathSaveModeWithFilePath(t *testing.T) {
115+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, "/tmp/foo/bar.txt")
116+ require.Equal(t, "/tmp/foo", info.setFolder)
117+ require.Equal(t, "bar.txt", info.setName)
118+ require.Empty(t, info.setFile)
119+}
120+
121+func TestParseDefaultDialogPathSaveModeWithRelativeFilePath(t *testing.T) {
122+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, "bar.txt")
123+ require.Empty(t, info.setFolder)
124+ require.Equal(t, "bar.txt", info.setName)
125+ require.Empty(t, info.setFile)
126+}
127+
128+func TestParseDefaultDialogPathOpenModeWithDirectory(t *testing.T) {
129+ dir := t.TempDir()
130+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpen, dir)
131+ require.Equal(t, dir, info.setFolder)
132+ require.Empty(t, info.setFile)
133+}
134+
135+func TestParseDefaultDialogPathOpenModeWithFilePath(t *testing.T) {
136+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpen, "/tmp/foo/bar.txt")
137+ require.Equal(t, "/tmp/foo", info.setFolder)
138+ require.Equal(t, "/tmp/foo/bar.txt", info.setFile)
139+}
140+
141+func TestParseDefaultDialogPathOpenFolderMode(t *testing.T) {
142+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpenFolder, "/tmp/foo/bar.txt")
143+ require.Equal(t, "/tmp/foo", info.setFolder)
144+ require.Empty(t, info.setFile)
145+}
146+
147+func TestParseDefaultDialogPathEmptyPath(t *testing.T) {
148+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, "")
149+ require.Empty(t, info.setFolder)
150+ require.Empty(t, info.setName)
151+ require.Empty(t, info.setFile)
152+
153+ info = parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpen, " ")
154+ require.Empty(t, info.setFolder)
155+ require.Empty(t, info.setName)
156+ require.Empty(t, info.setFile)
157+}
158+
159+func TestFolderUploadPathsTruncationCancels(t *testing.T) {
160+ prev := maxExpandedFolderUploadFiles
161+ maxExpandedFolderUploadFiles = 2
162+ defer func() { maxExpandedFolderUploadFiles = prev }()
163+
164+ dir := t.TempDir()
165+ for i := 0; i < 5; i++ {
166+ p := filepath.Join(dir, fmt.Sprintf("file_%d.txt", i))
167+ require.NoError(t, os.WriteFile(p, []byte("data"), 0o644))
168+ }
169+
170+ got := folderUploadPaths(&WebView{ctx: context.Background()}, purecef.FileDialogModeFileDialogOpenFolder, []string{dir})
171+ require.Nil(t, got)
172+}
173+
174+func TestDispatchFileDialogResultCancelsWhenEngineMissing(t *testing.T) {
175+ callback := &stubFileDialogCallback{}
176+ dispatchFileDialogResult(&WebView{ctx: context.Background()}, callback, []string{"/tmp/test.txt"})
177+ require.True(t, callback.canceled)
178+}
179+
180+func TestDispatchFileDialogResultCancelsWhenCefPostTaskFails(t *testing.T) {
181+ prevNewTask := cefNewTask
182+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
183+ defer func() { cefNewTask = prevNewTask }()
184+
185+ prevPost := cefPostTask
186+ cefPostTask = func(_ purecef.ThreadID, _ purecef.Task) int32 {
187+ return 0
188+ }
189+ defer func() { cefPostTask = prevPost }()
190+
191+ callback := &stubFileDialogCallback{}
192+ wv := &WebView{
193+ ctx: context.Background(),
194+ engine: &Engine{},
195+ }
196+ dispatchFileDialogResult(wv, callback, []string{"/tmp/test.txt"})
197+ require.True(t, callback.canceled)
198+}
199+
105200 func TestOnFileDialogPresenterCanContinueCallback(t *testing.T) {
106201 prevDecode := decodeCEFStringList
107202 decodeCEFStringList = func(purecef.StringList) []string { return nil }
@@ -2,6 +2,7 @@ package cef
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 "os"6 "os"
6 "path/filepath"7 "path/filepath"
7 "testing"8 "testing"
@@ -102,6 +103,100 @@ func TestFolderUploadPathsKeepsEmptyDirectoryPath(t *testing.T) {
102 require.Equal(t, []string{dir}, got)103 require.Equal(t, []string{dir}, got)
103 }104 }
104 105
106+func TestParseDefaultDialogPathSaveModeWithExistingDirectory(t *testing.T) {
107+ dir := t.TempDir()
108+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, dir)
109+ require.Equal(t, dir, info.setFolder)
110+ require.Empty(t, info.setName)
111+ require.Empty(t, info.setFile)
112+}
113+
114+func TestParseDefaultDialogPathSaveModeWithFilePath(t *testing.T) {
115+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, "/tmp/foo/bar.txt")
116+ require.Equal(t, "/tmp/foo", info.setFolder)
117+ require.Equal(t, "bar.txt", info.setName)
118+ require.Empty(t, info.setFile)
119+}
120+
121+func TestParseDefaultDialogPathSaveModeWithRelativeFilePath(t *testing.T) {
122+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, "bar.txt")
123+ require.Empty(t, info.setFolder)
124+ require.Equal(t, "bar.txt", info.setName)
125+ require.Empty(t, info.setFile)
126+}
127+
128+func TestParseDefaultDialogPathOpenModeWithDirectory(t *testing.T) {
129+ dir := t.TempDir()
130+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpen, dir)
131+ require.Equal(t, dir, info.setFolder)
132+ require.Empty(t, info.setFile)
133+}
134+
135+func TestParseDefaultDialogPathOpenModeWithFilePath(t *testing.T) {
136+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpen, "/tmp/foo/bar.txt")
137+ require.Equal(t, "/tmp/foo", info.setFolder)
138+ require.Equal(t, "/tmp/foo/bar.txt", info.setFile)
139+}
140+
141+func TestParseDefaultDialogPathOpenFolderMode(t *testing.T) {
142+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpenFolder, "/tmp/foo/bar.txt")
143+ require.Equal(t, "/tmp/foo", info.setFolder)
144+ require.Empty(t, info.setFile)
145+}
146+
147+func TestParseDefaultDialogPathEmptyPath(t *testing.T) {
148+ info := parseDefaultDialogPath(purecef.FileDialogModeFileDialogSave, "")
149+ require.Empty(t, info.setFolder)
150+ require.Empty(t, info.setName)
151+ require.Empty(t, info.setFile)
152+
153+ info = parseDefaultDialogPath(purecef.FileDialogModeFileDialogOpen, " ")
154+ require.Empty(t, info.setFolder)
155+ require.Empty(t, info.setName)
156+ require.Empty(t, info.setFile)
157+}
158+
159+func TestFolderUploadPathsTruncationCancels(t *testing.T) {
160+ prev := maxExpandedFolderUploadFiles
161+ maxExpandedFolderUploadFiles = 2
162+ defer func() { maxExpandedFolderUploadFiles = prev }()
163+
164+ dir := t.TempDir()
165+ for i := 0; i < 5; i++ {
166+ p := filepath.Join(dir, fmt.Sprintf("file_%d.txt", i))
167+ require.NoError(t, os.WriteFile(p, []byte("data"), 0o644))
168+ }
169+
170+ got := folderUploadPaths(&WebView{ctx: context.Background()}, purecef.FileDialogModeFileDialogOpenFolder, []string{dir})
171+ require.Nil(t, got)
172+}
173+
174+func TestDispatchFileDialogResultCancelsWhenEngineMissing(t *testing.T) {
175+ callback := &stubFileDialogCallback{}
176+ dispatchFileDialogResult(&WebView{ctx: context.Background()}, callback, []string{"/tmp/test.txt"})
177+ require.True(t, callback.canceled)
178+}
179+
180+func TestDispatchFileDialogResultCancelsWhenCefPostTaskFails(t *testing.T) {
181+ prevNewTask := cefNewTask
182+ cefNewTask = func(task purecef.Task) purecef.Task { return task }
183+ defer func() { cefNewTask = prevNewTask }()
184+
185+ prevPost := cefPostTask
186+ cefPostTask = func(_ purecef.ThreadID, _ purecef.Task) int32 {
187+ return 0
188+ }
189+ defer func() { cefPostTask = prevPost }()
190+
191+ callback := &stubFileDialogCallback{}
192+ wv := &WebView{
193+ ctx: context.Background(),
194+ engine: &Engine{},
195+ }
196+ dispatchFileDialogResult(wv, callback, []string{"/tmp/test.txt"})
197+ require.True(t, callback.canceled)
198+}
199+
105 func TestOnFileDialogPresenterCanContinueCallback(t *testing.T) {200 func TestOnFileDialogPresenterCanContinueCallback(t *testing.T) {
106 prevDecode := decodeCEFStringList201 prevDecode := decodeCEFStringList
107 decodeCEFStringList = func(purecef.StringList) []string { return nil }202 decodeCEFStringList = func(purecef.StringList) []string { return nil }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close