@@ -120,4 +120,105 @@ describe('RENAME operation', () => {
120120 expect ( getattr . status ) . toBe ( Nfsv4Stat . NFS4_OK ) ; // Must succeed with fix
121121 await stop ( ) ;
122122 } ) ;
123+
124+ describe ( 'AppleDouble file handling' , ( ) => {
125+ test ( 'renames AppleDouble file when renaming main file' , async ( ) => {
126+ 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' ) ] ) ;
130+ 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 ) ] ) ;
193+ 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 ) ;
196+ await stop ( ) ;
197+ } ) ;
198+
199+ test ( 'does not rename AppleDouble if it is a directory' , async ( ) => {
200+ 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 ) ;
208+ await stop ( ) ;
209+ } ) ;
210+
211+ test ( 'handles case where main file starts with ._' , async ( ) => {
212+ 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 ) ;
221+ await stop ( ) ;
222+ } ) ;
223+ } ) ;
123224} ) ;
0 commit comments