@@ -2,10 +2,17 @@ import { spawnSync } from "node:child_process";
22import { createHash } from "node:crypto" ;
33import { readFileSync } from "node:fs" ;
44import { fileURLToPath } from "node:url" ;
5- import { brotliDecompressSync , gunzipSync } from "node:zlib" ;
5+ import { gunzipSync } from "node:zlib" ;
6+ import {
7+ assertPublicPackageContents ,
8+ MAX_EXPANDED_ASSET_BYTES ,
9+ } from "./package-public-content.mjs" ;
610import { assertExpectedGitHead } from "./package-provenance.mjs" ;
711import { packageSmokeTimeouts } from "./package-smoke-timeouts.mjs" ;
8- import { regularTarListingLines } from "./package-tar-listing.mjs" ;
12+ import {
13+ readTarContents ,
14+ regularTarListingLines ,
15+ } from "./package-tar-listing.mjs" ;
916
1017const PACKAGE_SMOKE_PROCESS_TIMEOUT_MS =
1118 packageSmokeTimeouts ( ) . processTimeoutMs ;
@@ -25,7 +32,6 @@ if (archive === undefined || args.length > 2) {
2532 ) ;
2633}
2734
28- const MAX_EXPANDED_ASSET_BYTES = 32 * 1024 * 1024 ;
2935const archiveBytes = gunzipSync ( readFileSync ( archive ) , {
3036 maxOutputLength : MAX_EXPANDED_ASSET_BYTES ,
3137} ) ;
@@ -47,54 +53,6 @@ function tar(args, encoding = "buffer") {
4753 return result . stdout ;
4854}
4955
50- let offset = 0 ;
51- const archiveFiles = new Map ( ) ;
52- for ( ; offset + 512 <= archiveBytes . byteLength ; ) {
53- const header = archiveBytes . subarray ( offset , offset + 512 ) ;
54- if ( header . every ( ( byte ) => byte === 0 ) ) {
55- offset += 512 ;
56- continue ;
57- }
58- const name = header . subarray ( 0 , 100 ) . toString ( "utf8" ) . split ( "\0" , 1 ) [ 0 ] ;
59- const prefix = header . subarray ( 345 , 500 ) . toString ( "utf8" ) . split ( "\0" , 1 ) [ 0 ] ;
60- const path = prefix === "" ? name : `${ prefix } /${ name } ` ;
61- const sizeField = header
62- . subarray ( 124 , 136 )
63- . toString ( "ascii" )
64- . split ( "\0" , 1 ) [ 0 ]
65- . trim ( ) ;
66- if ( ! / ^ [ 0 - 7 ] * $ / u. test ( sizeField ) ) {
67- throw new Error ( "npm tarball contains an invalid tar entry." ) ;
68- }
69- if ( path . endsWith ( "/" ) && header [ 156 ] !== 0x35 ) {
70- throw new Error ( "npm tarball contains an invalid tar entry." ) ;
71- }
72- const size = Number . parseInt ( sizeField || "0" , 8 ) ;
73- const contentsStart = offset + 512 ;
74- const nextOffset = contentsStart + Math . ceil ( size / 512 ) * 512 ;
75- if ( nextOffset > archiveBytes . byteLength ) {
76- throw new Error ( "npm tarball contains an invalid tar entry." ) ;
77- }
78- if ( header [ 156 ] === 0 || header [ 156 ] === 0x30 ) {
79- archiveFiles . set (
80- path ,
81- archiveBytes . subarray ( contentsStart , contentsStart + size ) ,
82- ) ;
83- }
84- offset = nextOffset ;
85- }
86- if ( archiveBytes . subarray ( offset ) . some ( ( byte ) => byte !== 0 ) ) {
87- throw new Error ( "npm tarball contains trailing tar data." ) ;
88- }
89-
90- function archiveFile ( path ) {
91- const contents = archiveFiles . get ( path ) ;
92- if ( contents === undefined ) {
93- throw new Error ( "npm tarball contains an invalid tar entry: " + path + "." ) ;
94- }
95- return contents ;
96- }
97-
9856const entries = tar ( [ "-tzf" , archive ] , "utf8" ) . split ( / \r ? \n / u) . filter ( Boolean ) ;
9957const files = new Set ( entries ) ;
10058if ( files . size !== entries . length ) {
@@ -303,13 +261,17 @@ for (const file of files) {
303261
304262const listing = tar ( [ "-tvzf" , archive ] , "utf8" ) ;
305263const listingLines = regularTarListingLines ( listing ) ;
306- if (
307- listingLines . length !== entries . length ||
308- listingLines . some (
309- ( line , index ) => line . startsWith ( "d" ) !== entries [ index ] . endsWith ( "/" ) ,
310- )
311- ) {
312- throw new Error ( "npm tarball contains an invalid tar entry." ) ;
264+ const { files : archiveFiles , metadata : archiveMetadata } = readTarContents (
265+ archiveBytes ,
266+ entries ,
267+ listingLines ,
268+ ) ;
269+ function archiveFile ( path ) {
270+ const contents = archiveFiles . get ( path ) ;
271+ if ( contents === undefined ) {
272+ throw new Error ( "npm tarball contains an invalid tar entry: " + path + "." ) ;
273+ }
274+ return contents ;
313275}
314276for ( const [ path , name ] of [
315277 [ "package/bin/codex-security.mjs" , "CLI" ] ,
@@ -335,40 +297,6 @@ assertExpectedGitHead(
335297 process . env . CODEX_SECURITY_EXPECTED_GIT_HEAD ,
336298) ;
337299
338- const internalMarker =
339- / (?: i n t e r n a l \. a p i \. o p e n a i \. o r g | g a t e w a y \. [ a - z 0 - 9 . - ] * i n t e r n a l | \. o p e n a i \. o r g | o p e n a i \. f i r e w a l l \. s o c k e t \. d e v | s o c k e t \x2d f i r e w a l l \x2d r e g i s t r y | o p e n a i \. (?: e n t e r p r i s e \. ) ? s l a c k \. c o m | a p p \. s l a c k \. c o m \/ c l i e n t | (?: a p p \. n o t i o n \. c o m \/ p | n o t i o n \. s o ) \/ o p e n a i | l i n e a r \. a p p \/ o p e n a i | (?: g i t h u b \. c o m [: / ] | a p i \. g i t h u b \. c o m \/ r e p o s \/ | r a w \. g i t h u b u s e r c o n t e n t \. c o m \/ ) o p e n a i \/ o p e n a i (?: \. g i t ) ? (?: [ ^ a - z 0 - 9 _ - ] | $ ) | L i c e n s e R e f \x2d P r o p r i e t a r y | \/ U s e r s \/ | \/ h o m e \/ d e v - u s e r | f l o w \. a p p s \. o p e n a i \. o r g | (?: ^ | [ ^ a - z 0 - 9 _ - ] ) g o \/ [ a - z 0 - 9 _ - ] + ) / iu;
340-
341- const payloads = [ archiveBytes . toString ( "utf8" ) ] ;
342- const compressedFiles = [ ...files ] . filter ( ( file ) => / \. b r $ / iu. test ( file ) ) ;
343- const compressedParts = new Map ( ) ;
344- for ( const file of files ) {
345- const match = / ^ ( .* \. b r ) \. p a r t - ( [ 0 - 9 ] + ) $ / iu. exec ( file ) ;
346- if ( match === null ) continue ;
347- const [ , name , part ] = match ;
348- const parts = compressedParts . get ( name ) ?? [ ] ;
349- parts . push ( { file, part : Number ( part ) } ) ;
350- compressedParts . set ( name , parts ) ;
351- }
352-
353- function brotliPayload ( bytes , file ) {
354- const result = brotliDecompressSync ( bytes , {
355- info : true ,
356- maxOutputLength : MAX_EXPANDED_ASSET_BYTES ,
357- } ) ;
358- if ( result . engine . bytesWritten !== bytes . length ) {
359- throw new Error ( `npm tarball contains trailing Brotli data: ${ file } .` ) ;
360- }
361- return result . buffer ;
362- }
363-
364- for ( const file of compressedFiles ) {
365- payloads . push ( brotliPayload ( archiveFile ( file ) , file ) . toString ( "utf8" ) ) ;
366- }
367- for ( const parts of compressedParts . values ( ) ) {
368- parts . sort ( ( left , right ) => left . part - right . part ) ;
369- const bytes = Buffer . concat ( parts . map ( ( { file } ) => archiveFile ( file ) ) ) ;
370- payloads . push ( brotliPayload ( bytes , parts [ 0 ] . file ) . toString ( "utf8" ) ) ;
371- }
372300for ( const file of files ) {
373301 if ( / \. p n g $ / iu. test ( file ) ) {
374302 const digest = createHash ( "sha256" ) . update ( archiveFile ( file ) ) . digest ( "hex" ) ;
@@ -378,11 +306,7 @@ for (const file of files) {
378306 }
379307}
380308
381- for ( const contents of payloads ) {
382- if ( internalMarker . test ( contents ) ) {
383- throw new Error ( "npm tarball contains an internal reference." ) ;
384- }
385- }
309+ assertPublicPackageContents ( archiveFiles , archiveMetadata ) ;
386310
387311if ( args . length === 1 ) {
388312 const smoke = spawnSync (
0 commit comments