/*
 * Copyright ©2026 Naomi Alterman.  All rights reserved.  Permission is
 * hereby granted to students registered for University of Washington
 * CSE 333 for use solely during Spring Quarter 2026 for purposes of
 * the course.  No other use, copying, distribution, or modification
 * is permitted without prior written consent. Copyrights for
 * third-party components of this work must be honored.  Instructors
 * interested in reusing these course materials should contact the
 * author.
 */

#include <dirent.h>   // for directory ops
#include <stdio.h>    // for printf, snprintf
#include <stdlib.h>   // for EXIT_SUCCESS, malloc, free
#include <string.h>   // for strlen, strcmp, strrchr
#include <stdbool.h>  // for bool

#include "ro_file.h"

#define BUFFER_SIZE 256


/*** HELPER FUNCTION DECLARATIONS ******************************************/

// Returns whether or not a filename ends in ".txt".
bool IsTxtFile(char* filename);

// Concatenate the directory and file names into a full path. The caller is
// responsible for freeing the allocated string. Exits if an error occurs.
char* Concatenate(char* dirname, char* filename);

// Print the content of the file to stdout. Returns false if an error occurs.
bool ReadAndPrintFile(char* fullpath);


/*
 * This program:
 * - Accepts a directory name as a command-line argument.
 * - Scans through the directory looking for all files whose names end with
 *   the four characters ".txt".
 * - For every such file found, read the contents of those files using the
 *   ro_file module and then write them without adding any additional
 *   characters or formatting to stdout using cstdlib.
 */
int main(int argc, char** argv) {
  // Make sure that the user passed us a directory that we can open.
  if (argc != 2) {
    fprintf(stderr, "Usage: ./ex4 directory_name\n");
    return EXIT_FAILURE;
  }
  DIR* dir_p = opendir(argv[1]);
  if (dir_p == NULL) {
    perror("Open directory failed");
    return EXIT_FAILURE;
  }
  struct dirent* next_entry;
  // Iterate through, reading filenames.
  for (next_entry = readdir(dir_p);
        next_entry != NULL;
        next_entry = readdir(dir_p)) {
    char* entry_name = next_entry->d_name;
    // Only read if the filename ends in ".txt".
    if (IsTxtFile(entry_name)) {
      char* fullpath = Concatenate(argv[1], entry_name);
      if (!ReadAndPrintFile(fullpath)) {
        free(fullpath);
        closedir(dir_p);
        return EXIT_FAILURE;
      }
      free(fullpath);
    }
  }

  // Clean up and exit.
  closedir(dir_p);
  return EXIT_SUCCESS;
}


/*** HELPER FUNCTION DEFINITIONS *******************************************/

bool IsTxtFile(char* filename) {
  char* dot = strrchr(filename, '.');
  return dot && !strcmp(dot, ".txt");
}

char* Concatenate(char* dirname, char* filename) {
  bool has_trailing_slash = dirname[strlen(dirname) - 1] == '/';
  // Concatenate directory and file name.
  size_t dlen = strlen(dirname);
  size_t flen = strlen(filename);
  // Malloc space for full path name:
  // dlen + strlen("/") + flen + strlen('\0') = dlen + flen + 2
  int size_to_malloc = has_trailing_slash ? dlen + flen + 1 : dlen + flen + 2;
  char* fullpath = (char*) malloc(sizeof(char) * (size_to_malloc));
  if (fullpath == NULL) {
    fprintf(stderr, "Error on malloc.\n");
    exit(EXIT_FAILURE);
  }
  if (has_trailing_slash) {
    snprintf(fullpath, size_to_malloc, "%s%s", dirname, filename);
  } else {
    snprintf(fullpath, size_to_malloc, "%s/%s", dirname, filename);
  }
  return fullpath;
}

