Files
browser/platform/validator/artifact_download.go
T
2026-09-03 11:27:19 +08:00

145 lines
5.9 KiB
Go

package validator
import (
"fmt"
"strings"
"browser.local/platform/domain"
)
const MaxArtifactDownloadBytes = MaxArtifactChunkBytes
func ValidateArtifactDownloadReferenceRequest(request domain.ArtifactDownloadReferenceRequest) error {
var violations []string
violations = appendRequired(violations, "artifactId", request.ArtifactID)
violations = appendArtifactIDViolations(violations, request.ArtifactID)
return finish(violations)
}
func ValidateArtifactDownloadReference(reference domain.ArtifactDownloadReference) error {
var violations []string
violations = appendRequired(violations, "artifactId", reference.ArtifactID)
violations = appendRequired(violations, "ownerId", reference.OwnerID)
violations = appendRequired(violations, "filename", reference.Filename)
violations = appendRequired(violations, "contentType", reference.ContentType)
violations = appendRequired(violations, "checksum", reference.Checksum)
violations = appendRequired(violations, "downloadUrl", reference.DownloadURL)
violations = appendRequired(violations, "storageBehavior", reference.StorageBehavior)
violations = appendArtifactIDViolations(violations, reference.ArtifactID)
if !validArtifactOwnerKind(reference.OwnerKind) {
violations = append(violations, "ownerKind is invalid")
}
if reference.State != domain.ArtifactStateAvailable {
violations = append(violations, "state must be available")
}
if reference.SizeBytes <= 0 {
violations = append(violations, "sizeBytes must be positive")
}
if reference.Checksum != "" && !validSHA256Checksum(reference.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if reference.ChunkSizeBytes <= 0 || reference.ChunkSizeBytes > MaxArtifactDownloadBytes {
violations = append(violations, fmt.Sprintf("chunkSizeBytes must be between 1 and %d", MaxArtifactDownloadBytes))
}
if reference.ExpiresAt.IsZero() {
violations = append(violations, "expiresAt is required")
}
for _, value := range []fieldString{
{field: "filename", value: reference.Filename},
{field: "contentType", value: reference.ContentType},
{field: "downloadUrl", value: reference.DownloadURL},
{field: "storageBehavior", value: reference.StorageBehavior},
} {
if unsafeArtifactString(value.value) {
violations = append(violations, value.field+" contains unsafe content")
}
}
if !strings.HasPrefix(reference.DownloadURL, "/api/v1/artifacts/") || !strings.HasSuffix(reference.DownloadURL, "/content") {
violations = append(violations, "downloadUrl must be a platform artifact content route")
}
return finish(violations)
}
func ValidateArtifactContentRequest(request domain.ArtifactContentRequest) error {
var violations []string
violations = appendRequired(violations, "artifactId", request.ArtifactID)
violations = appendArtifactIDViolations(violations, request.ArtifactID)
if request.Offset < 0 {
violations = append(violations, "offset must not be negative")
}
if request.Limit < 0 {
violations = append(violations, "limit must not be negative")
}
if request.Limit > MaxArtifactDownloadBytes {
violations = append(violations, fmt.Sprintf("limit must not exceed %d", MaxArtifactDownloadBytes))
}
return finish(violations)
}
func ValidateArtifactContent(content domain.ArtifactContent) error {
content = domain.CopyArtifactContent(content)
var violations []string
violations = appendRequired(violations, "artifactId", content.ArtifactID)
violations = appendRequired(violations, "filename", content.Filename)
violations = appendRequired(violations, "contentType", content.ContentType)
violations = appendRequired(violations, "checksum", content.Checksum)
violations = appendRequired(violations, "contentChecksum", content.ContentChecksum)
violations = appendArtifactIDViolations(violations, content.ArtifactID)
if content.Offset < 0 {
violations = append(violations, "offset must not be negative")
}
if content.SizeBytes < 0 {
violations = append(violations, "sizeBytes must not be negative")
}
if content.TotalSizeBytes <= 0 {
violations = append(violations, "totalSizeBytes must be positive")
}
maxContentBytes := int64(MaxArtifactDownloadBytes)
if !content.Partial {
maxContentBytes = MaxArtifactBytes
}
if content.SizeBytes > maxContentBytes {
violations = append(violations, fmt.Sprintf("sizeBytes must not exceed %d", maxContentBytes))
}
if int64(len(content.Payload)) != content.SizeBytes {
violations = append(violations, "payload size must match sizeBytes")
}
if content.Offset+content.SizeBytes > content.TotalSizeBytes {
violations = append(violations, "range exceeds artifact size")
}
if content.Checksum != "" && !validSHA256Checksum(content.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if content.ContentChecksum != "" && content.ContentChecksum != BytesChecksum(content.Payload) {
violations = append(violations, "contentChecksum does not match payload")
}
for _, value := range []fieldString{
{field: "filename", value: content.Filename},
{field: "contentType", value: content.ContentType},
{field: "storageBehavior", value: content.StorageBehavior},
} {
if unsafeArtifactString(value.value) {
violations = append(violations, value.field+" contains unsafe content")
}
}
return finish(violations)
}
func appendArtifactIDViolations(violations []string, artifactID string) []string {
trimmed := strings.TrimSpace(artifactID)
if trimmed == "" {
return violations
}
if trimmed != artifactID || len([]rune(trimmed)) > 120 || strings.Contains(trimmed, "/") || strings.Contains(trimmed, `\`) || strings.Contains(trimmed, "://") || strings.Contains(trimmed, "..") {
violations = append(violations, "artifactId is invalid")
}
if unsafeArtifactString(trimmed) {
violations = append(violations, "artifactId contains unsafe content")
}
return violations
}
func unsafeArtifactString(value string) bool {
return containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "file://")
}