1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 2001, Matthes Bender(RedWolf Design GmbH)
5 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* C4Group command line executable */
18
19// Version 1.0 November 1997
20// 1.1 November 1997
21// 1.2 February 1998
22// 1.3 March 1998
23// 1.4 April 1998
24// 1.5 May 1998
25// 1.6 November 1998
26// 1.7 December 1998
27// 1.8 February 1999
28// 1.9 May 1999
29// 2.0 June 1999
30// 2.6 March 2001
31// 2.7 June 2001
32// 2.8 June 2002
33// 4.95.0 November 2003
34// 4.95.4 July 2005 PORT/HEAD mixmax
35// 4.95.4 September 2005 Unix-flavour
36
37#include <C4Group.h>
38#include <C4Version.h>
39#include <C4Update.h>
40#include <C4Config.h>
41
42#include <format>
43#include <print>
44#include <string_view>
45
46#include <fmt/printf.h>
47
48#ifdef _WIN32
49#include "StdRegistry.h"
50
51#include <shellapi.h>
52#include <conio.h>
53
54#define getch _getch
55#else
56
57// from http://cboard.cprogramming.com/archive/index.php/t-27714.html
58#include <stdio.h>
59#include <termios.h>
60#include <unistd.h>
61int mygetch()
62{
63 struct termios oldt, newt;
64 int ch;
65 tcgetattr(STDIN_FILENO, termios_p: &oldt);
66 newt = oldt;
67 newt.c_lflag &= ~(ICANON | ECHO);
68 tcsetattr(STDIN_FILENO, TCSANOW, termios_p: &newt);
69 ch = getchar();
70 tcsetattr(STDIN_FILENO, TCSANOW, termios_p: &oldt);
71 return ch;
72}
73
74#define getch mygetch
75
76#endif
77
78int globalArgC;
79char **globalArgV;
80int iFirstCommand = 0;
81
82bool fQuiet = true;
83bool fRecursive = false;
84bool fRegisterShell = false;
85bool fUnregisterShell = false;
86bool fPromptAtEnd = false;
87char strExecuteAtEnd[_MAX_PATH + 1] = "";
88
89int iResult = 0;
90
91C4Config Config;
92C4Config *GetCfg()
93{
94 return &Config;
95}
96
97bool Log(const std::string_view msg)
98{
99 if (!fQuiet)
100 std::println(fmt: "{}", args: msg);
101 return 1;
102}
103
104template<typename... Args>
105bool Log(const std::format_string<Args...> fmt, Args &&...args)
106{
107 return Log(std::format(fmt, std::forward<Args>(args)...));
108}
109
110bool ProcessGroup(const char *FilenamePar)
111{
112 C4Group hGroup;
113 hGroup.SetStdOutput(!fQuiet);
114 bool fDeleteGroup = false;
115
116 int argc = globalArgC;
117 char **argv = globalArgV;
118
119 // Strip trailing slash
120 char *szFilename = strdup(s: FilenamePar);
121 size_t len = strlen(s: szFilename);
122 if (szFilename[len - 1] == DirectorySeparator) szFilename[len - 1] = 0;
123 // Current filename
124 Log(fmt: "Group: {}", args&: szFilename);
125
126 // Open group file
127 if (hGroup.Open(szGroupName: szFilename, fCreate: true))
128 {
129 // No commands: display contents
130 if (iFirstCommand >= argc)
131 {
132 hGroup.SetStdOutput(true);
133 hGroup.View(szFiles: "*");
134 hGroup.SetStdOutput(!fQuiet);
135 }
136 // Process commands
137 else
138 {
139 for (int iArg = iFirstCommand; iArg < argc; ++iArg)
140 {
141 // This argument is a command
142 if (argv[iArg][0] == '-')
143 {
144 // Handle commands
145 switch (argv[iArg][1])
146 {
147 // Add
148 case 'a':
149 if ((iArg + 1 >= argc) || (argv[iArg + 1][0] == '-'))
150 std::println(stderr, fmt: "Missing argument for add command");
151 else
152 {
153 if ((argv[iArg][2] == 's') || (argv[iArg][2] && (argv[iArg][3] == 's')))
154 {
155 if ((iArg + 2 >= argc) || (argv[iArg + 2][0] == '-'))
156 {
157 std::println(stderr, fmt: "Missing argument for add as command");
158 }
159 else
160 {
161 hGroup.Add(szFile: argv[iArg + 1], szAddAs: argv[iArg + 2]);
162 iArg += 2;
163 }
164 }
165 else
166 {
167 while (iArg + 1 < argc && argv[iArg + 1][0] != '-')
168 {
169 ++iArg;
170#ifdef _WIN32
171 // manually expand wildcards
172 hGroup.Add(argv[iArg]);
173#else
174 hGroup.Add(szFile: argv[iArg], szAddAs: GetFilename(path: argv[iArg]));
175#endif
176 }
177 }
178 }
179 break;
180 // Move
181 case 'm':
182 if ((iArg + 1 >= argc) || (argv[iArg + 1][0] == '-'))
183 {
184 std::println(stderr, fmt: "Missing argument for move command");
185 }
186 else
187 {
188 while (iArg + 1 < argc && argv[iArg + 1][0] != '-')
189 {
190 ++iArg;
191#ifdef _WIN32
192 // manually expand wildcards
193 hGroup.Move(argv[iArg]);
194#else
195 hGroup.Move(szFile: argv[iArg], szAddAs: argv[iArg]);
196#endif
197 }
198 }
199 break;
200 // Extract
201 case 'e':
202 if ((iArg + 1 >= argc) || (argv[iArg + 1][0] == '-'))
203 {
204 std::println(stderr, fmt: "Missing argument for extract command");
205 }
206 else
207 {
208 if ((argv[iArg][2] == 't') || (argv[iArg][2] && (argv[iArg][3] == 's')))
209 {
210 if ((iArg + 2 >= argc) || (argv[iArg + 2][0] == '-'))
211 {
212 std::println(stderr, fmt: "Missing argument for extract as command");
213 }
214 else
215 {
216 hGroup.Extract(szFiles: argv[iArg + 1], szExtractTo: argv[iArg + 2]);
217 iArg += 2;
218 }
219 }
220 else
221 {
222 hGroup.Extract(szFiles: argv[iArg + 1]);
223 iArg++;
224 }
225 }
226 break;
227 // Delete
228 case 'd':
229 if ((iArg + 1 >= argc) || (argv[iArg + 1][0] == '-'))
230 {
231 std::println(stderr, fmt: "Missing argument for delete command");
232 }
233 else
234 {
235 hGroup.Delete(szFiles: argv[iArg + 1], fRecursive);
236 iArg++;
237 }
238 break;
239 // Sort
240 case 's':
241 // First sort parameter overrides default Clonk sort list
242 C4Group_SetSortList(ppSortList: nullptr);
243 // Missing argument
244 if ((iArg + 1 >= argc) || (argv[iArg + 1][0] == '-'))
245 {
246 std::println(stderr, fmt: "Missing argument for sort command");
247 }
248 // Sort, advance to next argument
249 else
250 {
251 hGroup.Sort(szSortList: argv[iArg + 1]);
252 hGroup.Save(fReOpen: true);
253 iArg++;
254 }
255 break;
256 // Rename
257 case 'r':
258 if ((iArg + 2 >= argc) || (argv[iArg + 1][0] == '-')
259 || (argv[iArg + 2][0] == '-'))
260 {
261 std::println(stderr, fmt: "Missing argument(s) for rename command");
262 }
263 else
264 {
265 hGroup.Rename(szFile: argv[iArg + 1], szNewName: argv[iArg + 2]);
266 iArg += 2;
267 }
268 break;
269 // View
270 case 'l':
271 case 'v':
272 hGroup.SetStdOutput(true);
273 if ((iArg + 1 >= argc) || (argv[iArg + 1][0] == '-'))
274 {
275 hGroup.View(szFiles: "*");
276 }
277 else
278 {
279 hGroup.View(szFiles: argv[iArg + 1]);
280 iArg++;
281 }
282 hGroup.SetStdOutput(!fQuiet);
283 break;
284 // Make original
285 case 'o':
286 hGroup.MakeOriginal(fOriginal: true);
287 break;
288 // Pack
289 case 'p':
290 std::println(fmt: "Packing...");
291 // Close
292 if (!hGroup.Close())
293 {
294 std::println(stderr, fmt: "Closing failed: {}", args: hGroup.GetError());
295 }
296 // Pack
297 else if (!C4Group_PackDirectory(szFilename))
298 {
299 std::println(stderr, fmt: "Pack failed");
300 }
301 // Reopen
302 else if (!hGroup.Open(szGroupName: szFilename))
303 {
304 std::println(stderr, fmt: "Reopen failed: {}", args: hGroup.GetError());
305 }
306 break;
307 // Unpack
308 case 'u':
309 Log(msg: "Unpacking...");
310 // Close
311 if (!hGroup.Close())
312 {
313 std::println(stderr, fmt: "Closing failed: {}", args: hGroup.GetError());
314 }
315 // Pack
316 else if (!C4Group_UnpackDirectory(szFilename))
317 {
318 std::println(stderr, fmt: "Unpack failed");
319 }
320 // Reopen
321 else if (!hGroup.Open(szGroupName: szFilename))
322 {
323 std::println(stderr, fmt: "Reopen failed: {}", args: hGroup.GetError());
324 }
325 break;
326 // Unpack
327 case 'x':
328 std::println(fmt: "Exploding...");
329 // Close
330 if (!hGroup.Close())
331 {
332 std::println(stderr, fmt: "Closing failed: {}", args: hGroup.GetError());
333 }
334 // Pack
335 else if (!C4Group_ExplodeDirectory(szFilename))
336 {
337 std::println(stderr, fmt: "Unpack failed");
338 }
339 // Reopen
340 else if (!hGroup.Open(szGroupName: szFilename))
341 {
342 std::println(stderr, fmt: "Reopen failed: {}", args: hGroup.GetError());
343 }
344 break;
345 // Print maker
346 case 'k':
347 std::println(fmt: "{}", args: hGroup.GetMaker());
348 break;
349 // Generate update
350 case 'g':
351 if ((iArg + 3 >= argc) || (argv[iArg + 1][0] == '-')
352 || (argv[iArg + 2][0] == '-')
353 || (argv[iArg + 3][0] == '-'))
354 {
355 std::println(stderr, fmt: "Update generation failed: too few arguments");
356 }
357 else
358 {
359 C4UpdatePackage Upd;
360 // Close
361 if (!hGroup.Close())
362 {
363 std::println(stderr, fmt: "Closing failed: {}", args: hGroup.GetError());
364 }
365 // generate
366 else if (!Upd.MakeUpdate(strFile1: argv[iArg + 1], strFile2: argv[iArg + 2], strUpdateFile: szFilename, strName: argv[iArg + 3], allowMissingTarget: argv[iArg][2] == 'a'))
367 {
368 std::println(stderr, fmt: "Update generation failed.");
369 }
370 // Reopen
371 else if (!hGroup.Open(szGroupName: szFilename))
372 {
373 std::println(stderr, fmt: "Reopen failed: {}", args: hGroup.GetError());
374 }
375 iArg += 3;
376 }
377 break;
378
379 // Apply an update
380 case 'y':
381 std::println(fmt: "Applying update...");
382 if (C4Group_ApplyUpdate(hGroup))
383 {
384 if (argv[iArg][2] == 'd') fDeleteGroup = true;
385 }
386 else
387 std::println(stderr, fmt: "Update failed.");
388 break;
389#ifdef _DEBUG
390 case 'z':
391 hGroup.PrintInternals();
392 break;
393#endif
394
395#ifdef _WIN32
396 // Wait
397 case 'w':
398 if (iArg + 1 >= argc || argv[iArg + 1][0] == '-')
399 {
400 std::println("Missing argument for wait command");
401 }
402 else
403 {
404 int milliseconds{0};
405 sscanf(argv[iArg + 1], "%d", &milliseconds);
406
407 if (milliseconds > 0)
408 {
409 // Wait for specified time
410 std::println("Waiting..");
411 Sleep(milliseconds);
412 }
413 else
414 {
415 // Wait for specified process to end
416 std::println("Waiting for {} to end", argv[iArg + 1]);
417
418 for (std::size_t i{0}; i < 5 && FindWindowA(nullptr, argv[iArg + 1]); ++i)
419 {
420 Sleep(1000);
421 std::print(".");
422 }
423
424 std::println("");
425 }
426
427 ++iArg;
428 }
429 break;
430#endif
431 // Undefined
432 default:
433 std::println(stderr, fmt: "Unknown command: {}", args&: argv[iArg]);
434 break;
435 }
436 }
437 else
438 {
439 std::println(stderr, fmt: "Invalid parameter {}", args&: argv[iArg]);
440 }
441 }
442 }
443 // Error: output status
444 if (!SEqual(szStr1: hGroup.GetError(), szStr2: "No Error"))
445 {
446 std::println(stderr, fmt: "Status: {}", args: hGroup.GetError());
447 }
448 // Close group file
449 if (!hGroup.Close())
450 {
451 std::println(stderr, fmt: "Closing: {}", args: hGroup.GetError());
452 }
453 // Delete group file if desired (i.e. after apply update)
454 if (fDeleteGroup)
455 {
456 Log(fmt: "Deleting {}...", args: GetFilename(path: szFilename));
457 EraseItem(szItemName: szFilename);
458 }
459 }
460 // Couldn't open group
461 else
462 {
463 std::println(stderr, fmt: "Status: {}", args: hGroup.GetError());
464 }
465 free(ptr: szFilename);
466 // Done
467 return true;
468}
469
470int RegisterShellExtensions()
471{
472#ifdef _WIN32
473 char strModule[2048];
474 char strCommand[2048];
475 char strClass[128];
476 GetModuleFileNameA(nullptr, strModule, 2048);
477 // Groups
478 const char *strClasses = "Clonk4.Definition;Clonk4.Folder;Clonk4.Group;Clonk4.Player;Clonk4.Scenario;Clonk4.Update;Clonk4.Weblink;Clonk4.Object";
479 for (int i = 0; SCopySegment(strClasses, i, strClass); i++)
480 {
481 // Unpack
482 FormatWithNull(strCommand, "\"{}\" \"%1\" \"-u\"", +strModule);
483 if (!SetRegShell(strClass, "MakeFolder", "C4Group Unpack", strCommand))
484 return 0;
485 // Explode
486 FormatWithNull(strCommand, "\"{}\" \"%1\" \"-x\"", +strModule);
487 if (!SetRegShell(strClass, "ExplodeFolder", "C4Group Explode", strCommand))
488 return 0;
489 }
490 // Directories
491 const char *strClasses2 = "Directory";
492 for (int i = 0; SCopySegment(strClasses2, i, strClass); i++)
493 {
494 // Pack
495 FormatWithNull(strCommand, "\"{}\" \"%1\" \"-p\"", +strModule);
496 if (!SetRegShell(strClass, "MakeGroupFile", "C4Group Pack", strCommand))
497 return 0;
498 }
499 // Done
500#endif
501 return 1;
502}
503
504int UnregisterShellExtensions()
505{
506#ifdef _WIN32
507 char strClass[128];
508 // Groups
509 const char *strClasses = "Clonk4.Definition;Clonk4.Folder;Clonk4.Group;Clonk4.Player;Clonk4.Scenario;Clonk4.Update;Clonk4.Weblink";
510 for (int i = 0; SCopySegment(strClasses, i, strClass); i++)
511 {
512 // Unpack
513 if (!RemoveRegShell(strClass, "MakeFolder")) return 0;
514 // Explode
515 if (!RemoveRegShell(strClass, "ExplodeFolder")) return 0;
516 }
517 // Directories
518 const char *strClasses2 = "Directory";
519 for (int i = 0; SCopySegment(strClasses2, i, strClass); i++)
520 {
521 // Pack
522 if (!RemoveRegShell(strClass, "MakeGroupFile")) return 0;
523 }
524 // Done
525#endif
526 return 1;
527}
528
529int main(int argc, char *argv[])
530{
531#ifndef _WIN32
532 // Always line buffer mode, even if the output is not sent to a terminal
533 setvbuf(stdout, buf: nullptr, _IOLBF, n: 0);
534#endif
535 // Scan options
536 int iFirstGroup = 0;
537 for (int i = 1; i < argc; ++i)
538 {
539 // Option encountered
540#ifdef _WIN32
541 if (argv[i][0] == '/' || argv[i][0] == '-')
542#else
543 if (argv[i][0] == '-')
544#endif
545 {
546 switch (argv[i][1])
547 {
548 // Quiet mode
549 case 'q':
550 fQuiet = true;
551 break;
552 // Verbose mode
553 case 'v':
554 fQuiet = false;
555 break;
556 // Recursive mode
557 case 'r':
558 fRecursive = true;
559 break;
560 // Register shell
561 case 'i':
562 fRegisterShell = true;
563 break;
564 // Unregister shell
565 case 'u':
566 fUnregisterShell = true;
567 break;
568 // Prompt at end
569 case 'p': fPromptAtEnd = true; break;
570 // Execute at end
571 case 'x': SCopy(szSource: argv[i] + 3, sTarget: strExecuteAtEnd, _MAX_PATH); break;
572 // Unknown
573 default:
574 std::println(stderr, fmt: "Unknown option {}", args&: argv[i]);
575 break;
576 }
577 }
578 else
579 {
580 // filename encountered: no more options expected
581 iFirstGroup = i;
582 break;
583 }
584 }
585 iFirstCommand = iFirstGroup;
586 while (iFirstCommand < argc && argv[iFirstCommand][0] != '-')
587 ++iFirstCommand;
588
589 // Program info
590 Log(fmt: "LegacyClonk C4Group {}", C4VERSION);
591
592 // Load configuration
593 Config.Init();
594 Config.Load(forceWorkingDirectory: false);
595
596 // Init C4Group
597 C4Group_SetMaker(szMaker: Config.General.Name);
598 C4Group_SetTempPath(szPath: Config.General.TempPath);
599 C4Group_SetSortList(ppSortList: C4CFN_FLS);
600
601 // Display current working directory
602 if (!fQuiet)
603 {
604 std::println(fmt: "Location: {}", args: GetWorkingDirectory());
605 }
606
607 // Store command line parameters
608 globalArgC = argc;
609 globalArgV = argv;
610
611 // Register shell
612 if (fRegisterShell)
613 if (RegisterShellExtensions())
614 std::println(fmt: "Shell extensions registered.");
615 else
616 std::println(fmt: "Error registering shell extensions.");
617 // Unregister shell
618 if (fUnregisterShell)
619 if (UnregisterShellExtensions())
620 std::println(fmt: "Shell extensions removed.");
621 else
622 std::println(fmt: "Error removing shell extensions.");
623
624 // At least one parameter (filename, not option or command): process file(s)
625 if (iFirstGroup)
626 {
627#ifdef _WIN32
628 // Wildcard in filename: use file search
629 if (SCharCount('*', argv[iFirstGroup]))
630 ForEachFile(argv[iFirstGroup], &ProcessGroup);
631 // Only one file
632 else
633 ProcessGroup(argv[iFirstGroup]);
634#else
635 for (int i = iFirstGroup; i < argc && argv[i][0] != '-'; ++i)
636 ProcessGroup(FilenamePar: argv[i]);
637#endif
638 }
639 // Too few parameters: output help (if we didn't register stuff)
640 else if (!fRegisterShell && !fUnregisterShell)
641 {
642 std::println(fmt: "");
643 std::println(fmt: "Usage: c4group [options] group(s) command(s)\n");
644 std::println(fmt: "Commands: -a[s] Add [as] -m Move -e[t] Extract [to]");
645 std::println(fmt: " -v View -l List -d Delete -r Rename -s Sort");
646 std::println(fmt: " -p Pack -u Unpack -x Explode");
647 std::println(fmt: " -k Print maker");
648 std::println(fmt: " -g[a] [source] [target] [title] Make update [and allow missing target group when applying update]");
649 std::println(fmt: " -y[d] Apply update [and delete group file]");
650 std::println(fmt: "");
651 std::println(fmt: "Options: -v Verbose -r Recursive -p Prompt at end");
652 std::println(fmt: " -i Register shell -u Unregister shell");
653 std::println(fmt: " -x:<command> Execute shell command when done");
654 std::println(fmt: "");
655 std::println(fmt: "Examples: c4group pack.c4g -a myfile.dat -l \"*.dat\"");
656 std::println(fmt: " c4group pack.c4g -as myfile.dat myfile.bin");
657 std::println(fmt: " c4group -v pack.c4g -et \"*.dat\" \\data\\mydatfiles\\");
658 std::println(fmt: " c4group pack.c4g -et myfile.dat myfile.bak");
659 std::println(fmt: " c4group pack.c4g -s \"*.bin|*.dat\"");
660 std::println(fmt: " c4group pack.c4g -x");
661 std::println(fmt: " c4group pack.c4g -k");
662 std::println(fmt: " c4group update.c4u -g ver1.c4f ver2.c4f New_Version");
663 std::println(fmt: " c4group -i");
664 }
665
666 // Prompt at end
667 if (fPromptAtEnd)
668 {
669 std::println(fmt: "\nDone. Press any key to continue.");
670 getch();
671 }
672
673 // Execute when done
674 if (strExecuteAtEnd[0])
675 {
676 std::println(fmt: "Executing: {}", args&: strExecuteAtEnd);
677
678#ifdef _WIN32
679 STARTUPINFOA startInfo{};
680 startInfo.cb = sizeof(startInfo);
681
682 PROCESS_INFORMATION procInfo;
683
684 CreateProcessA(strExecuteAtEnd, nullptr, nullptr, nullptr, false, 0, nullptr, nullptr, &startInfo, &procInfo);
685#else
686 switch (fork())
687 {
688 // Error
689 case -1:
690 std::println(stderr, fmt: "Error forking.");
691 break;
692 // Child process
693 case 0:
694 execl(path: strExecuteAtEnd, arg: strExecuteAtEnd, static_cast<char *>(0)); // currently no parameters are passed to the executed program
695 exit(status: 1);
696 // Parent process
697 default:
698 break;
699 }
700#endif
701 }
702
703 // Done
704 return iResult;
705}
706