diff --git a/sdbsc.c b/sdbsc.c new file mode 100644 index 0000000000000000000000000000000000000000..c2a8516d8aec1ff45d8330399e62f05aeac51ca5 --- /dev/null +++ b/sdbsc.c @@ -0,0 +1,573 @@ +#include <stdio.h> +#include <stdlib.h> +#include <fcntl.h> // for open() +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#include <stdbool.h> +#include <errno.h> + + +#define MIN_STD_ID 1 +#define MAX_STD_ID 100000 +#define MIN_STD_GPA 0 +#define MAX_STD_GPA 400 + +/* Filenames for the active database and temporary (compressed) db */ +#define DB_FILE "student.db" +#define TMP_DB_FILE ".tmp_student.db" + +/* Our student record is exactly 64 bytes. + We store an integer id (4 bytes), an integer gpa (4 bytes) and two fixed-length strings. + Using 28 bytes each for first and last names gives: + 4 + 4 + 28 + 28 = 64 bytes. +*/ +typedef struct{ + int id; // 4 bytes + int gpa; // 4 bytes; store gpa as an integer (e.g. 345 means 3.45) + char fname[28]; // 28 bytes + char lname[28]; // 28 bytes +} student_t; + +#define RECORD_SIZE (sizeof(student_t)) +#define DB_SIZE (100000 * RECORD_SIZE) + +/* Exit codes: note that all error cases exit with status 1 */ +#define EXIT_OK 0 +#define EXIT_FAIL_ARGS 1 +#define EXIT_FAIL_DB 1 + +/* Function return codes */ +#define NO_ERROR 0 +#define ERR_DB_FILE -1 +#define ERR_DB_OP -2 +#define SRCH_NOT_FOUND -3 +#define NOT_IMPLEMENTED_YET -4 + +/* Messages (exact text as expected by the tests) */ +#define M_ERR_DB_OPEN "Error opening database file.\n" +#define M_ERR_DB_READ "Error reading from database file.\n" +#define M_ERR_DB_WRITE "Error writing to database file.\n" +#define M_ERR_DB_ADD_DUP "Cant add student with ID=%d, already exists in db.\n" +#define M_STD_ADDED "Student %d added to database.\n" +#define M_STD_DEL_MSG "Student %d was deleted from database.\n" +#define M_STD_NOT_FND_MSG "Student %d was not found in database.\n" +#define M_ERR_STD_RNG "Error: Student ID or GPA out of range.\n" +#define M_ERR_STD_PRINT "Error: Cannot print invalid student.\n" +#define M_DB_EMPTY "Database contains no student records.\n" +#define M_DB_RECORD_CNT "Database contains %d student record(s).\n" +#define M_DB_ZERO_OK "Database file zeroed.\n" +#define M_DB_COMPRESSED_OK "Database successfully compressed!\n" + +/* Strings used for printing a student record. + Note: the header and record formatting must match the test expectations. */ +#define STUDENT_PRINT_HDR_STRING "ID FIRST_NAME LAST_NAME GPA\n" +#define STUDENT_PRINT_FMT_STRING "%d %s %s %.2f\n" + + +int open_db(char *dbFile, bool should_truncate); +int get_student(int fd, int id, student_t *s); +int add_student(int fd, int id, char *fname, char *lname, int gpa); +int del_student(int fd, int id); +int count_db_records(int fd); +int print_db(int fd); +void print_student(student_t *s); +int compress_db(int fd); +int validate_range(int id, int gpa); +void usage(char *exename); + +/* + * open_db + * Opens (or creates) the database file with read/write access. + * If should_truncate is true the file is re-created (zeroed) and then pre-allocated to DB_SIZE bytes. + * Otherwise, if the file is new (or not already DB_SIZE bytes long) it is extended (with zeros) to DB_SIZE bytes. + * + * Returns the file descriptor on success, or ERR_DB_FILE on failure. + * On error prints M_ERR_DB_OPEN. + */ +int open_db(char *dbFile, bool should_truncate) +{ + // Set permissions: rw-rw---- + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; + + // Open for read/write and create if necessary. + int flags = O_RDWR | O_CREAT; + if (should_truncate) + flags |= O_TRUNC; + + int fd = open(dbFile, flags, mode); + if (fd == -1) + { + printf(M_ERR_DB_OPEN); + return ERR_DB_FILE; + } + + // If the file was just created or we are truncating, preallocate to DB_SIZE bytes. + struct stat st; + if (fstat(fd, &st) == -1) + { + printf(M_ERR_DB_READ); + close(fd); + return ERR_DB_FILE; + } + if (st.st_size != DB_SIZE) + { + if (ftruncate(fd, DB_SIZE) == -1) + { + printf(M_ERR_DB_WRITE); + close(fd); + return ERR_DB_FILE; + } + } + return fd; +} + +/* + * get_student + * Reads the student record for the given id. + * If the record is empty (all zeros) returns SRCH_NOT_FOUND. + * + * Returns NO_ERROR on success, ERR_DB_FILE on I/O error, or SRCH_NOT_FOUND if not found. + */ +int get_student(int fd, int id, student_t *s) +{ + off_t offset = (id - MIN_STD_ID) * RECORD_SIZE; + if (lseek(fd, offset, SEEK_SET) == -1) + return ERR_DB_FILE; + ssize_t bytesRead = read(fd, s, RECORD_SIZE); + if (bytesRead != RECORD_SIZE) + return ERR_DB_FILE; // unexpected read error + + student_t empty; + memset(&empty, 0, RECORD_SIZE); + if (memcmp(s, &empty, RECORD_SIZE) == 0) + return SRCH_NOT_FOUND; + return NO_ERROR; +} + +/* + * add_student + * Adds a student to the database at the slot corresponding to its id. + * If the slot is already occupied, prints an error and returns ERR_DB_OP. + * + * Returns NO_ERROR on success, ERR_DB_FILE on I/O error, or ERR_DB_OP if student exists. + */ +int add_student(int fd, int id, char *fname, char *lname, int gpa) +{ + off_t offset = (id - MIN_STD_ID) * RECORD_SIZE; + student_t existing; + + /* Seek to the proper offset in the file */ + if (lseek(fd, offset, SEEK_SET) == -1) { + printf(M_ERR_DB_READ); + return ERR_DB_FILE; + } + + /* Read the existing record */ + if (read(fd, &existing, RECORD_SIZE) != RECORD_SIZE) { + printf(M_ERR_DB_READ); + return ERR_DB_FILE; + } + + /* Create an empty record to compare against */ + student_t empty; + memset(&empty, 0, RECORD_SIZE); + + /* If the record is not empty, a student already exists */ + if (memcmp(&existing, &empty, RECORD_SIZE) != 0) { + printf(M_ERR_DB_ADD_DUP, id); + return ERR_DB_OP; + } + + /* Prepare the new student record */ + student_t new_student; + memset(&new_student, 0, RECORD_SIZE); + new_student.id = id; + new_student.gpa = gpa; + + /* Copy the names safely into the record */ + strncpy(new_student.fname, fname, sizeof(new_student.fname) - 1); + new_student.fname[sizeof(new_student.fname) - 1] = '\0'; + strncpy(new_student.lname, lname, sizeof(new_student.lname) - 1); + new_student.lname[sizeof(new_student.lname) - 1] = '\0'; + + /* Seek back to the record location and write the new record */ + if (lseek(fd, offset, SEEK_SET) == -1) { + printf(M_ERR_DB_WRITE); + return ERR_DB_FILE; + } + if (write(fd, &new_student, RECORD_SIZE) != RECORD_SIZE) { + printf(M_ERR_DB_WRITE); + return ERR_DB_FILE; + } + + /* Success */ + printf(M_STD_ADDED, id); + return NO_ERROR; +} + +/* + * del_student + * Deletes the student with the given id by writing an empty record (all zeros) in its place. + * If the student does not exist, prints an error. + * + * Returns NO_ERROR on success, ERR_DB_FILE on I/O error, or ERR_DB_OP if student not found. + */ +int del_student(int fd, int id) +{ + student_t student; + int rc = get_student(fd, id, &student); + if (rc == SRCH_NOT_FOUND) + { + printf(M_STD_NOT_FND_MSG, id); + return ERR_DB_OP; + } + else if (rc != NO_ERROR) + { + printf(M_ERR_DB_READ); + return ERR_DB_FILE; + } + student_t empty; + memset(&empty, 0, RECORD_SIZE); + off_t offset = (id - MIN_STD_ID) * RECORD_SIZE; + if (lseek(fd, offset, SEEK_SET) == -1) + { + printf(M_ERR_DB_WRITE); + return ERR_DB_FILE; + } + if (write(fd, &empty, RECORD_SIZE) != RECORD_SIZE) + { + printf(M_ERR_DB_WRITE); + return ERR_DB_FILE; + } + printf(M_STD_DEL_MSG, id); + return NO_ERROR; +} + +/* + * count_db_records + * Scans through the entire database file (all 100000 records) and counts the number of valid (non‑empty) records. + * + * If count is zero, prints M_DB_EMPTY; otherwise prints M_DB_RECORD_CNT with the count. + * + * Returns the count on success or ERR_DB_FILE on error. + */ +int count_db_records(int fd) +{ + if (lseek(fd, 0, SEEK_SET) == -1) + { + printf(M_ERR_DB_READ); + return ERR_DB_FILE; + } + int count = 0; + student_t student; + student_t empty; + memset(&empty, 0, RECORD_SIZE); + ssize_t bytesRead; + for (int i = 0; i < 100000; i++) { + bytesRead = read(fd, &student, RECORD_SIZE); + if (bytesRead != RECORD_SIZE) + { + printf(M_ERR_DB_READ); + return ERR_DB_FILE; + } + if (memcmp(&student, &empty, RECORD_SIZE) != 0) + count++; + } + if (count == 0) + printf(M_DB_EMPTY); + else + printf(M_DB_RECORD_CNT, count); + return count; +} + +/* + * print_db + * Scans the entire database file and prints all valid student records. + * For the first valid record found, prints the header first. + * + * Returns NO_ERROR on success, or ERR_DB_FILE on error. + */ +int print_db(int fd) +{ + if (lseek(fd, 0, SEEK_SET) == -1) + { + printf(M_ERR_DB_READ); + return ERR_DB_FILE; + } + student_t student; + student_t empty; + memset(&empty, 0, RECORD_SIZE); + bool headerPrinted = false; + bool anyRecord = false; + ssize_t bytesRead; + for (int i = 0; i < 100000; i++) { + bytesRead = read(fd, &student, RECORD_SIZE); + if (bytesRead != RECORD_SIZE) + { + printf(M_ERR_DB_READ); + return ERR_DB_FILE; + } + if (memcmp(&student, &empty, RECORD_SIZE) != 0) + { + if (!headerPrinted) + { + printf(STUDENT_PRINT_HDR_STRING); + headerPrinted = true; + } + float realGpa = student.gpa / 100.0; + printf(STUDENT_PRINT_FMT_STRING, student.id, student.fname, student.lname, realGpa); + anyRecord = true; + } + } + if (!anyRecord) + printf(M_DB_EMPTY); + return NO_ERROR; +} + +/* + * print_student + * If the provided student pointer is valid (non-NULL and id nonzero), prints the header then the record. + * Otherwise prints M_ERR_STD_PRINT. + */ +void print_student(student_t *s) +{ + if (s == NULL || s->id == 0) + { + printf(M_ERR_STD_PRINT); + return; + } + printf(STUDENT_PRINT_HDR_STRING); + float realGpa = s->gpa / 100.0; + printf(STUDENT_PRINT_FMT_STRING, s->id, s->fname, s->lname, realGpa); +} + +/* + * compress_db + * (Extra credit) Compresses the database file by copying only valid records into a temporary file, + * then renames that file to be the active database. + * + * Returns the file descriptor of the new database on success, or ERR_DB_FILE on failure. + */ +int compress_db(int fd) +{ + // Open temporary file. + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; + int tmp_fd = open(TMP_DB_FILE, O_RDWR | O_CREAT | O_TRUNC, mode); + if (tmp_fd == -1) + { + printf(M_ERR_DB_OPEN); + return ERR_DB_FILE; + } + + if (lseek(fd, 0, SEEK_SET) == -1) + { + printf(M_ERR_DB_READ); + close(tmp_fd); + return ERR_DB_FILE; + } + student_t student; + student_t empty; + memset(&empty, 0, RECORD_SIZE); + ssize_t bytesRead; + // Copy only valid records. + for (int i = 0; i < 100000; i++) { + bytesRead = read(fd, &student, RECORD_SIZE); + if (bytesRead != RECORD_SIZE) + { + printf(M_ERR_DB_READ); + close(tmp_fd); + return ERR_DB_FILE; + } + if (memcmp(&student, &empty, RECORD_SIZE) != 0) + { + if (write(tmp_fd, &student, RECORD_SIZE) != RECORD_SIZE) + { + printf(M_ERR_DB_WRITE); + close(tmp_fd); + return ERR_DB_FILE; + } + } + } + close(fd); + close(tmp_fd); + // Rename the temporary file to the active database file. + if (rename(TMP_DB_FILE, DB_FILE) == -1) + { + printf(M_ERR_DB_WRITE); + return ERR_DB_FILE; + } + int new_fd = open_db(DB_FILE, false); + if (new_fd < 0) + { + printf(M_ERR_DB_OPEN); + return ERR_DB_FILE; + } + printf(M_DB_COMPRESSED_OK); + return new_fd; +} + +/* + * validate_range + * Validates that the provided id and gpa are within the allowed ranges. + * + * Returns NO_ERROR if valid, or EXIT_FAIL_ARGS otherwise. + */ +int validate_range(int id, int gpa) +{ + if ((id < MIN_STD_ID) || (id > MAX_STD_ID)) + return EXIT_FAIL_ARGS; + if ((gpa < MIN_STD_GPA) || (gpa > MAX_STD_GPA)) + return EXIT_FAIL_ARGS; + return NO_ERROR; +} + +/* + * usage + * Prints the proper usage for this program. + */ +void usage(char *exename) +{ + printf("usage: %s -[h|a|c|d|f|p|x|z] options. Where:\n", exename); + printf("\t-h: prints help\n"); + printf("\t-a id first_name last_name gpa(as 3 digit int): adds a student\n"); + printf("\t-c: counts the records in the database\n"); + printf("\t-d id: deletes a student\n"); + printf("\t-f id: finds and prints a student in the database\n"); + printf("\t-p: prints all records in the student database\n"); + printf("\t-x: compress the database file [EXTRA CREDIT]\n"); + printf("\t-z: zero db file (remove all records)\n"); +} + +/*============================================================*/ +/* MAIN */ +/*============================================================*/ +int main(int argc, char *argv[]) +{ + char opt; + int fd; + int rc; + int exit_code = EXIT_OK; + int id, gpa; + student_t student = {0}; + + if (argc < 2 || argv[1][0] != '-') + { + usage(argv[0]); + exit(EXIT_FAIL_ARGS); + } + + opt = argv[1][1]; + + if (opt == 'h') + { + usage(argv[0]); + exit(EXIT_OK); + } + + /* For most options open the db without truncation. + (For -z we will reopen with truncation.) */ + fd = open_db(DB_FILE, false); + if (fd < 0) + exit(EXIT_FAIL_DB); + + switch(opt) + { + case 'a': + // Expect: prog -a id first_name last_name gpa + if (argc != 6) + { + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + break; + } + id = atoi(argv[2]); + gpa = atoi(argv[5]); + if (validate_range(id, gpa) != NO_ERROR) + { + printf(M_ERR_STD_RNG); + exit_code = EXIT_FAIL_ARGS; + break; + } + rc = add_student(fd, id, argv[3], argv[4], gpa); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'c': + rc = count_db_records(fd); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'd': + if (argc != 3) + { + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + break; + } + id = atoi(argv[2]); + rc = del_student(fd, id); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'f': + if (argc != 3) + { + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + break; + } + id = atoi(argv[2]); + rc = get_student(fd, id, &student); + if (rc == NO_ERROR) + print_student(&student); + else if (rc == SRCH_NOT_FOUND) + { + printf(M_STD_NOT_FND_MSG, id); + exit_code = EXIT_FAIL_DB; + } + else + { + printf(M_ERR_DB_READ); + exit_code = EXIT_FAIL_DB; + } + break; + + case 'p': + rc = print_db(fd); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'x': + // compress the database file (extra credit) + fd = compress_db(fd); + if (fd < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'z': + // Zero the db: close and reopen with truncation. + close(fd); + fd = open_db(DB_FILE, true); + if (fd < 0) + { + exit_code = EXIT_FAIL_DB; + break; + } + printf(M_DB_ZERO_OK); + exit_code = EXIT_OK; + break; + + default: + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + } + + close(fd); + exit(exit_code); +} \ No newline at end of file