@@ -2,6 +2,7 @@ import type { Stats } from "node:fs";
22import fsSync from "node:fs" ;
33import fs from "node:fs/promises" ;
44import path from "node:path" ;
5+ import { sameFileIdentity } from "./file-identity.js" ;
56import { isNotFoundPathError } from "./path.js" ;
67import { assertNoSymlinkParents , assertNoSymlinkParentsSync } from "./symlink-parents.js" ;
78
@@ -34,6 +35,15 @@ export function resolveRegularFileAppendFlags(
3435 ) ;
3536}
3637
38+ function resolveRegularFileReadFlags ( ) : number {
39+ return (
40+ fsSync . constants . O_RDONLY |
41+ ( typeof fsSync . constants . O_NOFOLLOW === "number" && process . platform !== "win32"
42+ ? fsSync . constants . O_NOFOLLOW
43+ : 0 )
44+ ) ;
45+ }
46+
3747export async function statRegularFile ( filePath : string ) : Promise < RegularFileStatResult > {
3848 let stat : Stats ;
3949 try {
@@ -77,11 +87,67 @@ export async function readRegularFile(params: {
7787 if ( params . maxBytes !== undefined && result . stat . size > params . maxBytes ) {
7888 throw new Error ( `File exceeds ${ params . maxBytes } bytes: ${ params . filePath } ` ) ;
7989 }
80- const buffer = await fs . readFile ( params . filePath ) ;
90+
91+ const handle = await fs . open ( params . filePath , resolveRegularFileReadFlags ( ) ) ;
92+ try {
93+ const stat = await handle . stat ( ) ;
94+ verifyStableReadTarget ( {
95+ filePath : params . filePath ,
96+ pathStat : await fs . lstat ( params . filePath ) ,
97+ postOpenStat : stat ,
98+ preOpenStat : result . stat ,
99+ } ) ;
100+ if ( params . maxBytes !== undefined && stat . size > params . maxBytes ) {
101+ throw new Error ( `File exceeds ${ params . maxBytes } bytes: ${ params . filePath } ` ) ;
102+ }
103+ const buffer = await handle . readFile ( ) ;
104+ if ( params . maxBytes !== undefined && buffer . byteLength > params . maxBytes ) {
105+ throw new Error ( `File exceeds ${ params . maxBytes } bytes: ${ params . filePath } ` ) ;
106+ }
107+ return { buffer, stat } ;
108+ } finally {
109+ await handle . close ( ) ;
110+ }
111+ }
112+
113+ function verifyStableReadTarget ( params : {
114+ preOpenStat : Stats ;
115+ postOpenStat : Stats ;
116+ pathStat : Stats ;
117+ filePath : string ;
118+ } ) : void {
119+ if ( ! params . postOpenStat . isFile ( ) || params . pathStat . isSymbolicLink ( ) || ! params . pathStat . isFile ( ) ) {
120+ throw new Error ( `File is not a regular file: ${ params . filePath } ` ) ;
121+ }
122+ if (
123+ ! sameFileIdentity ( params . preOpenStat , params . postOpenStat ) ||
124+ ! sameFileIdentity ( params . pathStat , params . postOpenStat )
125+ ) {
126+ throw new Error ( `File changed during read: ${ params . filePath } ` ) ;
127+ }
128+ }
129+
130+ function readOpenedRegularFileSync ( params : {
131+ fd : number ;
132+ filePath : string ;
133+ preOpenStat : Stats ;
134+ maxBytes ?: number ;
135+ } ) : { buffer : Buffer ; stat : Stats } {
136+ const stat = fsSync . fstatSync ( params . fd ) ;
137+ verifyStableReadTarget ( {
138+ filePath : params . filePath ,
139+ pathStat : fsSync . lstatSync ( params . filePath ) ,
140+ postOpenStat : stat ,
141+ preOpenStat : params . preOpenStat ,
142+ } ) ;
143+ if ( params . maxBytes !== undefined && stat . size > params . maxBytes ) {
144+ throw new Error ( `File exceeds ${ params . maxBytes } bytes: ${ params . filePath } ` ) ;
145+ }
146+ const buffer = fsSync . readFileSync ( params . fd ) ;
81147 if ( params . maxBytes !== undefined && buffer . byteLength > params . maxBytes ) {
82148 throw new Error ( `File exceeds ${ params . maxBytes } bytes: ${ params . filePath } ` ) ;
83149 }
84- return { buffer, stat : result . stat } ;
150+ return { buffer, stat } ;
85151}
86152
87153export function readRegularFileSync ( params : { filePath : string ; maxBytes ?: number } ) : {
@@ -95,11 +161,18 @@ export function readRegularFileSync(params: { filePath: string; maxBytes?: numbe
95161 if ( params . maxBytes !== undefined && result . stat . size > params . maxBytes ) {
96162 throw new Error ( `File exceeds ${ params . maxBytes } bytes: ${ params . filePath } ` ) ;
97163 }
98- const buffer = fsSync . readFileSync ( params . filePath ) ;
99- if ( params . maxBytes !== undefined && buffer . byteLength > params . maxBytes ) {
100- throw new Error ( `File exceeds ${ params . maxBytes } bytes: ${ params . filePath } ` ) ;
164+
165+ const fd = fsSync . openSync ( params . filePath , resolveRegularFileReadFlags ( ) ) ;
166+ try {
167+ return readOpenedRegularFileSync ( {
168+ fd,
169+ filePath : params . filePath ,
170+ preOpenStat : result . stat ,
171+ maxBytes : params . maxBytes ,
172+ } ) ;
173+ } finally {
174+ fsSync . closeSync ( fd ) ;
101175 }
102- return { buffer, stat : result . stat } ;
103176}
104177
105178function verifyStableAppendTarget ( params : {
0 commit comments