"use strict"; const mapObj = require("map-obj"); const {B, KiB} = require("../units/bytes/iec"); const {minutes, seconds, microseconds} = require("../units/time"); const mapValue = require("../map-value"); const parseOctalMode = require("./octal-mode"); const parseIECBytes = require("./bytes/iec"); const matchOrError = require("../match-or-error"); let Value = (value) => value; let NumericValue = (value) => parseInt(value); let ByteValue = (value) => B(parseInt(value)); let Include = Symbol("Include"); let All = Symbol("All"); function MappedValue(mapping) { return (value) => mapValue(value, mapping); } let mountOptionMap = { // https://www.systutorials.com/docs/linux/man/8-mount/ /* TODO: UDF / iso9660? */ /* TODO: sshfs, fuseiso, and other FUSE-y things? */ [All]: { async: { asynchronous: true }, sync: { asynchronous: false }, atime: { updateAccessTime: true }, noatime: { updateAccessTime: false }, auto: { automaticallyMountable: true }, noauto: { automaticallyMountable: false }, dev: { allowDeviceNodes: true }, nodev: { allowDeviceNodes: false }, diratime: { updateDirectoryAccessTime: true }, nodiratime: { updateDirectoryAccessTime: false }, dirsync: { asynchronousDirectoryUpdates: false }, exec: { allowExecution: true }, noexec: { allowExecution: false }, iversion: { incrementIVersion: true }, noiversion: { incrementIVersion: false }, mand: { allowMandatoryLocks: true }, nomand: { allowMandatoryLocks: true }, _netdev: { requiresNetworkAccess: true }, nofail: { reportMountingErrors: false }, relatime: { updateAccessTimeRelatively: true }, norelatime: { updateAccessTimeRelatively: false }, strictatime: { updateAccessTimeStrictly: true }, nostrictatime: { updateAccessTimeStrictly: false }, suid: { allowSetUIDBits: true }, nosuid: { allowSetUIDBits: false }, rw: { writable: true }, ro: { writable: false }, user: { userMountable: true, allowExecution: false, allowSetUIDBits: false, allowDeviceNodes: false }, nouser: { userMountable: false }, users: { freelyMountable: true, allowExecution: false, allowSetUIDBits: false, allowDeviceNodes: false }, _rnetdev: { requiresNetworkAccess: true, allowOfflineChecks: true }, owner: { owner: Value, allowSetUIDBits: false, allowDeviceNodes: false }, group: { group: Value, allowSetUIDBits: false, allowDeviceNodes: false }, defaults: { writable: true, allowSetUIDBits: true, allowDeviceNodes: true, allowExecution: true, automaticallyMountable: true, userMountable: false, asynchronous: true, updateAccessTimeRelatively: true }, /* Various specific filesystems support the below options */ errors: { onError: Value }, }, _filesystemModes: { uid: { filesystemOwnerId: Value }, gid: { filesystemGroupId: Value }, mode: { filesystemPermissions: (value) => parseOctalMode(value) }, }, tmpfs: { uid: { rootOwnerId: Value }, gid: { rootGroupId: Value }, mode: { rootPermissions: (value) => parseOctalMode(value) }, size: { size: ByteValue }, nr_blocks: { blockCount: NumericValue }, nr_inodes: { maximumInodes: NumericValue }, mpol: { numaPolicy: Value }, }, devtmpfs: { [Include]: ["tmpfs"] }, // https://www.systutorials.com/docs/linux/man/5-ext4/ ext2: { acl: { supportACL: true }, noacl: { supportACL: false }, bsddf: { showOnlyUsableSpace: true }, minixdf: { showOnlyUsableSpace: false }, check: (value) => { if (value === "none") { return { checkFilesystemOnMount: false }; } }, nocheck: { checkFilesystemOnMount: false }, debug: { printDebugInformation: true }, grpid: { useGroupIDFromDirectory: true }, bsdgroups: { useGroupIDFromDirectory: true }, nogrpid: { useGroupIDFromDirectory: false }, sysvgroups: { useGroupIDFromDirectory: false }, grpquota: { enableGroupQuota: true }, usrquota: { enableUserQuota: true }, quota: { enableUserQuota: true }, noquota: { enableUserQuota: false, enableGroupQuota: false }, bh: { attachBufferHeads: true }, nobh: { attachBufferHeads: false }, nouid32: { allow32bitIdentifiers: false }, oldalloc: { allocator: "old" }, orlov: { allocator: "orlov" }, resgid: { reservedSpaceForGroupID: NumericValue }, resuid: { reservedSpaceForUserID: NumericValue }, sb: { superblockIndex: NumericValue }, user_xattr: { allowExtendedAttributes: true }, nouser_xattr: { allowExtendedAttributes: false }, }, ext3: { [Include]: ["ext2"], journal: (value) => { if (value === "update") { return { updateJournalFormat: true }; } else { return { journalInode: parseInt(value) }; } }, journal_dev: { journalDeviceNumber: Value }, journal_path: { journalPath: Value }, norecovery: { processJournal: false }, noload: { processJournal: false }, data: { journalingMode: Value }, data_err: { onBufferError: MappedValue({ ignore: "ignoreError", abort: "abortJournal" }) }, barrier: (value) => { if (value === "0") { return { enableWriteBarriers: false }; } else if (value === "1") { return { enableWriteBarriers: true }; } else { throw new Error(`Invalid value for 'barrier': ${value}`); } }, commit: (value) => { if (value === "0") { return { commitInterval: null }; } else { return { commitInterval: seconds(parseInt(value)) }; } }, jqfmt: { quotaSystem: Value }, usrjquota: { userQuotaFile: Value }, grpjquota: { groupQuotaFile: Value } }, ext4: { [Include]: ["ext3"], journal_checksum: { enableJournalChecksumming: true }, nojournal_checksum: { enableJournalChecksumming: false }, journal_async_commit: { asynchronousJournalCommits: true, enableJournalChecksumming: true }, barrier: (value) => { if (value === "1" || value === true) { return { enableWriteBarriers: true }; } else if (value === "0") { return { enableWriteBarriers: false }; } else { throw new Error(`Invalid value for 'barrier': ${value}`); } }, nobarrier: { enableWriteBarriers: false }, inode_readahead_blks: { inodeBlockReadAheadLimit: NumericValue }, stripe: { stripeBlocks: NumericValue }, delalloc: { allowDeferredAllocations: true }, nodelalloc: { allowDeferredAllocations: false }, max_batch_time: { batchingTimeLimit: (value) => microseconds(parseInt(value)) }, min_batch_time: { batchingTimeMinimum: (value) => microseconds(parseInt(value)) }, journal_ioprio: { journalIOPriority: NumericValue }, abort: { simulateAbort: true }, auto_da_alloc: { automaticallySynchronizeBeforeRename: true }, noauto_da_alloc: { automaticallySynchronizeBeforeRename: false }, noinit_itable: { enableInodeTableBlockInitialization: false }, init_itable: { enableInodeTableBlockInitialization: true, inodeTableBlockInitializationDelay: NumericValue }, discard: { enableHardwareDeleteCalls: true }, nodiscard: { enableHardwareDeleteCalls: false }, resize: { allowAutomaticFilesystemResizing: true }, block_validity: { enableMetadataBlockTracking: true }, noblock_validity: { enableMetadataBlockTracking: false }, dioread_lock: { enableDirectIOReadLocking: true }, nodioread_lock: { enableDirectIOReadLocking: false }, max_dir_size_kb: { directorySizeLimit: (value) => KiB(parseInt(value)) }, i_version: { allow64bitInodeVersions: true }, nombcache: { enableMetadataBlockCache: false }, prjquota: { enableProjectQuota: true } }, fat: { [Include]: ["_filesystemModes"], blocksize: { blockSize: ByteValue }, umask: { defaultFileMode: (value) => parseOctalMode("666", { mask: value }), defaultFolderMode: (value) => parseOctalMode("777", { mask: value }), }, dmask: { defaultFolderMode: (value) => parseOctalMode("777", { mask: value }) }, fmask: { defaultFileMode: (value) => parseOctalMode("666", { mask: value }) }, /* TODO: Figure out a way to make the below nicer, interacting with dmask etc. */ allow_utime: (value) => { if (value === "2") { return { timestampChangesAllowedFrom: "everybody" }; } else if (value === "20") { return { timestampChangesAllowedFrom: "group" }; } }, check: { nameStrictness: MappedValue({ r: "relaxed", relaxed: "relaxed", n: "normal", normal: "normal", s: "strict", strict: "strict" }) }, codepage: { codepage: NumericValue }, conv: { lineEndingConversionPolicy: MappedValue({ b: "binary", binary: "binary", t: "text", text: "text", a: "auto", auto: "auto" }) }, cvf_format: { compressedVolumeFileModule: Value }, cvf_option: { compressedVolumeFileOption: Value }, debug: { printDebugInformation: true }, dos1xfloppy: { enableDOS1xFallbackConfiguration: true }, fat: { allocationTableBitness: NumericValue }, iocharset: { conversionCharacterSet: Value }, nfs: (value) => { if (value === true || value === "stale_rw") { return { enableNFSInodeCache: true }; } else if (value === "nostale_ro") { return { enableNFSInodeCache: false, writable: false }; } else { throw new Error(`Unrecognized value for 'nfs' option: ${value}`); } }, tz: (value) => { if (value === "UTC") { return { enableTimezoneConversion: false }; } else { throw new Error(`Unrecognized value for 'tz' option: ${value}`); } }, time_offset: { timestampOffset: (value) => minutes(parseInt(value)) }, quiet: { enableQuietModeFailures: true }, rodir: { enableReadOnlyFlagSupport: true }, showexec: { restrictExecutableModeToWindowsBinaries: true }, sys_immutable: { treatAttrSysFlagAsImmutable: true }, flush: { enableEagerFlushing: true }, usefree: { enableFreeClusterCache: true }, // omitted: dots, nodots, dotsOK=[yes|no] }, vfat: { [Include]: ["fat"], uni_xlate: { enableUnicodeCharacterEscaping: true }, posix: { allowDifferentlyCasedNameConflicts: true }, nonumtail: { preferShortNamesWithoutSequenceNumber: true }, utf8: (value) => { if (value === true) { return { supportUtf8: true }; } else if (value === "false" || value === "0" || value === "no") { return { supportUtf8: false }; } else { throw new Error(`Unrecognized value for 'utf8' option: ${value}`); } }, shortname: { shortNameMode: Value } }, msdos: { [Include]: ["fat"] /* FIXME */ }, umsdos: { [Include]: ["fat"] /* FIXME */ }, devpts: { uid: { newPTYOwnerId: Value }, gid: { newPTYGroupId: Value }, mode: { newPTYPermissions: (value) => parseOctalMode(value) }, newinstance: { isolatePTYs: true }, ptmxmode: { ptmxPermissions: (value) => parseOctalMode(value) } }, mqueue: { /* This pseudo-filesystem does not appear to have any specific mount options. */ }, proc: { /* This pseudo-filesystem does not appear to have any specific mount options. */ }, sysfs: { // https://www.kernel.org/doc/Documentation/filesystems/sysfs.txt /* This pseudo-filesystem does not appear to have any specific mount options. */ }, securityfs: { // https://lwn.net/Articles/153366/ /* This pseudo-filesystem does not appear to have any specific mount options. */ }, efivarfs: { // https://www.kernel.org/doc/Documentation/filesystems/efivarfs.txt /* This pseudo-filesystem does not appear to have any specific mount options. */ }, ramfs: { /* FILEBUG: manpage for `ramfs` incorrectly claims that it has no mount options; it has `mode` (https://github.com/torvalds/linux/blob/master/fs/ramfs/inode.c#L41) */ mode: { rootPermissions: (value) => parseOctalMode(value) }, }, debugfs: { // https://www.kernel.org/doc/Documentation/filesystems/debugfs.txt uid: { rootOwnerId: Value }, gid: { rootGroupId: Value }, mode: { rootPermissions: (value) => parseOctalMode(value) }, }, hugetlbfs: { // https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt uid: { rootOwnerId: Value }, gid: { rootGroupId: Value }, mode: { rootPermissions: (value) => parseOctalMode(value, { mask: "6000" }) }, pagesize: { pageSize: (value) => parseIECBytes(value) }, size: (value) => { if (value.includes("%")) { let [percentage] = matchOrError(/^([0-9]+(?:\.[0-9]+))%$/, value); return { sizeAsPoolPercentage: parseFloat(percentage) }; } else { return { size: parseIECBytes(value) }; } }, min_size: (value) => { if (value.includes("%")) { let [percentage] = matchOrError(/^([0-9]+(?:\.[0-9]+))%$/, value); return { minimumSizeAsPoolPercentage: parseFloat(percentage) }; } else { return { minimumSize: parseIECBytes(value) }; } }, nr_inodes: { maximumInodes: (value) => parseIECBytes(value) }, }, cgroup: { /* TODO */ }, cgroup2: { /* TODO */ }, bpf: { /* TODO */ }, pstore: { /* TODO */ }, }; function optionsForFilesystem(filesystem) { let ownOptions = mountOptionMap[filesystem]; if (ownOptions == null) { throw new Error(`No options found for filesystem '${filesystem}'`); } if (ownOptions[Include] != null) { return ownOptions[Include] .map((target) => { return optionsForFilesystem(target); }) .concat([ownOptions]) .reduce((combined, targetOptions) => { return Object.assign(combined, targetOptions); }, {}); } else { return ownOptions; } } function applyMapping(sourceValue, mapping) { if (typeof mapping === "function") { return mapping(sourceValue); } else { return mapObj(mapping, (key, value) => { let mappedValue; if (value === Value) { mappedValue = sourceValue; } else if (typeof value === "function") { mappedValue = value(sourceValue); } else { mappedValue = value; } return [key, mappedValue]; }); } } module.exports = function parseOptions(filesystem, optionString) { let optionMap = Object.assign({}, mountOptionMap[All], optionsForFilesystem(filesystem)); return optionString .split(",") .map((item) => { if (item.includes("=")) { let [key, value] = item.split("="); return [ key, value ]; } else { return [ item, true ]; } }) .reduce(({ parsed, missing }, [key, value]) => { let mapping = optionMap[key]; if (mapping != null) { return { parsed: Object.assign(parsed, applyMapping(value, mapping)), missing: missing }; } else { return { parsed: parsed, missing: missing.concat(key) }; } }, { parsed: {}, missing: [] }); };