Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions cmd/help_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func TestJSONHelpRealRootManifestIncludesPublicCommands(t *testing.T) {
"share-link download",
"share-link info",
"share-link list",
"share-link ls",
"share-link revoke",
"share-link update",
"team",
Expand Down
12 changes: 12 additions & 0 deletions cmd/help_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,17 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
DropboxScopes: []string{"sharing.read"},
Known: true,
},
"share-link ls": {
Args: []jsonCommandArg{commandArg("url", true, false, "url", "Folder shared link URL")},
Examples: []jsonCommandExample{{Description: "List shared folder contents", Command: "dbxcli share-link ls https://www.dropbox.com/s/example/folder --recursive"}},
Flags: mergeCommandFlagMetadata(sharedLinkPasswordFlagMetadata, map[string]jsonCommandFlagMetadata{
"long": {ValueKind: "boolean"},
"path": {ValueKind: "dropbox_path"},
"recursive": {ValueKind: "boolean"},
}),
DropboxScopes: []string{"sharing.read", "files.metadata.read"},
Known: true,
},
"share-link list": {
Args: []jsonCommandArg{commandArg("path", false, false, "dropbox_path", "Dropbox path filter")},
Examples: []jsonCommandExample{{Description: "List shared links", Command: "dbxcli share-link list /Reports/report.pdf"}},
Expand Down Expand Up @@ -370,6 +381,7 @@ var commandContractRegistry = map[string]jsonCommandContractMetadata{
"share list link": {Statuses: []string{"listed"}, Kinds: []string{"file", "folder", "link"}, Warnings: []string{jsonWarningCodeDeprecatedCommand}},
"share-link create": {Statuses: []string{"created", "existing", jsonStatusPlanned}, Kinds: []string{"file", "folder", "link"}},
"share-link download": {Statuses: []string{"downloaded"}, Kinds: []string{"file", "folder", "link"}},
"share-link ls": {Statuses: []string{"listed"}, Kinds: []string{"file", "folder"}},
"share-link info": {Statuses: []string{"found"}, Kinds: []string{"file", "folder", "link"}},
"share-link list": {Statuses: []string{"listed"}, Kinds: []string{"file", "folder", "link"}},
"share-link revoke": {Statuses: []string{"revoked", jsonStatusPlanned}, Kinds: []string{"file", "folder", "link", "shared_link"}},
Expand Down
9 changes: 9 additions & 0 deletions cmd/json_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ func expectedStructuredOutputCommands() []string {
"share list link",
"share-link create",
"share-link download",
"share-link ls",
"share-link info",
"share-link list",
"share-link revoke",
Expand Down Expand Up @@ -451,6 +452,7 @@ func jsonSuccessFixtureCoverage() map[string]jsonSuccessFixture {
file: "share_link_json_test.go",
tests: []string{"TestShareLinkDownloadJSONOutputsTargetAndMetadata"},
},
"share-link ls": {file: "share_link_ls_test.go", tests: []string{"TestShareLinkLSJSON"}},
"share-link info": {
file: "share_link_json_test.go",
tests: []string{"TestShareLinkInfoJSONOutputsPermissions"},
Expand Down Expand Up @@ -766,6 +768,10 @@ func jsonGoldenSuccessOutputExamples() map[string]jsonOperationOutput {
"share-link download": newJSONOperationOutput(shareLinkDownloadInput{URL: sharedLink.URL, Target: "old.pdf", Path: "/old.pdf", Recursive: false, Password: true}, []jsonOperationResult{
newJSONOperationResult(shareLinkJSONStatusDownloaded, sharedLink.Type, nil, shareLinkDownloadResult{Target: "old.pdf", Link: sharedLink}),
}, nil),
"share-link ls": newJSONOperationOutput(shareLinkLSInput{URL: sharedLink.URL, Path: "/documents", Recursive: true, Password: true, Long: true}, []jsonOperationResult{
newJSONOperationResult(shareLinkJSONStatusListed, "folder", nil, shareLinkLSEntry{Name: "nested", Path: "/documents/nested"}),
newJSONOperationResult(shareLinkJSONStatusListed, "file", nil, shareLinkLSEntry{Name: "report.pdf", Path: "/documents/report.pdf", Size: uint64Ptr(123)}),
}, nil),
"share-link info": newJSONOperationOutput(shareLinkInfoInput{URL: sharedLink.URL, Path: "/old.pdf", Password: true}, []jsonOperationResult{
shareLinkJSONOperationResult(shareLinkJSONStatusFound, sharedLink),
}, nil),
Expand Down Expand Up @@ -1066,6 +1072,8 @@ func jsonContractDefinitions() map[string][]string {
"share_link_create_result_input": jsonFieldNames[shareLinkCreateResultInput](),
"share_link_download_input": jsonFieldNames[shareLinkDownloadInput](),
"share_link_download_result": jsonFieldNames[shareLinkDownloadResult](),
"share_link_ls_input": jsonFieldNames[shareLinkLSInput](),
"share_link_ls_entry": jsonFieldNames[shareLinkLSEntry](),
"share_link_info_input": jsonFieldNames[shareLinkInfoInput](),
"share_link_list_input": jsonFieldNames[shareLinkListInput](),
"share_link_metadata": jsonFieldNames[shareLinkJSONMetadata](),
Expand Down Expand Up @@ -1113,6 +1121,7 @@ func jsonCommandSchemas() map[string]jsonGoldenCommandSchema {
shareLinkKinds(),
nil,
),
"share-link ls": operationSchema("share_link_ls_input", schemaRef("empty"), "share_link_ls_entry", []string{shareLinkJSONStatusListed}, []string{"file", "folder"}, nil),
"share-link info": operationSchema("share_link_info_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusFound}, shareLinkKinds(), nil),
"share-link list": operationSchema("share_link_list_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), nil),
"share-link revoke": operationSchema("share_link_revoke_input", schemaRef("share_link_revoke_result_input"), "share_link_revoke_result", []string{shareLinkJSONStatusRevoked, jsonStatusPlanned}, append(shareLinkKinds(), shareLinkJSONKindSharedLink), nil),
Expand Down
16 changes: 12 additions & 4 deletions cmd/share_link_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,20 +291,26 @@ func listSharedLinkFolderEntries(dbx filesClient, arg *sharing.GetSharedLinkMeta
listArg.SharedLink = files.NewSharedLink(arg.Url)
listArg.SharedLink.Password = arg.LinkPassword

if err := currentContext().Err(); err != nil {
return nil, err
}
res, err := dbx.ListFolderContext(currentContext(), listArg)
if err != nil {
return nil, fmt.Errorf("list shared link folder %q: %v", relFolder, err)
return nil, fmt.Errorf("list shared link folder %q: %w", relFolder, err)
}

entries := append([]files.IsMetadata{}, res.Entries...)
for res.HasMore {
if err := currentContext().Err(); err != nil {
return entries, err
}
if res.Cursor == "" {
return entries, errors.New("list shared link folder has more results but no cursor")
}
cont := files.NewListFolderContinueArg(res.Cursor)
res, err = dbx.ListFolderContinueContext(currentContext(), cont)
if err != nil {
return entries, fmt.Errorf("list shared link folder continue: %v", err)
return entries, fmt.Errorf("list shared link folder continue: %w", err)
}
entries = append(entries, res.Entries...)
}
Expand Down Expand Up @@ -333,8 +339,10 @@ func downloadSharedLinkRelativeFile(dbx sharedLinkClient, baseArg *sharing.GetSh

// Shared-link listings may omit path_display and path_lower when the folder is
// not mounted in the caller's Dropbox. Names are relative to the folder listed.
// Validate Dropbox names independently of the local OS; Windows device names
// and colons are not filesystem paths until sharedLinkLocalPath is called.
func sharedLinkEntryRelativePath(parentRel, name string) (string, error) {
if name == "." || !filepath.IsLocal(name) || strings.ContainsAny(name, `/\`) {
if name == "" || name == "." || name == ".." || strings.ContainsAny(name, "/\\\x00") {
return "", fmt.Errorf("invalid shared link entry name %q", name)
}
return path.Join(parentRel, name), nil
Expand All @@ -349,7 +357,7 @@ func sharedLinkAPIPath(rel string) string {

func sharedLinkLocalPath(root, rel string) (string, error) {
localRel := filepath.Clean(filepath.FromSlash(rel))
if localRel == "." || localRel == ".." || strings.HasPrefix(localRel, ".."+string(filepath.Separator)) || filepath.IsAbs(localRel) {
if localRel == "." || !filepath.IsLocal(localRel) {
return "", fmt.Errorf("invalid shared link relative path %q", rel)
}
return filepath.Join(root, localRel), nil
Expand Down
25 changes: 25 additions & 0 deletions cmd/share_link_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -680,6 +681,9 @@ func TestSharedLinkEntryRelativePath(t *testing.T) {
{"docs", "docs", "docs/docs"},
{"", "İ.txt", "İ.txt"},
{"sub", "a..b", "sub/a..b"},
{"資料", "e\u0301.txt", "資料/e\u0301.txt"},
{"sub", "CON", "sub/CON"},
{"sub", "C:notes", "sub/C:notes"},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
Expand Down Expand Up @@ -946,3 +950,24 @@ func assertFileContent(t *testing.T, path string, want string) {
t.Fatalf("%s = %q, want %q", path, string(got), want)
}
}

func TestSharedLinkLocalPathKeepsLocalFilesystemChecks(t *testing.T) {
root := t.TempDir()
for _, rel := range []string{"", ".", "..", "../escape", "/absolute", "nested/../../escape"} {
if got, err := sharedLinkLocalPath(root, rel); err == nil {
t.Fatalf("local path %q = %q, want rejection", rel, got)
}
}
// Device names with extensions can be valid on newer Windows versions.
// Bare device names remain reserved on all supported Windows versions.
for _, rel := range []string{"CON", "nested/NUL", "C:notes", "nested/report:2026.txt"} {
got, err := sharedLinkLocalPath(root, rel)
if runtime.GOOS == "windows" {
if err == nil {
t.Fatalf("unsafe Windows local path %q = %q", rel, got)
}
} else if err != nil || got != filepath.Join(root, filepath.FromSlash(rel)) {
t.Fatalf("valid local path %q = %q, %v", rel, got, err)
}
}
}
1 change: 1 addition & 0 deletions cmd/share_link_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ func TestShareLinkCommandsSupportStructuredOutput(t *testing.T) {
shareLinkListCmd,
shareListLinksCmd,
shareLinkInfoCmd,
shareLinkLSCmd,
shareLinkUpdateCmd,
shareLinkRevokeCmd,
shareLinkDownloadCmd,
Expand Down
Loading
Loading