@push.rocks/smartlinuxrepo 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ts/arch/reader.ts CHANGED
@@ -53,7 +53,7 @@ export async function readArchPackage(
53
53
  const walked = await withDecompressingReader(
54
54
  source,
55
55
  container.decoder,
56
- { maxOutputBytes: limits.maxPayloadBytes, outputLimit: 'maxPayloadBytes' },
56
+ { maxOutputBytes: limits.maxPayloadBytes, outputLimit: 'maxPayloadBytes', maxZstdFrames: limits.maxEntries },
57
57
  async (payloadArg) => {
58
58
  const result = await walkPayload(
59
59
  new TarReader(payloadArg, { maxEntries: limits.maxEntries, maxHeaderBytes: limits.maxHeaderBytes }),
@@ -1,7 +1,9 @@
1
+ import { isIndexableArchitecture } from '../shared/architecture.js';
1
2
  import { LinuxRepoError, limitExceeded } from '../shared/errors.js';
2
3
  import { isValidUrl } from '../shared/url.js';
3
4
  import type { IDebianPackageMetadata } from '../shared/types.js';
4
5
  import { fromByteString, goTrimSpace, toByteString } from './gostring.js';
6
+ import { hasPoolFileNameRefusedCharacter } from './rules.js';
5
7
  import { parseMailAddress } from './mailaddress.js';
6
8
 
7
9
  /**
@@ -29,7 +31,13 @@ export interface IDebianControl {
29
31
  * verbatim, and the Depends text split on `,` (`['']` when there is none).
30
32
  *
31
33
  * Gitea's scanner refuses lines over 64 KiB; the codec accepts lines up to
32
- * `maxLineBytes` (`maxHeaderBytes`), which is at least that.
34
+ * `maxLineBytes` (`maxHeaderBytes`), which is at least that. A version that
35
+ * matches Gitea's pattern but holds `/` or `?` (both inside the revision's
36
+ * range) is refused with `invalid-field` (`Version`): no pool file name can
37
+ * hold it (`hasPoolFileNameRefusedCharacter`), where Gitea accepts the upload
38
+ * and then cannot serve it. An Architecture that fails `isIndexableArchitecture`
39
+ * or `hasPoolFileNameRefusedCharacter` (decoded) is refused with
40
+ * `invalid-field` (`Architecture`), where Gitea accepts any non-empty value.
33
41
  */
34
42
  export function parseDebianControlFile(bytesArg: Uint8Array, maxLineBytesArg: number): IDebianControl {
35
43
  const text = toByteString(bytesArg);
@@ -121,7 +129,10 @@ export function parseDebianControlFile(bytesArg: Uint8Array, maxLineBytesArg: nu
121
129
  if (version === '') {
122
130
  throw new LinuxRepoError('missing-field', 'the control file has no Version field', { field: 'Version' });
123
131
  }
124
- if (!VERSION_PATTERN.test(version)) {
132
+ // a departure from Gitea's version pattern: the revision range admits
133
+ // `/` and `?`, which the pool file name cannot hold, so the version is
134
+ // refused here rather than by buildDebianIndex
135
+ if (!VERSION_PATTERN.test(version) || hasPoolFileNameRefusedCharacter(version)) {
125
136
  throw new LinuxRepoError('invalid-field', 'the package version is invalid', {
126
137
  field: 'Version',
127
138
  value: fromByteString(version),
@@ -132,11 +143,22 @@ export function parseDebianControlFile(bytesArg: Uint8Array, maxLineBytesArg: nu
132
143
  field: 'Architecture',
133
144
  });
134
145
  }
146
+ // a departure from Gitea, which accepts any non-empty Architecture: the
147
+ // file name embeds it, so it must pass the builder's rules; checked on the
148
+ // decoded text the record carries (on the byte string, `\s` and `\p{Cc}`
149
+ // would match the UTF-8 continuation bytes 0x85 and 0xA0)
150
+ const decodedArchitecture = fromByteString(architecture);
151
+ if (!isIndexableArchitecture(decodedArchitecture) || hasPoolFileNameRefusedCharacter(decodedArchitecture)) {
152
+ throw new LinuxRepoError('invalid-field', 'the package architecture cannot name an index path or pool file', {
153
+ field: 'Architecture',
154
+ value: decodedArchitecture,
155
+ });
156
+ }
135
157
 
136
158
  return {
137
159
  name,
138
160
  version,
139
- architecture: fromByteString(architecture),
161
+ architecture: decodedArchitecture,
140
162
  metadata: {
141
163
  control: fromByteString(goTrimSpace(control)),
142
164
  maintainer: fromByteString(maintainer),
@@ -2,11 +2,11 @@ import * as plugins from '../plugins.js';
2
2
  import { isIndexableArchitecture } from '../shared/architecture.js';
3
3
  import { LinuxRepoError } from '../shared/errors.js';
4
4
  import type { IDebianPackageRecord, ILinuxIndexFile } from '../shared/types.js';
5
- import { assertSegmentName, assertString, assertTimestamp } from '../shared/validation.js';
5
+ import { assertString, assertTimestamp } from '../shared/validation.js';
6
6
  import type { IOpenPgpSigner } from '../signing/signers.js';
7
7
  import { createCleartextSignedMessage, createDetachedArmoredSignature } from '../signing/openpgpframing.js';
8
8
  import { compareUtf8, goTrimSpace, utf8ByteString } from './gostring.js';
9
- import { DEBIAN_SYMBOL_PATTERN, isDebianReleaseValue, isDebianSymbol } from './rules.js';
9
+ import { DEBIAN_SYMBOL_PATTERN, hasPoolFileNameRefusedCharacter, isDebianReleaseValue, isDebianSymbol } from './rules.js';
10
10
 
11
11
  /** The fields the `Packages` stanza writes itself; a control file's own copies are dropped. */
12
12
  const INDEX_OWNED_FIELDS = new Set(['filename', 'size', 'md5sum', 'sha1', 'sha256', 'sha512']);
@@ -234,7 +234,8 @@ export function renderDebianPackagesStanza(input: {
234
234
 
235
235
  /**
236
236
  * Refuses a record the index cannot list: wrong format, an architecture that
237
- * is not a path segment, a file name that is not one, digests that are not
237
+ * is not a path segment, a file name apt cannot fetch as one pool path
238
+ * segment (`hasPoolFileNameRefusedCharacter`), digests that are not
238
239
  * hex of the right length, or a control text that is not one stanza (empty,
239
240
  * or holding a blank line that would start another stanza).
240
241
  */
@@ -249,12 +250,13 @@ function assertRecord(recordArg: IDebianPackageRecord): void {
249
250
  { field: 'record.architecture', value: String(recordArg.fileName) },
250
251
  );
251
252
  }
252
- assertSegmentName(recordArg.fileName, 'record.fileName');
253
- if (/\s/.test(recordArg.fileName)) {
254
- throw new LinuxRepoError('invalid-input', 'record.fileName must not contain whitespace', {
255
- field: 'record.fileName',
256
- value: recordArg.fileName,
257
- });
253
+ assertString(recordArg.fileName, 'record.fileName');
254
+ if (recordArg.fileName === '' || hasPoolFileNameRefusedCharacter(recordArg.fileName)) {
255
+ throw new LinuxRepoError(
256
+ 'invalid-input',
257
+ 'record.fileName must be non-empty and contain no "/", "#", "?", whitespace or control characters',
258
+ { field: 'record.fileName', value: recordArg.fileName },
259
+ );
258
260
  }
259
261
  const digests = recordArg.digests;
260
262
  if (digests === null || typeof digests !== 'object' || !Number.isSafeInteger(digests.size) || digests.size < 0) {
@@ -89,7 +89,11 @@ async function readControl(inputArg: PackageByteSource, limitsArg: Readonly<ILin
89
89
  const control = await withDecompressingReader(
90
90
  member.body,
91
91
  compression,
92
- { maxOutputBytes: limitsArg.maxDecompressedMetadataBytes, outputLimit: 'maxDecompressedMetadataBytes' },
92
+ {
93
+ maxOutputBytes: limitsArg.maxDecompressedMetadataBytes,
94
+ outputLimit: 'maxDecompressedMetadataBytes',
95
+ maxZstdFrames: limitsArg.maxEntries,
96
+ },
93
97
  async (decompressed) => {
94
98
  const tar = new TarReader(decompressed, {
95
99
  maxEntries: limitsArg.maxEntries,
@@ -21,3 +21,24 @@ export function isDebianSymbol(valueArg: string): boolean {
21
21
  export function isDebianReleaseValue(valueArg: string): boolean {
22
22
  return typeof valueArg === 'string' && valueArg !== '' && !hasControlCharacter(valueArg) && valueArg === valueArg.trim();
23
23
  }
24
+
25
+ /**
26
+ * The characters a pool file name (`<name>_<version>_<architecture>.deb`,
27
+ * written as a `Packages` `Filename`) must not hold: `/`, which would make it
28
+ * more than one path segment; whitespace and control characters, which split
29
+ * or end the field; and `#` and `?`, which apt sends unescaped in the request
30
+ * path (apt 2.6.1 `pkgAcquire::URIEncode` escapes only `%`, `+`, `~`, space,
31
+ * control and non-ASCII bytes), so a server reads the rest as a fragment or a
32
+ * query and apt cannot fetch the file.
33
+ */
34
+ const POOL_FILE_NAME_REFUSED_PATTERN = /[/#?\s\p{Cc}]/u;
35
+
36
+ /**
37
+ * Whether a text holds a character a pool file name must not hold. The one
38
+ * rule both `buildDebianIndex` (on `record.fileName`) and `readDebianPackage`
39
+ * (on the version and the decoded architecture) apply, so the reader never
40
+ * returns a record the builder refuses for its file name.
41
+ */
42
+ export function hasPoolFileNameRefusedCharacter(textArg: string): boolean {
43
+ return POOL_FILE_NAME_REFUSED_PATTERN.test(textArg);
44
+ }