1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "fmt"
7 "strings"
8 "time"
9 "unicode"
10 "unicode/utf8"
11)
12
13// Limits and defaults of the CI artifacts (artifacts v4, CI-4 D3).
14const (
15 // CIArtifactMaxNameLen is the longest artifact name, in characters.
16 CIArtifactMaxNameLen = 256
17 // CIArtifactMaxMediaTypeLen is the longest media type of an artifact.
18 CIArtifactMaxMediaTypeLen = 255
19 // CIArtifactMaxBlockIDLen is the longest block id (Azure's, decoded).
20 CIArtifactMaxBlockIDLen = 64
21 // CIArtifactMaxBlocks bounds the blocks of a commit and the blocks staged
22 // for one artifact.
23 CIArtifactMaxBlocks = 50_000
24 // CIArtifactMaxPendingPerJob bounds the unfinished uploads of a job, and
25 // CIArtifactMaxPerRun the artifacts of a run, unfinished ones included
26 // (the toolkit uploads one artifact at a time).
27 CIArtifactMaxPendingPerJob = 32
28 CIArtifactMaxPerRun = 1000
29 // CIArtifactChunk is the largest piece of data of one message, both ways.
30 CIArtifactChunk = 1 << 20
31 // CIArtifactMinRetention is the shortest retention of an artifact.
32 CIArtifactMinRetention = 24 * time.Hour
33 // CIArtifactPendingMax is how long an upload may stay unfinished before the
34 // sweep deletes it, whatever its job does.
35 CIArtifactPendingMax = 24 * time.Hour
36 // DefaultCIArtifactQuota is the space the artifacts of one repository may
37 // use, uploads in progress included.
38 DefaultCIArtifactQuota = 10 << 30
39 // DefaultCIArtifactMaxSize is the largest artifact.
40 DefaultCIArtifactMaxSize = 5 << 30
41 // CIArtifactZip is the media type of an archive.
42 CIArtifactZip = "application/zip"
43)
44
45// CIArtifactState is where an artifact is in its upload.
46type CIArtifactState string
47
48const (
49 // CIArtifactPending is created, its blocks being uploaded.
50 CIArtifactPending CIArtifactState = "pending"
51 // CIArtifactCommitted has its content; not visible yet.
52 CIArtifactCommitted CIArtifactState = "committed"
53 // CIArtifactFinal is finalized: visible to the run and its owners.
54 CIArtifactFinal CIArtifactState = "final"
55)
56
57// CIArtifact is a file that a job of a run uploaded with
58// actions/upload-artifact. It belongs to the run: every job of the run, in any
59// attempt, sees the finalized ones.
60type CIArtifact struct {
61 // ID is global, monotonic and never reused.
62 ID int64
63 Repo RepoName
64 RunID int64
65 // Attempt is the attempt of the run of the job that uploads it.
66 Attempt int32
67 JobID string
68 // Assignment is the hash of the job token of the assignment that uploads
69 // it: a pending artifact belongs to that assignment only.
70 Assignment TokenHash
71 Name string
72 MediaType string
73 Size int64
74 // Digest is "sha256:<hex>" of the content, known from the commit.
75 Digest string
76 State CIArtifactState
77 CreatedAt time.Time
78 ExpiresAt time.Time
79 // Charged is what the artifact counts against the quota of its
80 // repository: the blocks staged while pending, then the content.
81 Charged int64
82 // ListDigest identifies the block list of the commit.
83 ListDigest []byte
84 // Version is the store version of the record; 0 for a new one.
85 Version uint64
86}
87
88// Final reports whether the artifact is finalized.
89func (a CIArtifact) Final() bool { return a.State == CIArtifactFinal }
90
91// Visible reports whether the artifact is finalized and not expired at now.
92func (a CIArtifact) Visible(now time.Time) bool { return a.Final() && now.Before(a.ExpiresAt) }
93
94// UploadedBy reports whether the job, at its run attempt, uploads or uploaded
95// the artifact. It says nothing of the assignment.
96func (a CIArtifact) UploadedBy(j CIJob) bool {
97 return a.RunID == j.RunID && a.Attempt == j.RunAttempt && a.JobID == j.ID
98}
99
100// PendingOf reports whether the artifact is an unfinished upload of the
101// current assignment of the job.
102func (a CIArtifact) PendingOf(j CIJob) bool {
103 return !a.Final() && a.UploadedBy(j) && a.Assignment == j.TokenHash && j.HasToken()
104}
105
106// ParseArtifactName checks an artifact name: 1 to 256 characters of valid
107// UTF-8, without control characters, '"', ':', '<', '>', '|', '*', '?', '\'
108// or '/' (the characters @actions/artifact refuses). A name is never used as a
109// path: these rules are the toolkit's.
110func ParseArtifactName(name string) error {
111 if name == "" || !utf8.ValidString(name) || utf8.RuneCountInString(name) > CIArtifactMaxNameLen {
112 return fmt.Errorf("%w: an artifact name has 1 to %d characters of valid UTF-8", ErrInvalid, CIArtifactMaxNameLen)
113 }
114 for _, r := range name {
115 if unicode.IsControl(r) || strings.ContainsRune(`":<>|*?\/`, r) {
116 return fmt.Errorf("%w: an artifact name has no control character and none of \" : < > | * ? \\ /", ErrInvalid)
117 }
118 }
119 return nil
120}
121
122// ParseArtifactMediaType checks the media type of an artifact uploaded as is
123// and returns it, CIArtifactZip for an empty one: at most 255 printable ASCII
124// characters.
125func ParseArtifactMediaType(mt string) (string, error) {
126 if mt == "" {
127 return CIArtifactZip, nil
128 }
129 if len(mt) > CIArtifactMaxMediaTypeLen {
130 return "", fmt.Errorf("%w: a media type has at most %d characters", ErrInvalid, CIArtifactMaxMediaTypeLen)
131 }
132 for i := 0; i < len(mt); i++ {
133 if mt[i] < 0x20 || mt[i] > 0x7e {
134 return "", fmt.Errorf("%w: a media type has printable ASCII characters only", ErrInvalid)
135 }
136 }
137 return mt, nil
138}
139
140// ParseArtifactDigest checks a digest given by an uploader: "sha256:" and 64
141// hexadecimal characters, returned in lower case. Empty is no digest.
142func ParseArtifactDigest(d string) (string, error) {
143 if d == "" {
144 return "", nil
145 }
146 hex, ok := strings.CutPrefix(strings.ToLower(d), "sha256:")
147 if !ok || len(hex) != 64 || strings.Trim(hex, "0123456789abcdef") != "" {
148 return "", fmt.Errorf("%w: a digest is \"sha256:\" and 64 hexadecimal characters", ErrInvalid)
149 }
150 return "sha256:" + hex, nil
151}
152
153// CheckArtifactBlockID checks a block id: 1 to 64 bytes.
154func CheckArtifactBlockID(id []byte) error {
155 if len(id) == 0 || len(id) > CIArtifactMaxBlockIDLen {
156 return fmt.Errorf("%w: a block id has 1 to %d bytes", ErrInvalid, CIArtifactMaxBlockIDLen)
157 }
158 return nil
159}
160
161// ArtifactExpiry is the expiry an artifact gets: the one asked for (zero:
162// none) kept between one day from now and the longest retention, now plus the
163// retention without one. A retention below one day counts as one day.
164func ArtifactExpiry(asked, now time.Time, retention time.Duration) time.Time {
165 retention = max(retention, CIArtifactMinRetention)
166 switch {
167 case asked.IsZero():
168 return now.Add(retention)
169 case asked.Before(now.Add(CIArtifactMinRetention)):
170 return now.Add(CIArtifactMinRetention)
171 case asked.After(now.Add(retention)):
172 return now.Add(retention)
173 }
174 return asked
175}