/* * cmsfs-fuse - CMS EDF filesystem support for Linux * * Main function * * Copyright IBM Corp. 2010, 2018 * * s390-tools is free software; you can redistribute it and/or modify * it under the terms of the MIT license. See LICENSE for details. */ #define FUSE_USE_VERSION 30 #include #include #include #include #include #include #include #include #include #ifdef HAVE_SETXATTR #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "lib/util_base.h" #include "lib/util_libc.h" #include "lib/util_list.h" #include "lib/zt_common.h" #include "cmsfs-fuse.h" #include "ebcdic.h" #include "edf.h" #include "helper.h" struct cmsfs cmsfs; static struct util_list open_file_list; static struct util_list text_type_list; FILE *logfile; #define FSNAME_MAX_LEN 200 #define MAX_FNAME 18 #define CMSFS_OPT(t, p, v) { t, offsetof(struct cmsfs, p), v } enum { KEY_HELP, KEY_VERSION, }; static const struct fuse_opt cmsfs_opts[] = { CMSFS_OPT("-a", mode, TEXT_MODE), CMSFS_OPT("--ascii", mode, TEXT_MODE), CMSFS_OPT("-t", mode, TYPE_MODE), CMSFS_OPT("--filetype", mode, TYPE_MODE), CMSFS_OPT("--from=%s", codepage_from, 0), CMSFS_OPT("--to=%s", codepage_to, 0), FUSE_OPT_KEY("-h", KEY_HELP), FUSE_OPT_KEY("--help", KEY_HELP), FUSE_OPT_KEY("-v", KEY_VERSION), FUSE_OPT_KEY("--version", KEY_VERSION), FUSE_OPT_END }; static void usage(const char *progname) { fprintf(stdout, "Usage: %s DEVICE MOUNTPOINT [OPTIONS]\n" "\n" "Use the cmsfs-fuse command to read and write files stored on a z/VM CMS disk.\n" "\n" "General options:\n" " -o opt,[opt...] Mount options\n" " -h --help Print help, then exit\n" " -v --version Print version, then exit\n" " -t --filetype ASCII translation based on file type\n" " -a --ascii Force ascii translation\n" " --from= Codepage used on the CMS disk\n" " --to= Codepage used for conversion to Linux\n" "\n", progname); } static char CODEPAGE_EDF[] = "CP1047"; static char CODEPAGE_LINUX[] = "ISO-8859-1"; #define READDIR_FILE_ENTRY -1 #define READDIR_END_OF_DIR -2 #define READDIR_DIR_ENTRY -3 #define READDIR_MAP_ENTRY -4 #define LINEFEED_OFFSET ((struct record *) -1) #define LINEFEED_ASCII 0xa #define LINEFEED_EBCDIC 0x25 #define LINEFEED_NOT_FOUND -1 #define FILLER_EBCDIC 0x40 #define FILLER_ASCII 0x20 #define RSS_HEADER_STARTED 0x1 #define RSS_HEADER_COMPLETE 0x2 #define RSS_DATA_BLOCK_STARTED 0x4 #define RSS_DATA_BLOCK_EXT 0x8 #define RWS_HEADER_STARTED 0x1 #define RWS_HEADER_COMPLETE 0x2 #define RWS_RECORD_INCOMPLETE 0x4 #define RWS_RECORD_COMPLETE 0x8 #define BWS_BLOCK_NOT_INIT 0x0 #define BWS_BLOCK_NEW 0x1 #define BWS_BLOCK_USED 0x2 #define WCACHE_MAX (MAX_RECORD_LEN + 1) struct block { off_t disk_addr; unsigned int disp; int hi_record_nr; }; struct record_ext { /* start addr of the extension */ off_t disk_start; /* length of extension in this disk block */ int len; /* null block start flag */ int null_block_started; /* corresponding disk block number */ int block_nr; struct record_ext *prev; struct record_ext *next; }; struct record { /* length of the complete record */ unsigned int total_len; /* offset of first record block on the disk */ off_t disk_start; /* bytes in first record block */ int first_block_len; /* logical offset, dependent on line feed mode */ off_t file_start; /* null block start flag */ int null_block_started; /* spanned record extension */ struct record_ext *ext; /* corresponding disk block number */ int block_nr; }; struct file; struct file_operations { int (*cache_data) (struct file *f, off_t addr, int level, int *block, unsigned int *disp, int *record, off_t *total); int (*write_data) (struct file *f, const char *buf, int len, size_t size, int rlen); int (*delete_pointers) (struct file *f, int level, off_t addr); int (*write_pointers) (struct file *f, int level, off_t dst, int offset); }; static struct file_operations fops_fixed; static struct file_operations fops_variable; struct io_operations { int (*read) (void *buf, size_t size, off_t addr); int (*write) (const void *buf, size_t size, off_t addr); }; static struct io_operations io_ops; struct write_state { int block_state; /* number of free bytes in the current block */ int block_free; int var_record_state; /* remaining record bytes for a write request */ int var_record_len; /* only used for var records hi_record_nr by now */ int var_records_written; }; /* * File object for operations that follow open */ struct file { /* pointer to the fst entry */ struct fst_entry *fst; /* fst address on disk */ off_t fst_addr; /* translate mode enabled */ int translate; /* linefeed mode enabled */ int linefeed; /* list of records */ struct record *rlist; /* record scan state machine flag */ int record_scan_state; /* next record for sequential reads */ int next_record_hint; /* counter for null bytes to detect block start */ int null_ctr; /* list of disk blocks */ struct block *blist; /* disk address of next byte to write */ off_t write_ptr; /* the filesize while the file is opened */ off_t session_size; /* number of null blocks for fixed files */ int nr_null_blocks; /* number of written padding bytes for a fixed file */ int pad_bytes; /* old levels value, needed to rewrite pointers */ int old_levels; /* state information needed for a write request */ struct write_state *wstate; /* path name for open and unlink */ char path[MAX_FNAME + 1]; /* counter for pseudo null length records */ int null_records; /* write cache for text mode */ char *wcache; /* buffer for iconv */ char *iconv_buf; /* used bytes in write cache */ int wcache_used; /* committed written bytes to FUSE */ int wcache_commited; /* dirty flag for file meta data */ int ptr_dirty; /* fops pointers */ struct file_operations *fops; /* pointers per block constant */ int ptr_per_block; /* open list head */ struct util_list_node list; /* usage counter for all openers */ int use_count; /* usage counter for all writers */ int write_count; /* unlink flag */ int unlinked; }; struct xattr { char name[20]; size_t size; }; /* * Record format: 'F' (fixed) or 'V' (variable), 1 byte * Record lrecl: 0-65535, 5 bytes * Record mode: [A-Z][0-6], 2 bytes */ static struct xattr xattr_format = { .name = "user.record_format", .size = 1 }; static struct xattr xattr_lrecl = { .name = "user.record_lrecl", .size = 5 }; static struct xattr xattr_mode = { .name = "user.file_mode", .size = 2 }; #define SHOW_UNLINKED 0 #define HIDE_UNLINKED 1 #define WALK_FLAG_LOOKUP 0x1 #define WALK_FLAG_READDIR 0x2 #define WALK_FLAG_LOCATE_EMPTY 0x4 #define WALK_FLAG_CACHE_DBLOCKS 0x8 struct walk_file { int flag; char *name; char *type; void *buf; off_t addr; fuse_fill_dir_t filler; off_t *dlist; int dlist_used; }; /* * Prototypes */ static struct file *create_file_object(struct fst_entry *fst, int *rc); static void destroy_file_object(struct file *f); static unsigned long dec_to_hex(unsigned long long num) { unsigned long res; asm volatile("cvb %0,%1" : "=d" (res) : "Q" (num)); return res & 0xffffffff; } static unsigned int hex_to_dec(unsigned int num) { unsigned long long res; asm volatile("cvd %1,%0" : "=Q" (res) : "d" (num)); return res & 0xffffffff; } static void setup_iconv(iconv_t *conv, const char *from, const char *to) { *conv = iconv_open(to, from); if (*conv == ((iconv_t) -1)) DIE("Could not initialize conversion table %s->%s.\n", from, to); } static inline struct file *get_fobj(struct fuse_file_info *fi) { return (struct file *)(unsigned long) fi->fh; } static int access_ok(size_t size, off_t addr) { if (((addr + (off_t) size - 1) & ~DATA_BLOCK_MASK) > (addr & ~DATA_BLOCK_MASK)) DIE("crossing disk blocks, addr: %x size: %d\n", (int) addr, (int) size); if ((unsigned long long) addr < (unsigned long long) cmsfs.label || addr > cmsfs.size) return 0; return 1; } static int read_memory(void *buf, size_t size, off_t addr) { memcpy(buf, cmsfs.map + addr, size); return 0; } static int read_syscall(void *buf, size_t size, off_t addr) { int rc; rc = pread(cmsfs.fd, buf, size, addr); if (rc < 0) perror("pread failed"); return rc; } int _read(void *buf, size_t size, off_t addr) { if (!access_ok(size, addr)) return -EIO; return io_ops.read(buf, size, addr); } static int write_syscall(const void *buf, size_t size, off_t addr) { char *zbuf; int rc; if (buf == NULL) { zbuf = malloc(size); if (zbuf == NULL) return -ENOMEM; memset(zbuf, 0, size); rc = pwrite(cmsfs.fd, zbuf, size, addr); free(zbuf); } else rc = pwrite(cmsfs.fd, buf, size, addr); if (rc < 0) perror("pwrite failed"); return rc; } static int write_memory(const void *buf, size_t size, off_t addr) { if (buf == NULL) memset(cmsfs.map + addr, 0, size); else memcpy(cmsfs.map + addr, buf, size); return 0; } int _write(const void *buf, size_t size, off_t addr) { if (!access_ok(size, addr)) return -EIO; return io_ops.write(buf, size, addr); } int _zero(off_t addr, size_t size) { return _write(NULL, size, addr); } static off_t get_filled_block(void) { off_t addr = get_free_block(); if (addr < 0) return -ENOSPC; memset(cmsfs.map + addr, FILLER_EBCDIC, cmsfs.blksize); return addr; } static int get_fop(off_t addr) { struct fst_entry fst; int rc; rc = _read(&fst, sizeof(fst), addr); BUG(rc < 0); return ABS(fst.fop); } static int get_levels(off_t addr) { struct fst_entry fst; int rc; rc = _read(&fst, sizeof(fst), addr); BUG(rc < 0); return fst.levels; } static int get_files_count(off_t addr) { struct fst_entry fst; int rc; rc = _read(&fst, sizeof(fst), addr); BUG(rc < 0); /* ignore director and allocmap entries */ return fst.nr_records - 2; } static int get_order(int shift) { int count = 0; while (!(shift & 0x1)) { shift >>= 1; count++; } return count; } /* * Read pointer from fixed size pointer block and return * absolute address on disk. */ off_t get_fixed_pointer(off_t addr) { struct fixed_ptr ptr; int rc; if (!addr) return NULL_BLOCK; rc = _read(&ptr, sizeof(ptr), addr); if (rc < 0) return -EIO; if (!ptr.next) return NULL_BLOCK; else return ABS((off_t)ptr.next); } /* * Read variable pointer from block and return absolute address on disk * and highest record number. */ static off_t get_var_pointer(off_t addr, int *max_record, unsigned int *disp) { struct var_ptr vptr; off_t ptr = 0; int rc; BUG(!addr); rc = _read(&vptr, VPTR_SIZE, addr); if (rc < 0) return -EIO; ptr = (off_t) vptr.next; *max_record = vptr.hi_record_nr; *disp = vptr.disp; if (!ptr) { if (vptr.hi_record_nr) return NULL_BLOCK; else return VAR_FILE_END; } else return ABS(ptr); } int is_edf_char(int c) { switch (c) { case 'A' ... 'Z': break; case 'a' ... 'z': break; case '0' ... '9': break; case '#': break; case '@': break; case '+': break; case '$': break; case '-': break; case ':': break; case '_': break; default: return 0; } return 1; } /* * Force conversion to upper case since lower case file names although * valid are not accepted by many CMS tools. */ static void str_toupper(char *str) { int i; for (i = 0; i < (int) strlen(str); i++) str[i] = toupper(str[i]); } /* * Set the FST date to the specified date. */ static void update_fst_date(struct fst_entry *fst, struct tm *tm) { unsigned int num; int i; if (tm->tm_year >= 100) fst->flag |= FST_FLAG_CENTURY; else fst->flag &= ~FST_FLAG_CENTURY; fst->date[0] = tm->tm_year; fst->date[1] = tm->tm_mon + 1; fst->date[2] = tm->tm_mday; fst->date[3] = tm->tm_hour; fst->date[4] = tm->tm_min; fst->date[5] = tm->tm_sec; /* convert hex to decimal */ for (i = 0; i < 6; i++) { num = fst->date[i]; num = hex_to_dec(num); fst->date[i] = num >> 4; } } /* * Set the FST date to the current date. */ static int set_fst_date_current(struct fst_entry *fst) { struct timeval tv; struct tm tm; /* convert timespec to tm */ memset(&tm, 0, sizeof(struct tm)); if (gettimeofday(&tv, NULL) < 0) { perror(COMP "gettimeofday failed"); return -EINVAL; } if (localtime_r(&tv.tv_sec, &tm) == NULL) return -EINVAL; update_fst_date(fst, &tm); return 0; } /* * Check if the file is on the opened list. */ static struct file *file_open(const char *name) { char uc_name[MAX_FNAME]; struct file *f; util_strlcpy(uc_name, name, MAX_FNAME); str_toupper(uc_name); util_list_iterate(&open_file_list, f) if (strncmp(f->path + 1, uc_name, MAX_FNAME) == 0) return f; return NULL; } /* * Check if the file is open and unlinked. */ static int file_unlinked(const char *name) { struct file *f = file_open(name); if (f && f->unlinked) return 1; else return 0; } /* * Convert EDF date to time_t. */ static time_t fst_date_to_time_t(char *date, int century) { unsigned long long num; unsigned int res[6]; struct tm tm; time_t time; int i; /* * date : YY MM DD HH MM SS (decimal!) * century: 0=19, 1=20, dead=21 * convert decimal to hex */ for (i = 0; i < 6; i++) { num = date[i]; num <<= 4; num += 0xc; /* plus */ res[i] = dec_to_hex(num); } memset(&tm, 0, sizeof(tm)); tm.tm_year = res[0]; tm.tm_mon = res[1]; tm.tm_mday = res[2]; tm.tm_hour = res[3]; tm.tm_min = res[4]; tm.tm_sec = res[5]; /* see man 3 tzset */ tm.tm_isdst = -1; /* prepare for mktime */ tm.tm_mon--; if (century == FST_FLAG_CENTURY) tm.tm_year += 100; time = mktime(&tm); if (time == -1) { fprintf(stderr, COMP "mktime failed!\n"); memset(&time, 0, sizeof(time)); } return time; } /* * Read one FST entry into *fst from offset on disk addr and detect type. * * Return values: * ret > 0 : disk address of additional FOP block * ret = -1 : file entry filled * ret = -2 : end of directory * ret = -3 : directory entry * ret = -4 : allocmap entry */ static int readdir_entry(struct fst_entry *fst, off_t addr) { int rc; BUG(addr & (sizeof(struct fst_entry) - 1)); rc = _read(fst, sizeof(*fst), addr); BUG(rc < 0); if (is_directory(fst->name, fst->type)) { /* check for multi-block directory */ if (ABS(fst->fop) != addr) return ABS(fst->fop); return READDIR_DIR_ENTRY; } if (is_allocmap(fst->name, fst->type)) return READDIR_MAP_ENTRY; if (is_file(fst->name, fst->type)) return READDIR_FILE_ENTRY; return READDIR_END_OF_DIR; } /* * Return number of characters excluding trailing spaces. */ static inline int strip_right(const char *str, int size) { while (size > 0 && str[size - 1] == 0x20) size--; return size; } /* * Convert ASCII name to EBCDIC name. */ static int encode_edf_name(const char *name, char *fname, char *ftype) { int dot_pos, tlen; char *tmp; /* * name is ascii string "FILE.EXT" * readdir_entry returns fst.name fst.type as EBCDIC including spaces * pre-fill name and type with ascii spaces, remove dot and convert * to EBCDIC. */ memset(fname, 0x20, 8); memset(ftype, 0x20, 8); tmp = index(name, '.'); /* filenames without a dot are invalid! */ if (tmp == NULL) return -EINVAL; dot_pos = tmp - name; if (dot_pos == 0 || dot_pos > 8) return -EINVAL; memcpy(fname, name, dot_pos); ebcdic_enc(fname, fname, 8); tlen = strlen(name) - (dot_pos + 1); if (tlen == 0 || tlen > 8) return -EINVAL; memcpy(ftype, name + dot_pos + 1, tlen); ebcdic_enc(ftype, ftype, 8); return 0; } /* * Convert EBCDIC name to ASCII name. */ static void decode_edf_name(char *file, char *fname, char *ftype) { int len, pos = 0; ebcdic_dec(fname, fname, 8); ebcdic_dec(ftype, ftype, 8); /* strip spaces but only from the end */ len = strip_right(fname, 8); memcpy(file, fname, len); /* add dot */ pos += len; file[pos] = '.'; pos++; len = strip_right(ftype, 8); memcpy(&file[pos], ftype, len); pos += len; /* terminate string */ file[pos] ='\0'; } static int edf_name_valid(const char *name) { int name_len, i; char *dot; /* name must contain . */ dot = index(name, '.'); if (dot == NULL) return -EINVAL; name_len = dot - name; for (i = 0; i < name_len; i++) if (!is_edf_char(name[i])) return -EINVAL; for (i = name_len + 1; i < (int) strlen(name); i++) if (!is_edf_char(name[i])) return -EINVAL; return 0; } /* * Summarize the number of bytes used in the last data block. */ static int walk_last_var_data_block(off_t addr, off_t *total) { ssize_t left = cmsfs.blksize; u16 len; int rc; /* subtract displacement */ left -= addr & DATA_BLOCK_MASK; while (left >= (int) sizeof(len)) { rc = _read(&len, sizeof(len), addr); if (rc < 0) return rc; /* * Null length means no more records follow. * Assumption: the last block is zero-padded. */ if (!len) return 0; /* add length of record with the header length */ *total += len + sizeof(len); left -= len + sizeof(len); /* point to next record */ addr += len + sizeof(len); } return 0; } /* * Return struct record for record number nr. */ static struct record *get_record(struct file *f, int nr) { BUG(nr > f->fst->nr_records - 1); return &f->rlist[nr]; } static int skip_header_byte(struct file *f) { if (f->fst->record_format == RECORD_LEN_FIXED) return 0; if (f->record_scan_state == RSS_HEADER_STARTED) return 1; else return 0; } static void set_record_len_upper(struct file *f, int record, u8 len) { struct record *r = &f->rlist[record]; if (f->record_scan_state != RSS_DATA_BLOCK_STARTED && f->record_scan_state != RSS_DATA_BLOCK_EXT) DIE("%s: internal error\n", __func__); r->total_len = len << 8; f->record_scan_state = RSS_HEADER_STARTED; } static void set_record_len_lower(struct file *f, int record, u8 len) { struct record *r = &f->rlist[record]; if (f->record_scan_state != RSS_HEADER_STARTED) DIE("%s: internal error\n", __func__); r->total_len += len; f->record_scan_state = RSS_HEADER_COMPLETE; } static void set_record_len(struct file *f, int record, u16 len) { struct record *r = &f->rlist[record]; if (f->fst->nr_records && f->fst->nr_records == record) DIE("%s: record nr: %d out of bounds\n", __func__, record); if (f->record_scan_state != RSS_DATA_BLOCK_STARTED && f->record_scan_state != RSS_DATA_BLOCK_EXT) DIE("%s: internal error\n", __func__); r->total_len = len; f->record_scan_state = RSS_HEADER_COMPLETE; } static void set_record(struct file *f, int *record, off_t addr, int len, off_t *total, int block) { struct record *r = &f->rlist[*record]; if (f->record_scan_state != RSS_HEADER_COMPLETE) DIE("%s: internal error\n", __func__); r->first_block_len = len; r->disk_start = addr; r->block_nr = block; if (addr == NULL_BLOCK) { if (f->null_ctr % cmsfs.blksize == 0) r->null_block_started = 1; f->null_ctr += len; } else f->null_ctr = 0; /* add previous record linefeed but not for the first record */ if (f->linefeed && *record) (*total)++; r->file_start = *total; (*total) += r->total_len; f->record_scan_state = RSS_DATA_BLOCK_STARTED; } static void add_record_ext(struct record *rec, struct record_ext *ext) { struct record_ext *tmp; if (rec->ext == NULL) { rec->ext = ext; ext->prev = NULL; ext->next = NULL; } else { tmp = rec->ext; while (tmp->next != NULL) { tmp = tmp->next; } tmp->next = ext; ext->prev = tmp; ext->next = NULL; } } static void set_record_extension(struct file *f, int *record, off_t addr, int len, int block) { struct record *rec = &f->rlist[*record]; struct record_ext *ext; if (f->record_scan_state != RSS_DATA_BLOCK_STARTED && f->record_scan_state != RSS_DATA_BLOCK_EXT) DIE("%s: internal error\n", __func__); BUG(*record >= f->fst->nr_records); ext = malloc(sizeof(struct record_ext)); if (ext == NULL) DIE_PERROR("malloc failed\n"); memset(ext, 0, sizeof(*ext)); ext->len = len - skip_header_byte(f); ext->disk_start = addr + skip_header_byte(f); ext->block_nr = block; if (ext->disk_start == NULL_BLOCK) { if (f->null_ctr % cmsfs.blksize == 0) ext->null_block_started = 1; f->null_ctr += len; } else f->null_ctr = 0; add_record_ext(rec, ext); f->record_scan_state = RSS_DATA_BLOCK_EXT; } /* check for file end by record number */ static int end_of_file(struct file *f, int record) { if (record == f->fst->nr_records - 1) return 1; return 0; } static int walk_fixed_data_block(struct file *f, off_t addr, int *record, off_t *total, int block, int disp) { off_t offset = (off_t ) block * cmsfs.blksize + disp; int len = (f->fst->record_len > cmsfs.blksize) ? cmsfs.blksize : f->fst->record_len; int left = cmsfs.blksize - disp; int first = 0; if (offset % f->fst->record_len) { if (f->fst->record_len - offset >= cmsfs.blksize) first = cmsfs.blksize; else first = (f->fst->record_len % cmsfs.blksize) - (offset % len); } if (first) { BUG(first > left); set_record_extension(f, record, addr, first, block); left -= first; if (addr != NULL_BLOCK) addr += first; } while (left >= len) { /* * Increment record number only after adding a possible * extension. *record starts with -1 so the first is 0. */ if (end_of_file(f, *record)) return 1; (*record)++; set_record_len(f, *record, f->fst->record_len); set_record(f, record, addr, len, total, block); left -= len; if (addr != NULL_BLOCK) addr += len; } /* partial record left */ if (left > 0) { if (end_of_file(f, *record)) return 1; (*record)++; set_record_len(f, *record, f->fst->record_len); set_record(f, record, addr, left, total, block); return 0; } return 0; } static int get_record_unused_bytes(struct file *f, int nr) { struct record *rec = get_record(f, nr); struct record_ext *rext; int used = 0; /* no data bytes yet */ if (f->record_scan_state == RSS_HEADER_COMPLETE) return rec->total_len; used = rec->first_block_len; /* only first block */ if (f->record_scan_state == RSS_DATA_BLOCK_STARTED) goto out; rext = rec->ext; while (rext != NULL) { used += rext->len; rext = rext->next; } out: return rec->total_len - used; } static int walk_var_data_block(struct file *f, off_t addr, unsigned int disp, int *record, off_t *total, int block, int skip) { ssize_t left = cmsfs.blksize - skip; int last, rc; u8 half_len; u16 len; /* * If records are skipped on this block there is no record extension, * overwrite disp and start with scanning the record. */ if (skip) disp = 0; /* * disp set means 1 or 2 header bytes and possibly data bytes on the * last block or a null block. */ if (disp) { if (addr == NULL_BLOCK) { last = cmsfs.blksize; /* * Special case: last block can be a null block with * not all bytes used on it. */ if (f->fst->nr_blocks - 1 == block) last = get_record_unused_bytes(f, *record); /* * Special case: record header on last block wo. data. * That means no record data yet for this block. */ if (f->record_scan_state == RSS_HEADER_COMPLETE) set_record(f, record, addr, last, total, block); else set_record_extension(f, record, addr, last, block); return 0; } if (disp == VAR_RECORD_SPANNED) len = cmsfs.blksize; else len = disp; /* split header -> read second length byte */ if (f->record_scan_state == RSS_HEADER_STARTED) { rc = _read(&half_len, sizeof(half_len), addr); if (rc < 0) return rc; set_record_len_lower(f, *record, half_len); left--; len--; addr++; } if (f->record_scan_state == RSS_HEADER_COMPLETE) set_record(f, record, addr, len, total, block); else set_record_extension(f, record, addr, len, block); if (disp == VAR_RECORD_SPANNED) return 0; left -= len; addr += len; } /* at least one data byte */ while (left >= (int) sizeof(len) + 1) { rc = _read(&len, sizeof(len), addr); if (rc < 0) return rc; /* * Null length means no more records follow. * Assumption: the last block is zero-padded. */ if (!len) return 0; /* * Increment record number only after adding a possible * extension. *record starts with -1 so the first is 0. */ (*record)++; set_record_len(f, *record, len); /* account consumed header bytes */ left -= sizeof(len); addr += sizeof(len); /* limit to block end */ if (len > left) len = left; /* add length of record including the header length */ set_record(f, record, addr, len, total, block); left -= len; /* point to next record header */ addr += len; } /* 2 header bytes left */ if (left == 2) { rc = _read(&len, sizeof(len), addr); if (rc < 0) return rc; if (!len) return 0; (*record)++; set_record_len(f, *record, len); return 0; } /* split header */ if (left == 1) { if (end_of_file(f, *record)) return 0; rc = _read(&half_len, sizeof(half_len), addr); if (rc < 0) return rc; (*record)++; set_record_len_upper(f, *record, half_len); f->record_scan_state = RSS_HEADER_STARTED; } return 0; } static int cache_fixed_data_block(struct file *f, off_t addr, int *block, int *record, off_t *total, int disp) { /* * Cannot distinguish null block pointers from not existing pointers, * so this fn is called for the whole pointer block and maybe for * non-existing blocks and records too. Check and bail out if EOF. */ if (*block >= f->fst->nr_blocks || *record >= f->fst->nr_records) return 0; f->blist[*block].disk_addr = addr & ~DATA_BLOCK_MASK; walk_fixed_data_block(f, addr, record, total, *block, disp); /* record starts with 0 but on-disk record number with 1 */ f->blist[*block].hi_record_nr = *record + 1; (*block)++; return 0; } /* * Walk all pointer blocks of a fixed file and call function for every * data block respecting the sequence of the data. */ static int cache_file_fixed(struct file *f, off_t addr, int level, int *block, unsigned int *disp, int *record, off_t *total) { int left = f->ptr_per_block; off_t ptr; if (level > 0) { level--; while (left--) { ptr = get_fixed_pointer(addr); if (ptr < 0) return ptr; /* * In difference to variable record format a null pointer * may be valid for a null block so we cannot determine * the file end from a pointer entry. Check for the number * of scanned blocks instead. */ if (*block >= f->fst->nr_blocks || *record >= f->fst->nr_records) return 0; cache_file_fixed(f, ptr, level, block, disp, record, total); /* don't increment for null block pointers */ if (addr) addr += PTR_SIZE; } return 0; } return cache_fixed_data_block(f, addr, block, record, total, 0); } static int cache_variable_data_block(struct file *f, off_t addr, int *block, int *record, int disp, off_t *total, int skip) { int rc; /* * Cannot distinguish null block pointers from not existing pointers, * so this fn is called for the whole pointer block and maybe for * non-existing blocks and records too. Check and bail out if EOF. */ if (*block >= f->fst->nr_blocks || *record >= f->fst->nr_records) return 0; f->blist[*block].disk_addr = addr & ~DATA_BLOCK_MASK; rc = walk_var_data_block(f, addr, disp, record, total, *block, skip); if (rc < 0) return rc; /* record starts with 0 but on-disk record number with 1 */ f->blist[*block].hi_record_nr = *record + 1; f->blist[*block].disp = disp; (*block)++; return 0; } /* * Walk all pointer blocks of a variable file and call function for every * data block respecting the sequence of the data. */ static int cache_file_variable(struct file *f, off_t addr, int level, int *block, unsigned int *disp, int *record, off_t *total) { int nr, left = f->ptr_per_block; off_t ptr; if (level > 0) { level--; /* 4 or 8 bytes are left at the end (offset) which we ignore */ while (left--) { ptr = get_var_pointer(addr, &nr, disp); if (ptr < 0) return ptr; if (ptr == VAR_FILE_END) return 0; cache_file_variable(f, ptr, level, block, disp, record, total); addr += VPTR_SIZE; } return 0; } return cache_variable_data_block(f, addr, block, record, *disp, total, 0); } static int locate_last_data_vptr(off_t addr, int level, struct fst_entry *fst, struct var_ptr *vptr) { int last, rc; if (!level) return 0; level--; /* read offset pointer from the end of the var pointer block */ rc = _read(&last, sizeof(last), addr + cmsfs.blksize - sizeof(last)); if (rc < 0) return rc; if (last % VPTR_SIZE || last > cmsfs.blksize) return -EIO; rc = _read(vptr, VPTR_SIZE, addr + last); if (rc < 0) return rc; if (vptr->hi_record_nr != fst->nr_records) return -EIO; /* vptr should contain the highest data block pointer */ if (!level) return 0; if (vptr->next == NULL_BLOCK) return 0; return locate_last_data_vptr(ABS(vptr->next), level, fst, vptr); } static int is_textfile(struct fst_entry *fst) { char type[MAX_TYPE_LEN]; struct filetype *ft; if (!fst) return 0; memset(type, 0, sizeof(type)); ebcdic_dec(type, fst->type, 8); util_list_iterate(&text_type_list, ft) if (strncmp(ft->name, type, strlen(ft->name)) == 0) return 1; return 0; } /* * Decide if linefeeds are needed for this file type. */ static int linefeed_mode_enabled(struct fst_entry *fst) { if (cmsfs.mode == BINARY_MODE) return 0; if (cmsfs.mode == TEXT_MODE) return 1; return is_textfile(fst); } /* * Workaround glibc 2.9 bug with less than 3 files and give room for some * new files. If cache is full it will be purged and rebuild. */ static int max_cache_entries(void) { return cmsfs.files + 10 + cmsfs.files / 4; } static void resize_htab(void) { int i; for (i = 0; i < cmsfs.fcache_used; i++) free(cmsfs.fcache[i].str); hdestroy_r(&cmsfs.htab); free(cmsfs.fcache); cmsfs.fcache_used = 0; cmsfs.fcache_max = max_cache_entries(); cmsfs.fcache = calloc(cmsfs.fcache_max, sizeof(struct fcache_entry)); if (!hcreate_r(cmsfs.fcache_max, &cmsfs.htab)) DIE("hcreate failed\n"); } static void cache_fst_addr(off_t addr, const char *file) { struct fcache_entry *fce; ENTRY e, *eptr; e.key = strdup(file); again: if (hsearch_r(e, FIND, &eptr, &cmsfs.htab) == 0) { /* cache it */ if (cmsfs.fcache_used == cmsfs.fcache_max - 1) { DEBUG("hsearch: hash table full: %d\n", cmsfs.fcache_used); resize_htab(); goto again; } fce = &cmsfs.fcache[cmsfs.fcache_used]; cmsfs.fcache_used++; fce->fst_addr = addr; fce->str = e.key; e.data = fce; if (hsearch_r(e, ENTER, &eptr, &cmsfs.htab) == 0) DIE("hsearch: hash table full\n"); } else free(e.key); } static void update_htab_entry(off_t addr, const char *file) { struct fcache_entry *fce; ENTRY e, *eptr; e.key = strdup(file); if (hsearch_r(e, FIND, &eptr, &cmsfs.htab) == 0) { /* not yet cached, nothing to do */ free(e.key); return; } else { /* update it */ fce = eptr->data; fce->fst_addr = addr; e.data = fce; if (hsearch_r(e, ENTER, &eptr, &cmsfs.htab) == 0) DIE("%s: hash table full\n", __func__); } } static void invalidate_htab_entry(const char *name) { struct fcache_entry *fce; ENTRY e, *eptr; e.key = strdup(name); if (hsearch_r(e, FIND, &eptr, &cmsfs.htab) == 0) { /* nothing to do if not cached */ free(e.key); return; } fce = eptr->data; fce->fst_addr = 0; e.data = fce; if (hsearch_r(e, ENTER, &eptr, &cmsfs.htab) == 0) DIE("hsearch: hash table full\n"); } /* * For each FST entry in a directory block do action. * * Return: * hit == NULL : lookup file not found * hit != NULL : lookup file found, addr of the fst entry */ static void walk_dir_block(struct fst_entry *fst, struct walk_file *walk, int level, off_t *hit) { off_t ptr, addr = walk->addr; char file[MAX_FNAME]; int ret, left; /* handle higher level directory pointer blocks */ if (level > 0) { level--; left = PTRS_PER_BLOCK; while (left--) { ptr = get_fixed_pointer(addr); BUG(ptr < 0); if (!ptr) break; walk->addr = ptr; walk_dir_block(fst, walk, level, hit); if (hit != NULL && *hit) return; addr += PTR_SIZE; } return; } if (walk->flag == WALK_FLAG_CACHE_DBLOCKS) { walk->dlist[walk->dlist_used++] = walk->addr; return; } left = cmsfs.blksize / sizeof(struct fst_entry); while (left--) { ret = readdir_entry(fst, walk->addr); /* directory and allocmap type are skipped */ if (ret == READDIR_FILE_ENTRY) { if (walk->flag == WALK_FLAG_LOOKUP) { if ((memcmp(fst->name, walk->name, 8) == 0) && (memcmp(fst->type, walk->type, 8) == 0)) { /* got it */ *hit = walk->addr; return; } } if (walk->flag == WALK_FLAG_READDIR) { memset(file, 0, sizeof(file)); decode_edf_name(file, fst->name, fst->type); if (!file_unlinked(file)) { cache_fst_addr(walk->addr, file); walk->filler(walk->buf, file, NULL, 0, 0); } } } if (ret == READDIR_END_OF_DIR) { if (walk->flag == WALK_FLAG_LOCATE_EMPTY) { *hit = walk->addr; return; } break; } walk->addr += sizeof(struct fst_entry); }; return; } static void walk_directory(struct fst_entry *fst, struct walk_file *walk, off_t *hit) { if (cmsfs.dir_levels == 0) walk->addr = cmsfs.fdir; else walk->addr = get_fop(cmsfs.fdir); walk_dir_block(fst, walk, cmsfs.dir_levels, hit); } /* * Check FST record format only when reading FST entry from disk. */ static int check_fst_valid(struct fst_entry *fst) { if (fst->record_format != RECORD_LEN_FIXED && fst->record_format != RECORD_LEN_VARIABLE) return 0; else return 1; } /* * Locate the file's fst_entry in any of the directory blocks. */ static off_t lookup_file(const char *name, struct fst_entry *fst, int flag) { struct fcache_entry *fce; char uc_name[MAX_FNAME]; char fname[8], ftype[8]; struct walk_file walk; ENTRY e, *eptr; off_t faddr = 0; int rc; util_strlcpy(uc_name, name, MAX_FNAME); str_toupper(uc_name); if (flag == HIDE_UNLINKED && file_unlinked(uc_name)) return 0; e.key = strdup(uc_name); /* already cached ? */ if (hsearch_r(e, FIND, &eptr, &cmsfs.htab)) { fce = eptr->data; /* check if fst is valid, may be zero for a stale entry */ if (!fce->fst_addr) goto renew; /* read in the fst entry */ rc = _read(fst, sizeof(*fst), fce->fst_addr); BUG(rc < 0); if (!check_fst_valid(fst)) DIE("Invalid file format in file: %s\n", uc_name); free(e.key); return fce->fst_addr; } renew: free(e.key); if (encode_edf_name(uc_name, fname, ftype)) return 0; memset(&walk, 0, sizeof(walk)); walk.flag = WALK_FLAG_LOOKUP; walk.name = fname; walk.type = ftype; walk_directory(fst, &walk, &faddr); if (!faddr) return 0; if (!check_fst_valid(fst)) DIE("Invalid file format in file: %s\n", uc_name); cache_fst_addr(faddr, uc_name); return faddr; } static int cache_file(struct file *f) { int block = 0, record = -1; unsigned int disp = 0; off_t total = 0; return f->fops->cache_data(f, ABS(f->fst->fop), f->fst->levels, &block, &disp, &record, &total); } /* * Caveat: for fixed files nr_blocks is excluding null blocks, * for variable files nr_blocks is including null blocks. * Add null blocks for fixed files so allocation and file end * checks work identical for both variants. */ static void workaround_nr_blocks(struct file *f) { int nr; if (f->fst->record_format == RECORD_LEN_VARIABLE) return; nr = (off_t) f->fst->nr_records * (off_t) f->fst->record_len / cmsfs.blksize; if ((off_t) f->fst->nr_records * (off_t) f->fst->record_len % cmsfs.blksize) nr++; f->nr_null_blocks = nr - f->fst->nr_blocks; f->fst->nr_blocks = nr; } static off_t get_file_size_fixed(struct fst_entry *fst) { return (off_t) fst->nr_records * (off_t) fst->record_len; } static off_t get_file_size_variable(struct fst_entry *fst) { struct var_ptr vptr; off_t total = 0; off_t ptr; int rc; if (fst->levels > 0) { rc = locate_last_data_vptr(ABS(fst->fop), fst->levels, fst, &vptr); if (rc < 0) return rc; if (vptr.next == 0) { /* * Last block is a null block. No more records can * follow, so the displacement value points to EOF. */ total = vptr.disp; goto skip_scan; } ptr = ABS(vptr.next); if (vptr.disp != VAR_RECORD_SPANNED) { ptr += vptr.disp; /* count displacement as used space */ total += vptr.disp; } else { total += cmsfs.blksize; goto skip_scan; } } else ptr = ABS(fst->fop); /* now count the remaining used space in the last block */ rc = walk_last_var_data_block(ptr, &total); if (rc < 0) return rc; skip_scan: /* * Add the full blocks. For variable record file nr_blocks contains * also null blocks. */ if (fst->nr_blocks) total += ((off_t) fst->nr_blocks - 1) * cmsfs.blksize; return total; } /* * Return the file size as it is on the disk. Includes headers for * variable records. */ static off_t get_file_size(struct fst_entry *fst) { if (fst->record_format == RECORD_LEN_FIXED) return get_file_size_fixed(fst); else if (fst->record_format == RECORD_LEN_VARIABLE) return get_file_size_variable(fst); return 0; } static off_t get_file_size_logical(struct fst_entry *fst) { off_t total; if (fst->nr_records == 0) return 0; if (!fst->fop) return -EIO; total = get_file_size(fst); if (total < 0) return -EIO; /* subtract the record headers */ if (fst->record_format == RECORD_LEN_VARIABLE) total -= (off_t) fst->nr_records * VAR_RECORD_HEADER_SIZE; if (linefeed_mode_enabled(fst)) total += fst->nr_records; return total; } static int cmsfs_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi) { (void) fi; int mask = (cmsfs.allow_other) ? 0444 : 0440; struct fst_entry fst; if (!cmsfs.readonly) mask |= ((cmsfs.allow_other) ? 0222 : 0220); memset(stbuf, 0, sizeof(*stbuf)); stbuf->st_uid = getuid(); stbuf->st_gid = getgid(); stbuf->st_blksize = cmsfs.blksize; if (strcmp(path, "/") == 0) { stbuf->st_mode = S_IFDIR | mask | ((cmsfs.allow_other) ? 0111 : 0110); stbuf->st_nlink = 2; readdir_entry(&fst, cmsfs.fdir); /* date */ stbuf->st_mtime = fst_date_to_time_t(&fst.date[0], fst.flag & FST_FLAG_CENTURY); stbuf->st_atime = stbuf->st_ctime = stbuf->st_mtime; /* size */ stbuf->st_size = (off_t) fst.record_len * (off_t) fst.nr_records; stbuf->st_blocks = MAX(stbuf->st_size, cmsfs.blksize); stbuf->st_blocks = ((stbuf->st_blocks + cmsfs.data_block_mask) & ~cmsfs.data_block_mask) >> 9; } else { if (!lookup_file(path + 1, &fst, HIDE_UNLINKED)) return -ENOENT; stbuf->st_mode = S_IFREG | mask; stbuf->st_nlink = 1; /* date */ stbuf->st_mtime = stbuf->st_atime = stbuf->st_ctime = fst_date_to_time_t(&fst.date[0], fst.flag & FST_FLAG_CENTURY); /* size */ stbuf->st_size = get_file_size_logical(&fst); if (stbuf->st_size < 0) return -EIO; /* * Include potential sparse blocks for variable files which * are included in nr_blocks to avoid scanning the whole file. */ stbuf->st_blocks = (off_t) fst.nr_blocks * cmsfs.nr_blocks_512; } return 0; } static int cmsfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) { struct walk_file walk; struct fst_entry fst; (void) offset; (void) fi; (void) flags; /* * Offset is ignored and 0 passed to the filler fn so the whole * directory is read at once. */ /* EDF knows only the root directory */ if (strcmp(path, "/") != 0) return -ENOENT; filler(buf, ".", NULL, 0, 0); filler(buf, "..", NULL, 0, 0); memset(&walk, 0, sizeof(walk)); /* readdir is possible without open so fi->fh is not set */ walk.flag = WALK_FLAG_READDIR; walk.buf = buf; walk.filler = filler; walk_directory(&fst, &walk, NULL); return 0; } static int cmsfs_open(const char *path, struct fuse_file_info *fi) { struct fst_entry fst; struct file *f; off_t fst_addr; int rc = 0; /* * open flags: * O_DIRECTORY: FUSE captures open on / so not needed. * O_NOATIME: ignored because there is no atime in EDF. * O_NOFOLLOW: can be ignored since EDF has no links. * O_SYNC: ignored since IO is always sync. * O_TRUNC, O_CREAT, O_EXCL: avoided by FUSE. */ fst_addr = lookup_file(path + 1, &fst, SHOW_UNLINKED); if (!fst_addr) return -ENOENT; f = file_open(path + 1); if (f == NULL) { f = create_file_object(&fst, &rc); if (f == NULL) return rc; f->fst_addr = fst_addr; /* * Store file size in file object. Needed for write of fixed record * length files when the write is not a multiple of the record length. * In this case a second write would fail since the file size would * be calculated by lrecl * nr_records. Use session_size therefore. */ f->session_size = get_file_size_logical(&fst); if (f->session_size < 0) return -EIO; f->wcache = malloc(WCACHE_MAX); if (f->wcache == NULL) return -ENOMEM; /* * For fixed-length records f->fst->record_len contains * the fixed record length, which will not change. For * variable-length records it contains the (current) maximum * record length, which could be increased later by appending * new records, so use MAX_RECORD_LEN for the iconv buffer. * The MAX_RECORD_LEN is not valid for fixed-length records, * only for variable-length records, so use the actual record * length (f->fst->record_len) for fixed-length records. */ if (f->fst->record_format == RECORD_LEN_FIXED) f->iconv_buf = malloc(f->fst->record_len + 1); else f->iconv_buf = malloc(MAX_RECORD_LEN + 1); if (f->iconv_buf == NULL) { destroy_file_object(f); return -ENOMEM; } util_strlcpy(f->path, path, MAX_FNAME + 1); str_toupper(f->path); f->use_count = 1; util_list_add_head(&open_file_list, f); } else f->use_count++; if (fi->flags & O_RDWR || fi->flags & O_WRONLY) f->write_count++; fi->fh = (uint64_t)(unsigned long) f; return 0; } static void set_fdir_date_current(void) { struct fst_entry fst; int rc; rc = _read(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); set_fst_date_current(&fst); rc = _write(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); } static void increase_file_count(void) { struct fst_entry fst; int rc; rc = _read(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); fst.nr_records = ++cmsfs.files + 2; set_fst_date_current(&fst); rc = _write(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); } static void decrease_file_count(void) { struct fst_entry fst; int rc; rc = _read(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); fst.nr_records = --cmsfs.files + 2; set_fst_date_current(&fst); rc = _write(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); } static off_t get_reserved_block(void) { off_t addr; if (cmsfs.reserved_blocks > 0) cmsfs.reserved_blocks--; addr = get_zero_block(); BUG(addr < 0); return addr; } static void cache_dblocks(struct walk_file *walk) { double dblocks; /* calculate number of data blocks used for FST entries */ dblocks = (cmsfs.files + 2) * sizeof(struct fst_entry); dblocks = ceil(dblocks / cmsfs.blksize); /* add a spare one in case of create file */ dblocks++; memset(walk, 0, sizeof(*walk)); walk->flag = WALK_FLAG_CACHE_DBLOCKS; walk->dlist = calloc(dblocks, sizeof(off_t)); if (walk->dlist == NULL) DIE_PERROR("malloc failed"); walk_directory(NULL, walk, NULL); } static void free_dblocks(struct walk_file *walk) { free(walk->dlist); } static void purge_dblock_ptrs(int level, off_t addr) { int left = PTRS_PER_BLOCK; off_t ptr, start = addr; if (!level) return; level--; while (left--) { ptr = get_fixed_pointer(addr); BUG(ptr < 0); if (ptr != NULL_BLOCK) purge_dblock_ptrs(level, ptr); /* don't increment for null block pointers */ if (addr) addr += PTR_SIZE; } free_block(start); } /* * Return total number of pointer entries for level. */ static int pointers_per_level(struct file *f, int level, int nr_blocks) { double entries = nr_blocks; if (!level || nr_blocks < 2) return 0; if (level == 1) return nr_blocks; level--; while (level--) entries = ceil(entries / f->ptr_per_block); return (int) entries; } static int per_level_fixed(int level, int entries) { double val = entries; while (level--) val = ceil(val / PTRS_PER_BLOCK); return (int) val; } static void rewrite_dir_ptr_block(struct walk_file *walk, int level, off_t dst, int start) { struct fixed_ptr ptr; int rc, i, end; off_t addr; if (!level) return; end = MIN(start + PTRS_PER_BLOCK, per_level_fixed(level - 1, walk->dlist_used)); BUG(start > end); for (i = start; i < end; i++) { if (level == 1) { addr = walk->dlist[i]; if (addr) ptr.next = REL(addr); else ptr.next = 0; } else { addr = get_zero_block(); BUG(addr < 0); ptr.next = REL(addr); } rc = _write(&ptr, sizeof(ptr), dst); BUG(rc < 0); dst += sizeof(ptr); rewrite_dir_ptr_block(walk, level - 1, addr, i * PTRS_PER_BLOCK); } } static int update_dir_levels(int blocks) { int levels = 1; if (blocks < 2) return 0; while (blocks / PTRS_PER_BLOCK) { levels++; blocks /= PTRS_PER_BLOCK; } return levels; } static void rewrite_dblock_ptrs(struct walk_file *walk) { int rc, nr_blocks = walk->dlist_used; struct fst_entry fst; off_t dst; BUG(!nr_blocks); /* read in the directory FST */ rc = _read(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); if (nr_blocks == 1) { fst.fop = REL(cmsfs.fdir); fst.levels = 0; cmsfs.dir_levels = fst.levels; goto store; } dst = get_zero_block(); BUG(dst < 0); fst.fop = REL(dst); fst.levels = update_dir_levels(walk->dlist_used); cmsfs.dir_levels = fst.levels; rewrite_dir_ptr_block(walk, fst.levels, dst, 0); store: rc = _write(&fst, sizeof(fst), cmsfs.fdir); BUG(rc < 0); } /* * Update used block count in disk label. */ static void update_block_count(void) { int rc; rc = _write(&cmsfs.used_blocks, sizeof(unsigned int), cmsfs.label + 32); BUG(rc < 0); } static int cmsfs_create(const char *path, mode_t mode, struct fuse_file_info *fi) { char fname[8], ftype[8]; char uc_name[MAX_FNAME]; struct walk_file walk; struct fst_entry fst; off_t fst_addr = 0; int rc; /* no permissions in EDF */ (void) mode; /* * Note: creating a file that was unlinked but not yet deleted from * disk is not supported. That means the unlinked file can still be * opened. */ if (lookup_file(path + 1, &fst, SHOW_UNLINKED)) return cmsfs_open(path, fi); if (cmsfs.readonly) return -EACCES; rc = edf_name_valid(path + 1); if (rc) return rc; /* force uppercase */ util_strlcpy(uc_name, path + 1, MAX_FNAME); str_toupper(uc_name); rc = encode_edf_name(uc_name, fname, ftype); if (rc) return rc; /* find free fst entry */ memset(&walk, 0, sizeof(walk)); walk.flag = WALK_FLAG_LOCATE_EMPTY; walk_directory(&fst, &walk, &fst_addr); /* no free slot found, allocate new directory block */ if (!fst_addr) { /* * Be conservative and check if enough blocks for all * directory levels are available. */ if (cmsfs.total_blocks - cmsfs.used_blocks < cmsfs.dir_levels + 2) return -ENOSPC; fst_addr = get_zero_block(); if (fst_addr < 0) return fst_addr; cache_dblocks(&walk); /* add the newly allocated block to dlist */ walk.dlist[walk.dlist_used++] = fst_addr; purge_dblock_ptrs(cmsfs.dir_levels, get_fop(cmsfs.fdir)); rewrite_dblock_ptrs(&walk); free_dblocks(&walk); update_block_count(); } /* * Fill fst entry. Default template: * format: variable * record_len: 0 * mode: A1 * flag: 0, century bit (0x8) set by following utimens * fop: 0 * nr_records: 0 * nr_blocks: 0 * levels: 0 * ptr_size: 0xc for variable format * date: set to current date */ memset(&fst, 0, sizeof(fst)); memcpy(fst.name, fname, 8); memcpy(fst.type, ftype, 8); ebcdic_enc((char *) &fst.mode, "A1", 2); fst.record_format = RECORD_LEN_VARIABLE; fst.ptr_size = sizeof(struct var_ptr); rc = set_fst_date_current(&fst); if (rc != 0) return rc; rc = _write(&fst, sizeof(fst), fst_addr); BUG(rc < 0); cache_fst_addr(fst_addr, uc_name); increase_file_count(); return cmsfs_open(path, fi); } static int purge_pointer_block_fixed(struct file *f, int level, off_t addr) { int left = f->ptr_per_block; off_t ptr, start = addr; if (!level) return 0; level--; while (left--) { if (!level) break; ptr = get_fixed_pointer(addr); if (ptr < 0) return ptr; if (ptr != NULL_BLOCK) purge_pointer_block_fixed(f, level, ptr); /* don't increment for null block pointers */ if (addr) addr += PTR_SIZE; } free_block(start); return 0; } static int purge_pointer_block_variable(struct file *f, int level, off_t addr) { int nr, left = f->ptr_per_block; off_t ptr, start = addr; unsigned int disp; if (!level) return 0; level--; while (left--) { if (!level) break; ptr = get_var_pointer(addr, &nr, &disp); if (ptr < 0) return ptr; if (ptr == VAR_FILE_END) break; if (ptr != NULL_BLOCK) purge_pointer_block_variable(f, level, ptr); /* don't increment for null block pointers */ if (addr) addr += VPTR_SIZE; } free_block(start); return 0; } /* * Store the back pointer for a variable pointer block. * Pointer is offset of last VPTR to block start. */ static int store_back_pointer(off_t dst, int entries) { unsigned int back; back = (entries - 1) * VPTR_SIZE; return _write(&back, sizeof(back), ((dst | DATA_BLOCK_MASK) + 1) - sizeof(back)); } /* * Rewrite one pointer block starting from the highest level. */ static int rewrite_pointer_block_fixed(struct file *f, int level, off_t dst, int start) { struct fixed_ptr ptr; int i, end, rc = 0; off_t addr; if (!level) return 0; /* * start is always the first entry of a pointer block, * end is the last used entry in this pointer block. */ end = MIN(start + f->ptr_per_block, per_level_fixed(level - 1, f->fst->nr_blocks)); BUG(start > end); for (i = start; i < end; i++) { if (level == 1) { addr = f->blist[i].disk_addr; if (addr) ptr.next = REL(addr); else ptr.next = 0; } else { addr = get_reserved_block(); ptr.next = REL(addr); } rc = _write(&ptr, sizeof(ptr), dst); if (rc < 0) return rc; dst += sizeof(ptr); rc = rewrite_pointer_block_fixed(f, level - 1, addr, i * f->ptr_per_block); if (rc < 0) return rc; } return rc; } static int get_first_block_nr(int level, int entry) { while (level-- > 1) entry *= VPTRS_PER_BLOCK; return entry; } static int get_last_block_nr(int level, int entry, int nr_blocks) { while (level-- > 1) entry *= VPTRS_PER_BLOCK; entry--; if (entry > nr_blocks - 1) entry = nr_blocks - 1; return entry; } static int per_level_var(int level, int entries) { double val = entries; while (level--) val = ceil(val / VPTRS_PER_BLOCK); return (int) val; } /* * Rewrite one pointer block starting from the highest level. */ static int rewrite_pointer_block_variable(struct file *f, int level, off_t dst, int start) { int i, bnr, end, rc = 0; struct var_ptr ptr; off_t addr; if (!level) return 0; /* * start is always the first entry of a pointer block, * end is the last used entry in this pointer block. */ end = MIN(start + f->ptr_per_block, per_level_var(level - 1, f->fst->nr_blocks)); BUG(start > end); for (i = start; i < end; i++) { if (level == 1) { addr = f->blist[i].disk_addr; if (addr) ptr.next = REL(addr); else ptr.next = 0; } else { addr = get_reserved_block(); ptr.next = REL(addr); } bnr = get_first_block_nr(level, i); ptr.disp = f->blist[bnr].disp; bnr = get_last_block_nr(level, i + 1, f->fst->nr_blocks); ptr.hi_record_nr = f->blist[bnr].hi_record_nr; rc = _write(&ptr, sizeof(ptr), dst); if (rc < 0) return rc; dst += sizeof(ptr); rc = rewrite_pointer_block_variable(f, level - 1, addr, i * f->ptr_per_block); if (rc < 0) return rc; } return store_back_pointer(dst, end - start); } /* * Update fop and pointer blocks if needed. */ static int rewrite_pointers(struct file *f) { struct record *rec; off_t dst; if (f->fst->nr_blocks == 0) { f->fst->fop = 0; return 0; } if (f->fst->nr_blocks == 1) { rec = get_record(f, 0); if (rec->disk_start == NULL_BLOCK) f->fst->fop = 0; else f->fst->fop = REL(rec->disk_start); return 0; } /* allocate root block for fst */ dst = get_reserved_block(); f->fst->fop = REL(dst); return f->fops->write_pointers(f, f->fst->levels, dst, 0); } /* * Guess position in record table. */ static int guess_record_number(struct file *f, off_t offset) { int nr; if (f->linefeed) nr = (offset / (f->fst->record_len + 1)); else nr = (offset / f->fst->record_len); if (nr >= f->fst->nr_records) nr = f->fst->nr_records - 1; return nr; } static int offset_is_linefeed(off_t offset, struct record *prev, struct record *next) { if ((offset < next->file_start) && (offset >= prev->file_start + prev->total_len)) return 1; return 0; } static int offset_in_record(off_t offset, struct record *rec) { if (offset >= rec->file_start && offset < rec->file_start + rec->total_len) return 1; return 0; } static void set_hint(struct file *f, int hint) { f->next_record_hint = hint; /* limit hint to last record */ if (f->next_record_hint >= f->fst->nr_records) f->next_record_hint = f->fst->nr_records - 1; } /* * Find record by file offset. * * Returns: record number in *nr * > 0 : ptr to found record * -1 : linefeed offset * NULL : error */ static struct record *find_record(struct file *f, off_t offset, int *nr) { int i, start, step, max = f->fst->nr_records; struct record *rec; /* * next_record_hint is a guess which is optimal for sequential * single-threaded reads. */ i = f->next_record_hint; rec = &f->rlist[i]; if (offset_in_record(offset, rec)) { /* increment hint for sequential read, fails for extensions */ set_hint(f, i + 1); *nr = i; return rec; } /* look out for previous record linefeed from sequential read hint */ if (f->linefeed && i > 0) if (offset_is_linefeed(offset, &f->rlist[i - 1], rec)) return LINEFEED_OFFSET; start = guess_record_number(f, offset); /* because of potential linefeed we need to check the next record */ rec = &f->rlist[start]; if (offset < rec->file_start) step = -1; else step = 1; for (i = start; i >= 0 && i < max; i += step) { rec = &f->rlist[i]; if (offset_in_record(offset, rec)) { set_hint(f, i + 1); *nr = i; return rec; } /* last record reached, only one linefeed can follow */ if (i == max - 1) { if (f->linefeed && (offset == rec->file_start + rec->total_len)) return LINEFEED_OFFSET; else return NULL; } /* check for linefeed byte between two records */ if (step == 1) { if (offset_is_linefeed(offset, rec, &f->rlist[i + 1])) return LINEFEED_OFFSET; } else /* * No need to check if i > 0 since no linefeed before * record 0 possible. */ if (offset_is_linefeed(offset, &f->rlist[i - 1], rec)) return LINEFEED_OFFSET; } DEBUG("find: record not found!\n"); return NULL; } /* * Get disk address and block size from a record. */ static void get_block_data_from_record(struct record *rec, off_t offset, off_t *addr, int *chunk) { int record_off = offset - rec->file_start; struct record_ext *rext; if (record_off >= rec->first_block_len) { record_off -= rec->first_block_len; rext = rec->ext; BUG(rext == NULL); while (record_off >= rext->len) { record_off -= rext->len; rext = rext->next; BUG(rext == NULL); } /* found the right record extension */ if (rext->disk_start == NULL_BLOCK) *addr = NULL_BLOCK; else *addr = rext->disk_start + record_off; *chunk = rext->len - record_off; } else { if (rec->disk_start == NULL_BLOCK) *addr = NULL_BLOCK; else *addr = rec->disk_start + record_off; *chunk = rec->first_block_len - record_off; } } static int convert_text(iconv_t conv, char *in_buf, char *out_buf, int size) { size_t out_count = size; size_t in_count = size; int rc; rc = iconv(conv, &in_buf, &in_count, &out_buf, &out_count); if ((rc == -1) || (in_count != 0)) { DEBUG("Code page translation EBCDIC-ASCII failed\n"); return -EIO; } return 0; } static int cmsfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { struct file *f = get_fobj(fi); size_t len, copied = 0; struct record *rec; int chunk, nr, rc; off_t addr; (void) path; len = f->session_size; if ((size_t) offset >= len) return 0; if (offset + size > len) size = len - offset; while (size > 0) { rec = find_record(f, offset, &nr); if (rec == NULL) { copied = -EINVAL; DEBUG("%s: invalid addr, size: %lu copied: %lu len: %lu\n", __func__, size, copied, len); goto out; } /* write linefeed directly to buffer and go to next record */ if (rec == LINEFEED_OFFSET) { BUG(!f->linefeed); if (f->translate) *buf = LINEFEED_ASCII; else *buf = LINEFEED_EBCDIC; buf++; copied++; offset++; size--; continue; } /* get addr and block size from record */ get_block_data_from_record(rec, offset, &addr, &chunk); if (chunk <= 0 || addr < 0) DIE("Invalid record data\n"); /* copy less if there is not enough space in the fuse buffer */ if (size < (size_t) chunk) chunk = size; /* read one record */ if (addr == NULL_BLOCK) memset(buf, 0, chunk); else if (f->translate) { rc = _read(f->iconv_buf, chunk, addr); if (rc < 0) return rc; rc = convert_text(cmsfs.iconv_from, f->iconv_buf, buf, chunk); if (rc < 0) return rc; } else { rc = _read(buf, chunk, addr); if (rc < 0) return rc; } copied += chunk; size -= chunk; buf += chunk; offset += chunk; } out: DEBUG("%s: copied: %lu\n", __func__, copied); return copied; } static int cmsfs_statfs(const char *path, struct statvfs *buf) { unsigned int inode_size = cmsfs.blksize + sizeof(struct fst_entry); unsigned int free_blocks = cmsfs.total_blocks - cmsfs.used_blocks; unsigned long long tmp; (void) path; buf->f_bsize = buf->f_frsize = cmsfs.blksize; buf->f_blocks = cmsfs.total_blocks; buf->f_bfree = buf->f_bavail = free_blocks; /* number of possible inodes */ tmp = (unsigned long long) cmsfs.total_blocks * cmsfs.blksize / inode_size; buf->f_files = (long) tmp; tmp = (unsigned long long) free_blocks * cmsfs.blksize / inode_size; buf->f_ffree = (long) tmp; buf->f_namemax = MAX_FNAME - 1; return 0; } static int cmsfs_utimens(const char *path, const struct timespec ts[2], struct fuse_file_info *fi) { struct fst_entry fst; off_t fst_addr; struct tm tm; int rc; (void) fi; if (cmsfs.readonly) return -EACCES; fst_addr = lookup_file(path + 1, &fst, HIDE_UNLINKED); if (!fst_addr) return -ENOENT; /* convert timespec to tm */ memset(&tm, 0, sizeof(struct tm)); if (localtime_r(&ts[0].tv_sec, &tm) == NULL) return -EINVAL; update_fst_date(&fst, &tm); rc = _write(&fst, sizeof(fst), fst_addr); BUG(rc < 0); return 0; } /* * Get the address of the last directory entry. */ static off_t find_last_fdir_entry(off_t addr, int level) { struct fst_entry fst; int left, rc; off_t ptr; if (level > 0) { level--; left = PTRS_PER_BLOCK; while (left--) { ptr = get_fixed_pointer(addr + left * PTR_SIZE); BUG(ptr < 0); if (ptr) return find_last_fdir_entry(ptr, level); } DIE("Directory entry not found\n"); return 0; } left = cmsfs.blksize / sizeof(struct fst_entry); while (left--) { rc = _read(&fst, sizeof(fst), addr + left * sizeof(struct fst_entry)); BUG(rc < 0); if (is_file(fst.name, fst.type)) return addr + left * sizeof(struct fst_entry); } DIE("Directory entry not found\n"); } static int delete_file(const char *path) { off_t fst_kill, fst_last; struct walk_file walk; struct file *f_moved; char file[MAX_FNAME]; struct fst_entry fst; struct file *f; int rc = 0, i; if (cmsfs.readonly) return -EROFS; fst_kill = lookup_file(path + 1, &fst, SHOW_UNLINKED); if (!fst_kill) return -ENOENT; f = create_file_object(&fst, &rc); if (f == NULL) return rc; /* delete all data blocks */ for (i = 0; i < f->fst->nr_blocks; i++) free_block(f->blist[i].disk_addr); if (f->fst->fop) { rc = f->fops->delete_pointers(f, f->fst->levels, ABS(f->fst->fop)); if (rc < 0) goto error; } if (cmsfs.dir_levels) fst_last = find_last_fdir_entry(get_fop(cmsfs.fdir), cmsfs.dir_levels); else fst_last = find_last_fdir_entry(cmsfs.fdir, cmsfs.dir_levels); /* remove unlinked file from fcache */ util_strlcpy(file, path + 1, MAX_FNAME); str_toupper(file); invalidate_htab_entry(file); if (fst_last == fst_kill) goto skip_copy; /* copy last entry over unlinked entry */ rc = _read(&fst, sizeof(struct fst_entry), fst_last); BUG(rc < 0); rc = _write(&fst, sizeof(struct fst_entry), fst_kill); BUG(rc < 0); /* update moved fcache entry */ memset(file, 0, sizeof(file)); decode_edf_name(file, fst.name, fst.type); update_htab_entry(fst_kill, file); /* update cached address of moved FST */ f_moved = file_open(file); if (f_moved != NULL) f->fst_addr = fst_kill; skip_copy: /* delete last entry */ rc = _zero(fst_last, sizeof(struct fst_entry)); BUG(rc < 0); /* if the deleted entry was the first of a block, free the block */ if (fst_last % cmsfs.blksize == 0) { cache_dblocks(&walk); /* delete the last block from dlist */ walk.dlist_used--; free_block(fst_last); purge_dblock_ptrs(cmsfs.dir_levels, get_fop(cmsfs.fdir)); rewrite_dblock_ptrs(&walk); free_dblocks(&walk); } destroy_file_object(f); decrease_file_count(); update_block_count(); return 0; error: destroy_file_object(f); return rc; } static int cmsfs_rename(const char *path, const char *new_path, unsigned int flags) { struct fst_entry fst, fst_new; off_t fst_addr, fst_addr_new; char uc_old_name[MAX_FNAME]; char fname[8], ftype[8]; char *uc_new_name; struct file *f; int rc; (void) flags; if (cmsfs.readonly) return -EACCES; fst_addr = lookup_file(path + 1, &fst, HIDE_UNLINKED); if (!fst_addr) return -ENOENT; /* if new file already exists it must be overwritten so delete it */ fst_addr_new = lookup_file(new_path + 1, &fst_new, HIDE_UNLINKED); if (fst_addr_new) { delete_file(new_path); /* fst_addr may have changed due to copy-up */ fst_addr = lookup_file(path + 1, &fst, HIDE_UNLINKED); } rc = edf_name_valid(new_path + 1); if (rc) return rc; /* force uppercase */ uc_new_name = strdup(new_path + 1); if (uc_new_name == NULL) return -ENOMEM; str_toupper(uc_new_name); rc = encode_edf_name(uc_new_name, fname, ftype); free(uc_new_name); if (rc) return rc; memcpy(&fst.name[0], fname, 8); memcpy(&fst.type[0], ftype, 8); util_strlcpy(uc_old_name, path + 1, MAX_FNAME); str_toupper(uc_old_name); invalidate_htab_entry(uc_old_name); /* update name in file object if the file is opened */ f = file_open(uc_old_name); if (f != NULL) { util_strlcpy(f->path, new_path, MAX_FNAME + 1); str_toupper(f->path); memcpy(f->fst->name, fname, 8); memcpy(f->fst->type, ftype, 8); } rc = _write(&fst, sizeof(fst), fst_addr); BUG(rc < 0); return 0; } static int cmsfs_fsync(const char *path, int datasync, struct fuse_file_info *fi) { (void) path; (void) datasync; (void) fi; if (cmsfs.readonly) return -EROFS; return msync(cmsfs.map, cmsfs.size, MS_SYNC); } /* * Detect whether the whole block can be freed. */ static int block_started(struct file *f, struct record *rec) { if (rec->disk_start == NULL_BLOCK) { if (rec->null_block_started) return 1; else return 0; } if (f->fst->record_format == RECORD_LEN_FIXED) { if (rec->disk_start % cmsfs.blksize == 0) return 1; else return 0; } if (f->fst->record_format == RECORD_LEN_VARIABLE) { if ((rec->disk_start % cmsfs.blksize == 0) || (rec->disk_start % cmsfs.blksize == 1) || (rec->disk_start % cmsfs.blksize == 2)) return 1; else return 0; } return 0; } /* * Note: only called for the very last record of a file. That means if the * data starts on a block offset the block can be freed. It does not free * header bytes for variable record files if they are on the previous block! */ static void free_record(struct file *f, struct record *rec) { struct record_ext *rext = rec->ext; f->fst->nr_records--; if (block_started(f, rec)) { free_block(rec->disk_start); f->fst->nr_blocks--; if (!rec->disk_start && f->fst->record_format == RECORD_LEN_FIXED) f->nr_null_blocks--; } while (rext != NULL) { /* extensions always start on a new block */ free_block(rext->disk_start); f->fst->nr_blocks--; if (!rext->disk_start && f->fst->record_format == RECORD_LEN_FIXED) f->nr_null_blocks--; rext = rext->next; } } static int update_var_header_len(struct file *f, u16 *header, struct record *rec) { off_t prev_block_end; int rc, split = 0; if (rec->disk_start % cmsfs.blksize == 0) split = 2; if (rec->disk_start % cmsfs.blksize == 1) split = 1; /* header is completely in this block */ if (!split) { rc = _write(header, sizeof(*header), rec->disk_start - sizeof(*header)); if (rc < 0) return rc; return 0; } BUG(!rec->block_nr); prev_block_end = f->blist[rec->block_nr - 1].disk_addr | DATA_BLOCK_MASK; if (split == 1) { rc = _write((char *) header + 1, 1, rec->disk_start - 1); if (rc < 0) return rc; rc = _write((char *) header, 1, prev_block_end); if (rc < 0) return rc; return 0; } if (split == 2) { rc = _write(header, sizeof(*header), prev_block_end - 1); if (rc < 0) return rc; return 0; } return 0; } /* * Update the displacement of the last block if the block was spanned and * the new end is inside the previously spanned block. The displacement * must point after the last record to a null length header. * If the block wasn't spanned the displacement of the trimmed record needs * no update. */ static void adjust_displacement(struct file *f, int bnr, unsigned int disp) { if (f->blist[bnr].disp == VAR_RECORD_SPANNED) f->blist[bnr].disp = disp; } /* * Split the last record if needed and wipe until block end. * offset points to the last byte of the trimmed record that is * not a line feed. */ static int trim_record(struct file *f, off_t offset, struct record *rec) { int rc, wipe_off, wipe_len, free = 0; off_t file_start = rec->file_start; struct record_ext *rext; u16 header; BUG(!offset_in_record(offset, rec)); if (offset >= rec->file_start && offset < rec->file_start + rec->first_block_len) { wipe_off = offset + 1 - rec->file_start; wipe_len = cmsfs.blksize - ((rec->disk_start & DATA_BLOCK_MASK) + wipe_off); if (!wipe_len) goto ext; if (rec->disk_start) { rc = _zero(rec->disk_start + wipe_off, wipe_len); BUG(rc < 0); } if (f->fst->record_format == RECORD_LEN_VARIABLE) adjust_displacement(f, rec->block_nr, (rec->disk_start + wipe_off) & DATA_BLOCK_MASK); free = 1; } ext: if (rec->ext == NULL) goto header; file_start += rec->first_block_len; rext = rec->ext; do { if (free) { free_block(rext->disk_start); f->fst->nr_blocks--; if (!rext->disk_start && f->fst->record_format == RECORD_LEN_FIXED) f->nr_null_blocks--; } else { if (offset >= file_start && offset < file_start + rext->len) { wipe_off = offset + 1 - file_start; wipe_len = cmsfs.blksize - ((rec->disk_start & DATA_BLOCK_MASK) + wipe_off); if (!wipe_len) continue; if (rext->disk_start) { rc = _zero(rext->disk_start + wipe_off, wipe_len); BUG(rc < 0); } if (f->fst->record_format == RECORD_LEN_VARIABLE) adjust_displacement(f, rext->block_nr, (rext->disk_start + wipe_off) & DATA_BLOCK_MASK); free = 1; } } file_start += rext->len; rext = rext->next; } while (rext != NULL); header: /* update variable record header with new record length */ if (f->fst->record_format == RECORD_LEN_VARIABLE) { header = offset + 1 - rec->file_start; rc = update_var_header_len(f, &header, rec); if (rc < 0) return rc; /* update total_len in rlist, needed to recalculate lrecl */ rec->total_len = header; } return 0; } /* * Update levels count. */ static void update_levels(struct file *f) { int per_block = f->ptr_per_block; int levels = 1, blocks = f->fst->nr_blocks; if (blocks < 2) { f->fst->levels = 0; return; } while (blocks / per_block) { levels++; blocks /= per_block; } f->fst->levels = levels; } /* * Called by write only using the cached value. */ static void update_lrecl_fast(struct file *f, int rlen) { if (rlen > (int) f->fst->record_len) f->fst->record_len = rlen; } /* * Update longest record length for variable files. */ static void update_lrecl(struct file *f) { unsigned int lrecl = 0; struct record *rec; int i; if (f->fst->record_format == RECORD_LEN_FIXED) return; if (!f->fst->nr_records) { f->fst->record_len = 0; return; } for (i = 0; i < f->fst->nr_records; i++) { rec = get_record(f, i); if (rec->total_len > lrecl) lrecl = rec->total_len; } f->fst->record_len = lrecl; } static int shrink_file(struct file *f, off_t size) { struct record *new_end_rec, *end_rec; int rlen = f->fst->record_len; off_t offset = size; int new_end_nr, rc; /* truncate MUST be aligned to record length for fixed files */ if (f->fst->record_format == RECORD_LEN_FIXED) { if (f->linefeed) rlen++; if (size % rlen) return -EINVAL; } if (size == 0) { new_end_nr = -1; new_end_rec = NULL; goto free; } /* * offset may point to the linefeed after a record, let it point to the * last byte of the new last record instead. The linefeed is virtual * and will be generated automatically. */ if (f->linefeed) { new_end_rec = find_record(f, offset - 1, &new_end_nr); if (new_end_rec == LINEFEED_OFFSET) offset--; } /* get the new last record of the file */ new_end_rec = find_record(f, offset - 1, &new_end_nr); BUG(new_end_rec == NULL || new_end_rec == LINEFEED_OFFSET); free: /* free from the end until new_end_rec */ while (f->fst->nr_records - 1 > new_end_nr) { /* get the currently last record of the file */ end_rec = get_record(f, f->fst->nr_records - 1); free_record(f, end_rec); } if (new_end_rec != NULL) { rc = trim_record(f, offset - 1, new_end_rec); if (rc < 0) return rc; } f->session_size = size; if (f->fst->fop) f->fops->delete_pointers(f, f->fst->levels, ABS(f->fst->fop)); update_levels(f); update_lrecl(f); rc = rewrite_pointers(f); if (rc < 0) return rc; update_block_count(); return 0; } static void hide_null_blocks(struct file *f) { if (f->fst->record_format == RECORD_LEN_VARIABLE) return; f->fst->nr_blocks -= f->nr_null_blocks; } static void unhide_null_blocks(struct file *f) { if (f->fst->record_format == RECORD_LEN_VARIABLE) return; f->fst->nr_blocks += f->nr_null_blocks; } static void update_fst(struct file *f, off_t addr) { int rc; hide_null_blocks(f); rc = _write(f->fst, sizeof(*f->fst), addr); BUG(rc < 0); unhide_null_blocks(f); } static int cmsfs_truncate(const char *path, off_t size, struct fuse_file_info *fi) { struct fst_entry fst; off_t fst_addr, len; struct file *f; int rc = 0; (void) fi; if (cmsfs.readonly) return -EROFS; /* * If file is opened and modified disk content may be obsolete. * Must use the file object to get the current version of the file. */ f = file_open(path + 1); if (f != NULL) { fst_addr = f->fst_addr; len = f->session_size; } else { fst_addr = lookup_file(path + 1, &fst, HIDE_UNLINKED); if (!fst_addr) return -ENOENT; len = get_file_size_logical(&fst); if (len < 0) return -EIO; f = create_file_object(&fst, &rc); if (f == NULL) return rc; } if (len == size) return 0; if (size < len) { rc = shrink_file(f, size); if (rc != 0) return rc; } else return -EINVAL; rc = set_fst_date_current(f->fst); if (rc != 0) return rc; update_fst(f, fst_addr); if (!f->use_count) destroy_file_object(f); return rc; } #ifdef HAVE_SETXATTR static int cmsfs_setxattr(const char *path, const char *name, const char *value, size_t size, int flags) { struct fst_entry fst; off_t fst_addr; int mode_n, rc; long int rlen; char mode_l; char *in; /* meaningless since our xattrs are virtual and not stored on disk */ (void) flags; if (cmsfs.readonly) return -EROFS; fst_addr = lookup_file(path + 1, &fst, HIDE_UNLINKED); if (!fst_addr) return -ENOENT; if (strcmp(name, xattr_format.name) == 0) { /* only allowed for empty files */ if (fst.nr_records != 0) return -EINVAL; if (size != xattr_format.size) return -ERANGE; if (*value == 'F') { fst.record_format = RECORD_LEN_FIXED; fst.ptr_size = 0x4; } else if (*value == 'V') { fst.record_format = RECORD_LEN_VARIABLE; fst.ptr_size = 0xc; } else return -EINVAL; goto write; } if (strcmp(name, xattr_lrecl.name) == 0) { /* done by fs for variable files */ if (fst.record_format == RECORD_LEN_VARIABLE) return -EINVAL; /* only allowed for empty files */ if (fst.nr_records != 0) return -EINVAL; if (size > xattr_lrecl.size) return -ERANGE; in = calloc(size + 1, 1); if (in == NULL) return -ENOMEM; memcpy(in, value, size); errno = 0; rlen = strtol(in, (char **) NULL, 10); free(in); if (errno != 0 || rlen == 0 || rlen > MAX_RECORD_LEN) return -EINVAL; fst.record_len = rlen; goto write; } if (strcmp(name, xattr_mode.name) == 0) { if (size != xattr_mode.size) return -ERANGE; mode_l = value[0]; if (mode_l < 'A' || mode_l > 'Z') return -EINVAL; mode_n = atoi(&value[1]); if (!isdigit(value[1]) || mode_n < 0 || mode_n > 6) return -EINVAL; ebcdic_enc((char *) &fst.mode, value, sizeof(fst.mode)); goto write; } return -ENODATA; write: rc = _write(&fst, sizeof(fst), fst_addr); BUG(rc < 0); return 0; } static int cmsfs_getxattr(const char *path, const char *name, char *value, size_t size) { char buf[xattr_lrecl.size + 1]; struct fst_entry fst; /* nothing for root directory but clear error code needed */ if (strcmp(path, "/") == 0) return -ENODATA; if (!lookup_file(path + 1, &fst, HIDE_UNLINKED)) return -ENOENT; /* null terminate strings */ memset(value, 0, size); /* format */ if (strcmp(name, xattr_format.name) == 0) { if (size == 0) return xattr_format.size; if (size < xattr_format.size) return -ERANGE; if (fst.record_format == RECORD_LEN_FIXED) *value = 'F'; else if (fst.record_format == RECORD_LEN_VARIABLE) *value = 'V'; return xattr_format.size; } /* lrecl */ if (strcmp(name, xattr_lrecl.name) == 0) { if (size == 0) return xattr_lrecl.size; if (size < xattr_lrecl.size) return -ERANGE; memset(buf, 0, sizeof(buf)); snprintf(buf, sizeof(buf), "%d", fst.record_len); memcpy(value, buf, strlen(buf)); return strlen(buf); } /* mode */ if (strcmp(name, xattr_mode.name) == 0) { if (size == 0) return xattr_mode.size; if (size < xattr_mode.size) return -ERANGE; ebcdic_dec(value, (char *) &fst.mode, 2); return xattr_mode.size; } return -ENODATA; } static int cmsfs_listxattr(const char *path, char *list, size_t size) { struct fst_entry fst; size_t list_len; int pos = 0; if (!lookup_file(path + 1, &fst, HIDE_UNLINKED)) return -ENOENT; list_len = strlen(xattr_format.name) + 1 + strlen(xattr_lrecl.name) + 1 + strlen(xattr_mode.name); if (!size) return list_len; if (size < list_len) return -ERANGE; strcpy(list, xattr_format.name); pos += strlen(xattr_format.name) + 1; strcpy(&list[pos], xattr_lrecl.name); pos += strlen(xattr_lrecl.name) + 1; strcpy(&list[pos], xattr_mode.name); pos += strlen(xattr_mode.name) + 1; return pos; } #endif /* HAVE_SETXATTR */ /* * Return the number of unused bytes in the last block. */ static int examine_last_block(struct file *f) { struct record_ext *rext; struct record *rec; int last_bnr, len; off_t start; if (!f->fst->nr_blocks || !f->fst->nr_records) return 0; /* * For subsequent writes we know how much is left on the current block. * */ if (f->wstate->block_state != BWS_BLOCK_NOT_INIT) return f->wstate->block_free; last_bnr = f->fst->nr_blocks - 1; rec = &f->rlist[f->fst->nr_records - 1]; /* last block may be an extension */ if (rec->block_nr == last_bnr) { start = rec->disk_start; len = rec->first_block_len; goto out; } /* if write is split and exactly the extension not yet started */ if (rec->ext == NULL) return 0; rext = rec->ext; do { if (rext->block_nr == last_bnr) { start = rext->disk_start; len = rext->len; goto out; } rext = rext->next; } while (rext != NULL); return 0; out: if (start == NULL_BLOCK) start = f->null_ctr; return ((start | DATA_BLOCK_MASK) + 1) - (start + len); } static int get_record_len(struct file *f, size_t size) { int chunk = 0; if (f->fst->record_format == RECORD_LEN_FIXED) { if (!f->fst->record_len) { chunk = (size > MAX_RECORD_LEN) ? MAX_RECORD_LEN : size; f->fst->record_len = chunk; } else chunk = f->fst->record_len; if (chunk > MAX_RECORD_LEN) return -EINVAL; if (size < (size_t) chunk) chunk = size; } else if (f->fst->record_format == RECORD_LEN_VARIABLE) { chunk = size; if (chunk > MAX_RECORD_LEN) chunk = MAX_RECORD_LEN; } return chunk; } /* * Get the disk address of the first byte after the last record. */ static off_t disk_end(struct file *f) { struct record_ext *rext; struct record *rec; if (!f->fst->nr_records) return 0; /* * Only the first write on a newly opened file should set the write_ptr * according to what is on the disk. Subsequent writes should use the * actual write_ptr. This avoids the problem for fixed records that * for a record that is not yet completely written the calculated * write_ptr would be wrong. */ if (f->wstate->block_state != BWS_BLOCK_NOT_INIT) return f->write_ptr; rec = &f->rlist[f->fst->nr_records - 1]; if (rec->ext == NULL) { if (rec->disk_start == NULL_BLOCK) return 0; else return rec->disk_start + rec->first_block_len; } rext = rec->ext; while (rext->next != NULL) rext = rext->next; if (rext->disk_start == NULL_BLOCK) return 0; else return rext->disk_start + rext->len; } /* * Store the displacement for the first write to a new block. * nr_blocks already contains the new block. If the block is completely filled * the displacement is spanned, also if one header byte is used * since the header belongs to the record regarding the * displacement but not if both header bytes are on the block. */ void store_displacement(struct file *f, int disp) { f->blist[f->fst->nr_blocks - 1].disp = disp; f->wstate->block_state = BWS_BLOCK_USED; } /* * Allocate a new block if needed, set write pointer and return the number * of bytes available on the block. */ static int write_prepare_block(struct file *f, int null_block, ssize_t size) { int len, new_block = 0; if (null_block) { /* * TODO: need to write header before killing write_ptr for * sparse files. */ BUG(f->fst->record_format == RECORD_LEN_VARIABLE); f->write_ptr = 0; /* new null block started ? */ if (f->null_ctr % cmsfs.blksize == 0) { new_block = 1; len = cmsfs.blksize; /* * Prevent allocating a null block if the block would * not be complete. Use a normal block instead. */ if (size < cmsfs.blksize) { f->write_ptr = get_zero_block(); if (f->write_ptr < 0) return f->write_ptr; } } else len = ((f->null_ctr | DATA_BLOCK_MASK) + 1) - f->null_ctr; } else { if (f->write_ptr % cmsfs.blksize == 0) { /* * For fixed files use a different padding in text * mode to pad records with spaces. */ if (f->fst->record_format == RECORD_LEN_FIXED && f->translate) f->write_ptr = get_filled_block(); else f->write_ptr = get_zero_block(); if (f->write_ptr < 0) { /* reset to catch subsequent writes */ f->write_ptr = 0; return -ENOSPC; } new_block = 1; len = cmsfs.blksize; } else len = ((f->write_ptr | DATA_BLOCK_MASK) + 1) - f->write_ptr; } if (new_block) { f->wstate->block_state = BWS_BLOCK_NEW; f->blist[f->fst->nr_blocks].disk_addr = f->write_ptr; if (!f->write_ptr && f->fst->record_format == RECORD_LEN_FIXED) f->nr_null_blocks++; f->wstate->block_free = cmsfs.blksize; f->fst->nr_blocks++; } return len; } /* * Write variable record length header and return number of written bytes. */ static int write_var_header(struct file *f, int len, u16 vheader) { u8 half_vheader; int rc; if (f->wstate->var_record_state == RWS_HEADER_COMPLETE || f->wstate->var_record_state == RWS_RECORD_INCOMPLETE) return 0; if (f->wstate->var_record_state == RWS_HEADER_STARTED) { /* write secord header byte */ half_vheader = vheader & 0xff; rc = _write(&half_vheader, 1, f->write_ptr); if (rc < 0) return rc; f->write_ptr++; f->wstate->block_free--; f->wstate->var_record_state = RWS_HEADER_COMPLETE; f->wstate->var_records_written++; return 1; } /* block cannot be spanned if a header starts on it */ if (f->wstate->block_state == BWS_BLOCK_NEW) store_displacement(f, f->write_ptr & DATA_BLOCK_MASK); if (len >= 2) { rc = _write(&vheader, 2, f->write_ptr); if (rc < 0) return rc; f->write_ptr += 2; f->wstate->block_free -= 2; f->wstate->var_record_len = vheader; f->wstate->var_record_state = RWS_HEADER_COMPLETE; f->wstate->var_records_written++; return 2; } else { /* len = 1, write first header byte */ half_vheader = vheader >> 8; rc = _write(&half_vheader, 1, f->write_ptr); if (rc < 0) return rc; f->write_ptr++; f->wstate->block_free--; f->wstate->var_record_len = vheader; f->wstate->var_record_state = RWS_HEADER_STARTED; return 1; } } static int extend_block_fixed(struct file *f, const char *buf, int len, size_t size, int rlen) { int rc; (void) rlen; if (size < (size_t) len) len = size; if (f->write_ptr) { rc = _write(buf, len, f->write_ptr); if (rc < 0) return rc; f->write_ptr += len; f->wstate->block_free -= len; } return len; } static int extend_block_variable(struct file *f, const char *buf, int len, size_t size, int rlen) { int rc, copied = 0, vh_len = 0, max = cmsfs.blksize; if (!f->write_ptr) return len; while (len > 0) { /* record may be party written already */ if (size < (size_t) rlen) rlen = size; vh_len = write_var_header(f, len, rlen); if (vh_len < 0) return vh_len; len -= vh_len; if (!len) return copied; /* record does not fit on block */ if (len < rlen) rlen = len; /* remaining record data less than block len */ if (f->wstate->var_record_len < rlen) rlen = f->wstate->var_record_len; rc = _write(buf, rlen, f->write_ptr); if (rc < 0) return rc; f->write_ptr += rlen; f->wstate->block_free -= rlen; if (f->wstate->block_state == BWS_BLOCK_NEW) { /* * If the second byte of a split header was written * (blocksize - 1) is enough to make the block spanned. */ if (vh_len == 1) max--; if (rlen >= max) store_displacement(f, VAR_RECORD_SPANNED); else store_displacement(f, f->write_ptr & DATA_BLOCK_MASK); } copied += rlen; size -= rlen; len -= rlen; f->wstate->var_record_len -= rlen; BUG(f->wstate->var_record_len < 0); if (!f->wstate->var_record_len) { f->wstate->var_record_state = RWS_RECORD_COMPLETE; /* reset rlen for the next record */ rlen = get_record_len(f, size); } DEBUG("%s: wrote %d record bytes\n", __func__, rlen); if (size == 0) return copied; } /* record is not yet finished */ f->wstate->var_record_state = RWS_RECORD_INCOMPLETE; return copied; } /* * Extend an existing block or write data on a new block. * * size: requestes bytes to write to disk * rlen: projected record len * len: bytes left on the block */ static int extend_block(struct file *f, const char *buf, size_t size, int rlen) { int len = write_prepare_block(f, (buf == NULL) ? 1 : 0, size); if (len < 0) return -ENOSPC; BUG(!len); return f->fops->write_data(f, buf, len, size, rlen); } /* * Delete the record data from rlist and free extensions. */ static void delete_record(struct file *f, int nr) { struct record *rec = &f->rlist[nr]; struct record_ext *tmp, *rext = rec->ext; memset(rec, 0, sizeof(struct record)); while (rext != NULL) { tmp = rext->next; free(rext); rext = tmp; } } /* * Update records from a start record to the end. The start record is one less * than the previous last record since the previous last record may be * incomplete. */ static int update_records(struct file *f, int nrecords) { int i, rc, rnr, bnr = 0, skip = 0; off_t total = 0; rnr = f->fst->nr_records - 1; if (rnr >= 0) { total = f->rlist[rnr].file_start; if (f->linefeed && total) total--; bnr = f->rlist[rnr].block_nr; skip = f->rlist[rnr].disk_start & DATA_BLOCK_MASK; /* skip must point before a variable header */ if (f->fst->record_format == RECORD_LEN_VARIABLE) { if (skip >= VAR_RECORD_HEADER_SIZE) skip -= VAR_RECORD_HEADER_SIZE; else if (skip == 1) { bnr--; skip = cmsfs.blksize - 1; } else if (skip == 0 && f->rlist[rnr].disk_start) { bnr--; skip = cmsfs.blksize - 2; } } delete_record(f, rnr); rnr--; } if (rnr < -1) { rnr = -1; skip = 0; total = 0; bnr = 0; } f->fst->nr_records += nrecords; f->record_scan_state = RSS_DATA_BLOCK_STARTED; for (i = bnr; i < f->fst->nr_blocks; i++) { if (f->fst->record_format == RECORD_LEN_FIXED) cache_fixed_data_block(f, f->blist[i].disk_addr + skip, &bnr, &rnr, &total, skip); else { rc = cache_variable_data_block(f, f->blist[i].disk_addr + skip, &bnr, &rnr, f->blist[i].disp, &total, skip); if (rc < 0) return rc; } skip = 0; } return 0; } /* * Calculate the number of new records. */ static int new_records(struct file *f, size_t size, int rlen) { double tmp = size; int len; if (f->fst->record_format == RECORD_LEN_FIXED) { len = f->fst->record_len; if (f->linefeed) len++; /* need to fill a previously started record first */ if (f->session_size && f->session_size % len) tmp = tmp - (len - (f->session_size % len)); } if (tmp <= 0) return 0; tmp = ceil(tmp / rlen); return (int) tmp; } /* * Calculate number of new blocks. */ static int new_blocks(struct file *f, size_t size, int last, int nrecords) { int last_usable = last; double tmp = size; /* subtract header bytes */ if (last_usable && f->fst->record_format == RECORD_LEN_VARIABLE) { if (last_usable == 1) last_usable = 0; else last_usable -= VAR_RECORD_HEADER_SIZE; } if ((int) size <= last_usable) return 0; if (f->fst->record_format == RECORD_LEN_VARIABLE) tmp += (double) nrecords * VAR_RECORD_HEADER_SIZE; tmp -= last; if (tmp <= 0) return 0; tmp = ceil(tmp / cmsfs.blksize); return (int) tmp; } /* * Increase record list count. */ static void resize_rlist(struct file *f, int new) { if (!new) return; f->rlist = realloc(f->rlist, (f->fst->nr_records + new) * sizeof(struct record)); if (f->rlist == NULL) DIE_PERROR("realloc failed"); memset(&f->rlist[f->fst->nr_records], 0, new * sizeof(struct record)); } /* * Increase block list count. */ static void resize_blist(struct file *f, int new) { if (!new) return; f->blist = realloc(f->blist, (f->fst->nr_blocks + new) * sizeof(struct block)); if (f->blist == NULL) DIE_PERROR("realloc failed"); memset(&f->blist[f->fst->nr_blocks], 0, new * sizeof(struct block)); } /* * Reserve blocks for meta data (pointer blocks) of a file so that * blocks for meta-data are available if the disk runs full. */ static void reserve_meta_blocks(struct file *f, int old, int new, int level) { double nentries, oentries; int newp; if (!new) return; newp = pointers_per_level(f, level, old + new); oentries = pointers_per_level(f, level, old); oentries = ceil(oentries / f->ptr_per_block); nentries = newp; nentries = ceil(nentries / f->ptr_per_block); cmsfs.reserved_blocks += nentries - oentries; if (newp > f->ptr_per_block) reserve_meta_blocks(f, oentries, nentries, ++level); } /* * Update the max record number in the last pointer block per level. */ static int update_last_block_vptr(struct file *f, off_t addr, int level, struct var_ptr *vptr) { int last, rc; if (!level) return 0; level--; /* read offset pointer from the end of the var pointer block */ rc = _read(&last, sizeof(last), addr + cmsfs.blksize - sizeof(last)); if (rc < 0) return rc; if (last % sizeof(*vptr) || last > cmsfs.blksize) return -EIO; rc = _read(vptr, sizeof(*vptr), addr + last); if (rc < 0) return rc; /* update max record number */ vptr->hi_record_nr = f->fst->nr_records; rc = _write(vptr, sizeof(*vptr), addr + last); if (rc < 0) return rc; if (!level) return 0; if (vptr->next == NULL_BLOCK) return 0; return update_last_block_vptr(f, ABS(vptr->next), level, vptr); } static void reset_write_state(struct file *f) { f->wstate->var_record_state = RWS_RECORD_COMPLETE; f->wstate->var_record_len = 0; f->wstate->var_records_written = 0; } /* * Append records at current file end. If buf is NULL write zero bytes. */ static int write_append(struct file *f, const char *buf, size_t size) { int rc, i, nrecords, nblocks, last, len, copied = 0; int rlen = get_record_len(f, size); if (rlen < 0) return rlen; nrecords = new_records(f, size, rlen); /* get last block unused bytes for block count */ last = examine_last_block(f); /* initialize write_ptr once */ f->write_ptr = disk_end(f); nblocks = new_blocks(f, size, last, nrecords); if (nblocks > 0) f->ptr_dirty = 1; resize_rlist(f, nrecords); resize_blist(f, nblocks); if (f->fst->nr_blocks + nblocks > 1) reserve_meta_blocks(f, f->fst->nr_blocks, nblocks, 1); reset_write_state(f); /* first use existing last block */ if (last) { len = extend_block(f, buf, size, rlen); if (len < 0) return len; copied += len; size -= len; if (buf != NULL) buf += len; } for (i = 0; i < nblocks; i++) { len = extend_block(f, buf, size, rlen); if (len < 0) { if (copied > 0) { /* * Not all records may be written, need to * update nrecords to store a correct * hi_record_nr in the last vptr. */ if (f->fst->record_format == RECORD_LEN_VARIABLE) nrecords = f->wstate->var_records_written; goto out; } else return len; } copied += len; size -= len; if (buf != NULL) buf += len; DEBUG("%s: wrote: %d bytes\n", __func__, copied); } out: rc = update_records(f, nrecords); if (rc < 0) return rc; if (f->fst->record_format == RECORD_LEN_VARIABLE) update_lrecl_fast(f, rlen); return copied; } static int do_write(struct file *f, const char *buf, size_t size, off_t offset) { off_t len, copied = 0; struct var_ptr vptr; int rc; if (!size) return 0; len = f->session_size; if (f->linefeed) len -= f->fst->nr_records; BUG(len < 0); if (offset < len) return -EINVAL; /* * Writes with null blocks (sparse files) may be prevented by tools * which call lseek instead. Since we don't implement lseek fuse may * call us with an offset after file. We don't support sparse file * writes currently. */ if (offset > len) return -EINVAL; copied = write_append(f, buf, size); if (copied <= 0) return copied; f->session_size += copied; /* add linefeed byte */ if (f->linefeed) f->session_size++; if (f->ptr_dirty) { f->old_levels = f->fst->levels; update_levels(f); if (f->fst->fop) f->fops->delete_pointers(f, f->old_levels, ABS(f->fst->fop)); rc = rewrite_pointers(f); if (rc < 0) return rc; f->ptr_dirty = 0; } else if (f->fst->levels > 0 && f->fst->record_format == RECORD_LEN_VARIABLE) { rc = update_last_block_vptr(f, ABS(f->fst->fop), f->fst->levels, &vptr); if (rc < 0) return rc; } rc = set_fst_date_current(f->fst); if (rc != 0) return rc; update_fst(f, f->fst_addr); set_fdir_date_current(); update_block_count(); return copied; } static void cache_write_data(struct file *f, const char *buf, int len) { if (f->wcache_used + len > WCACHE_MAX) len = WCACHE_MAX - f->wcache_used; if (buf == NULL) memset(&f->wcache[f->wcache_used], FILLER_ASCII, len); else memcpy(&f->wcache[f->wcache_used], buf, len); f->wcache_used += len; } static void purge_wcache(struct file *f) { f->wcache_used = 0; f->wcache_commited = 0; } /* * Scan for the next newline character and return the number of bytes in * this record. */ static ssize_t find_newline(const char *buf, int len) { char *pos; pos = memchr(buf, LINEFEED_ASCII, len); if (pos == NULL) return LINEFEED_NOT_FOUND; else return pos - buf; } static int cmsfs_write_strings(struct file *f, const char *buf, size_t size, off_t offset) { int scan_len = MIN(size, (size_t)MAX_RECORD_LEN + 1); int rc, nl_byte = 1, null_record = 0, pad = 0; ssize_t rsize; /* remove already committed bytes */ offset -= f->wcache_commited; /* write offset must be at the end of the file */ if (offset + f->null_records + f->pad_bytes != f->session_size) return -EINVAL; if (f->fst->record_format == RECORD_LEN_FIXED && f->fst->record_len) scan_len = MIN(scan_len, f->fst->record_len + 1); rsize = find_newline(buf, scan_len); BUG(rsize < LINEFEED_NOT_FOUND); if (rsize == LINEFEED_NOT_FOUND) { if (f->wcache_used + scan_len >= WCACHE_MAX) { purge_wcache(f); return -EINVAL; } else { if (f->fst->record_format == RECORD_LEN_FIXED && f->wcache_commited + scan_len > f->fst->record_len) { purge_wcache(f); return -EINVAL; } cache_write_data(f, buf, scan_len); f->wcache_commited += scan_len; return scan_len; } } cache_write_data(f, buf, rsize); if (f->fst->record_format == RECORD_LEN_FIXED && f->wcache_used < f->fst->record_len) { pad = f->fst->record_len - f->wcache_used; cache_write_data(f, NULL, pad); } /* translate */ rc = convert_text(cmsfs.iconv_to, f->wcache, f->iconv_buf, f->wcache_used); if (rc < 0) return rc; /* * Note: empty records are forbidden by design. CMS converts * an empty record to a single space. Follow that convention. */ if (!f->wcache_used) { *f->wcache = FILLER_EBCDIC; *f->iconv_buf = FILLER_EBCDIC; f->wcache_used = 1; nl_byte = 0; null_record = 1; } /* correct file offset by removing the virtual linefeeds */ offset -= (f->fst->nr_records - f->null_records); offset += f->pad_bytes; BUG(offset < 0); rc = do_write(f, f->iconv_buf, f->wcache_used, offset); if (rc < 0) return rc; rc += nl_byte; if (null_record) f->null_records++; rc -= f->wcache_commited; rc -= pad; BUG(rc < 0); purge_wcache(f); f->pad_bytes += pad; return rc; } static int cmsfs_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { struct file *f = get_fobj(fi); int rc, written, nbytes; (void) path; if (cmsfs.readonly) return -EROFS; if (!f->linefeed) return do_write(f, buf, size, offset); /* Limit the size to what we can report back as written */ nbytes = MIN(size, (size_t) INT_MAX); written = 0; while (nbytes) { rc = cmsfs_write_strings(f, buf, nbytes, offset); if (rc < 0) return written ? written : rc; written += rc; offset += rc; buf += rc; nbytes -= rc; } return written; } static int cmsfs_unlink(const char *path) { struct fst_entry fst; off_t fst_addr; struct file *f; if (cmsfs.readonly) return -EROFS; fst_addr = lookup_file(path + 1, &fst, HIDE_UNLINKED); if (!fst_addr) return -ENOENT; f = file_open(path + 1); if (f != NULL) { f->unlinked = 1; return 0; } return delete_file(path); } static int flush_wcache(struct file *f) { off_t offset = f->session_size; int rc; /* translate */ rc = convert_text(cmsfs.iconv_to, f->wcache, f->iconv_buf, f->wcache_used); if (rc < 0) return rc; /* correct file offset by removing the virtual linefeeds */ offset -= (f->fst->nr_records - f->null_records); BUG(offset < 0); rc = do_write(f, f->iconv_buf, f->wcache_used, offset); purge_wcache(f); f->null_records = 0; if (rc < 0) return rc; return 0; } static int cmsfs_release(const char *path, struct fuse_file_info *fi) { struct file *f = get_fobj(fi); int rc = 0; (void) path; if (f == NULL) { DEBUG("release internal error\n"); return -EINVAL; } if (fi->flags & O_RDWR || fi->flags & O_WRONLY) { f->write_count--; if (f->wcache_used) rc = flush_wcache(f); } if (f->use_count == 1) { if (f->unlinked) delete_file(f->path); util_list_remove(&open_file_list, f); destroy_file_object(f); } else f->use_count--; fi->fh = 0; return rc; } static void init_fops(struct file *f) { if (f->fst->record_format == RECORD_LEN_FIXED) f->fops = &fops_fixed; else f->fops = &fops_variable; } /* * Create a file object to cache all needed file data. * Note: the caller must ensure that the file exists. */ static struct file *create_file_object(struct fst_entry *fst, int *rc) { struct file *f; f = malloc(sizeof(*f)); if (f == NULL) goto oom; memset(f, 0, sizeof(*f)); f->fst = malloc(sizeof(struct fst_entry)); if (f->fst == NULL) goto oom_f; memcpy(f->fst, fst, sizeof(*fst)); workaround_nr_blocks(f); init_fops(f); f->linefeed = linefeed_mode_enabled(f->fst); f->translate = f->linefeed; f->record_scan_state = RSS_DATA_BLOCK_STARTED; if (f->fst->record_format == RECORD_LEN_FIXED) f->ptr_per_block = cmsfs.fixed_ptrs_per_block; else f->ptr_per_block = cmsfs.var_ptrs_per_block; f->wstate = malloc(sizeof(*f->wstate)); if (f->wstate == NULL) goto oom_fst; memset(f->wstate, 0, sizeof(*f->wstate)); /* * Prevent calloc for zero records since it returns a pointer != NULL * which causes trouble at free. Also don't call cache_file. */ if (f->fst->nr_records == 0) return f; f->rlist = calloc(f->fst->nr_records, sizeof(struct record)); if (f->rlist == NULL) goto oom_wstate; f->blist = calloc(f->fst->nr_blocks, sizeof(struct block)); if (f->blist == NULL) goto oom_rlist; *rc = cache_file(f); if (*rc < 0) goto error; return f; error: if (*rc == 0) *rc = -ENOMEM; oom_rlist: free(f->rlist); oom_wstate: free(f->wstate); oom_fst: free(f->fst); oom_f: free(f); oom: return NULL; } static void destroy_file_object(struct file *f) { struct record_ext *rext, *tmp; struct record *rec; int i; free(f->iconv_buf); free(f->wcache); free(f->wstate); for (i = 0; i < f->fst->nr_records; i++) { rec = &f->rlist[i]; rext = rec->ext; while (rext != NULL) { tmp = rext->next; free(rext); rext = tmp; } } free(f->rlist); free(f->blist); free(f->fst); free(f); } static void *cmsfs_oper_init(struct fuse_conn_info *conn, struct fuse_config *cfg) { (void)conn; /* force immediate file removal */ cfg->hard_remove = 1; return NULL; } static struct file_operations fops_fixed = { .cache_data = cache_file_fixed, .write_data = extend_block_fixed, .delete_pointers = purge_pointer_block_fixed, .write_pointers = rewrite_pointer_block_fixed, }; static struct file_operations fops_variable = { .cache_data = cache_file_variable, .write_data = extend_block_variable, .delete_pointers = purge_pointer_block_variable, .write_pointers = rewrite_pointer_block_variable, }; static struct fuse_operations cmsfs_oper = { .init = cmsfs_oper_init, .getattr = cmsfs_getattr, .statfs = cmsfs_statfs, .readdir = cmsfs_readdir, .open = cmsfs_open, .release = cmsfs_release, .read = cmsfs_read, .utimens = cmsfs_utimens, .rename = cmsfs_rename, .fsync = cmsfs_fsync, .truncate = cmsfs_truncate, .create = cmsfs_create, .write = cmsfs_write, .unlink = cmsfs_unlink, #ifdef HAVE_SETXATTR .listxattr = cmsfs_listxattr, .getxattr = cmsfs_getxattr, .setxattr = cmsfs_setxattr, /* no removexattr since our xattrs are virtual */ #endif }; static int cmsfs_fuse_main(struct fuse_args *args, struct fuse_operations *cmsfs_oper) { #if FUSE_VERSION >= 26 return fuse_main(args->argc, args->argv, cmsfs_oper, NULL); #else return fuse_main(args->argc, args->argv, cmsfs_oper); #endif } static int cmsfs_process_args(void *data, const char *arg, int key, struct fuse_args *outargs) { (void) data; switch (key) { case FUSE_OPT_KEY_OPT: if (strcmp(arg, "allow_other") == 0) cmsfs.allow_other = 1; return 1; case FUSE_OPT_KEY_NONOPT: if (cmsfs.device == NULL) { cmsfs.device = strdup(arg); return 0; } return 1; case KEY_HELP: usage(outargs->argv[0]); /* * Usage output needs to go to stdout to be consistent with * coding guidelines. FUSE versions before 3.0.0 print help * output to stderr. Redirect stderr to stdout here to enforce * consistent behavior. */ fflush(stderr); dup2(STDOUT_FILENO, STDERR_FILENO); fuse_opt_add_arg(outargs, "-ho"); cmsfs_fuse_main(outargs, &cmsfs_oper); exit(0); case KEY_VERSION: fprintf(stdout, COMP "FUSE file system for CMS disks " "program version %s\n", RELEASE_STRING); fprintf(stdout, "Copyright IBM Corp. 2010, 2017\n"); exit(0); default: DIE("Process arguments error\n"); } } static void init_io(int fd) { int prot; DEBUG("read-only: %d", cmsfs.readonly); cmsfs.fd = fd; cmsfs.size = (off_t) cmsfs.total_blocks * cmsfs.blksize; DEBUG(" mmap size: %lu", cmsfs.size); /* try to map the whole block device for speeding-up access */ if (cmsfs.readonly) prot = PROT_READ; else prot = PROT_READ | PROT_WRITE; cmsfs.map = mmap(NULL, cmsfs.size, prot, MAP_SHARED, fd, 0); if (cmsfs.map == MAP_FAILED) { DEBUG("\nmmap failed, using pread/write for disk I/O.\n"); io_ops.read = &read_syscall; io_ops.write = &write_syscall; } else { DEBUG(" addr: %p\n", cmsfs.map); io_ops.read = &read_memory; io_ops.write = &write_memory; } } static void cmsfs_init(int fd) { init_io(fd); /* calculate blocksize dependent values */ cmsfs.data_block_mask = cmsfs.blksize - 1; cmsfs.nr_blocks_512 = cmsfs.blksize / 512; cmsfs.fixed_ptrs_per_block = cmsfs.blksize / sizeof(struct fixed_ptr); cmsfs.var_ptrs_per_block = cmsfs.blksize / sizeof(struct var_ptr); cmsfs.bits_per_data_block = get_order(cmsfs.blksize); /* store directory information */ cmsfs.dir_levels = get_levels(cmsfs.fdir); cmsfs.files = get_files_count(cmsfs.fdir); /* alloc cache entries for all files */ cmsfs.fcache_max = max_cache_entries(); cmsfs.fcache = calloc(cmsfs.fcache_max, sizeof(struct fcache_entry)); cmsfs.amap = get_fop(cmsfs.fdir + sizeof(struct fst_entry)); cmsfs.amap_levels = get_levels(cmsfs.fdir + sizeof(struct fst_entry)); cmsfs.amap_bytes_per_block = cmsfs.blksize * 8 * cmsfs.blksize; if (!hcreate_r(cmsfs.fcache_max, &cmsfs.htab)) DIE("hcreate failed\n"); util_list_init(&open_file_list, struct file, list); util_list_init(&text_type_list, struct filetype, list); scan_conf_file(&text_type_list); } int main(int argc, char *argv[]) { struct fuse_args args = FUSE_ARGS_INIT(argc, argv); char *fsname; int rc, fd; #ifdef DEBUG_ENABLED logfile = fopen(DEBUG_LOGFILE, "w"); if (logfile == NULL) DIE_PERROR("Cannot open file " DEBUG_LOGFILE " for writing"); #endif if (fuse_opt_parse(&args, &cmsfs, cmsfs_opts, cmsfs_process_args) == -1) DIE("Failed to parse option\n"); if (!cmsfs.device) DIE("Missing device\n" "Try '%s --help' for more information\n", argv[0]); DEBUG("using device: %s", cmsfs.device); fd = get_device_info(&cmsfs); DEBUG(" blocksize: %d\n", cmsfs.blksize); fsname = malloc(FSNAME_MAX_LEN); if (fsname == NULL) DIE_PERROR("malloc failed"); #if FUSE_VERSION >= 27 snprintf(fsname, FSNAME_MAX_LEN, "-osubtype=cmsfs,fsname=%s", cmsfs.device); #else snprintf(fsname, FSNAME_MAX_LEN, "-ofsname=%s", cmsfs.device); #endif fuse_opt_add_arg(&args, fsname); free(fsname); cmsfs_init(fd); if (cmsfs.readonly) fuse_opt_add_arg(&args, "-oro"); /* force single threaded mode which requires no locking */ fuse_opt_add_arg(&args, "-s"); if (cmsfs.mode == BINARY_MODE && (cmsfs.codepage_from != NULL || cmsfs.codepage_to != NULL)) DIE("Incompatible options, select -a or -t if using --from or --to\n"); if (cmsfs.mode != BINARY_MODE) { if (cmsfs.codepage_from == NULL) cmsfs.codepage_from = CODEPAGE_EDF; if (cmsfs.codepage_to == NULL) cmsfs.codepage_to = CODEPAGE_LINUX; setup_iconv(&cmsfs.iconv_from, cmsfs.codepage_from, cmsfs.codepage_to); setup_iconv(&cmsfs.iconv_to, cmsfs.codepage_to, cmsfs.codepage_from); } rc = cmsfs_fuse_main(&args, &cmsfs_oper); fuse_opt_free_args(&args); #ifdef DEBUG_ENABLED fclose(logfile); #endif hdestroy_r(&cmsfs.htab); return rc; }