From 1a4b71e83ccb5cb495f3fd45cd6e081e219ca187 Mon Sep 17 00:00:00 2001
From: luishernandez <lahr730@gmail.com>
Date: Sun, 2 Feb 2025 22:45:39 -0500
Subject: [PATCH] a2

---
 sdbsc.c | 573 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 573 insertions(+)
 create mode 100644 sdbsc.c

diff --git a/sdbsc.c b/sdbsc.c
new file mode 100644
index 0000000..c2a8516
--- /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
-- 
GitLab