1 /*
2  * PROJECT:     ReactOS Replace Command
3  * LICENSE:     GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
4  * PURPOSE:     Implements 'replace' command
5  * COPYRIGHT:   Copyright Samuel Erdtman (samuel@erdtman.se)
6  * COPYRIGHT:   Copyright 2024 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
7  */
8 
9 #include "replace.h"
10 
11 enum
12 {
13     REPLACE_ADD       = 0x001,   /* /A  */
14     REPLACE_CONFIRM   = 0x002,   /* /P  */
15     REPLACE_READ_ONLY = 0x004,   /* /R */
16     REPLACE_SUBDIR    = 0x008,   /* /S  */
17     REPLACE_DISK      = 0x010,   /* /W  */
18     REPLACE_UPDATE    = 0x020,   /* /U */
19 };
20 
21 /* just makes a print out if there is a problem with the switches */
22 void invalid_switch(LPTSTR is)
23 {
24     ConOutResPrintf(STRING_REPLACE_ERROR1,is);
25     ConOutResPrintf(STRING_REPLACE_HELP3);
26 }
27 
28 /* retrieves the path dependent on the input file name */
29 void getPath(TCHAR* out, LPTSTR in)
30 {
31     if (_tcslen(in) == 2 && in[1] == _T(':'))
32         GetRootPath(in,out,MAX_PATH);
33     else
34         GetFullPathName (in, MAX_PATH, out, NULL);
35 }
36 
37 /* makes the replace */
38 INT replace(TCHAR source[MAX_PATH], TCHAR dest[MAX_PATH], DWORD dwFlags, BOOL *doMore)
39 {
40     TCHAR d[MAX_PATH];
41     TCHAR s[MAX_PATH];
42     HANDLE hFileSrc, hFileDest;
43     DWORD  dwAttrib, dwRead, dwWritten;
44     LPBYTE buffer;
45     BOOL   bEof = FALSE;
46     FILETIME srcCreationTime, destCreationTime, srcLastAccessTime, destLastAccessTime;
47     FILETIME srcLastWriteTime, destLastWriteTime;
48     GetPathCase(source, s);
49     GetPathCase(dest, d);
50     s[0] = _totupper(s[0]);
51     d[0] = _totupper(d[0]);
52     // ConOutPrintf(_T("old-src:  %s\n"), s);
53     // ConOutPrintf(_T("old-dest: %s\n"), d);
54     // ConOutPrintf(_T("src:  %s\n"), source);
55     // ConOutPrintf(_T("dest: %s\n"), dest);
56 
57     /* Open up the sourcefile */
58     hFileSrc = CreateFile (source, GENERIC_READ, FILE_SHARE_READ,NULL, OPEN_EXISTING, 0, NULL);
59     if (hFileSrc == INVALID_HANDLE_VALUE)
60     {
61         ConOutResPrintf(STRING_COPY_ERROR1, source);
62         return 0;
63     }
64 
65     /*
66      * Get the time from source file to be used in the comparison
67      * with dest time if update switch is set.
68      */
69     GetFileTime (hFileSrc, &srcCreationTime, &srcLastAccessTime, &srcLastWriteTime);
70 
71     /*
72      * Retrieve the source attributes so that they later on
73      * can be inserted in to the destination.
74      */
75     dwAttrib = GetFileAttributes (source);
76 
77     if (IsExistingFile (dest))
78     {
79         /*
80          * Resets the attributes to avoid problems with read only files,
81          * checks for read only has been made earlier.
82          */
83         SetFileAttributes(dest,FILE_ATTRIBUTE_NORMAL);
84         /*
85          * Is the update flas set? The time has to be controled so that
86          * only older files are replaced.
87          */
88         if (dwFlags & REPLACE_UPDATE)
89         {
90             /* Read destination time */
91             hFileDest = CreateFile(dest, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
92                 0, NULL);
93 
94             if (hFileDest == INVALID_HANDLE_VALUE)
95             {
96                 ConOutResPrintf(STRING_COPY_ERROR1, dest);
97                 CloseHandle (hFileSrc);
98                 return 0;
99             }
100 
101             /* Compare time */
102             GetFileTime (hFileDest, &destCreationTime, &destLastAccessTime, &destLastWriteTime);
103             if (!((srcLastWriteTime.dwHighDateTime > destLastWriteTime.dwHighDateTime) ||
104                     (srcLastWriteTime.dwHighDateTime == destLastWriteTime.dwHighDateTime &&
105                      srcLastWriteTime.dwLowDateTime > destLastWriteTime.dwLowDateTime)))
106             {
107                 CloseHandle (hFileSrc);
108                 CloseHandle (hFileDest);
109                 return 0;
110             }
111             CloseHandle (hFileDest);
112         }
113         /* Delete the old file */
114         DeleteFile (dest);
115     }
116 
117     /* Check confirm flag, and take appropriate action */
118     if (dwFlags & REPLACE_CONFIRM)
119     {
120         /* Output depending on add flag */
121         if (dwFlags & REPLACE_ADD)
122             ConOutResPrintf(STRING_REPLACE_HELP9, dest);
123         else
124             ConOutResPrintf(STRING_REPLACE_HELP10, dest);
125         if ( !FilePromptYNA (0))
126         {
127             CloseHandle (hFileSrc);
128             return 0;
129         }
130     }
131 
132     /* Output depending on add flag */
133     if (dwFlags & REPLACE_ADD)
134         ConOutResPrintf(STRING_REPLACE_HELP11, dest);
135     else
136         ConOutResPrintf(STRING_REPLACE_HELP5, dest);
137 
138     /* Make sure source and destination is not the same */
139     if (!_tcscmp(s, d))
140     {
141         ConOutResPrintf(STRING_REPLACE_ERROR7);
142         CloseHandle (hFileSrc);
143         *doMore = FALSE;
144         return 0;
145     }
146 
147     /* Open destination file to write to */
148     hFileDest = CreateFile (dest, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
149     if (hFileDest == INVALID_HANDLE_VALUE)
150     {
151         CloseHandle (hFileSrc);
152         ConOutResPrintf(STRING_REPLACE_ERROR7);
153         *doMore = FALSE;
154         return 0;
155     }
156 
157     /* Get buffer for the copy process */
158     buffer = VirtualAlloc(NULL, BUFF_SIZE, MEM_COMMIT, PAGE_READWRITE);
159     if (buffer == NULL)
160     {
161         CloseHandle (hFileDest);
162         CloseHandle (hFileSrc);
163         ConOutResPrintf(STRING_ERROR_OUT_OF_MEMORY);
164         return 0;
165     }
166 
167     /* Put attribute and time to the new destination file */
168     SetFileAttributes (dest, dwAttrib);
169     SetFileTime (hFileDest, &srcCreationTime, &srcLastAccessTime, &srcLastWriteTime);
170     do
171     {
172         /* Read data from source */
173         ReadFile (hFileSrc, buffer, BUFF_SIZE, &dwRead, NULL);
174 
175         /* Done? */
176         if (dwRead == 0)
177             break;
178 
179         /* Write to destination file */
180         WriteFile (hFileDest, buffer, dwRead, &dwWritten, NULL);
181 
182         /* Done! or ctrl break! */
183         if (dwWritten != dwRead || bCtrlBreak)
184         {
185             ConOutResPuts(STRING_COPY_ERROR3);
186             VirtualFree (buffer, 0, MEM_RELEASE);
187             CloseHandle (hFileDest);
188             CloseHandle (hFileSrc);
189             return 0;
190         }
191     }
192     while (!bEof);
193 
194     /* Return memory and close files */
195     VirtualFree (buffer, 0, MEM_RELEASE);
196     CloseHandle (hFileDest);
197     CloseHandle (hFileSrc);
198 
199     /* Return one file replaced */
200     return 1;
201 }
202 
203 
204 /* Function to iterate over source files and call replace for each of them */
205 INT recReplace(DWORD dwFlags,
206                TCHAR szSrcPath[MAX_PATH],
207                TCHAR szDestPath[MAX_PATH],
208                BOOL *doMore)
209 {
210     TCHAR tmpDestPath[MAX_PATH], tmpSrcPath[MAX_PATH];
211     INT filesReplaced=0;
212     INT_PTR i;
213     DWORD dwAttrib = 0;
214     HANDLE hFile;
215     WIN32_FIND_DATA findBuffer;
216 
217     /* Get file handle to the sourcefile(s) */
218     hFile = FindFirstFile (szSrcPath, &findBuffer);
219 
220     /*
221      * Strip the paths back to the folder they are in, so that
222      * the different filenames can be added if more than one.
223      */
224     for(i = (_tcslen(szSrcPath) -  1); i > -1; i--)
225     {
226         if (szSrcPath[i] != _T('\\'))
227             szSrcPath[i] = _T('\0');
228         else
229             break;
230     }
231 
232     /* Go through all the sourcefiles and copy/replace them */
233     do
234     {
235         if (bCtrlBreak)
236             return filesReplaced;
237 
238         /* Problem with file handler */
239         if (hFile == INVALID_HANDLE_VALUE)
240             return filesReplaced;
241 
242         /* We do not want to replace any .. . ocr directory */
243         if (!_tcscmp (findBuffer.cFileName, _T("."))  ||
244                 !_tcscmp (findBuffer.cFileName, _T(".."))||
245                 findBuffer.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
246                 continue;
247 
248         /* Add filename to destpath */
249         _tcscpy(tmpDestPath,szDestPath);
250         _tcscat (tmpDestPath, findBuffer.cFileName);
251 
252         dwAttrib = GetFileAttributes(tmpDestPath);
253         /* Check add flag */
254         if (dwFlags & REPLACE_ADD)
255         {
256             if (IsExistingFile(tmpDestPath))
257                 continue;
258             else
259                 dwAttrib = 0;
260         }
261         else
262         {
263             if (!IsExistingFile(tmpDestPath))
264                 continue;
265         }
266 
267         /* Check if file is read only, if so check if that should be ignored */
268         if (dwAttrib & FILE_ATTRIBUTE_READONLY)
269         {
270             if (!(dwFlags & REPLACE_READ_ONLY))
271             {
272                 ConOutResPrintf(STRING_REPLACE_ERROR5, tmpDestPath);
273                 *doMore = FALSE;
274                 break;
275             }
276         }
277 
278         /* Add filename to sourcepath, insted of wildcards */
279         _tcscpy(tmpSrcPath,szSrcPath);
280         _tcscat (tmpSrcPath, findBuffer.cFileName);
281 
282         /* Make the replace */
283         if (replace(tmpSrcPath,tmpDestPath, dwFlags, doMore))
284         {
285             filesReplaced++;
286         }
287         else if (!*doMore)
288         {
289             /* The file to be replaced was the same as the source */
290             filesReplaced = -1;
291             break;
292         }
293 
294     /* Take next sourcefile if any */
295     } while(FindNextFile (hFile, &findBuffer));
296 
297     FindClose(hFile);
298 
299     return filesReplaced;
300 }
301 
302 /* If /s switch is specifyed all subdirs has to be considered */
303 INT recFindSubDirs(DWORD dwFlags,
304                    TCHAR szSrcPath[MAX_PATH],
305                    TCHAR szDestPath[MAX_PATH],
306                    BOOL *doMore)
307 {
308     HANDLE hFile;
309     WIN32_FIND_DATA findBuffer;
310     TCHAR tmpDestPath[MAX_PATH], tmpSrcPath[MAX_PATH];
311     INT filesReplaced = 0;
312     INT_PTR i;
313 
314     /*
315      * Add a wildcard to dest end so the it will be easy to iterate
316      * over all the files and directorys in the dest directory.
317      */
318     _tcscat(szDestPath, _T("*"));
319 
320     /* Get the first file in the directory */
321     hFile = FindFirstFile (szDestPath, &findBuffer);
322 
323     /* Remove the star added earlier to dest path */
324     for(i = (_tcslen(szDestPath) -  1); i > -1; i--)
325     {
326         if (szDestPath[i] != _T('\\'))
327             szDestPath[i] = _T('\0');
328         else
329             break;
330     }
331 
332     /* Iterate over all filed directories in the dest dir */
333     do
334     {
335         /* Save the source path so that it will not be wrecked */
336         _tcscpy(tmpSrcPath,szSrcPath);
337         /* Check for reading problems */
338         if (hFile == INVALID_HANDLE_VALUE)
339         {
340             ConOutFormatMessage (GetLastError(), tmpSrcPath);
341             return filesReplaced;
342         }
343 
344         /*
345          * Check if the we should enter the dir or if it is a file
346          * or . or .. if so thake the next object to process.
347          */
348         if (!_tcscmp (findBuffer.cFileName, _T("."))  ||
349             !_tcscmp (findBuffer.cFileName, _T(".."))||
350             IsExistingFile(findBuffer.cFileName))
351             continue;
352         /* Add the destpath and the new dir path to tempDestPath */
353         _tcscpy(tmpDestPath,szDestPath);
354         _tcscat (tmpDestPath, findBuffer.cFileName);
355         /* Make sure that we have a directory */
356         if (IsExistingDirectory(tmpDestPath))
357         {
358             /* Add a \ to the end or the path */
359             if (szDestPath[_tcslen(tmpDestPath) -  1] != _T('\\'))
360                 _tcscat(tmpDestPath, _T("\\"));
361             /* Call the function to replace files in the new directory */
362             filesReplaced += recReplace(dwFlags, tmpSrcPath, tmpDestPath, doMore);
363             /* If there were problems break e.g. read-only file */
364             if (!*doMore)
365                 break;
366             _tcscpy(tmpSrcPath,szSrcPath);
367             /* Control the next level of subdirs */
368             filesReplaced += recFindSubDirs(dwFlags,tmpSrcPath,tmpDestPath, doMore);
369             if (!*doMore)
370                 break;
371         }
372         /* Get the next handle */
373     } while(FindNextFile (hFile, &findBuffer));
374 
375     FindClose(hFile);
376 
377     return filesReplaced;
378 }
379 
380 INT cmd_replace(INT argc, WCHAR **argv)
381 {
382     LPTSTR *arg;
383     INT i, filesReplaced = 0, nFiles, srcIndex = -1, destIndex = -1;
384     DWORD dwFlags = 0;
385     TCHAR szDestPath[MAX_PATH], szSrcPath[MAX_PATH], tmpSrcPath[MAX_PATH];
386     BOOL doMore = TRUE;
387 
388     --argc;
389     ++argv;
390 
391     /* Help wanted? */
392     if (argc == 1 && !_tcscmp(argv[0], _T("/?")))
393     {
394         ConOutResPrintf(STRING_REPLACE_HELP1);
395         return EXIT_SUCCESS;
396     }
397 
398     /* Divide the argument in to an array of c-strings */
399     arg = argv;
400     nFiles = argc;
401 
402     /* Read options */
403     for (i = 0; i < argc; i++)
404     {
405         if (arg[i][0] == _T('/'))
406         {
407             if (_tcslen(arg[i]) == 2)
408             {
409                 switch (_totupper(arg[i][1]))
410                 {
411                 case _T('A'):
412                     dwFlags |= REPLACE_ADD;
413                     break;
414                 case _T('P'):
415                     dwFlags |= REPLACE_CONFIRM;
416                     break;
417                 case _T('R'):
418                     dwFlags |= REPLACE_READ_ONLY;
419                     break;
420                 case _T('S'):
421                     dwFlags |= REPLACE_SUBDIR;
422                     break;
423                 case _T('W'):
424                     dwFlags |= REPLACE_DISK;
425                     break;
426                 case _T('U'):
427                     dwFlags |= REPLACE_UPDATE;
428                     break;
429                 default:
430                     invalid_switch(arg[i]);
431                     return 11; /* Error */
432                 }
433             }
434             else
435             {
436                 invalid_switch(arg[i]);
437                 return 11; /* Error */
438             }
439             nFiles--;
440         }
441         else
442         {
443             if (srcIndex == -1)
444             {
445                 srcIndex = i;
446             }
447             else if (destIndex == -1)
448             {
449                 destIndex = i;
450             }
451             else
452             {
453                 invalid_switch(arg[i]);
454                 return 11; /* Error */
455             }
456         }
457     }
458 
459     /* See so that at least source is there */
460     if (nFiles < 1)
461     {
462         ConOutResPrintf(STRING_REPLACE_HELP2);
463         ConOutResPrintf(STRING_REPLACE_HELP3);
464         return 11; /* Error */
465     }
466 
467     /* Check so that not both update and add switch is added and subdir */
468     if ((dwFlags & REPLACE_UPDATE || dwFlags & REPLACE_SUBDIR) && (dwFlags & REPLACE_ADD))
469     {
470         ConOutResPrintf(STRING_REPLACE_ERROR4);
471         ConOutResPrintf(STRING_REPLACE_HELP7);
472         return 11; /* Error */
473     }
474 
475     /* If we have a destination get the full path */
476     if (destIndex != -1)
477     {
478         if (_tcslen(arg[destIndex]) == 2 && arg[destIndex][1] == ':')
479             GetRootPath(arg[destIndex],szDestPath,MAX_PATH);
480         else
481         {
482             /* Check for wildcards in destination directory */
483             if (_tcschr (arg[destIndex], _T('*')) != NULL ||
484                 _tcschr (arg[destIndex], _T('?')) != NULL)
485             {
486                 ConOutResPrintf(STRING_REPLACE_ERROR2,arg[destIndex]);
487                 ConOutResPrintf(STRING_REPLACE_HELP3);
488                 return 3; /* Error */
489             }
490             getPath(szDestPath, arg[destIndex]);
491             /* Make sure that destination exists */
492             if (!IsExistingDirectory(szDestPath))
493             {
494                 ConOutResPrintf(STRING_REPLACE_ERROR2, szDestPath);
495                 ConOutResPrintf(STRING_REPLACE_HELP3);
496                 return 3; /* Error */
497             }
498         }
499     }
500     else
501     {
502         /* Dest is current dir */
503         GetCurrentDirectory(MAX_PATH,szDestPath);
504     }
505 
506     /* Get the full source path */
507     if (!(_tcslen(arg[srcIndex]) == 2 && arg[srcIndex][1] == ':'))
508         getPath(szSrcPath, arg[srcIndex]);
509     else
510         _tcscpy(szSrcPath,arg[srcIndex]);
511 
512     /* Source does not have wildcards */
513     if (_tcschr (arg[srcIndex], _T('*')) == NULL &&
514         _tcschr (arg[srcIndex], _T('?')) == NULL)
515     {
516         /* Check so that source is not a directory, because that is not allowed */
517         if (IsExistingDirectory(szSrcPath))
518         {
519             ConOutResPrintf(STRING_REPLACE_ERROR6, szSrcPath);
520             ConOutResPrintf(STRING_REPLACE_HELP3);
521             return 2; /* Error */
522         }
523         /* Check if the file exists */
524         if (!IsExistingFile(szSrcPath))
525         {
526             ConOutResPrintf(STRING_REPLACE_HELP3);
527             return 2; /* Error */
528         }
529     }
530 
531     /* /w switch is set so wait for any key to be pressed */
532     if (dwFlags & REPLACE_DISK)
533     {
534         msg_pause();
535         cgetchar();
536     }
537 
538     /* Add an extra \ to the destination path if needed */
539     if (szDestPath[_tcslen(szDestPath) -  1] != _T('\\'))
540         _tcscat(szDestPath, _T("\\"));
541 
542     /* Save source path */
543     _tcscpy(tmpSrcPath,szSrcPath);
544     /* Replace in dest dir */
545     filesReplaced += recReplace(dwFlags, tmpSrcPath, szDestPath, &doMore);
546     /* If subdir switch is set replace in the subdirs to */
547     if (dwFlags & REPLACE_SUBDIR && doMore)
548     {
549         filesReplaced += recFindSubDirs(dwFlags, szSrcPath,  szDestPath, &doMore);
550     }
551 
552     /* If source == dest write no more */
553     if (filesReplaced != -1)
554     {
555         /* No files replaced */
556         if (filesReplaced==0)
557         {
558             /* Add switch dependent output */
559             if (dwFlags & REPLACE_ADD)
560                 ConOutResPrintf(STRING_REPLACE_HELP7);
561             else
562                 ConOutResPrintf(STRING_REPLACE_HELP3);
563         }
564         /* Some files replaced */
565         else
566         {
567             /* Add switch dependent output */
568             if (dwFlags & REPLACE_ADD)
569                 ConOutResPrintf(STRING_REPLACE_HELP8, filesReplaced);
570             else
571                 ConOutResPrintf(STRING_REPLACE_HELP4, filesReplaced);
572         }
573     }
574 
575     /* Return memory */
576     return EXIT_SUCCESS;
577 }
578 
579 static BOOL CALLBACK
580 CtrlHandlerRoutine(DWORD dwCtrlType)
581 {
582     switch (dwCtrlType)
583     {
584         case CTRL_C_EVENT: /* Ctrl+C */
585         case CTRL_CLOSE_EVENT: /* Closing console? */
586             bCtrlBreak = TRUE;
587             return TRUE; /* Handled */
588 
589         default:
590             return FALSE; /* Ignored */
591     }
592 }
593 
594 int wmain(int argc, WCHAR **argvW)
595 {
596     /* Handle Ctrl+C and console closing */
597     SetConsoleCtrlHandler(CtrlHandlerRoutine, TRUE);
598 
599     /* Initialize the Console Standard Streams */
600     ConInitStdStreams();
601 
602     return cmd_replace(argc, argvW);
603 }
604