mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
strip_right() strips spaces beginning at the end of the string moving to the beginning. However, it doesn't check whether it's at beginning of the array, and would continue reading if the string only contains spaces. Fix this by adding the necessary check. Signed-off-by: Sven Schnelle <svens@linux.ibm.com> Reviewed-by: Mete Durlu <meted@linux.ibm.com> Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
4722 lines
103 KiB
C
4722 lines
103 KiB
C
/*
|
|
* 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 <assert.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <fuse.h>
|
|
#include <fuse_opt.h>
|
|
#include <iconv.h>
|
|
#include <limits.h>
|
|
#include <linux/fs.h>
|
|
#ifdef HAVE_SETXATTR
|
|
#include <linux/xattr.h>
|
|
#endif
|
|
#include <math.h>
|
|
#include <search.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#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;
|
|
}
|