1 /**
2  * diskmanager.c
3  *
4  * Copyright (C) 2018 by David McNaughton
5  *
6  * History:
7  * 12-JUL-18    1.0     Initial Release
8  */
9 
10 /**
11  * This diskmanager module provides an alternate interface for the array
12  *
13  *      char *disks[];
14  *
15  * that is typically implemented in a disk controller simulators e.g.
16  *      - imsai-fif.c
17  *      - tarbell_fdc.c
18  *
19  * It persists the array in a file, typically "disk.map" [DISKMAP] in the
20  * provided path.
21  *
22  * The "disk.map" file is a simple text file with a line for each disk
23  * starting at 'A' upto [LAST_DISK], typically 'D'.
24  *      - If a line is empty of starts with '#' the disk is "ejected"
25  *      - If a disk image is "inserted" the line conatains only the file name
26  *
27  * The diskmanager provides functions to:
28  *      - populate the array from the file
29  *      - write the array to the file
30  *      - insert a disk
31  *          - stat() disk image files to validate them before inserting
32  *          - reject inserting the same disk image in 2 disk drives
33  *      - eject a disk
34  *      - and some other support functions.
35  *
36  * TODO:
37  *      - fully support paths to allow a disk libabry hierarchy
38  *      - support more complex disk arrays that conatin structures e.g.
39  *          * cpmsim::iosim.c
40  *          * cromemco-fdc.c
41  */
42 
43 #include <unistd.h>
44 #include <stdio.h>
45 #include <stdlib.h>
46 #include <errno.h>
47 #include <string.h>
48 #include <sys/stat.h>
49 #define LOCAL_LOG_LEVEL LOG_DEBUG
50 #include "log.h"
51 #include "sim.h"
52 #ifdef HAS_NETSERVER
53 #include "civetweb.h"
54 #include "netsrv.h"
55 #endif
56 
57 
58 #ifdef HAS_DISKMANAGER
59 #ifndef UNUSED
60 #define UNUSED(x) (void)(x)
61 #endif
62 
63 #ifndef DISKMAP
64 #define DISKMAP     "disk.map"
65 #endif
66 
67 #define LAST_DISK   'D'
68 #define _MAX_DISK   (LAST_DISK - '@')
69 
70 static const char *TAG = "diskmanager";
71 
72 char *disks[_MAX_DISK];
73 extern char *disks[];
74 
75 static char path[MAX_LFN+1];		/* path/filename for disk image */
76 static char *file_start;
77 #define APPENDTOPATH(file) strncpy(file_start, file, MAX_LFN - strlen(path));
78 
79 enum disk_err {
80     SUCCESS,
81     INVALID_DISK_NUM,
82     DRIVE_EMPTY,
83     DRIVE_NOT_EMPTY,
84     IMAGE_ALREADY_INSERTED,
85     IMAGE_NOT_VALID,
86     FAILURE
87 };
88 
89 typedef enum disk_err disk_err_t;
90 
findDiskImage(const char * image)91 int findDiskImage(const char *image) {
92     int i;
93 
94     for (i = 0; i < _MAX_DISK; i++) {
95         if (disks[i] != NULL) {
96             if (strcmp(image, disks[i]) == 0) {
97                 return 1;
98             }
99         }
100     }
101     return 0;
102 }
103 
getDiskNumByID(const char * id)104 int getDiskNumByID(const char *id) {
105     int disk = -1;
106 
107     /* id is in the form X:DSK: making it 6 characters long */
108     LOGD(TAG, "GET ID: %s, %ld", id, strlen(id));
109     if (id != NULL && strlen(id) == 6 && strcmp(id+1, ":DSK:") == 0) {
110         disk = *id - 'A';
111         if (disk < 0 || disk > (_MAX_DISK - 1)) {
112             LOGW(TAG, "BAD DISK ID: %c", *id);
113             disk = -1;
114         }
115     } else {
116         LOGW(TAG, "DISK ID required, got: %s", id);
117         disk = -1;
118     }
119     return disk;
120 }
121 
insertDisk(int disk,const char * image)122 disk_err_t insertDisk(int disk, const char *image) {
123 
124     char *name;
125     struct stat image_status;
126     int i;
127 
128     if (disk >= 0 && disk < _MAX_DISK) {
129 
130         if (disks[disk] != NULL) {
131             return DRIVE_NOT_EMPTY;
132         }
133 
134         if (image != NULL && strlen(image) < MAX_LFN) {
135 
136             for (i = 0; i < _MAX_DISK; i++) {
137                 if (disks[i] != NULL && strcmp(image, disks[i]) == 0) {
138                     return IMAGE_ALREADY_INSERTED;
139                 }
140             }
141 
142             APPENDTOPATH(image);
143 
144             if (stat(path, &image_status) == 0) {
145                 if (S_ISREG(image_status.st_mode)) {
146                     name = strndup(image, MAX_LFN);
147                     if (name == NULL) {
148                         LOGW(TAG, "Failed to insert disk, not enough memory.");
149                         return FAILURE;
150                     } else {
151                         /* Everything is OK, we can insert the disk */
152                         disks[disk] = name;
153                         return SUCCESS;
154                     }
155                 } else {
156                     return IMAGE_NOT_VALID;
157                 }
158             } else {
159                 LOGW(TAG, "Failed to stat disk image: %s, error: %d", image, errno);
160                 return IMAGE_NOT_VALID;
161             }
162         }
163 
164         return IMAGE_NOT_VALID;
165     }
166 
167     return INVALID_DISK_NUM;
168 }
169 
ejectDisk(int disk)170 disk_err_t ejectDisk(int disk) {
171 
172     char *name;
173 
174     if (disk >= 0 || disk < _MAX_DISK) {
175 
176         if (disks[disk] == NULL) {
177             return DRIVE_EMPTY;
178         }
179 
180         name = disks[disk];
181         disks[disk] = NULL;
182         free(name);
183 
184         return SUCCESS;
185     }
186 
187     return INVALID_DISK_NUM;
188 }
189 
writeDiskmap(void)190 void writeDiskmap(void) {
191     FILE *map;
192     int i;
193 
194     if (*path == '\0') {
195          LOGW(TAG, "Path to disk map not set. Call readDiskmap(path) first.");
196         return;
197     }
198 
199     APPENDTOPATH(DISKMAP);
200 
201     map = fopen(path, "w");
202     if (map == NULL) {
203         LOGW(TAG, "Can't create disk map: %s", path);
204         return;
205     }
206 
207     for (i = 0; i < _MAX_DISK; i++) {
208         fprintf(map, "%s\n", disks[i]==NULL?"#":disks[i]);
209     }
210     fclose(map);
211 }
212 
readDiskmap(char * path_name)213 void readDiskmap(char *path_name) {
214     FILE *map;
215     char *line = NULL;
216     char *name;
217     size_t len;
218     ssize_t res;
219     int i;
220     disk_err_t insert;
221 
222     for (i = 0; i < _MAX_DISK; i++) {
223         disks[i] = NULL;
224     }
225 
226     strncpy(path, path_name, MAX_LFN);
227     strncat(path, "/", MAX_LFN - strlen(path));
228     file_start = path + strlen(path);
229 
230     APPENDTOPATH(DISKMAP);
231     LOGD(TAG, "LIB: path: %s, diskmap: %s", path, file_start);
232 
233     i = 0;
234 again:
235     map = fopen(path, "r");
236     if (map == NULL) {
237         if (i++ == 0) {
238             LOGW(TAG, "No disk map: %s, attempting to create file.", path);
239             writeDiskmap();
240             goto again;
241         }
242         return;
243     }
244 
245     LOG(TAG, "DISK MAP: [%s]\r\n", path);
246 
247     for (i = 0; i < _MAX_DISK; i++) {
248         line = NULL;
249         res = getline(&line, &len, map);
250 
251         if (res != -1) {
252             if (line[res-1] == '\n')
253                 line[res-1] = '\0';
254 
255             /* empty lines or lines that begin with # are empty disks */
256             name = ((line[0]=='\0') || (line[0]=='#'))?NULL:line;
257             if (name != NULL) {
258                 insert = insertDisk(i, name);
259 
260                 switch (insert) {
261                     case SUCCESS :
262                         LOG(TAG, "%c:DSK:='%s'\r\n", i+'A', disks[i]);
263                         break;
264                     case IMAGE_ALREADY_INSERTED :
265                         LOGW(TAG, "%c:DSK: Image file '%s' already in use", i+'A', name);
266                         break;
267                     case IMAGE_NOT_VALID :
268                         LOGW(TAG, "%c:DSK: Image file '%s' is invalid", i+'A', name);
269                         break;
270                     default:
271                         LOGW(TAG, "%c:DSK: Failed to insert disk, error: %d", i+'A', insert)
272                         break;
273                 }
274             }
275         }
276         free(line);
277     }
278     fclose(map);
279 }
280 
281 #ifdef HAS_NETSERVER
282 /**
283  * Web Server handlers for LIB: and X:DSK:
284  *
285  */
286 extern int DirectoryHandler(struct mg_connection *, void *);
287 extern int UploadHandler(struct mg_connection *, void *);
288 
LibraryHandler(HttpdConnection_t * conn,void * unused)289 int LibraryHandler(HttpdConnection_t *conn, void *unused) {
290     request_t *req = get_request(conn);
291 	int i = 0;
292     UNUSED(unused);
293 
294     *file_start = '\0';
295 
296     if (*path == '\0') {
297          LOGW(TAG, "Path to disk map not set. Call readDiskmap(path) first.");
298         return 0;
299     }
300 
301     switch(req->method) {
302     case HTTP_GET:
303         DirectoryHandler(conn, path);
304         break;
305     case HTTP_PUT:
306         UploadHandler(conn, path);
307         LOGI(TAG, "PUT image: image uploaded.");
308         break;
309     case HTTP_DELETE:
310         if (req->len > 0) {
311             i = mg_read(conn, file_start, MAX_LFN - strlen(path));
312             *(file_start + i) = '\0';
313 
314             LOGD(TAG, "DELETE image: %s, length: %lld", file_start, req->len);
315 
316             if (findDiskImage(file_start)) {
317                 LOGW(TAG, "DELETE image: %s, currently inserted in disks", file_start);
318                 httpdStartResponse(conn, 404);  /* http error code 'Not Found' */
319                 httpdEndHeaders(conn);
320                 return 1;
321             }
322 
323             if (unlink(path) < 0) {
324                 LOGW(TAG, "DELETE image: %s, unlink failed [%d]", path, errno);
325                 httpdStartResponse(conn, 410);  /* http error code 'Gone' */
326                 httpdEndHeaders(conn);
327             } else {
328                 LOGI(TAG, "DELETE image: %s, deleted.", path);
329                 httpdStartResponse(conn, 200);
330                 httpdEndHeaders(conn);
331                 httpdPrintf(conn, "Deleted");
332             };
333         }
334         break;
335     default:
336         httpdStartResponse(conn, 405);  /* http error code 'Method Not Allowed' */
337         httpdEndHeaders(conn);
338         break;
339     }
340 	return 1;
341 }
342 
sendDisks(struct mg_connection * conn)343 static void sendDisks(struct mg_connection *conn) {
344 
345     int i;
346 
347     httpdStartResponse(conn, 200);
348     httpdHeader(conn, "Content-Type", "application/json");
349     httpdEndHeaders(conn);
350 
351     httpdPrintf(conn, "{");
352 
353     for (i = 0; i < _MAX_DISK; i++) {
354         httpdPrintf(conn, "\"%c\": \"%s\"", i+'A', disks[i]==NULL?"":disks[i]);
355         if (i < (_MAX_DISK - 1)) httpdPrintf(conn, ",");
356     }
357 
358     httpdPrintf(conn, "}");
359 }
360 
DiskHandler(HttpdConnection_t * conn,void * unused)361 int DiskHandler(HttpdConnection_t *conn, void *unused) {
362     request_t *req = get_request(conn);
363     int disk, i;
364     char image[MAX_LFN];
365     disk_err_t result;
366     UNUSED(unused);
367 
368     switch(req->method) {
369     case HTTP_GET:
370         LOGD(TAG, "GET /disks");
371         sendDisks(conn);
372         break;
373     case HTTP_PUT:
374         LOGD(TAG, "PUT /disks: %s", req->args[0]);
375 
376         disk = getDiskNumByID(req->args[0]);
377         image[0] = '\0';
378 
379         if (req->len > 0) {
380             i = mg_read(conn, image, MAX_LFN);
381             image[i] = '\0';
382         };
383 
384         LOGD(TAG, "PUT image length: %d [%s]", (int) req->len, image);
385 
386         result = insertDisk(disk, image);
387 
388         switch (result) {
389             case SUCCESS :
390                 writeDiskmap();
391                 sendDisks(conn);
392                 break;
393             case DRIVE_NOT_EMPTY :
394                 LOGW(TAG, "PUT /disks NOT EMPTY");
395                 httpdStartResponse(conn, 404);  /* http error code 'Not Found' */
396                 httpdEndHeaders(conn);
397                 break;
398             case IMAGE_ALREADY_INSERTED :
399                 LOGW(TAG, "PUT image: %s, already inserted in disks", image);
400                 httpdStartResponse(conn, 404);  /* http error code 'Not Found' */
401                 httpdEndHeaders(conn);
402                 break;
403             default:
404                 LOGW(TAG, "PUT image: %s, failed to insert disk: %d, error: %d", image, disk, result)
405                 httpdStartResponse(conn, 404);  /* http error code 'Not Found' */
406                 httpdEndHeaders(conn);
407                 break;
408         }
409         break;
410     case HTTP_DELETE:
411         LOGD(TAG, "DELETE /disks: %s", req->args[0]);
412 
413         disk = getDiskNumByID(req->args[0]);
414         result = ejectDisk(disk);
415 
416         switch (result) {
417             case SUCCESS :
418                 sendDisks(conn);
419                 writeDiskmap();
420                 break;
421             case DRIVE_EMPTY :
422                 LOGW(TAG, "DELETE /disks ALREADY EMPTY");
423                 httpdStartResponse(conn, 404);  /* http error code 'Not Found' */
424                 httpdEndHeaders(conn);
425                 break;
426             default:
427                 LOGW(TAG, "DELETE /disks failed to eject disk: %d, error: %d", disk, result)
428                 httpdStartResponse(conn, 404);  /* http error code 'Not Found' */
429                 httpdEndHeaders(conn);
430                 break;
431         }
432         break;
433     default:
434         httpdStartResponse(conn, 405);  /* http error code 'Method Not Allowed' */
435         httpdEndHeaders(conn);
436 	}
437 
438     return 1;
439 }
440 #endif
441 #endif
442