mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
harden search parsing and qobuz refresh validation
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -1995,6 +1996,12 @@ func parseSearchArgs(args []string, defaultLimit int) (searchOptions, error) {
|
|||||||
first := false
|
first := false
|
||||||
outputFile := ""
|
outputFile := ""
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
|
if args[i] == "--" {
|
||||||
|
if i+1 < len(args) {
|
||||||
|
parts = append(parts, args[i+1:]...)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
switch args[i] {
|
switch args[i] {
|
||||||
case "--force", "--ignore-db":
|
case "--force", "--ignore-db":
|
||||||
ignoreDB = true
|
ignoreDB = true
|
||||||
@@ -2010,6 +2017,9 @@ func parseSearchArgs(args []string, defaultLimit int) (searchOptions, error) {
|
|||||||
return searchOptions{}, fmt.Errorf("--output-file requires a path")
|
return searchOptions{}, fmt.Errorf("--output-file requires a path")
|
||||||
}
|
}
|
||||||
outputFile = strings.TrimSpace(args[i+1])
|
outputFile = strings.TrimSpace(args[i+1])
|
||||||
|
if outputFile == "" {
|
||||||
|
return searchOptions{}, fmt.Errorf("--output-file requires a non-empty path")
|
||||||
|
}
|
||||||
i++
|
i++
|
||||||
continue
|
continue
|
||||||
case "--num-results":
|
case "--num-results":
|
||||||
@@ -2036,6 +2046,9 @@ func parseSearchArgs(args []string, defaultLimit int) (searchOptions, error) {
|
|||||||
i++
|
i++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(args[i], "-") {
|
||||||
|
return searchOptions{}, fmt.Errorf("unknown option %q", args[i])
|
||||||
|
}
|
||||||
parts = append(parts, args[i])
|
parts = append(parts, args[i])
|
||||||
}
|
}
|
||||||
return searchOptions{
|
return searchOptions{
|
||||||
@@ -2196,6 +2209,12 @@ func writeSearchResultsToFile(source, mediaType string, results []searchResult,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if dir != "" && dir != "." {
|
||||||
|
if err = os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return os.WriteFile(path, b, 0o644)
|
return os.WriteFile(path, b, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -198,6 +201,38 @@ func TestParseSearchArgsAllowsFirstAndOutputFileButCallerCanReject(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSearchArgsRejectsUnknownOption(t *testing.T) {
|
||||||
|
_, err := parseSearchArgs([]string{"query", "--bogus"}, 20)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "unknown option") {
|
||||||
|
t.Fatalf("expected unknown option error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSearchArgsSupportsDoubleDashTerminator(t *testing.T) {
|
||||||
|
opts, err := parseSearchArgs([]string{"--limit", "10", "--", "--weird", "track"}, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseSearchArgs() error = %v", err)
|
||||||
|
}
|
||||||
|
if opts.limit != 10 {
|
||||||
|
t.Fatalf("limit = %d, want 10", opts.limit)
|
||||||
|
}
|
||||||
|
if opts.query != "--weird track" {
|
||||||
|
t.Fatalf("query = %q, want %q", opts.query, "--weird track")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteSearchResultsToFileCreatesParentDirectory(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
out := filepath.Join(tmp, "nested", "search", "results.json")
|
||||||
|
results := []searchResult{{ID: "1", Title: "Dreams"}}
|
||||||
|
if err := writeSearchResultsToFile("qobuz", "track", results, out); err != nil {
|
||||||
|
t.Fatalf("writeSearchResultsToFile() error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(out); err != nil {
|
||||||
|
t.Fatalf("expected output file, stat error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestErrorWithActionableHintForSSL(t *testing.T) {
|
func TestErrorWithActionableHintForSSL(t *testing.T) {
|
||||||
err := errors.New("x509: certificate signed by unknown authority")
|
err := errors.New("x509: certificate signed by unknown authority")
|
||||||
msg := errorWithActionableHint(err, globalOptions{})
|
msg := errorWithActionableHint(err, globalOptions{})
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ func (d *Downloader) FileDeezerEncrypted(ctx context.Context, sourceURL, outputP
|
|||||||
if resp.ContentLength > 0 && totalRead != resp.ContentLength {
|
if resp.ContentLength > 0 && totalRead != resp.ContentLength {
|
||||||
return io.ErrUnexpectedEOF
|
return io.ErrUnexpectedEOF
|
||||||
}
|
}
|
||||||
|
if err = out.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
success = true
|
success = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -229,6 +232,9 @@ func (d *Downloader) file(ctx context.Context, sourceURL, outputPath string, all
|
|||||||
if resp.ContentLength > 0 && totalWritten != resp.ContentLength {
|
if resp.ContentLength > 0 && totalWritten != resp.ContentLength {
|
||||||
return io.ErrUnexpectedEOF
|
return io.ErrUnexpectedEOF
|
||||||
}
|
}
|
||||||
|
if err = out.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
written, copyErr := io.Copy(out, reader)
|
written, copyErr := io.Copy(out, reader)
|
||||||
if copyErr != nil {
|
if copyErr != nil {
|
||||||
|
|||||||
@@ -146,9 +146,21 @@ func (c *Client) refreshAppCredentials(ctx context.Context, q *config.QobuzConfi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
q.AppID = strings.TrimSpace(appID)
|
q.AppID = strings.TrimSpace(appID)
|
||||||
q.Secrets = append([]string(nil), secrets...)
|
if q.AppID == "" {
|
||||||
|
return errors.New("qobuz app credential refresh returned empty app_id")
|
||||||
|
}
|
||||||
|
clean := make([]string, 0, len(secrets))
|
||||||
|
for _, s := range secrets {
|
||||||
|
if v := strings.TrimSpace(s); v != "" {
|
||||||
|
clean = append(clean, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(clean) == 0 {
|
||||||
|
return errors.New("qobuz app credential refresh returned no secrets")
|
||||||
|
}
|
||||||
|
q.Secrets = append([]string(nil), clean...)
|
||||||
c.cfg.File.Qobuz.AppID = q.AppID
|
c.cfg.File.Qobuz.AppID = q.AppID
|
||||||
c.cfg.File.Qobuz.Secrets = append([]string(nil), secrets...)
|
c.cfg.File.Qobuz.Secrets = append([]string(nil), clean...)
|
||||||
_ = c.cfg.SaveFile()
|
_ = c.cfg.SaveFile()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -373,3 +373,15 @@ func qobuzSecretSig(requestTS, secret string) string {
|
|||||||
hash := md5.Sum([]byte(raw))
|
hash := md5.Sum([]byte(raw))
|
||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRefreshAppCredentialsRejectsEmptyData(t *testing.T) {
|
||||||
|
d := config.DefaultConfigData()
|
||||||
|
c := New(&config.Config{File: d, Session: d})
|
||||||
|
c.fetchCfg = func(context.Context) (string, []string, error) {
|
||||||
|
return "", []string{" "}, nil
|
||||||
|
}
|
||||||
|
err := c.refreshAppCredentials(context.Background(), &c.cfg.Session.Qobuz)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for empty refreshed app credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user