Skip to content
Snippets Groups Projects
Select Git revision
  • 7d9c66e1ce3dd7a34ec2afef7ac19a216edf2178
  • main default
2 results

CONTRIBUTING.md

Blame
  • After you've reviewed these contribution guidelines, you'll be all set to contribute to this project.
    sdbsc.c 16.02 KiB
    #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);
    }