Skip to content

Commit 3875d9e

Browse files
committed
organize audiobook trees safely
1 parent e463b05 commit 3875d9e

2 files changed

Lines changed: 90 additions & 1 deletion

File tree

internal/organize/audio_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,85 @@ func TestOrganizeAudiobookMovesNestedTreeRecursively(t *testing.T) {
165165
t.Fatalf("expected source tree removed, stat err=%v", err)
166166
}
167167
}
168+
169+
func TestOrganizeAudiobookSkipsSymlinks(t *testing.T) {
170+
root := t.TempDir()
171+
src := filepath.Join(root, "incoming", "book")
172+
outside := filepath.Join(root, "outside.txt")
173+
174+
if err := os.MkdirAll(src, 0755); err != nil {
175+
t.Fatal(err)
176+
}
177+
if err := os.WriteFile(filepath.Join(src, "track01.m4b"), []byte("audio"), 0644); err != nil {
178+
t.Fatal(err)
179+
}
180+
if err := os.WriteFile(outside, []byte("outside"), 0644); err != nil {
181+
t.Fatal(err)
182+
}
183+
if err := os.Symlink(outside, filepath.Join(src, "linked.m4b")); err != nil {
184+
t.Skipf("symlinks unavailable: %v", err)
185+
}
186+
187+
cfg := &config.Config{
188+
FileOrgEnabled: true,
189+
AudiobookDir: filepath.Join(root, "audiobooks"),
190+
}
191+
o := NewOrganizer(cfg)
192+
193+
dest, err := o.OrganizeAudiobook(src, "Symlink Book", "Careful Author")
194+
if err != nil {
195+
t.Fatalf("organize failed: %v", err)
196+
}
197+
198+
if _, err := os.Stat(filepath.Join(dest, "track01.m4b")); err != nil {
199+
t.Fatalf("expected regular track at destination: %v", err)
200+
}
201+
if _, err := os.Lstat(filepath.Join(dest, "linked.m4b")); !os.IsNotExist(err) {
202+
t.Fatalf("expected symlink skipped, lstat err=%v", err)
203+
}
204+
data, err := os.ReadFile(outside)
205+
if err != nil {
206+
t.Fatalf("expected symlink target left untouched: %v", err)
207+
}
208+
if string(data) != "outside" {
209+
t.Fatalf("outside file = %q, want %q", data, "outside")
210+
}
211+
}
212+
213+
func TestOrganizeAudiobookRejectsSymlinkSource(t *testing.T) {
214+
root := t.TempDir()
215+
realSrc := filepath.Join(root, "incoming", "book")
216+
linkSrc := filepath.Join(root, "incoming", "linked-book")
217+
218+
if err := os.MkdirAll(realSrc, 0755); err != nil {
219+
t.Fatal(err)
220+
}
221+
if err := os.WriteFile(filepath.Join(realSrc, "track01.m4b"), []byte("audio"), 0644); err != nil {
222+
t.Fatal(err)
223+
}
224+
if err := os.Symlink(realSrc, linkSrc); err != nil {
225+
t.Skipf("symlinks unavailable: %v", err)
226+
}
227+
228+
cfg := &config.Config{
229+
FileOrgEnabled: true,
230+
AudiobookDir: filepath.Join(root, "audiobooks"),
231+
}
232+
o := NewOrganizer(cfg)
233+
234+
_, err := o.OrganizeAudiobook(linkSrc, "Linked Book", "Careful Author")
235+
if err == nil {
236+
t.Fatal("expected symlink source error")
237+
}
238+
239+
destDir := filepath.Join(cfg.AudiobookDir, "Careful Author", "Linked Book")
240+
if _, err := os.Stat(destDir); !os.IsNotExist(err) {
241+
t.Fatalf("expected no destination for symlink source, stat err=%v", err)
242+
}
243+
if _, err := os.Stat(filepath.Join(realSrc, "track01.m4b")); err != nil {
244+
t.Fatalf("expected symlink target left untouched: %v", err)
245+
}
246+
if _, err := os.Lstat(linkSrc); err != nil {
247+
t.Fatalf("expected symlink source left untouched: %v", err)
248+
}
249+
}

internal/organize/pipeline.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package organize
22

33
import (
4+
"fmt"
45
"io"
56
"log/slog"
67
"os"
@@ -82,10 +83,13 @@ func (o *Organizer) OrganizeAudiobook(filePath, title, author string) (string, e
8283
}
8384

8485
// If source is a directory, move its contents.
85-
info, err := os.Stat(filePath)
86+
info, err := os.Lstat(filePath)
8687
if err != nil {
8788
return filePath, err
8889
}
90+
if info.Mode()&os.ModeSymlink != 0 {
91+
return filePath, fmt.Errorf("refusing to organize symlink source %q", filePath)
92+
}
8993

9094
safeAuthor := sanitizePath(author, 80)
9195
safeTitle := sanitizePath(title, 80)
@@ -243,6 +247,9 @@ func moveDirTree(srcDir, dstDir string) error {
243247
if path == srcDir {
244248
return nil
245249
}
250+
if d.Type()&os.ModeSymlink != 0 {
251+
return nil
252+
}
246253

247254
rel, err := filepath.Rel(srcDir, path)
248255
if err != nil {

0 commit comments

Comments
 (0)