uxn

Varvara Ordinator, written in ANSI C(SDL2)
git clone https://git.eamoncaddigan.net/uxn.git
Log | Files | Refs | README | LICENSE

file.c (9324B)


      1 #define _XOPEN_SOURCE 500
      2 #include <stdio.h>
      3 #include <dirent.h>
      4 #include <errno.h>
      5 #include <limits.h>
      6 #include <string.h>
      7 #include <stdlib.h>
      8 #include <sys/stat.h>
      9 #include <unistd.h>
     10 
     11 #ifdef _WIN32
     12 #include <direct.h>
     13 #include <libiberty/libiberty.h>
     14 #define realpath(s, dummy) lrealpath(s)
     15 #define DIR_SEP_CHAR '\\'
     16 #define DIR_SEP_STR "\\"
     17 #define pathcmp(path1, path2, length) strncasecmp(path1, path2, length) /* strncasecmp provided by libiberty */
     18 #define notdriveroot(file_name) (file_name[0] != DIR_SEP_CHAR && ((strlen(file_name) > 2 && file_name[1] != ':') || strlen(file_name) <= 2))
     19 #define mkdir(file_name) (_mkdir(file_name) == 0)
     20 #else
     21 #define DIR_SEP_CHAR '/'
     22 #define DIR_SEP_STR "/"
     23 #define pathcmp(path1, path2, length) strncmp(path1, path2, length)
     24 #define notdriveroot(file_name) (file_name[0] != DIR_SEP_CHAR)
     25 #define mkdir(file_name) (mkdir(file_name, 0755) == 0)
     26 #endif
     27 
     28 #ifndef PATH_MAX
     29 #define PATH_MAX 4096
     30 #endif
     31 
     32 #include "../uxn.h"
     33 #include "file.h"
     34 
     35 /*
     36 Copyright (c) 2021-2023 Devine Lu Linvega, Andrew Alderwick
     37 
     38 Permission to use, copy, modify, and distribute this software for any
     39 purpose with or without fee is hereby granted, provided that the above
     40 copyright notice and this permission notice appear in all copies.
     41 
     42 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     43 WITH REGARD TO THIS SOFTWARE.
     44 */
     45 
     46 typedef struct {
     47 	FILE *f;
     48 	DIR *dir;
     49 	char current_filename[4096];
     50 	struct dirent *de;
     51 	enum { IDLE,
     52 		FILE_READ,
     53 		FILE_WRITE,
     54 		DIR_READ,
     55 		DIR_WRITE
     56 	} state;
     57 	int outside_sandbox;
     58 } UxnFile;
     59 
     60 static UxnFile uxn_file[POLYFILEY];
     61 
     62 static void
     63 reset(UxnFile *c)
     64 {
     65 	if(c->f != NULL) {
     66 		fclose(c->f);
     67 		c->f = NULL;
     68 	}
     69 	if(c->dir != NULL) {
     70 		closedir(c->dir);
     71 		c->dir = NULL;
     72 	}
     73 	c->de = NULL;
     74 	c->state = IDLE;
     75 	c->outside_sandbox = 0;
     76 }
     77 
     78 static Uint16
     79 get_entry(char *p, Uint16 len, const char *pathname, const char *basename, int fail_nonzero)
     80 {
     81 	struct stat st;
     82 	if(len < strlen(basename) + 8)
     83 		return 0;
     84 	if(stat(pathname, &st))
     85 		return fail_nonzero ? snprintf(p, len, "!!!! %s\n", basename) : 0;
     86 	else if(S_ISDIR(st.st_mode))
     87 		return snprintf(p, len, "---- %s/\n", basename);
     88 	else if(st.st_size < 0x10000)
     89 		return snprintf(p, len, "%04x %s\n", (unsigned int)st.st_size, basename);
     90 	else
     91 		return snprintf(p, len, "???? %s\n", basename);
     92 }
     93 
     94 static Uint16
     95 file_read_dir(UxnFile *c, char *dest, Uint16 len)
     96 {
     97 	static char pathname[4352];
     98 	char *p = dest;
     99 	if(c->de == NULL) c->de = readdir(c->dir);
    100 	for(; c->de != NULL; c->de = readdir(c->dir)) {
    101 		Uint16 n;
    102 		if(c->de->d_name[0] == '.' && c->de->d_name[1] == '\0')
    103 			continue;
    104 		if(strcmp(c->de->d_name, "..") == 0) {
    105 			/* hide "sandbox/.." */
    106 			char cwd[PATH_MAX] = {'\0'}, *t;
    107 			/* Note there's [currently] no way of chdir()ing from uxn, so $PWD
    108 			 * is always the sandbox top level. */
    109 			getcwd(cwd, sizeof(cwd));
    110 			/* We already checked that c->current_filename exists so don't need a wrapper. */
    111 			t = realpath(c->current_filename, NULL);
    112 			if(strcmp(cwd, t) == 0) {
    113 				free(t);
    114 				continue;
    115 			}
    116 			free(t);
    117 		}
    118 		if(strlen(c->current_filename) + 1 + strlen(c->de->d_name) < sizeof(pathname))
    119 			snprintf(pathname, sizeof(pathname), "%s/%s", c->current_filename, c->de->d_name);
    120 		else
    121 			pathname[0] = '\0';
    122 		n = get_entry(p, len, pathname, c->de->d_name, 1);
    123 		if(!n) break;
    124 		p += n;
    125 		len -= n;
    126 	}
    127 	return p - dest;
    128 }
    129 
    130 static char *
    131 retry_realpath(const char *file_name)
    132 {
    133 	char *r, p[PATH_MAX] = {'\0'}, *x;
    134 	int fnlen;
    135 	if(file_name == NULL) {
    136 		errno = EINVAL;
    137 		return NULL;
    138 	} else if((fnlen = strlen(file_name)) >= PATH_MAX) {
    139 		errno = ENAMETOOLONG;
    140 		return NULL;
    141 	}
    142 	if(notdriveroot(file_name)) {
    143 		/* TODO: use a macro instead of '/' for absolute path first character so that other systems can work */
    144 		/* if a relative path, prepend cwd */
    145 		getcwd(p, sizeof(p));
    146 		if(strlen(p) + strlen(DIR_SEP_STR) + fnlen >= PATH_MAX) {
    147 			errno = ENAMETOOLONG;
    148 			return NULL;
    149 		}
    150 		strcat(p, DIR_SEP_STR); /* TODO: use a macro instead of '/' for the path delimiter */
    151 	}
    152 	strcat(p, file_name);
    153 	while((r = realpath(p, NULL)) == NULL) {
    154 		if(errno != ENOENT)
    155 			return NULL;
    156 		x = strrchr(p, DIR_SEP_CHAR); /* TODO: path delimiter macro */
    157 		if(x)
    158 			*x = '\0';
    159 		else
    160 			return NULL;
    161 	}
    162 	return r;
    163 }
    164 
    165 static void
    166 file_check_sandbox(UxnFile *c)
    167 {
    168 	char *x, *rp, cwd[PATH_MAX] = {'\0'};
    169 	x = getcwd(cwd, sizeof(cwd));
    170 	rp = retry_realpath(c->current_filename);
    171 	if(rp == NULL || (x && pathcmp(cwd, rp, strlen(cwd)) != 0)) {
    172 		c->outside_sandbox = 1;
    173 		fprintf(stderr, "file warning: blocked attempt to access %s outside of sandbox\n", c->current_filename);
    174 	}
    175 	free(rp);
    176 }
    177 
    178 static Uint16
    179 file_init(UxnFile *c, char *filename, size_t max_len, int override_sandbox)
    180 {
    181 	char *p = c->current_filename;
    182 	size_t len = sizeof(c->current_filename);
    183 	reset(c);
    184 	if(len > max_len) len = max_len;
    185 	while(len) {
    186 		if((*p++ = *filename++) == '\0') {
    187 			if(!override_sandbox) /* override sandbox for loading roms */
    188 				file_check_sandbox(c);
    189 			return 0;
    190 		}
    191 		len--;
    192 	}
    193 	c->current_filename[0] = '\0';
    194 	return 0;
    195 }
    196 
    197 static Uint16
    198 file_read(UxnFile *c, void *dest, int len)
    199 {
    200 	if(c->outside_sandbox) return 0;
    201 	if(c->state != FILE_READ && c->state != DIR_READ) {
    202 		reset(c);
    203 		if((c->dir = opendir(c->current_filename)) != NULL)
    204 			c->state = DIR_READ;
    205 		else if((c->f = fopen(c->current_filename, "rb")) != NULL)
    206 			c->state = FILE_READ;
    207 	}
    208 	if(c->state == FILE_READ)
    209 		return fread(dest, 1, len, c->f);
    210 	if(c->state == DIR_READ)
    211 		return file_read_dir(c, dest, len);
    212 	return 0;
    213 }
    214 
    215 static int
    216 is_dir_path(char *p)
    217 {
    218 	char c;
    219 	int saw_slash = 0;
    220 	while((c = *p++))
    221 		saw_slash = c == DIR_SEP_CHAR;
    222 	return saw_slash;
    223 }
    224 
    225 int
    226 dir_exists(char *p)
    227 {
    228 	struct stat st;
    229 	return stat(p, &st) == 0 && S_ISDIR(st.st_mode);
    230 }
    231 
    232 int
    233 ensure_parent_dirs(char *p)
    234 {
    235 	int ok = 1;
    236 	char c, *s = p;
    237 	for(; ok && (c = *p); p++) {
    238 		if(c == DIR_SEP_CHAR) {
    239 			*p = '\0';
    240 			ok = dir_exists(s) || mkdir(s);
    241 			*p = c;
    242 		}
    243 	}
    244 	return ok;
    245 }
    246 
    247 static Uint16
    248 file_write(UxnFile *c, void *src, Uint16 len, Uint8 flags)
    249 {
    250 	Uint16 ret = 0;
    251 	if(c->outside_sandbox) return 0;
    252 	ensure_parent_dirs(c->current_filename);
    253 	if(c->state != FILE_WRITE && c->state != DIR_WRITE) {
    254 		reset(c);
    255 		if(is_dir_path(c->current_filename))
    256 			c->state = DIR_WRITE;
    257 		else if((c->f = fopen(c->current_filename, (flags & 0x01) ? "ab" : "wb")) != NULL)
    258 			c->state = FILE_WRITE;
    259 	}
    260 	if(c->state == FILE_WRITE) {
    261 		if((ret = fwrite(src, 1, len, c->f)) > 0 && fflush(c->f) != 0)
    262 			ret = 0;
    263 	}
    264 	if(c->state == DIR_WRITE) {
    265 		ret = dir_exists(c->current_filename);
    266 	}
    267 	return ret;
    268 }
    269 
    270 static Uint16
    271 stat_fill(Uint8 *dest, Uint16 len, char c)
    272 {
    273 	Uint16 i;
    274 	for(i = 0; i < len; i++)
    275 		*(dest++) = c;
    276 	return len;
    277 }
    278 
    279 static Uint16
    280 stat_size(Uint8 *dest, Uint16 len, off_t size)
    281 {
    282 	Uint16 i;
    283 	dest += len - 1;
    284 	for(i = 0; i < len; i++) {
    285 		char c = '0' + (Uint8)(size & 0xf);
    286 		if(c > '9') c += 39;
    287 		*(dest--) = c;
    288 		size = size >> 4;
    289 	}
    290 	return size == 0 ? len : stat_fill(dest, len, '?');
    291 }
    292 
    293 static Uint16
    294 file_stat(UxnFile *c, void *dest, Uint16 len)
    295 {
    296 	struct stat st;
    297 	if(c->outside_sandbox)
    298 		return 0;
    299 	else if(stat(c->current_filename, &st))
    300 		return stat_fill(dest, len, '!');
    301 	else if(S_ISDIR(st.st_mode))
    302 		return stat_fill(dest, len, '-');
    303 	else
    304 		return stat_size(dest, len, st.st_size);
    305 }
    306 
    307 static Uint16
    308 file_delete(UxnFile *c)
    309 {
    310 	return c->outside_sandbox ? 0 : unlink(c->current_filename);
    311 }
    312 
    313 /* IO */
    314 
    315 void
    316 file_deo(Uint8 port)
    317 {
    318 	Uint16 addr, len, res;
    319 	switch(port) {
    320 	case 0xa5:
    321 		addr = PEEK2(&uxn.dev[0xa4]);
    322 		len = PEEK2(&uxn.dev[0xaa]);
    323 		if(len > 0x10000 - addr)
    324 			len = 0x10000 - addr;
    325 		res = file_stat(&uxn_file[0], &uxn.ram[addr], len);
    326 		POKE2(&uxn.dev[0xa2], res);
    327 		break;
    328 	case 0xa6:
    329 		res = file_delete(&uxn_file[0]);
    330 		POKE2(&uxn.dev[0xa2], res);
    331 		break;
    332 	case 0xa9:
    333 		addr = PEEK2(&uxn.dev[0xa8]);
    334 		res = file_init(&uxn_file[0], (char *)&uxn.ram[addr], 0x10000 - addr, 0);
    335 		POKE2(&uxn.dev[0xa2], res);
    336 		break;
    337 	case 0xad:
    338 		addr = PEEK2(&uxn.dev[0xac]);
    339 		len = PEEK2(&uxn.dev[0xaa]);
    340 		if(len > 0x10000 - addr)
    341 			len = 0x10000 - addr;
    342 		res = file_read(&uxn_file[0], &uxn.ram[addr], len);
    343 		POKE2(&uxn.dev[0xa2], res);
    344 		break;
    345 	case 0xaf:
    346 		addr = PEEK2(&uxn.dev[0xae]);
    347 		len = PEEK2(&uxn.dev[0xaa]);
    348 		if(len > 0x10000 - addr)
    349 			len = 0x10000 - addr;
    350 		res = file_write(&uxn_file[0], &uxn.ram[addr], len, uxn.dev[0xa7]);
    351 		POKE2(&uxn.dev[0xa2], res);
    352 		break;
    353 	/* File 2 */
    354 	case 0xb5:
    355 		addr = PEEK2(&uxn.dev[0xb4]);
    356 		len = PEEK2(&uxn.dev[0xba]);
    357 		if(len > 0x10000 - addr)
    358 			len = 0x10000 - addr;
    359 		res = file_stat(&uxn_file[1], &uxn.ram[addr], len);
    360 		POKE2(&uxn.dev[0xb2], res);
    361 		break;
    362 	case 0xb6:
    363 		res = file_delete(&uxn_file[1]);
    364 		POKE2(&uxn.dev[0xb2], res);
    365 		break;
    366 	case 0xb9:
    367 		addr = PEEK2(&uxn.dev[0xb8]);
    368 		res = file_init(&uxn_file[1], (char *)&uxn.ram[addr], 0x10000 - addr, 0);
    369 		POKE2(&uxn.dev[0xb2], res);
    370 		break;
    371 	case 0xbd:
    372 		addr = PEEK2(&uxn.dev[0xbc]);
    373 		len = PEEK2(&uxn.dev[0xba]);
    374 		if(len > 0x10000 - addr)
    375 			len = 0x10000 - addr;
    376 		res = file_read(&uxn_file[1], &uxn.ram[addr], len);
    377 		POKE2(&uxn.dev[0xb2], res);
    378 		break;
    379 	case 0xbf:
    380 		addr = PEEK2(&uxn.dev[0xbe]);
    381 		len = PEEK2(&uxn.dev[0xba]);
    382 		if(len > 0x10000 - addr)
    383 			len = 0x10000 - addr;
    384 		res = file_write(&uxn_file[1], &uxn.ram[addr], len, uxn.dev[0xb7]);
    385 		POKE2(&uxn.dev[0xb2], res);
    386 		break;
    387 	}
    388 }