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:") } 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") } if content.SizeBytes > MaxArtifactDownloadBytes { violations = append(violations, fmt.Sprintf("sizeBytes must not exceed %d", MaxArtifactDownloadBytes)) } 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:") } 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://") }