1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// HTTP resource encoding/decoding for varlink services that support binary data via upgrade
package varlinklib

import (
	"encoding/json"
	"fmt"
	"net/url"
	"strings"
)

// ResourceRequest represents a request for a binary resource from a varlink service
type ResourceRequest struct {
	// Service is the varlink service address (e.g., "unix:/run/user/1000/de.Profpatsch.Maildir")
	Service string `json:"service"`

	// Method is the full varlink method name (e.g., "de.profpatsch.Maildir.InvokeAction")
	Method string `json:"method"`

	// Parameters are the varlink method parameters (including token)
	Parameters map[string]any `json:"parameters"`

	// Path is the HTTP resource path to request after upgrade (e.g., "/parts/0")
	Path string `json:"path"`
}

// EncodeResourceURL encodes a ResourceRequest into a URL with query parameters.
// The resulting URL has the format: /r?request={url-encoded-json}
//
// This encoding is stable because json.Marshal produces deterministic output
// (sorts map keys alphabetically), ensuring the same parameters always produce
// the same URL.
func EncodeResourceURL(req ResourceRequest) (string, error) {
	// Marshal to JSON (deterministic key ordering)
	jsonBytes, err := json.Marshal(req)
	if err != nil {
		return "", fmt.Errorf("failed to marshal resource request: %w", err)
	}

	// Build URL with query parameter
	u := url.URL{
		Path: "/r",
		RawQuery: url.Values{
			"request": []string{string(jsonBytes)},
		}.Encode(),
	}

	return u.String(), nil
}

// DecodeResourceURL decodes a resource URL back into a ResourceRequest.
// Expects URLs in the format: /r?request={url-encoded-json}
func DecodeResourceURL(urlStr string) (*ResourceRequest, error) {
	// Parse URL
	u, err := url.Parse(urlStr)
	if err != nil {
		return nil, fmt.Errorf("failed to parse URL: %w", err)
	}

	// Check path
	if u.Path != "/r" {
		return nil, fmt.Errorf("invalid resource URL: expected /r path, got %s", u.Path)
	}

	// Get query parameter
	requestJSON := u.Query().Get("request")
	if requestJSON == "" {
		return nil, fmt.Errorf("invalid resource URL: missing 'request' parameter")
	}

	// Unmarshal JSON
	var req ResourceRequest
	if err := json.Unmarshal([]byte(requestJSON), &req); err != nil {
		return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
	}

	// Validate required fields
	if req.Service == "" {
		return nil, fmt.Errorf("invalid resource request: missing service")
	}
	if req.Method == "" {
		return nil, fmt.Errorf("invalid resource request: missing method")
	}
	if req.Path == "" {
		return nil, fmt.Errorf("invalid resource request: missing path")
	}

	return &req, nil
}

// ResourceDescriptor describes a binary resource available after HTTP upgrade.
// Services return these in their responses with resource_type: "http_listing"
type ResourceDescriptor struct {
	// Path is the HTTP path for this resource (e.g., "/parts/0")
	Path string `json:"path"`

	// ContentType is the MIME type (e.g., "application/pdf")
	ContentType string `json:"content_type"`

	// Filename is the suggested download filename (optional)
	Filename string `json:"filename,omitempty"`

	// Size is the resource size in bytes
	Size int64 `json:"size"`

	// Description is a human-readable description (optional)
	Description string `json:"description,omitempty"`
}

// MakeAbsoluteURL converts a relative resource path to an absolute URL using
// the provided base URL. This is useful for proxies that want to rewrite URLs.
//
// Example:
//
//	desc.Path = "/parts/0"
//	baseURL = "http://localhost:8080"
//	result = "http://localhost:8080/parts/0"
func (rd *ResourceDescriptor) MakeAbsoluteURL(baseURL string) string {
	baseURL = strings.TrimSuffix(baseURL, "/")
	return baseURL + rd.Path
}