From d7e1d7b005747e4ff08db77ab0eaade80e63636a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Hor=C3=A1k?= Date: Fri, 28 Jan 2011 13:19:48 +0100 Subject: [PATCH 50/61] cmsfs-fuse: support for CMS EDF filesystems via fuse Summary: cmsfs-fuse: support for CMS EDF filesystems via fuse Description: Use the cmsfs-fuse command to read and write files stored on a z/VM CMS disk. The cmsfs-fuse file system translates the record-based EDF file system on the CMS disk to UNIX semantics. It is possible to mount a CMS disk and use common Linux tools to access the files on the disk. --- Makefile | 2 +- README | 12 + cmsfs-fuse/Makefile | 31 + cmsfs-fuse/amap.c | 217 ++ cmsfs-fuse/cmsfs-fuse.1 | 206 ++ cmsfs-fuse/cmsfs-fuse.c | 4536 +++++++++++++++++++++++++++++++++++++++++ cmsfs-fuse/cmsfs-fuse.h | 134 ++ cmsfs-fuse/config.c | 122 ++ cmsfs-fuse/dasd.c | 224 ++ cmsfs-fuse/ebcdic.h | 153 ++ cmsfs-fuse/edf.h | 123 ++ cmsfs-fuse/etc/filetypes.conf | 107 + cmsfs-fuse/helper.h | 54 + 13 files changed, 5920 insertions(+), 1 deletions(-) create mode 100644 cmsfs-fuse/Makefile create mode 100644 cmsfs-fuse/amap.c create mode 100644 cmsfs-fuse/cmsfs-fuse.1 create mode 100644 cmsfs-fuse/cmsfs-fuse.c create mode 100644 cmsfs-fuse/cmsfs-fuse.h create mode 100644 cmsfs-fuse/config.c create mode 100644 cmsfs-fuse/dasd.c create mode 100644 cmsfs-fuse/ebcdic.h create mode 100644 cmsfs-fuse/edf.h create mode 100644 cmsfs-fuse/etc/filetypes.conf create mode 100644 cmsfs-fuse/helper.h diff --git a/Makefile b/Makefile index 89c5fc5..e1f6f83 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ LIB_DIRS = libvtoc libu2s SUB_DIRS = $(LIB_DIRS) zipl zdump fdasd dasdfmt dasdview tunedasd \ tape390 osasnmpd qetharp ip_watcher qethconf scripts zconf \ vmconvert vmcp man mon_tools dasdinfo vmur cpuplugd ipl_tools \ - ziomon iucvterm hyptop + ziomon iucvterm hyptop cmsfs-fuse all: subdirs_make diff --git a/README b/README index ffd5e54..4335b43 100644 --- a/README +++ b/README @@ -157,6 +157,11 @@ s390-tools (1.8.2) - ts-shell: Terminal server shell to authorize and control IUCV terminal connections for individual Linux users. + * cmsfs-fuse: + Use the cmsfs-fuse command to read and write files stored on a z/VM + CMS disk. The cmsfs-fuse file system translates the record-based EDF file + system on the CMS disk to UNIX semantics. It is possible to mount a CMS + disk and use common Linux tools to access the files on the disk. For more information refer to the following publications: * "Device Drivers, Features, and Commands" chapter "Useful Linux commands" @@ -179,6 +184,13 @@ Dependencies: For executing the ziomon tools an installed blktrace package is required. See: git://git.kernel.dk/blktrace.git + * cmsfs-fuse: + cmsfs-fuse depends on FUSE. FUSE is provided by installing the fuse and + libfuse packages and by a kernel compiled with CONFIG_FUSE_FS. + For compiling the s390-tools package the fuse-devel package is required. + For further information about FUSE see: http://fuse.sourceforge.net/ + cmsfs-fuse requires FUSE version 2.8.1 or newer for full functionality. + Release History: ================ 1.8.2 diff --git a/cmsfs-fuse/Makefile b/cmsfs-fuse/Makefile new file mode 100644 index 0000000..7df81e0 --- /dev/null +++ b/cmsfs-fuse/Makefile @@ -0,0 +1,31 @@ +#!/usr/bin/make -f + +include ../common.mak + +CPPFLAGS += -I../include + +all: cmsfs-fuse + +CFLAGS += -D_FILE_OFFSET_BITS=64 -DHAVE_SETXATTR -I/usr/include/fuse +LDLIBS += -lfuse -lpthread -lrt -ldl -lm + +OBJECTS = cmsfs-fuse.o dasd.o amap.o config.o +$(OBJECTS): *.h Makefile + +CMSFS_FUSE_DIR = $(SYSCONFDIR)/cmsfs-fuse +CONFIG_FILES = filetypes.conf + +cmsfs-fuse: $(OBJECTS) + +install: all + $(INSTALL) -g $(GROUP) -o $(OWNER) -m 755 cmsfs-fuse $(USRBINDIR) + $(INSTALL) -g $(GROUP) -o $(OWNER) -m 644 cmsfs-fuse.1 $(MANDIR)/man1 + $(INSTALL) -g $(GROUP) -o $(OWNER) -d $(CMSFS_FUSE_DIR) + for cnf in $(CONFIG_FILES); do \ + $(INSTALL) -g $(GROUP) -o $(OWNER) -m 644 etc/$$cnf $(CMSFS_FUSE_DIR) ; \ + done + +clean: + rm -f cmsfs-fuse *.o + +.PHONY: all install clean diff --git a/cmsfs-fuse/amap.c b/cmsfs-fuse/amap.c new file mode 100644 index 0000000..04f83fc --- /dev/null +++ b/cmsfs-fuse/amap.c @@ -0,0 +1,217 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * Allocation map functions. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include "zt_common.h" +#include "helper.h" +#include "edf.h" +#include "cmsfs-fuse.h" + +/* + * Get block number from address. + */ +static int amap_blocknumber(off_t addr) +{ + return addr / BYTES_PER_BLOCK; +} + +/* + * Get the block number for a specific level. + */ +static int amap_blocknumber_level(int level, off_t addr) +{ + int entry = amap_blocknumber(addr); + + while (level-- > 1) + entry /= PTRS_PER_BLOCK; + return entry; +} + +/* + * Return address of to the allocation map for a block number > 0. + */ +static off_t get_amap_addr(int level, off_t addr, off_t ptr) +{ + int block = amap_blocknumber_level(level, addr); + + if (cmsfs.amap_levels == 0) + return cmsfs.amap; + + if (level--) { + ptr = get_fixed_pointer(ptr + block * PTR_SIZE); + if (!ptr) + DIE("amap invalid ptr at addr: %lx\n", + ptr + block * PTR_SIZE); + return get_amap_addr(level, addr, ptr); + } + return ptr; +} + +/* + * Mark disk address as allocated in alloc map. Unaligned addr is tolerated. + */ +static void amap_block_set(off_t addr) +{ + off_t amap = get_amap_addr(cmsfs.amap_levels, addr, cmsfs.amap); + int rc, block = amap_blocknumber(addr); + unsigned int byte, bit; + u8 entry; + + if (block > 0) + addr -= block * BYTES_PER_BLOCK; + + addr >>= BITS_PER_DATA_BLOCK; + byte = addr / 8; + bit = addr % 8; + + rc = _read(&entry, sizeof(entry), amap + byte); + BUG(rc < 0); + + /* already used */ + BUG(entry & (1 << (7 - bit))); + + entry |= (1 << (7 - bit)); + rc = _write(&entry, sizeof(entry), amap + byte); + BUG(rc < 0); +} + +/* + * Mark disk address as free in alloc map. Unaligned addr is tolerated. + */ +static void amap_block_clear(off_t addr) +{ + off_t amap = get_amap_addr(cmsfs.amap_levels, addr, cmsfs.amap); + int rc, block = amap_blocknumber(addr); + unsigned int byte, bit; + u8 entry; + + if (block > 0) + addr -= block * BYTES_PER_BLOCK; + + addr >>= BITS_PER_DATA_BLOCK; + byte = addr / 8; + bit = addr % 8; + + rc = _read(&entry, sizeof(entry), amap + byte); + BUG(rc < 0); + + /* already cleared */ + BUG(!(entry & (1 << (7 - bit)))); + + entry &= ~(1 << (7 - bit)); + rc = _write(&entry, sizeof(entry), amap + byte); + BUG(rc < 0); +} + +/* + * Return the first free bit in one byte. + */ +static int find_first_empty_bit(u8 entry) +{ + u8 i; + + for (i = 0; i < 8; i++) + if (!(entry & 1 << (7 - i))) + return i; + /* unreachable */ + return -1; +} + +/* + * Look for the first unallocated block and return addr of allocated block. + */ +static off_t __get_free_block(int level, off_t amap, int block_nr) +{ + off_t ptr, addr = amap; + unsigned int bit; + int left, rc, i; + u8 entry; + + if (level > 0) { + level--; + left = PTRS_PER_BLOCK; + while (left--) { + ptr = get_fixed_pointer(addr); + if (!ptr) + return 0; + ptr = __get_free_block(level, ptr, block_nr); + if (ptr) + return ptr; + addr += PTR_SIZE; + block_nr++; + } + return 0; + } + + for (i = 0; i < cmsfs.blksize; i++) { + rc = _read(&entry, sizeof(entry), amap + i); + BUG(rc < 0); + + if (entry != 0xff) { + /* get first empty bit and add to addr */ + bit = find_first_empty_bit(entry); + + /* bit -> addr */ + addr = (i * cmsfs.blksize * 8 + cmsfs.blksize * bit) + + (block_nr * BYTES_PER_BLOCK); + amap_block_set(addr); + return addr; + } + } + return 0; +} + +/* + * Allocate a free block and increment label block counter. + */ +off_t get_free_block(void) +{ + off_t addr; + + if (cmsfs.used_blocks + cmsfs.reserved_blocks >= cmsfs.total_blocks) + return -ENOSPC; + addr = __get_free_block(cmsfs.amap_levels, cmsfs.amap, 0); + BUG(!addr); + + cmsfs.used_blocks++; + return addr; +} + +/* + * Allocate a zero-filled block and increment label block counter. + */ +off_t get_zero_block(void) +{ + off_t addr = get_free_block(); + int rc; + + if (addr < 0) + return -ENOSPC; + + rc = _zero(addr, cmsfs.blksize); + if (rc < 0) + return rc; + return addr; +} + +/* + * Free a block and decrement label block counter. + */ +void free_block(off_t addr) +{ + if (addr) { + amap_block_clear(addr); + cmsfs.used_blocks--; + } +} diff --git a/cmsfs-fuse/cmsfs-fuse.1 b/cmsfs-fuse/cmsfs-fuse.1 new file mode 100644 index 0000000..2dc825d --- /dev/null +++ b/cmsfs-fuse/cmsfs-fuse.1 @@ -0,0 +1,206 @@ +.\" Copyright 2010 Jan Glauber (jan.glauber@de.ibm.com) +.\" +.TH CMSFS-FUSE 1 "February 2010" "s390-tools" + +.SH NAME +cmsfs-fuse \- File system for z/VM CMS disks + +.SH SYNOPSIS +.SS mounting: +.TP +\fBcmsfs-fuse\fP DEVICE MOUNTPOINT [OPTIONS] +.SS unmounting: +.TP +\fBfusermount\fP -u MOUNTPOINT + +.SH DESCRIPTION +Use the \fBcmsfs-fuse\fP command to provide read and write access +to files stored on a z/VM CMS disk. +The cmsfs-fuse file system translates the record-based EDF file system on +the CMS disk to UNIX semantics. +After mounting the CMS disk, you can use common Linux tools to access +the files on the disk. You can enable automatic conversions of text files from +EBCDIC to ASCII. + +Attention: You can inadvertently damage files and lose data when directly +writing to files within the cmsfs-fuse file system. To avoid problems when writing, +multiple restrictions must be observed, especially with regard to linefeeds (see +section RESTRICTIONS). + +If you are unsure about how to safely write to a file on the cmsfs-fuse file +system, copy the file to a location outside the cmsfs-fuse file system, edit the file, +and then copy it back to its original location. + +.SH OPTIONS +.SS "general options:" +.TP +\fB\-o\fR opt,[opt...] +Fuse or mount command options. For fuse options see below, for mount options +see \fBmount(8)\fP. +.TP +\fB\-h\fR or \fB\-\-help\fR +Print usage information, then exit. +.TP +\fB\-v\fR or \fB\-\-version\fR +Print version information, then exit. +.SS "cmsfs-fuse options:" +.TP +\fB\-a\fR or \fB\-\-ascii\fR +Interpret all files on the CMS disk as text files and convert them from +EBCDIC to ASCII. +.TP +\fB--from\fR +The codepage of the files on the CMS disk. If this option is not +specified the default codepage CP1047 is used. For a list of all available +codepages see iconv --list. +.TP +\fB--to\fR +The codepage to which CMS files should be converted to. If this option is not +specified the default codepage ISO-8859-1 is used. For a list of all available +codepages see iconv --list. +.TP +\fB\-t\fR or \fB\-\-filetype\fR +Interpret files on the CMS disk as text files based on the file type +and convert them from EBCDIC to ASCII. The file types that are treated +as text files are taken from a configuration file (see section CONFIGURATION FILES). + +.SS "Applicable FUSE options (version 2.8):" +.TP +\fB\-d\fR or \fB\-o\fR debug +Enable debug output (implies \fB\-f\fR) +.TP +\fB\-f\fR +Foreground operation +.TP +\fB\-o\fR allow_other +Allow access by other users +.TP +\fB\-o\fR allow_root +Allow access by root +.TP +\fB\-o\fR nonempty +Allow mounts over non\-empty file/dir +.TP +\fB\-o\fR default_permissions +Enable permission checking by kernel +.TP +.TP +\fB\-o\fR max_read=N +Set maximum size of read requests +.TP +\fB\-o\fR kernel_cache +Cache files in kernel +.TP +\fB\-o\fR [no]auto_cache +Enable caching based on modification times +.TP +\fB\-o\fR umask=M +Set file permissions (octal) +.TP +\fB\-o\fR uid=N +Set file owner +.TP +\fB\-o\fR gid=N +Set file group +.TP +\fB\-o\fR max_write=N +Set maximum size of write requests +.TP +\fB\-o\fR max_readahead=N +Set maximum readahead +.TP +\fB\-o\fR async_read +Perform reads asynchronously (default) +.TP +\fB\-o\fR sync_read +Perform reads synchronously +.TP +\fB\-o big_writes\fR +Enable write operations with more than 4 KB + +.SH EXTENDED ATTRIBUTES +Use the following extended attributes to handle the CMS characteristics of a file: + +\fBuser.record_format\fR: The format of a file. Allowed values are F for fixed record length files +and V for variable record length files. This attribute can be set only if the file is empty. + +\fBuser.record_lrecl\fR: The record length of a file. This attribute can be set only for a fixed +record length file and if the file is empty. A valid record length is an integer in the range 1-65535. + +\fBuser.file_mode\fR: The file mode of a file which is interpreted by CMS. The file mode consists +of a mode letter from A-Z and mode number from 0-6. + +New files are created by default as variable files with file mode A1. + +.SH RESTRICTIONS +\fBrename\fR and \fBcreat\fR: +Uppercase file names are enforced. + +\fBtruncate\fR: +Only shrinking of a file is supported. For fixed length record files, the new file size must +be a multiple of the record length. + +\fBunlink\fR: +Creating a file with the name of a previously unlinked file which is still in use is not supported +and will fail with -ENOENT. + +\fBwrite\fR: +Writes are supported only at the end of the file. +A write on a fixed length record file always writes a multiple +of the record length. If additional bytes are added, the +bytes are filled with zero in binary mode or with spaces in ASCII mode. Sparse files are not supported. +If the cp tool is used to write files to a CMS disk the option "--sparse=never" must be specified. + +If ASCII translation is enabled for a file a linefeed character determines the end of a record. +The following restrictions must be observed for writing files in ASCII mode: +For fixed record length files a linefeed must occur exactly after a record of the length specified in the fixed record length. +For variable record length files a linefeed must occur after the maximum record length is reached or earlier. +If a record of a variable record length file consists only of a linefeed character cmsfs-fuse adds a space to this record since +empty records are not supported by the CMS file system. + +.SH CONFIGURATION FILES +cmsfs-fuse uses a configuration file for automatic translation based on the file type. +Upon startup, cmsfs-fuse evaluates the file .cmsfs-fuse/filetypes.conf in the user's home directory. If the file does not +exist cmsfs-fuse evaluates the file /etc/cmsfs-fuse/filetypes.conf. + +The filetypes.conf file contains the CMS file types that are automaticaly translated to ASCII if cmsfs-fuse is started +with the -t option. The syntax of the configuration file is one file type per line. Lines that start with a # followed by a space are treated as +comments and are ignored. The file type is 8 characters long and must consist of valid CMS file name characters only. + +The default file types in the configuration file were taken from the z/VM TCPIP.DATA file +(z/VM version 5.4.0). + +.SH EXAMPLES +To mount the CMS disk with the name dasde enter: +.br + + # cmsfs-fuse /dev/dasde /mnt + +.br +To mount the CMS disk with the name dasde and enable automatic translation +of known text files enter: +.br + + # cmsfs-fuse -t /dev/dasde /mnt + +To mount the CMS disk with the name dasde and enable automatic translation +of all files to UTF-8 enter: +.br + + # cmsfs-fuse --to=UTF-8 -a /dev/dasde /mnt + +To unmount the CMS disk mounted on /mnt enter: +.br + + # fusermount -u /mnt + +To show the record format of file PROFILE.EXEC assuming the CMS disk was mounted on /mnt: + + # getfattr -n user.record_format /mnt/PROFILE.EXEC + +The following example assumes that an empty, fixed record format file, PROFILE.EXEC, can be accessed on a CMS disk that has been mounted on /mnt. To set the record length of PROFILE.EXEC to 80 bytes: + + # setfattr -n user.record_lrecl -v 80 /mnt/PROFILE.EXEC + +.SH SEE ALSO +attr (5), getfattr (1), setfattr(1), iconv(1) and Linux on System z: Device Drivers, Features and Commands diff --git a/cmsfs-fuse/cmsfs-fuse.c b/cmsfs-fuse/cmsfs-fuse.c new file mode 100644 index 0000000..6c5b0b5 --- /dev/null +++ b/cmsfs-fuse/cmsfs-fuse.c @@ -0,0 +1,4536 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * Main functions. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#define FUSE_USE_VERSION 26 +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_SETXATTR +#include +#endif +#include +#include + +#include "zt_common.h" +#include "list.h" +#include "helper.h" +#include "edf.h" +#include "cmsfs-fuse.h" +#include "ebcdic.h" + +struct cmsfs cmsfs; +struct list open_file_list; +struct list text_type_list; +FILE *logfile; + +#define PAGE_SIZE 0xfff +#define FSNAME_MAX_LEN 50 +#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(stderr, +"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 USED_BLOCK_ADDR (cmsfs.blksize * 2 + 32) + +#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_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, size_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; + +/* + * 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 */ + ssize_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; + /* record write state for variable headers */ + struct var_record_state *vrstate; + /* 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; + /* used bytes in write cache */ + int wcache_used; + /* commited 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 list list; + /* usage counter for all openers */ + int use_count; + /* usage counter for all writers */ + int write_count; + /* unlink flag */ + int unlinked; +}; + +struct var_record_state { + int rlen; + int record_state; + int block_state; +}; + +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 + */ +struct xattr xattr_format = { .name = "user.record_format", .size = 1 }; +struct xattr xattr_lrecl = { .name = "user.record_lrecl", .size = 5 }; +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) : "m" (num)); + return res & 0xffffffff; +} + +static unsigned int hex_to_dec(unsigned int num) +{ + unsigned long long res; + + asm volatile("cvd %1,%0" : "=m" (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 *) fi->fh; +} + +int _read(void *buf, size_t size, off_t addr) +{ + if (((addr + (off_t) size - 1) & ~DATA_BLOCK_MASK) > + (addr & ~DATA_BLOCK_MASK)) + DIE("read: crossing blocks addr: %lx size: %ld\n", + addr, size); + if ((addr < cmsfs.fdir) || ((size_t) addr > cmsfs.size)) + return -EIO; + + memcpy(buf, cmsfs.map + addr, size); + return 0; +} + +int _write(const void *buf, size_t size, off_t addr) +{ + if (((addr + (off_t) size - 1) & ~DATA_BLOCK_MASK) > + (addr & ~DATA_BLOCK_MASK)) + DIE("write: crossing blocks addr: %x size: %d\n", + (int)addr, (int)size); + + if ((addr < (2 * cmsfs.blksize)) || ((size_t) addr > cmsfs.size)) + return -EIO; + + if (buf == NULL) + memset(cmsfs.map + addr, 0, size); + else + memcpy(cmsfs.map + addr, buf, size); + return 0; +} + +int _zero(off_t addr, size_t size) +{ + return _write(NULL, size, addr); +} + +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 + 1; + 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; + + strncpy(uc_name, name, MAX_FNAME); + str_toupper(uc_name); + + list_iterate(f, &open_file_list, list) + 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 = daylight; + + /* prepare for mktime */ + tm.tm_hour--; + 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((unsigned long long *) fst->name, + (unsigned long long *) 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 (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, ssize_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, + size_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; + int i = 0; + + if (rec->ext == NULL) { + rec->ext = ext; + ext->prev = NULL; + ext->next = NULL; + } else { + tmp = rec->ext; + i++; + while (tmp->next != NULL) { + i++; + 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: interal 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; +} + +static int end_of_file(struct file *f, int record) +{ + if (record == f->fst->nr_records) + return 1; + return 0; +} + +static void walk_fixed_data_block(struct file *f, off_t addr, int *record, + size_t *total, int block, int disp) +{ + off_t offset = block * cmsfs.blksize + disp; + int rlen = (f->fst->record_len > cmsfs.blksize) ? + cmsfs.blksize : f->fst->record_len; + int first = (offset % f->fst->record_len) ? + rlen - (offset % rlen) : 0; + int left = cmsfs.blksize - disp; + + if (first) { + BUG(first > left); + set_record_extension(f, record, addr, first, block); + left -= first; + if (addr != NULL_BLOCK) + addr += first; + } + + while (left >= rlen) { + /* + * Increment record number only after adding a possible + * extension. *record starts with -1 so the first is 0. + */ + (*record)++; + if (end_of_file(f, *record)) + return; + + set_record_len(f, *record, f->fst->record_len); + set_record(f, record, addr, rlen, total, block); + + left -= rlen; + if (addr != NULL_BLOCK) + addr += rlen; + } + + /* partial record left */ + if (left > 0) { + (*record)++; + if (end_of_file(f, *record)) + return; + + set_record_len(f, *record, f->fst->record_len); + set_record(f, record, addr, left, total, block); + return; + } +} + +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, size_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 + 1)) + 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 void cache_fixed_data_block(struct file *f, off_t addr, int *block, + int *record, size_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) + return; + + walk_fixed_data_block(f, addr, record, total, *block, disp); + f->blist[*block].disk_addr = addr & ~DATA_BLOCK_MASK; + f->blist[*block].hi_record_nr = *record + 1; + (*block)++; +} + +/* + * 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, size_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; + cache_file_fixed(f, ptr, level, block, disp, record, total); + /* don't increment for null block pointers */ + if (addr) + addr += PTR_SIZE; + } + return 0; + } + cache_fixed_data_block(f, addr, block, record, total, 0); + return 0; +} + +static int cache_variable_data_block(struct file *f, off_t addr, int *block, + int *record, int disp, size_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; + + rc = walk_var_data_block(f, addr, disp, record, total, *block, skip); + if (rc < 0) + return rc; + + f->blist[*block].disk_addr = addr & ~DATA_BLOCK_MASK; + /* 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, size_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); + + list_iterate(ft, &text_type_list, list) + 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); + } + } + } + + 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; + + strncpy(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; + size_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 = f->fst->nr_records * f->fst->record_len / cmsfs.blksize; + if (f->fst->nr_records * f->fst->record_len % cmsfs.blksize) + nr++; + f->nr_null_blocks = nr - f->fst->nr_blocks; + f->fst->nr_blocks = nr; +} + +static ssize_t get_file_size_fixed(struct fst_entry *fst) +{ + return fst->nr_records * fst->record_len; +} + +static ssize_t get_file_size_variable_slow(struct fst_entry *fst) +{ + struct record *rec; + ssize_t total = 0; + int rc = 0; + struct file *f = create_file_object(fst, &rc); + + if (f == NULL) + return rc; + + rec = get_record(f, f->fst->nr_records - 1); + total = rec->file_start + rec->total_len; + + /* + * Note: need to add header bytes since the record information does + * not contain them but get_file_size_logical will remove them... + */ + total += f->fst->nr_records * VAR_RECORD_HEADER_SIZE; + destroy_file_object(f); + return total; +} + +static ssize_t get_file_size_variable(struct fst_entry *fst) +{ + struct var_ptr vptr; + ssize_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. Cannot scan that block, + * need to scan the whole file instead... + */ + total = get_file_size_variable_slow(fst); + goto skip; + } + 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 += (fst->nr_blocks - 1) * cmsfs.blksize; +skip: + return total; +} + +/* + * Return the file size as it is on the disk. Includes headers for + * variable records. + */ +static ssize_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 ssize_t get_file_size_logical(struct fst_entry *fst) +{ + ssize_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 -= 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) +{ + 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 = fst.record_len * 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 = 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) +{ + struct walk_file walk; + struct fst_entry fst; + + (void) offset; + (void) fi; + + /* + * 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); + filler(buf, "..", NULL, 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 alwasy 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; + + strncpy(f->path, path, MAX_FNAME + 1); + str_toupper(f->path); + + f->use_count = 1; + list_add(&f->list, &open_file_list); + } else + f->use_count++; + + if (fi->flags & O_RDWR || fi->flags & O_WRONLY) + f->write_count++; + + fi->fh = (u64) 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 + 1)) { + 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), USED_BLOCK_ADDR); + 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 */ + strncpy(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 *buf, int size) +{ + size_t out_count = size; + size_t in_count = size; + char *data_ptr = buf; + int rc; + + rc = iconv(conv, &data_ptr, &in_count, &data_ptr, &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 { + rc = _read(buf, chunk, addr); + if (rc < 0) + return rc; + } + + if (f->translate) { + rc = convert_text(cmsfs.iconv_from, buf, chunk); + 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; + + (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 */ + buf->f_files = cmsfs.total_blocks * cmsfs.blksize / inode_size; + + buf->f_ffree = free_blocks * cmsfs.blksize / inode_size; + buf->f_namemax = MAX_FNAME - 1; + return 0; +} + +static int cmsfs_utimens(const char *path, const struct timespec ts[2]) +{ + struct fst_entry fst; + off_t fst_addr; + struct tm tm; + int rc; + + 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; +} + +static int cmsfs_rename(const char *path, const char *new_path) +{ + char uc_old_name[MAX_FNAME]; + char fname[8], ftype[8]; + struct fst_entry fst; + char *uc_new_name; + struct file *f; + off_t fst_addr; + int rc; + + if (cmsfs.readonly) + return -EACCES; + + fst_addr = lookup_file(path + 1, &fst, HIDE_UNLINKED); + if (!fst_addr) + return -ENOENT; + + 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); + if (rc) + return rc; + + memcpy(&fst.name[0], fname, 8); + memcpy(&fst.type[0], ftype, 8); + + strncpy(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) { + strncpy(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 + 1)) { + 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 fst_entry fst; + off_t fst_addr; + struct file *f; + ssize_t len; + int rc = 0; + + 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; + + 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; + + 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->vrstate->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) + return f->write_ptr; + new_block = 1; + len = cmsfs.blksize; + } else + len = ((f->write_ptr | DATA_BLOCK_MASK) + 1) - f->write_ptr; + } + + if (new_block) { + if (f->fst->record_format == RECORD_LEN_VARIABLE) + f->vrstate->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->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->vrstate->record_state == RWS_HEADER_COMPLETE || + f->vrstate->record_state == RWS_RECORD_INCOMPLETE) + return 0; + + if (f->vrstate->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->vrstate->record_state = RWS_HEADER_COMPLETE; + return 1; + } + + /* block cannot be spanned if a header starts on it */ + if (f->vrstate->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->vrstate->rlen = vheader; + f->vrstate->record_state = RWS_HEADER_COMPLETE; + 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->vrstate->rlen = vheader; + f->vrstate->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; + } + 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->vrstate->rlen < rlen) + rlen = f->vrstate->rlen; + rc = _write(buf, rlen, f->write_ptr); + if (rc < 0) + return rc; + + f->write_ptr += rlen; + + if (f->vrstate->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->vrstate->rlen -= rlen; + + BUG(f->vrstate->rlen < 0); + if (!f->vrstate->rlen) + f->vrstate->record_state = RWS_RECORD_COMPLETE; + + DEBUG("%s: wrote %d record bytes\n", __func__, rlen); + if (size <= 0) + return copied; + } + + /* record is not yet finished */ + f->vrstate->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; + size_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 += 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); +} + +/* + * 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); + + if (f->fst->record_format == RECORD_LEN_VARIABLE) + f->vrstate->record_state = RWS_RECORD_COMPLETE; + + /* 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) + 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) +{ + ssize_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) { + 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(const char *path, const char *buf, size_t size, + off_t offset, struct fuse_file_info *fi) +{ + int scan_len = min(size, MAX_RECORD_LEN + 1); + int rc, nl_byte = 1, null_record = 0, pad = 0; + struct file *f = get_fobj(fi); + ssize_t rsize; + + (void) path; + + if (cmsfs.readonly) + return -EROFS; + + if (!f->linefeed) + return do_write(f, buf, size, offset); + + /* remove already comitted bytes */ + offset -= f->wcache_used; + + /* 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->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->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->wcache, 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; +} + +/* + * Get the address of the last directory entry. + */ +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((unsigned long long *) fst.name, + (unsigned long long *) 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 */ + strncpy(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_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->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->wcache, 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); + list_del(&f->list); + 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->vrstate = malloc(sizeof(*f->vrstate)); + if (f->vrstate == NULL) + goto oom_fst; + memset(f->vrstate, 0, sizeof(*f->vrstate)); + + /* + * 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_vrstate; + + 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_vrstate: + free(f->vrstate); +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->wcache); + free(f->vrstate); + + 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 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 = { + .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]); + fuse_opt_add_arg(outargs, "-ho"); + cmsfs_fuse_main(outargs, &cmsfs_oper); + exit(0); + case KEY_VERSION: + fprintf(stderr, COMP "FUSE file system for CMS disks " + "program version %s\n", RELEASE_STRING); + fprintf(stderr, "Copyright IBM Corp. 2010\n"); + fuse_opt_add_arg(outargs, "--version"); + exit(0); + + default: + DIE("Process arguments error\n"); + } +} + +static void map_device(int fd) +{ + int prot; + + /* fstat on block device says st_size = 0... */ + lseek(fd, 0, SEEK_SET); + cmsfs.size = lseek(fd, 0, SEEK_END); + DEBUG("mmap size: %lu", cmsfs.size); + + /* 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) + DIE_PERROR("mmap failed"); + DEBUG(" addr: %p read-only: %d\n", cmsfs.map, cmsfs.readonly); +} + +static void cmsfs_init(int fd) +{ + map_device(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"); + + list_init(&text_type_list); + scan_conf_file(&text_type_list); + list_init(&open_file_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); + cmsfs.fd = fd; + + if (cmsfs.readonly) + fuse_opt_add_arg(&args, "-oro"); + /* force single threaded mode which requires no locking */ + fuse_opt_add_arg(&args, "-s"); + /* force immediate file removal */ + fuse_opt_add_arg(&args, "-ohard_remove"); + + 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; +} diff --git a/cmsfs-fuse/cmsfs-fuse.h b/cmsfs-fuse/cmsfs-fuse.h new file mode 100644 index 0000000..6e7fda6 --- /dev/null +++ b/cmsfs-fuse/cmsfs-fuse.h @@ -0,0 +1,134 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * Data structures. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#ifndef _CMSFS_H +#define _CMSFS_H + +#define _GNU_SOURCE +#include +#include +#include "list.h" +#include "list.h" + +#define COMP "cmsfs-fuse: " +extern struct cmsfs cmsfs; + +/* conversion between absolute and relative addresses */ +#define ABS(x) ((x - 1) * cmsfs.blksize) +#define REL(x) ((x / cmsfs.blksize) + 1) + +struct fcache_entry { + /* filename used as hash key */ + char name[18]; + /* location of fst entry */ + off_t fst_addr; + /* filename string address */ + char *str; +}; + +enum cmsfs_mode { + BINARY_MODE, + TEXT_MODE, + TYPE_MODE, +}; + +/* the per device global struture */ +struct cmsfs { + /* name of the block device, e.g. /dev/dasde */ + const char *device; + /* global file descriptor of the underlying block device */ + int fd; + /* start of mmap of the whole block device */ + char *map; + /* size of the disk */ + size_t size; + /* formatted blocksize */ + int blksize; + /* number of 512 byte blocks per block */ + int nr_blocks_512; + /* disk info */ + unsigned int format; + /* device is read only */ + int readonly; + /* access permission for other users */ + int allow_other; + /* offset to file directory root FST */ + off_t fdir; + /* offset to allocation map */ + off_t amap; + /* depth of directories */ + int dir_levels; + /* depth of allocation maps */ + int amap_levels; + /* files count on the device */ + int files; + /* conversion mode */ + enum cmsfs_mode mode; + /* iconv codepage options */ + const char *codepage_from; + const char *codepage_to; + iconv_t iconv_from; + iconv_t iconv_to; + + /* disk stats */ + int total_blocks; + int used_blocks; + /* blocks reserved for outstanding meta data */ + int reserved_blocks; + + /* constants */ + int fixed_ptrs_per_block; + int var_ptrs_per_block; + int bits_per_data_block; + int bits_per_ptr_block; + int data_block_mask; + int amap_bytes_per_block; + + /* file cache */ + struct fcache_entry *fcache; + int fcache_used; + int fcache_max; + struct hsearch_data htab; +}; +#define MAX_TYPE_LEN 9 + +struct filetype { + char name[MAX_TYPE_LEN]; + struct list list; +}; + + +#define MAX_TYPE_LEN 9 + +#define NULL_BLOCK 0 +#define VAR_FILE_END 1 + +#define PTRS_PER_BLOCK (cmsfs.fixed_ptrs_per_block) +#define VPTRS_PER_BLOCK (cmsfs.var_ptrs_per_block) +#define DATA_BLOCK_MASK (cmsfs.data_block_mask) +#define BITS_PER_DATA_BLOCK (cmsfs.bits_per_data_block) +extern int scan_conf_file(struct list *list); +extern int is_edf_char(int c); +#define BYTES_PER_BLOCK (cmsfs.amap_bytes_per_block) + +extern int get_device_info(struct cmsfs *cmsfs); +extern int scan_conf_file(struct list *list); +extern int is_edf_char(int c); + +#ifndef _CMSFS_FSCK +int _read(void *, size_t, off_t); +int _write(const void *, size_t, off_t); +int _zero(off_t, size_t); +off_t get_fixed_pointer(off_t); + +off_t get_free_block(void); +off_t get_zero_block(void); +void free_block(off_t); +#endif + +#endif diff --git a/cmsfs-fuse/config.c b/cmsfs-fuse/config.c new file mode 100644 index 0000000..9f9de45 --- /dev/null +++ b/cmsfs-fuse/config.c @@ -0,0 +1,122 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * Config option parsing. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "cmsfs-fuse.h" +#include "helper.h" +#include "zt_common.h" + +#define MAX_LINE_LEN 80 + +char *conffile; + +int open_conf_file(FILE **fh) +{ + const char *home_env = getenv("HOME"); + + if (home_env == NULL) + goto no_home; + + conffile = malloc(4096); + if (conffile == NULL) + DIE_PERROR("malloc failed"); + sprintf(conffile, "%s/.cmsfs-fuse/filetypes.conf", home_env); + *fh = fopen(conffile, "r"); + if (*fh == NULL) + goto no_home; +out: + DEBUG("using config file: %s\n", conffile); + return 0; + +no_home: + sprintf(conffile, "%s/%s", TOOLS_SYSCONFDIR, + "/cmsfs-fuse/filetypes.conf"); + *fh = fopen(conffile, "r"); + if (*fh == NULL) { + free(conffile); + return -ENOENT; + } + goto out; +} + +void add_filetype(char *name, struct list *head) +{ + struct filetype *entry; + + entry = malloc(sizeof(*entry)); + if (entry == NULL) + DIE_PERROR("malloc failed"); + strncpy(entry->name, name, MAX_TYPE_LEN); + list_add(&entry->list, head); +} + +int filetype_valid(const char *type, int line) +{ + unsigned int i; + + if (strlen(type) > 8) { + WARN("entry too long in line: %d in config file: %s\n", + line, conffile); + return 0; + } + + for (i = 0; i < strlen(type); i++) + if (!is_edf_char(*(type + i))) { + WARN("invalid character in line: %d in config file: %s\n", + line, conffile); + return 0; + } + + return 1; +} + +int scan_conf_file(struct list *head) +{ + char buf[MAX_LINE_LEN], *tmp; + int line = 0; + FILE *fh; + + if (open_conf_file(&fh) < 0) + return -ENOENT; + + while (fgets(buf, MAX_LINE_LEN, fh) != NULL) { + line++; + tmp = buf; + while (isblank(*tmp)) + tmp++; + + if (*tmp == '\n') + continue; + + /* + * Skip comments, comment must be "# " because # is a valid + * EDF character. + */ + if (strlen(tmp) > 1 && *tmp == '#' && *(tmp + 1) == ' ') + continue; + + /* remove trailing \n */ + if (strlen(tmp) && *(tmp + strlen(tmp) - 1) == '\n') + *(tmp + strlen(tmp) - 1) = '\0'; + + if (filetype_valid(tmp, line)) + add_filetype(tmp, head); + } + fclose(fh); + free(conffile); + return 0; +} diff --git a/cmsfs-fuse/dasd.c b/cmsfs-fuse/dasd.c new file mode 100644 index 0000000..d79d34d --- /dev/null +++ b/cmsfs-fuse/dasd.c @@ -0,0 +1,224 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * DASD specific functions. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "helper.h" +#include "edf.h" +#include "cmsfs-fuse.h" + +typedef struct dasd_info { + unsigned int devno; /* S/390 devno */ + unsigned int real_devno; /* for aliases */ + unsigned int schid; /* S/390 subchannel identifier */ + unsigned int cu_type :16; /* from SenseID */ + unsigned int cu_model :8; /* from SenseID */ + unsigned int dev_type :16; /* from SenseID */ + unsigned int dev_model :8; /* from SenseID */ + unsigned int open_count; + unsigned int req_queue_len; + unsigned int chanq_len; /* length of chanq */ + char type[4]; /* from discipline.name, 'none' for unknown */ + unsigned int status; /* current device level */ + unsigned int label_block; /* where to find the VOLSER */ + unsigned int FBA_layout; /* fixed block size (like AIXVOL) */ + unsigned int characteristics_size; + unsigned int confdata_size; + char characteristics[64]; /* from read_device_characteristics */ + char configuration_data[256]; /* from read_configuration_data */ +} dasd_info_t; + +typedef struct dasd_info2 { + unsigned int devno; /* S/390 devno */ + unsigned int real_devno; /* for aliases */ + unsigned int schid; /* S/390 subchannel identifier */ + unsigned int cu_type :16; /* from SenseID */ + unsigned int cu_model :8; /* from SenseID */ + unsigned int dev_type :16; /* from SenseID */ + unsigned int dev_model :8; /* from SenseID */ + unsigned int open_count; + unsigned int req_queue_len; + unsigned int chanq_len; /* length of chanq */ + char type[4]; /* from discipline.name, 'none' for unknown */ + unsigned int status; /* current device level */ + unsigned int label_block; /* where to find the VOLSER */ + unsigned int FBA_layout; /* fixed block size (like AIXVOL) */ + unsigned int characteristics_size; + unsigned int confdata_size; + char characteristics[64]; /* from read_device_characteristics */ + char configuration_data[256]; /* from read_configuration_data */ + unsigned int format; /* format info like formatted/cdl/ldl/... */ + unsigned int features; /* dasd features like 'ro',... */ + unsigned int reserved[8]; +} dasd_info2_t; + +#define HDIO_GETGEO 0x0301 +#define BLKSSZGET _IO(0x12, 104) +#define BIODASDINFO _IOR('D', 1, dasd_info_t) +#define BIODASDINFO2 _IOR('D', 3, dasd_info2_t) + +/* CMS disk label starts with ASCII string "CMS1" */ +#define VOL_LABEL_EBCDIC 0xc3d4e2f1 + +#define DASD_FORMAT_LDL 1 + +static int disk_supported(int fd, struct cmsfs *cmsfs) +{ + unsigned int cms_id = VOL_LABEL_EBCDIC; + struct cms_label label; + int offset, rc; + + /* check that this is a ldl disk */ + if (cmsfs->format != DASD_FORMAT_LDL) { + fprintf(stderr, COMP "Disk not LDL formatted\n"); + return 0; + } + + /* label is on block number 3 */ + offset = 2 * cmsfs->blksize; + + rc = lseek(fd, offset, SEEK_SET); + if (rc < 0) { + perror(COMP "lseek failed"); + return 0; + } + + rc = read(fd, &label, sizeof(label)); + if (rc < 0) { + perror(COMP "read failed"); + return 0; + } + + /* check that the label contains the CMS1 string */ + if (memcmp(label.id, &cms_id, sizeof(cms_id)) != 0) { + fprintf(stderr, COMP "Disk is not a CMS disk\n"); + return 0; + } + + DEBUG(" DOP: %d", label.dop); + /* block number 5 means 0x4000... */ + cmsfs->fdir = (label.dop - 1) * cmsfs->blksize; + DEBUG(" fdir: %lx", cmsfs->fdir); + /* get disk usage for statfs */ + cmsfs->total_blocks = label.total_blocks; + cmsfs->used_blocks = label.used_blocks; + DEBUG(" Total blocks: %d Used blocks: %d", + cmsfs->total_blocks, cmsfs->used_blocks); + return 1; +} + +static void get_device_info_bdev(int fd, struct cmsfs *cmsfs) +{ + struct dasd_info2 *info = NULL; + + if (ioctl(fd, BLKSSZGET, &cmsfs->blksize) != 0) + DIE("ioctl error get blocksize\n"); + + info = malloc(sizeof(struct dasd_info2)); + if (info == NULL) + DIE_PERROR("malloc failed"); + + /* get disk information */ + if (ioctl(fd, BIODASDINFO2, info) == 0) { + /* INFO2 failed - try INFO using the same (larger) buffer */ + if (ioctl(fd, BIODASDINFO, info) != 0) + DIE("ioctl dasd info failed\n"); + } + cmsfs->format = info->format; + free(info); +} + +static int blocksizes[] = { 4096, 512, 2048, 1024 }; + +static void get_device_info_file(int fd, struct cmsfs *cmsfs) +{ + unsigned int cms_id = VOL_LABEL_EBCDIC; + unsigned int i; + char label[4]; + off_t offset; + int rc; + + cmsfs->blksize = 0; + + /* + * Read the blocksize from label. Unfortunately the blocksize + * position depends on the blocksize... time for some heuristics. + */ + for (i = 0; i < ARRAY_SIZE(blocksizes); i++) { + offset = blocksizes[i] * 2; + + rc = lseek(fd, offset, SEEK_SET); + if (rc < 0) + DIE_PERROR("lseek failed"); + + rc = read(fd, &label, 4); + if (rc < 0) + DIE_PERROR("read failed"); + + /* check if the label contains the CMS1 string */ + if (memcmp(label, &cms_id, sizeof(cms_id)) == 0) { + cmsfs->blksize = blocksizes[i]; + break; + } + } + + if (!cmsfs->blksize) + DIE("Error detecting blocksize from file!\n"); + + /* + * Hardcoded since the label doesn't contain that info. + * Checking the disk identifier must be sufficient. + */ + cmsfs->format = DASD_FORMAT_LDL; +} + +int get_device_info(struct cmsfs *cmsfs) +{ + struct stat stat; + int fd; + + /* + * Open writable, if write access is not granted fall back to + * read only. + */ + fd = open(cmsfs->device, O_RDWR); + if (fd < 0) { + if (errno == EROFS) { + cmsfs->readonly = 1; + fd = open(cmsfs->device, O_RDONLY); + if (fd < 0) + DIE_PERROR("open failed"); + } else + DIE_PERROR("open failed"); + } + + if (fstat(fd, &stat) < 0) + DIE_PERROR("fstat failed"); + + if (S_ISBLK(stat.st_mode)) + get_device_info_bdev(fd, cmsfs); + else if (S_ISREG(stat.st_mode)) + get_device_info_file(fd, cmsfs); + else + goto error; + + if (!disk_supported(fd, cmsfs)) + goto error; + return fd; + +error: + DIE("Unsupported disk\n"); +} diff --git a/cmsfs-fuse/ebcdic.h b/cmsfs-fuse/ebcdic.h new file mode 100644 index 0000000..9183f09 --- /dev/null +++ b/cmsfs-fuse/ebcdic.h @@ -0,0 +1,153 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * EBCDIC to ASCII conversion. + * EDF uses an EBCDIC codepage based on 037 with some modifications. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#ifndef _EBCDIC_H +#define _EBCDIC_H + +#include +#include + +/* + * EBCDIC 037 -> ISO8859-1 + * changes: + * 0x5f: 0xaa -> 0x5e ^ + * 0xad: 0x07 -> 0x5b [ + * 0xbd: 0x07 -> 0x5d ] + */ + +static char ebc2asc[256] = { +/* 0x00 */ + 0x00, 0x01, 0x02, 0x03, 0x07, 0x09, 0x07, 0x7F, + 0x07, 0x07, 0x07, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, +/* 0x10 */ + 0x10, 0x11, 0x12, 0x13, 0x07, 0x0A, 0x08, 0x07, + 0x18, 0x19, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, +/* 0x20 */ + 0x07, 0x07, 0x1C, 0x07, 0x07, 0x0A, 0x17, 0x1B, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x05, 0x06, 0x07, +/* 0x30 */ + 0x07, 0x07, 0x16, 0x07, 0x07, 0x07, 0x07, 0x04, + 0x07, 0x07, 0x07, 0x07, 0x14, 0x15, 0x07, 0x1A, +/* 0x40 */ + 0x20, 0xFF, 0x83, 0x84, 0x85, 0xA0, 0x07, 0x86, + 0x87, 0xA4, 0x9B, 0x2E, 0x3C, 0x28, 0x2B, 0x7C, +/* 0x50 */ + 0x26, 0x82, 0x88, 0x89, 0x8A, 0xA1, 0x8C, 0x07, + 0x8D, 0xE1, 0x21, 0x24, 0x2A, 0x29, 0x3B, 0x5E, +/* 0x60 */ + 0x2D, 0x2F, 0x07, 0x8E, 0x07, 0x07, 0x07, 0x8F, + 0x80, 0xA5, 0x07, 0x2C, 0x25, 0x5F, 0x3E, 0x3F, +/* 0x70 */ + 0x07, 0x90, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x70, 0x60, 0x3A, 0x23, 0x40, 0x27, 0x3D, 0x22, +/* 0x80 */ + 0x07, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0xAE, 0xAF, 0x07, 0x07, 0x07, 0xF1, +/* 0x90 */ + 0xF8, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, + 0x71, 0x72, 0xA6, 0xA7, 0x91, 0x07, 0x92, 0x07, +/* 0xa0 */ + 0xE6, 0x7E, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7A, 0xAD, 0xAB, 0x07, 0x5B, 0x07, 0x07, +/* 0xb0 */ + 0x5E, 0x9C, 0x9D, 0xFA, 0x07, 0x07, 0x07, 0xAC, + 0xAB, 0x07, 0x5B, 0x5D, 0x07, 0x5D, 0x07, 0x07, +/* 0xc0 */ + 0x7B, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x07, 0x93, 0x94, 0x95, 0xA2, 0x07, +/* 0xd0 */ + 0x7D, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, + 0x51, 0x52, 0x07, 0x96, 0x81, 0x97, 0xA3, 0x98, +/* 0xe0 */ + 0x5C, 0xF6, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5A, 0xFD, 0x07, 0x99, 0x07, 0x07, 0x07, +/* 0xf0 */ + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x07, 0x07, 0x9A, 0x07, 0x07, 0x07 +}; + +/* ISO8859-1 -> EBCDIC 037 */ +static char asc2ebc[256] = { +/* 0x00 */ + 0x00, 0x01, 0x02, 0x03, 0x37, 0x2D, 0x2E, 0x2F, + 0x16, 0x05, 0x15, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, +/* 0x10 */ + 0x10, 0x11, 0x12, 0x13, 0x3C, 0x3D, 0x32, 0x26, + 0x18, 0x19, 0x3F, 0x27, 0x22, 0x1D, 0x1E, 0x1F, +/* 0x20 */ + 0x40, 0x5A, 0x7F, 0x7B, 0x5B, 0x6C, 0x50, 0x7D, + 0x4D, 0x5D, 0x5C, 0x4E, 0x6B, 0x60, 0x4B, 0x61, +/* 0x30 */ + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0x7A, 0x5E, 0x4C, 0x7E, 0x6E, 0x6F, +/* 0x40 */ + 0x7C, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, +/* 0x50 */ + 0xD7, 0xD8, 0xD9, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xBA, 0xE0, 0xBB, 0xB0, 0x6D, +/* 0x60 */ + 0x79, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, +/* 0x70 */ + 0x97, 0x98, 0x99, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, + 0xA7, 0xA8, 0xA9, 0xC0, 0x4F, 0xD0, 0xA1, 0x07, +/* 0x80 */ + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, +/* 0x90 */ + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, +/* 0xa0 */ + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, +/* 0xb0 */ + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, +/* 0xc0 */ + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, +/* 0xd0 */ + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, +/* 0xe0 */ + 0x3F, 0x59, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, +/* 0xf0 */ + 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, + 0x90, 0x3F, 0x3F, 0x3F, 0x3F, 0xEA, 0x3F, 0xFF +}; + +#define EBCDIC_ENCODE 0x0 +#define EBCDIC_DECODE 0x1 + +static inline void a2e(char *dst, const char *src, int len, int to) +{ + char *conv; + int i; + + if (to == EBCDIC_ENCODE) + conv = asc2ebc; + else + conv = ebc2asc; + for (i = 0; i < len; i++) + dst[i] = conv[(unsigned int)src[i]]; +} + +static inline void ebcdic_enc(char *dst, const char *src, int len) +{ + a2e(dst, src, len, EBCDIC_ENCODE); +} + +static inline void ebcdic_dec(char *dst, const char *src, int len) +{ + a2e(dst, src, len, EBCDIC_DECODE); +} + +#endif diff --git a/cmsfs-fuse/edf.h b/cmsfs-fuse/edf.h new file mode 100644 index 0000000..8c74a4e --- /dev/null +++ b/cmsfs-fuse/edf.h @@ -0,0 +1,123 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * EDF and label structures. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#ifndef _EDF_H +#define _EDF_H + +#include "helper.h" + +/* + * File status table entry + */ +struct fst_entry { + char name[8]; + char type[8]; + char res1[8]; + + short int mode; + char res2[4]; + + char record_format; + char flag; + int record_len; + char res3[4]; + + int fop; + /* number of data blocks (not incl. pointer blocks) */ + int nr_blocks; + int nr_records; + char levels; + char ptr_size; + char date[6]; + char res4[4]; +}; + +struct cms_label { + char id[6]; + char user_id[6]; + + unsigned int blocksize; + unsigned int dop; + unsigned int f_cylinders; + unsigned int max_cylinders; + + unsigned int total_blocks; + unsigned int used_blocks; + + unsigned int fst_entry_size; + unsigned int fst_per_block; + + char date[6]; + unsigned int res1[3]; + char res2[8]; +}; + +#define RECORD_LEN_VARIABLE 0xe5 +#define RECORD_LEN_FIXED 0xc6 + +/* TODO: correct for fixed? */ +#define MAX_RECORD_LEN 0xffff + +#define FST_ENTRY_SIZE sizeof(struct fst_entry) +#define FST_ENTRY_DIR_NAME 0x0000000100000000ULL +#define FST_ENTRY_DIR_TYPE 0xc4c9d9c5c3e3d6d9ULL /* 'DIRECTOR' */ +#define FST_ENTRY_ALLOC_NAME 0x0000000200000000ULL +#define FST_ENTRY_ALLOC_TYPE 0xc1d3d3d6c3d4c1d7ULL /* 'ALLOCMAP' */ + +#define FST_FLAG_CENTURY 0x0008 +#define FST_FOP_OFFSET 0x28 +#define FST_LEVEL_OFFSET 0x34 + +#define VAR_RECORD_HEADER_SIZE 0x2 +#define VAR_RECORD_SPANNED 0xffffffff + +#define PTR_SIZE (sizeof(struct fixed_ptr)) +#define VPTR_SIZE (sizeof(struct var_ptr)) + +struct fixed_ptr { + unsigned int next; +}; + +struct var_ptr { + unsigned int next; + int hi_record_nr; + unsigned int disp; +}; + +static inline int is_directory(const char *name, + const char *type) +{ + if ((*(unsigned long long *) name) != FST_ENTRY_DIR_NAME) + return 0; + if ((*(unsigned long long *) type) != FST_ENTRY_DIR_TYPE) + return 0; + return 1; +} + +static inline int is_allocmap(const char *name, + const char *type) +{ + if ((*(unsigned long long *) name) != FST_ENTRY_ALLOC_NAME) + return 0; + if ((*(unsigned long long *) type) != FST_ENTRY_ALLOC_TYPE) + return 0; + return 1; +} + +static inline int is_file(unsigned long long *name, unsigned long long *type) +{ + if (*name == 0ULL) + return 0; + + /* Assumption: type = 0 is not legal */ + if (*type == 0ULL) + return 0; + return 1; +} + +#endif diff --git a/cmsfs-fuse/etc/filetypes.conf b/cmsfs-fuse/etc/filetypes.conf new file mode 100644 index 0000000..6de94dc --- /dev/null +++ b/cmsfs-fuse/etc/filetypes.conf @@ -0,0 +1,107 @@ +# +# Filetypes that are interpreted as text files. If you want an EBCDIC +# file translated to ASCII, add the extension here. +# +# Comments must include a space after the # + +# Add your extensions here: +PRM +CONF + +# The following types were taken from the z/VM TCPIP.DATA file: +$EXEC +$REXX +$XEDIT +AMS +AMSERV +ANN +ANNOUNCE +APP +APPEND +ASC +ASCII +ASM +ASM3705 +ASSEMBLE +AVL +AVAIL +A37 +BASDATA +BASIC +BKS +BKSHELF +C +C++ +CAT +CATALOG +CNTRL +COB +COBOL +COPY +CPP +DIRECT +DLCS +DOCUMENT +ESERV +EXC +EXEC +FFT +FOR +FORM +FORTRAN +FREEFORT +GCS +GROUP +H +HPP +HTM +HTML +H++ +JOB +LISTING +LOG +LST +MAC +MACLIB +MACRO +MAK +MAKE +ME +MEMBER +MEMO +MODULE +NAM +NAMES +NETLOG +NONE +NOT +NOTE +NOTEBOOK +OFS +OPT +OPTIONS +PACKAGE +PASCAL +PKG +PLAS +PLI +PLIOPT +PLS +PVT +REXX +RPG +SCR +SCRIPT +STY +STYLE +TEXT +TEXTXXXX +TXT +TXTXXXX +UPDATE +UPDT +VMT +VSBASIC +VSBDATA +XED +XEDIT diff --git a/cmsfs-fuse/helper.h b/cmsfs-fuse/helper.h new file mode 100644 index 0000000..714b8a0 --- /dev/null +++ b/cmsfs-fuse/helper.h @@ -0,0 +1,54 @@ +/* + * cmsfs-fuse - CMS EDF filesystem support for Linux + * Common helper functions. + * + * Copyright IBM Corp. 2010 + * Author(s): Jan Glauber + */ + +#ifndef _HELPER_H +#define _HELPER_H + +extern FILE *logfile; +#define DEBUG_LOGFILE "/tmp/cmsfs-fuse.log" + +#ifdef DEBUG_ENABLED +#define DEBUG(...) \ + do { \ + fprintf(logfile, __VA_ARGS__); \ + fflush(logfile); \ + } while (0) +#else +#define DEBUG(...) +#endif + +#define DIE(...) \ + do { \ + fprintf(stderr, COMP __VA_ARGS__); \ + exit(1); \ + } while (0) + +#define DIE_PERROR(...) \ + do { \ + perror(COMP __VA_ARGS__); \ + exit(1); \ + } while (0) + +#define BUG(x) \ + if (x) { \ + fprintf(stderr, COMP " assert failed at " \ + __FILE__ ":%d in %s()\n", __LINE__, __func__); \ + exit(1); \ + } + +#define WARN(...) \ + do { \ + fprintf(stderr, COMP "Warning, " __VA_ARGS__); \ + } while (0) + +#define min(x, y) ((x) < (y) ? (x) : (y)) +#define max(x, y) ((x) > (y) ? (x) : (y)) + +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +#endif -- 1.7.3.5