@@ -121,103 +121,68 @@ describe('RENAME operation', () => {
121121 await stop ( ) ;
122122 } ) ;
123123
124- describe ( 'AppleDouble file handling ' , ( ) => {
125- test ( 'renames AppleDouble file when renaming main file ' , async ( ) => {
124+ describe ( 'change_info semantics ' , ( ) => {
125+ test ( 'returns before < after on successful rename ' , async ( ) => {
126126 const { client, stop, vol} = await setupNfsClientServerTestbed ( ) ;
127- vol . writeFileSync ( '/export/test.txt' , 'data' ) ;
128- vol . writeFileSync ( '/export/._test.txt' , 'xattr-data' ) ;
129- const res = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'test.txt' , 'new.txt' ) ] ) ;
127+ vol . writeFileSync ( '/export/old.txt' , 'data' ) ;
128+ const res = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'old.txt' , 'new.txt' ) ] ) ;
130129 expect ( res . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
131- expect ( vol . existsSync ( '/export/new.txt' ) ) . toBe ( true ) ;
132- expect ( vol . existsSync ( '/export/._new.txt' ) ) . toBe ( true ) ;
133- expect ( vol . existsSync ( '/export/test.txt' ) ) . toBe ( false ) ;
134- expect ( vol . existsSync ( '/export/._test.txt' ) ) . toBe ( false ) ;
135- await stop ( ) ;
136- } ) ;
137-
138- test ( 'succeeds even if AppleDouble file does not exist' , async ( ) => {
139- const { client, stop, vol} = await setupNfsClientServerTestbed ( ) ;
140- vol . writeFileSync ( '/export/test.txt' , 'data' ) ;
141- const res = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'test.txt' , 'new.txt' ) ] ) ;
142- expect ( res . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
143- expect ( vol . existsSync ( '/export/new.txt' ) ) . toBe ( true ) ;
144- expect ( vol . existsSync ( '/export/._new.txt' ) ) . toBe ( false ) ;
145- await stop ( ) ;
146- } ) ;
147-
148- test ( 'renames AppleDouble file for TextEdit-style save workflow' , async ( ) => {
149- const { client, stop, vol} = await setupNfsClientServerTestbed ( ) ;
150- vol . writeFileSync ( '/export/file.txt' , 'original content' ) ;
151- vol . writeFileSync ( '/export/file.txt.sb-temp-u9VvVu' , 'new content' ) ;
152- vol . writeFileSync ( '/export/._file.txt.sb-temp-u9VvVu' , 'new xattr' ) ;
153- const res = await client . compound ( [
154- nfs . PUTROOTFH ( ) ,
155- nfs . SAVEFH ( ) ,
156- nfs . RENAME ( 'file.txt.sb-temp-u9VvVu' , 'file.txt' ) ,
157- ] ) ;
158- expect ( res . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
159- expect ( vol . existsSync ( '/export/file.txt' ) ) . toBe ( true ) ;
160- expect ( vol . existsSync ( '/export/._file.txt' ) ) . toBe ( true ) ;
161- expect ( vol . existsSync ( '/export/file.txt.sb-temp-u9VvVu' ) ) . toBe ( false ) ;
162- expect ( vol . existsSync ( '/export/._file.txt.sb-temp-u9VvVu' ) ) . toBe ( false ) ;
163- expect ( vol . readFileSync ( '/export/file.txt' , 'utf8' ) ) . toBe ( 'new content' ) ;
164- expect ( vol . readFileSync ( '/export/._file.txt' , 'utf8' ) ) . toBe ( 'new xattr' ) ;
165- await stop ( ) ;
166- } ) ;
167-
168- test ( 'handles AppleDouble file with long filenames (ID-type FH)' , async ( ) => {
169- const { client, stop, vol} = await setupNfsClientServerTestbed ( ) ;
170- const oldName = 'long_filename_' + 'x' . repeat ( 100 ) + '.txt' ;
171- const newName = 'long_filename_' + 'y' . repeat ( 100 ) + '.txt' ;
172- vol . writeFileSync ( '/export/' + oldName , 'data' ) ;
173- vol . writeFileSync ( '/export/._' + oldName , 'xattr' ) ;
174- const res = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( oldName , newName ) ] ) ;
175- expect ( res . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
176- expect ( vol . existsSync ( '/export/' + newName ) ) . toBe ( true ) ;
177- expect ( vol . existsSync ( '/export/._' + newName ) ) . toBe ( true ) ;
178- expect ( vol . existsSync ( '/export/' + oldName ) ) . toBe ( false ) ;
179- expect ( vol . existsSync ( '/export/._' + oldName ) ) . toBe ( false ) ;
180- await stop ( ) ;
181- } ) ;
182-
183- test ( 'AppleDouble file handle remains valid after rename' , async ( ) => {
184- const { client, stop, vol} = await setupNfsClientServerTestbed ( ) ;
185- const oldName = 'original_' + 'a' . repeat ( 100 ) + '.txt' ;
186- const newName = 'renamed_' + 'b' . repeat ( 100 ) + '.txt' ;
187- vol . writeFileSync ( '/export/' + oldName , 'data' ) ;
188- vol . writeFileSync ( '/export/._' + oldName , 'xattr' ) ;
189- const lookupRes = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . LOOKUP ( '._' + oldName ) , nfs . GETFH ( ) ] ) ;
190- expect ( lookupRes . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
191- const appleDoubleFh = ( lookupRes . resarray [ 2 ] as msg . Nfsv4GetfhResponse ) . resok ! . object ;
192- const renameRes = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( oldName , newName ) ] ) ;
130+ const renameRes = res . resarray [ 2 ] as msg . Nfsv4RenameResponse ;
193131 expect ( renameRes . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
194- const getAttrRes = await client . compound ( [ nfs . PUTFH ( appleDoubleFh ) , nfs . GETATTR ( [ 0x00000020 ] ) ] ) ;
195- expect ( getAttrRes . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
132+ if ( renameRes . status === Nfsv4Stat . NFS4_OK && renameRes . resok ) {
133+ const sourceCinfo = renameRes . resok . sourceCinfo ;
134+ const targetCinfo = renameRes . resok . targetCinfo ;
135+ expect ( sourceCinfo . atomic ) . toBe ( true ) ;
136+ expect ( targetCinfo . atomic ) . toBe ( true ) ;
137+ expect ( sourceCinfo . after ) . toBeGreaterThan ( sourceCinfo . before ) ;
138+ expect ( targetCinfo . after ) . toBeGreaterThan ( targetCinfo . before ) ;
139+ expect ( sourceCinfo . after - sourceCinfo . before ) . toBe ( 1n ) ;
140+ }
196141 await stop ( ) ;
197142 } ) ;
198143
199- test ( 'does not rename AppleDouble if it is a directory ' , async ( ) => {
144+ test ( 'change counter increments across multiple renames ' , async ( ) => {
200145 const { client, stop, vol} = await setupNfsClientServerTestbed ( ) ;
201- vol . writeFileSync ( '/export/test.txt' , 'data' ) ;
202- vol . mkdirSync ( '/export/._test.txt' ) ;
203- const res = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'test.txt' , 'new.txt' ) ] ) ;
204- expect ( res . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
205- expect ( vol . existsSync ( '/export/new.txt' ) ) . toBe ( true ) ;
206- expect ( vol . existsSync ( '/export/._test.txt' ) ) . toBe ( true ) ;
207- expect ( vol . statSync ( '/export/._test.txt' ) . isDirectory ( ) ) . toBe ( true ) ;
146+ vol . writeFileSync ( '/export/file1.txt' , 'data1' ) ;
147+ vol . writeFileSync ( '/export/file2.txt' , 'data2' ) ;
148+ const res1 = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'file1.txt' , 'renamed1.txt' ) ] ) ;
149+ expect ( res1 . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
150+ const renameRes1 = res1 . resarray [ 2 ] as msg . Nfsv4RenameResponse ;
151+ const res2 = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'file2.txt' , 'renamed2.txt' ) ] ) ;
152+ expect ( res2 . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
153+ const renameRes2 = res2 . resarray [ 2 ] as msg . Nfsv4RenameResponse ;
154+ if (
155+ renameRes1 . status === Nfsv4Stat . NFS4_OK &&
156+ renameRes1 . resok &&
157+ renameRes2 . status === Nfsv4Stat . NFS4_OK &&
158+ renameRes2 . resok
159+ ) {
160+ expect ( renameRes2 . resok . sourceCinfo . after ) . toBeGreaterThan ( renameRes1 . resok . sourceCinfo . after ) ;
161+ expect ( renameRes2 . resok . sourceCinfo . before ) . toBe ( renameRes1 . resok . sourceCinfo . after ) ;
162+ }
208163 await stop ( ) ;
209164 } ) ;
210165
211- test ( 'handles case where main file starts with ._ ' , async ( ) => {
166+ test ( 'failed rename does not increment change counter ' , async ( ) => {
212167 const { client, stop, vol} = await setupNfsClientServerTestbed ( ) ;
213- vol . writeFileSync ( '/export/._special.txt' , 'data' ) ;
214- vol . writeFileSync ( '/export/._._special.txt' , 'xattr' ) ;
215- const res = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( '._special.txt' , '._renamed.txt' ) ] ) ;
216- expect ( res . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
217- expect ( vol . existsSync ( '/export/._renamed.txt' ) ) . toBe ( true ) ;
218- expect ( vol . existsSync ( '/export/._._renamed.txt' ) ) . toBe ( true ) ;
219- expect ( vol . existsSync ( '/export/._special.txt' ) ) . toBe ( false ) ;
220- expect ( vol . existsSync ( '/export/._._special.txt' ) ) . toBe ( false ) ;
168+ vol . writeFileSync ( '/export/existing.txt' , 'data' ) ;
169+ const res1 = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'existing.txt' , 'renamed.txt' ) ] ) ;
170+ expect ( res1 . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
171+ const renameRes1 = res1 . resarray [ 2 ] as msg . Nfsv4RenameResponse ;
172+ const res2 = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'nonexistent.txt' , 'fail.txt' ) ] ) ;
173+ expect ( res2 . status ) . not . toBe ( Nfsv4Stat . NFS4_OK ) ;
174+ vol . writeFileSync ( '/export/another.txt' , 'data' ) ;
175+ const res3 = await client . compound ( [ nfs . PUTROOTFH ( ) , nfs . SAVEFH ( ) , nfs . RENAME ( 'another.txt' , 'renamed3.txt' ) ] ) ;
176+ expect ( res3 . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ;
177+ const renameRes3 = res3 . resarray [ 2 ] as msg . Nfsv4RenameResponse ;
178+ if (
179+ renameRes1 . status === Nfsv4Stat . NFS4_OK &&
180+ renameRes1 . resok &&
181+ renameRes3 . status === Nfsv4Stat . NFS4_OK &&
182+ renameRes3 . resok
183+ ) {
184+ expect ( renameRes3 . resok . sourceCinfo . after - renameRes1 . resok . sourceCinfo . after ) . toBe ( 1n ) ;
185+ }
221186 await stop ( ) ;
222187 } ) ;
223188 } ) ;
0 commit comments