bool ReadAndPrintFile(char* fullpath) {
  char buf[BUFFER_SIZE];
  ssize_t res;

  // Open up the file.
  RO_FILE* file = ro_open(fullpath);
  if (file == NULL) {
    perror("ro_open failed");
    return false;
  }

  // Loop through, reading the file BUFFER_SIZE byte chunks at a time.
  for (res = ro_read(buf, BUFFER_SIZE, file);
       res > 0;  // no error and not EOF
       res = ro_read(buf, BUFFER_SIZE, file)) {
    // Print out what we read.
    if (fwrite(buf, 1, res, stdout) != res) {
      perror("fwrite failed");
      ro_close(file);
      return false;
    }
  }
  // Test to see if we encountered an error while reading.
  if (res < 0) {
    perror("ro_read failed");
    ro_close(file);
    return false;
  }
  // Close file.
  if (ro_close(file) != 0) {
    perror("ro_closed failed");
    return false;
  }
  return true;
ro_file.c


/*
 * Copyright ©2023 Justin Hsia.  All rights reserved.  Permission is
 * hereby granted to students registered for University of Washington
 * CSE 333 for use solely during Winter Quarter 2023 for purposes of
 * the course.  No other use, copying, distribution, or modification
 * is permitted without prior written consent. Copyrights for
 * third-party components of this work must be honored.  Instructors
 * interested in reusing these course materials should contact the
 * author.
 */

#include "ro_file.h"

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>


/*** INTERNAL DATA TYPES AND CONSTANTS **************************************/

static const int RO_FILE_BUF_LEN = 512;  // do not modify

struct ro_file_st {
  int fd;         // The file descriptor we are currently managing.

  char* buf;      // Pointer to our internal buffer for this file.

  off_t buf_pos;  // The position in the file that the beginning of our
                  // internal buffer currently corresponds to.

  int buf_index;  // The index in our internal buffer that corresponds to the
                  // user's current position in the file.

  int buf_end;    // How many bytes currently in our internal buffer are
                  // actually from the file.
                  // To understand why this is important to track, consider
                  // the case when the file length is < RO_FILE_BUF_LEN.
};


/*** STATIC HELPER FUNCTION DECLARATIONS ************************************/

// Copy up to 'amount' bytes from the RO_FILE's internal buffer to 'out'.
// Returns the number of bytes copied.
static size_t flush_buffer(RO_FILE* file, char* out, int amount);

// Fill in the RO_FILE's internal buffer with as many bytes as can be read
// from the file.
// Returns the number of bytes copied into the buffer, or -1 on any error.
static ssize_t fill_buffer(RO_FILE* file);


/*** FUNCTION DEFINITIONS ***************************************************/

RO_FILE* ro_open(char* filename) {
  // Allocate a new RO_FILE
  RO_FILE* result = (RO_FILE*) malloc(sizeof(RO_FILE));
  if (result == NULL) {
    return NULL;
  }
  // Get the file descriptor for the file
  int fd = open(filename, O_RDONLY);
  if (fd == -1) {
    free(result);
    return NULL;
  }
  result->fd = fd;
  // Allocate the internal buffer
  result->buf = (char*) malloc(sizeof(char) * RO_FILE_BUF_LEN);
  if (result->buf == NULL) {
    free(result->buf);
    free(result);
    close(fd);
    return NULL;
  }
  // Initialize the other fields (no reading done yet)
  result->buf_pos = 0;
  result->buf_index = 0;
  result->buf_end = 0;
  return result;
}

ssize_t ro_read(char* ptr, size_t len, RO_FILE* file) {
  // 1. If we have bytes in our internal buffer, flush as many as we can to
  //    'ptr'.
  size_t num_copied_out = flush_buffer(file, ptr, len);

  while (num_copied_out != len) {
    // 2. If we haven't provided the bytes requested, repopulate our buffer
    //    with bytes from the file.
    ssize_t num_filled = fill_buffer(file);
    if (num_filled == 0) {
      // reached EOF; no more bytes to copy
      break;
    } else if (num_filled < 0) {
      // pass error back to client
      return num_filled;
    }

    // 3. Copy filled bytes into 'ptr'.
    num_copied_out += flush_buffer(file, ptr+num_copied_out,
                                   len-num_copied_out);

    // 4. Repeat steps 2-3 until request is fulfilled.
  }

  return num_copied_out;
}

off_t ro_tell(RO_FILE* file) {
  if (file == NULL) {
    return -1;
  }
  return file->buf_pos + file->buf_index;
}

int ro_seek(RO_FILE* file, off_t offset, int whence) {
  if (file == NULL) {
    return 1;
  }
  // Seek to specified offset from specified whence
  off_t res = lseek(file->fd, offset, whence);
  if (res == -1) {
    return 1;
  }
  // Update the fields
  // Note: this is a naive implementation as we stated that you didn't have to
  // determine if the resulting offset is a position in our buffer.
  file->buf_pos = res;
  file->buf_index = 0;
  file->buf_end = 0;
  return 0;
}

int ro_close(RO_FILE* file) {
  if (file == NULL) {
    return -1;
  }
  // Clean up resources, returns non-zero on error
  int res = close(file->fd);
  free(file->buf);
  free(file);
  return res;
}


/*** STATIC HELPER FUNCTION DEFINITIONS *************************************/

size_t flush_buffer(RO_FILE* file, char* out, int amount) {
  // Flush bytes from buffer to "out" one byte at a time with
  // bytes to flush = min(amount, bytes available in buffer),
  // accumulating total number of bytes flushed
  int bytes_flushed;
  for (bytes_flushed = 0;
       bytes_flushed < (file->buf_end - file->buf_index)
        && bytes_flushed < amount;
       bytes_flushed++) {
    out[bytes_flushed] = file->buf[file->buf_index + bytes_flushed];
  }
  // Advance index into the buffer by number of bytes flushed
  file->buf_index += bytes_flushed;
  return bytes_flushed;
}

ssize_t fill_buffer(RO_FILE* file) {
  // Point into the file where the buffer left off
  file->buf_pos += file->buf_index;
  // Reset buffer index and len
  file->buf_index = 0;
  file->buf_end = 0;
  // Accumulate number of bytes buffer
  int bytes_buffered = 0;
  // POSIX read loop that attempts to buffer RO_FILE_BUF_LEN bytes
  while (bytes_buffered < RO_FILE_BUF_LEN) {
    ssize_t res = read(file->fd, (file->buf) + bytes_buffered,
                    RO_FILE_BUF_LEN - bytes_buffered);
    // Reached EOF
    if (res == 0) {
      break;
    // Reached error, check for error code
    } else if (res < 0) {
      if (errno == EINTR || errno == EAGAIN) {
        continue;
      }
      // Return -1 on POSIX error
      return res;
    }
    bytes_buffered += res;
  }
  // Update buffer status
  file->buf_end = bytes_buffered;
  return bytes_buffered;
}