1#define _DEFAULT_SOURCE
  2#include <stdio.h>
  3#include <stdlib.h>
  4#include <string.h>
  5#include <stdint.h>
  6#include <dirent.h>
  7#include <sys/stat.h>
  8#include <getopt.h>
  9#include <fnmatch.h>
 10#include <ctype.h>
 11
 12#include "../libraries/vfs.h"
 13
 14static void format_size(char* buf, size_t buf_size, uint64_t size) {
 15	const char* units[] = {"B", "KB", "MB", "GB", "TB"};
 16	int i = 0;
 17	double d_size = (double)size;
 18	while (d_size >= 1024 && i < 4) {
 19		d_size /= 1024.0;
 20		i++;
 21	}
 22	if (i == 0) {
 23		snprintf(buf, buf_size, "%llu B", (unsigned long long)size);
 24	} else {
 25		snprintf(buf, buf_size, "%.2f %s", d_size, units[i]);
 26	}
 27}
 28
 29#define MAX_FILES 4096
 30
 31typedef struct {
 32	char path[256];
 33	long size;
 34	void* data;
 35} PendingFile;
 36
 37static PendingFile g_pending[MAX_FILES];
 38static int g_count = 0;
 39
 40static char* g_ignore_patterns[MAX_FILES];
 41static int g_ignore_count = 0;
 42
 43void load_gitignore() {
 44	FILE* f = fopen(".gitignore", "r");
 45	if (!f) return;
 46
 47	char line[1024];
 48	while (fgets(line, sizeof(line), f)) {
 49		// Strip whitespace and newline
 50		char* p = line;
 51		while (isspace(*p)) p++;
 52		char* end = p + strlen(p) - 1;
 53		while (end >= p && isspace(*end)) {
 54			*end = '\0';
 55			end--;
 56		}
 57
 58		if (*p == '\0' || *p == '#') continue;
 59
 60		if (g_ignore_count < MAX_FILES) {
 61			char* s = strdup(p);
 62			if (s) g_ignore_patterns[g_ignore_count++] = s;
 63		}
 64	}
 65	fclose(f);
 66}
 67
 68void free_gitignore() {
 69	for (int i = 0; i < g_ignore_count; i++) {
 70		free(g_ignore_patterns[i]);
 71	}
 72	g_ignore_count = 0;
 73}
 74
 75int should_ignore(const char* path, int is_dir) {
 76	// Normalize path for matching: remove leading "./" if present
 77	const char* p = path;
 78	if (strncmp(p, "./", 2) == 0) p += 2;
 79
 80	for (int i = 0; i < g_ignore_count; i++) {
 81		const char* pattern = g_ignore_patterns[i];
 82		int pattern_is_dir_only = (pattern[strlen(pattern) - 1] == '/');
 83
 84		char clean_pattern[1024];
 85		strncpy(clean_pattern, pattern, 1023);
 86		clean_pattern[1023] = '\0';
 87		if (pattern_is_dir_only) {
 88			clean_pattern[strlen(clean_pattern) - 1] = '\0';
 89		}
 90
 91		// If pattern has a slash (not at the end), it's relative to root
 92		int has_slash = (strchr(clean_pattern, '/') != NULL);
 93
 94		if (has_slash) {
 95			// Match against full path from root
 96			if (fnmatch(clean_pattern, p, FNM_PATHNAME) == 0) {
 97				if (!pattern_is_dir_only || is_dir) return 1;
 98			}
 99		} else {
100			// Match against filename only
101			const char* filename = strrchr(p, '/');
102			if (filename) filename++;
103			else filename = p;
104
105			if (fnmatch(clean_pattern, filename, 0) == 0) {
106				if (!pattern_is_dir_only || is_dir) return 1;
107			}
108		}
109	}
110	return 0;
111}
112
113void add_file(const char* path) {
114	if (g_count >= MAX_FILES) {
115		fprintf(stderr, "Too many files!\n");
116		return;
117	}
118
119	FILE* f = fopen(path, "rb");
120	if (!f) {
121		fprintf(stderr, "Could not open %s\n", path);
122		return;
123	}
124
125	fseek(f, 0, SEEK_END);
126	long size = ftell(f);
127	fseek(f, 0, SEEK_SET);
128
129	void* data = malloc(size);
130	if (!data) {
131		fprintf(stderr, "Memory allocation failed for %s\n", path);
132		fclose(f);
133		return;
134	}
135	if (fread(data, 1, size, f) != (size_t)size) {
136		fprintf(stderr, "Failed to read %s\n", path);
137		free(data);
138		fclose(f);
139		return;
140	}
141	fclose(f);
142
143	strncpy(	g_pending[g_count].path, path, 255);
144	g_pending[g_count].size = size;
145	g_pending[g_count].data = data;
146	g_count++;
147
148	char size_str[32];
149	format_size(size_str, sizeof(size_str), (uint64_t)size);
150	printf("Added: %s (%s)\n", path, size_str);
151}
152
153
154void traverse(const char* dir_path) {
155	if (should_ignore(dir_path, 1)) return;
156
157	char clean_dir[1024];
158	strncpy(clean_dir, dir_path, 1023);
159	clean_dir[1023] = '\0';
160	size_t len = strlen(clean_dir);
161	if (len > 1 && clean_dir[len - 1] == '/') {
162		clean_dir[len - 1] = '\0';
163	}
164
165	DIR* dir = opendir(clean_dir);
166	if (!dir) {
167		struct stat st;
168		if (stat(clean_dir, &st) == 0 && !S_ISDIR(st.st_mode)) {
169			add_file(clean_dir);
170		}
171		return;
172	}
173
174	struct dirent* entry;
175	while ((entry = readdir(dir)) != NULL) {
176		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
177			continue;
178
179		char path[2048];
180		snprintf(path, sizeof(path), "%s/%s", clean_dir, entry->d_name);
181
182		struct stat st;
183		if (stat(path, &st) == 0) {
184			int is_dir = S_ISDIR(st.st_mode);
185			if (should_ignore(path, is_dir)) continue;
186
187			if (is_dir) {
188				traverse(path);
189			} else {
190				add_file(path);
191			}
192		}
193	}
194	closedir(dir);
195}
196
197void cmd_pack(const char* output_name, int num_folders, char** folders) {
198	load_gitignore();
199
200	for (int i = 0; i < num_folders; i++) {
201		traverse(folders[i]);
202	}
203
204	if (g_count == 0) {
205		fprintf(stderr, "No files to pack.\n");
206		free_gitignore();
207		return;
208	}
209
210	FILE* out = fopen(output_name, "wb");
211	if (!out) {
212		fprintf(stderr, "Failed to open output file %s\n", output_name);
213		free_gitignore();
214		return;
215	}
216
217	VfsHeader header;
218	memcpy(header.magic, "DRP1", 4);
219	header.num_files = (uint32_t)g_count;
220	fwrite(&header, sizeof(VfsHeader), 1, out);
221
222	VfsEntry* entries = malloc(sizeof(VfsEntry) * g_count);
223	uint64_t current_offset = sizeof(VfsHeader) + sizeof(VfsEntry) * g_count;
224
225	for (int i = 0; i < g_count; i++) {
226		strncpy(entries[i].path, g_pending[i].path, 255);
227		entries[i].size = (uint64_t)g_pending[i].size;
228		entries[i].offset = current_offset;
229		current_offset += entries[i].size;
230	}
231
232	fwrite(entries, sizeof(VfsEntry), g_count, out);
233
234	for (int i = 0; i < g_count; i++) {
235		fwrite(g_pending[i].data, 1, g_pending[i].size, out);
236		free(g_pending[i].data);
237	}
238
239	fclose(out);
240	free(entries);
241	free_gitignore();
242
243	printf("Successfully packed %d files into %s\n", g_count, output_name);
244}
245
246int read_pak(const char* path, VfsHeader* header, VfsEntry** entries) {
247	FILE* f = fopen(path, "rb");
248	if (!f) return 0;
249
250	if (fread(header, sizeof(VfsHeader), 1, f) != 1) {
251		fclose(f);
252		return 0;
253	}
254
255	if (memcmp(header->magic, "DRP1", 4) != 0) {
256		fclose(f);
257		return 0;
258	}
259
260	*entries = malloc(sizeof(VfsEntry) * header->num_files);
261	if (fread(*entries, sizeof(VfsEntry), header->num_files, f) != header->num_files) {
262		free(*entries);
263		fclose(f);
264		return 0;
265	}
266
267	fclose(f);
268	return 1;
269}
270
271void cmd_list(const char* path) {
272	VfsHeader header;
273	VfsEntry* entries = NULL;
274	if (!read_pak(path, &header, &entries)) {
275		fprintf(stderr, "Error: Could not read pak file %s\n", path);
276		return;
277	}
278
279	printf("Listing %u files in %s:\n", header.num_files, path);
280	printf("%-48s %16s %12s\n", "Path", "Size", "Offset");
281	printf("--------------------------------------------------------------------------------\n");
282	for (uint32_t i = 0; i < header.num_files; i++) {
283		char size_str[32];
284		format_size(size_str, sizeof(size_str), entries[i].size);
285		printf("%-48s %16s %12lu\n", entries[i].path, size_str, (unsigned long)entries[i].offset);
286	}
287
288	free(entries);
289}
290
291void cmd_info(const char* path) {
292	VfsHeader header;
293	VfsEntry* entries = NULL;
294	if (!read_pak(path, &header, &entries)) {
295		fprintf(stderr, "Error: Could not read pak file %s\n", path);
296		return;
297	}
298
299	uint64_t total_data_size = 0;
300	for (uint32_t i = 0; i < header.num_files; i++) {
301		total_data_size += entries[i].size;
302	}
303
304	struct stat st;
305	uint64_t file_size = 0;
306	if (stat(path, &st) == 0) file_size = st.st_size;
307
308	char data_size_str[32], archive_size_str[32];
309	format_size(data_size_str, sizeof(data_size_str), total_data_size);
310	format_size(archive_size_str, sizeof(archive_size_str), file_size);
311
312	printf("PAK Information: %s\n", path);
313	printf("  Magic:         %.4s\n", header.magic);
314	printf("  File Count:    %u\n", header.num_files);
315	printf("  Data Size:     %lu bytes (%s)\n", (unsigned long)total_data_size, data_size_str);
316	printf("  Archive Size:  %lu bytes (%s)\n", (unsigned long)file_size, archive_size_str);
317	printf("  Header Size:   %lu bytes\n", (unsigned long)sizeof(VfsHeader));
318	printf("  Index Size:    %lu bytes\n", (unsigned long)(sizeof(VfsEntry) * header.num_files));
319
320	free(entries);
321}
322
323typedef struct TreeNode {
324	char* name;
325	struct TreeNode* children;
326	struct TreeNode* next;
327	uint64_t size;
328	int is_file;
329} TreeNode;
330
331TreeNode* create_node(const char* name, int is_file, uint64_t size) {
332	TreeNode* n = calloc(1, sizeof(TreeNode));
333	n->name = strdup(name);
334	n->is_file = is_file;
335	n->size = size;
336	return n;
337}
338
339void add_to_tree(TreeNode* root, const char* path, uint64_t size) {
340	char temp[1024];
341	strncpy(temp, path, 1023);
342	temp[1023] = '\0';
343	char* saveptr;
344	char* token = strtok_r(temp, "/", &saveptr);
345	TreeNode* current = root;
346
347	while (token) {
348		char* next_token = strtok_r(NULL, "/", &saveptr);
349		int is_file = (next_token == NULL);
350
351		TreeNode* child = current->children;
352		TreeNode* prev = NULL;
353		while (child) {
354			if (strcmp(child->name, token) == 0) break;
355			prev = child;
356			child = child->next;
357		}
358
359		if (!child) {
360			child = create_node(token, is_file, is_file ? size : 0);
361			if (prev) prev->next = child;
362			else current->children = child;
363		} else if (!is_file) {
364			// Directory node already exists, continue traversal
365		} else {
366			child->size = size;
367			child->is_file = 1;
368		}
369
370		current = child;
371		token = next_token;
372	}
373}
374
375void print_tree(TreeNode* node, const char* prefix, int is_last) {
376	if (node->name[0] != '\0') {
377		printf("%s%s %s", prefix, is_last ? "└──" : "├──", node->name);
378		if (node->is_file) {
379			char size_str[32];
380			format_size(size_str, sizeof(size_str), node->size);
381			printf(" (%s)", size_str);
382		}
383		printf("\n");
384	}
385
386	char new_prefix[1024];
387	if (node->name[0] == '\0') {
388		new_prefix[0] = '\0';
389	} else {
390		snprintf(new_prefix, sizeof(new_prefix), "%s%s   ", prefix, is_last ? " " : "│");
391	}
392
393	TreeNode* child = node->children;
394	while (child) {
395		print_tree(child, new_prefix, child->next == NULL);
396		child = child->next;
397	}
398}
399
400void free_tree(TreeNode* node) {
401	TreeNode* child = node->children;
402	while (child) {
403		TreeNode* next = child->next;
404		free_tree(child);
405		child = next;
406	}
407	free(node->name);
408	free(node);
409}
410
411void cmd_tree(const char* path) {
412	VfsHeader header;
413	VfsEntry* entries = NULL;
414	if (!read_pak(path, &header, &entries)) {
415		fprintf(stderr, "Error: Could not read pak file %s\n", path);
416		return;
417	}
418
419	TreeNode* root = create_node("", 0, 0);
420	for (uint32_t i = 0; i < header.num_files; i++) {
421		add_to_tree(root, entries[i].path, entries[i].size);
422	}
423
424	printf("Tree view of %s:\n", path);
425	print_tree(root, "", 1);
426
427	free_tree(root);
428	free(entries);
429}
430
431void print_usage(const char* prog) {
432	printf("Usage:\n");
433	printf("  %s -p, --pack <output.pak> <folder1> [folder2] ...\n", prog);
434	printf("  %s -l, --list <input.pak>\n", prog);
435	printf("  %s -t, --tree <input.pak>\n", prog);
436	printf("  %s -i, --info <input.pak>\n", prog);
437	printf("  %s -h, --help\n", prog);
438}
439
440int main(int argc, char** argv) {
441	if (argc < 2) {
442		print_usage(argv[0]);
443		return 1;
444	}
445
446	static struct option long_options[] = {
447		{"pack", no_argument, 0, 'p'},
448		{"list", no_argument, 0, 'l'},
449		{"tree", no_argument, 0, 't'},
450		{"info", no_argument, 0, 'i'},
451		{"help", no_argument, 0, 'h'},
452		{0, 0, 0, 0}
453	};
454
455	int mode = 0;
456	int opt;
457	while ((opt = getopt_long(argc, argv, "pltih", long_options, NULL)) != -1) {
458		switch (opt) {
459			case 'p': mode = 'p'; break;
460			case 'l': mode = 'l'; break;
461			case 't': mode = 't'; break;
462			case 'i': mode = 'i'; break;
463			case 'h': print_usage(argv[0]); return 0;
464			default: print_usage(argv[0]); return 1;
465		}
466	}
467
468	if (mode == 0) {
469		print_usage(argv[0]);
470		return 1;
471	}
472
473	if (optind >= argc) {
474		fprintf(stderr, "Error: Missing target file argument.\n");
475		print_usage(argv[0]);
476		return 1;
477	}
478
479	const char* target = argv[optind];
480
481	switch (mode) {
482		case 'p':
483			cmd_pack(target, argc - optind - 1, argv + optind + 1);
484			break;
485		case 'l':
486			cmd_list(target);
487			break;
488		case 't':
489			cmd_tree(target);
490			break;
491		case 'i':
492			cmd_info(target);
493			break;
494	}
495
496	return 0;
497}