From d2de4370071d4e750dada5d07cc96d1e3d192780 Mon Sep 17 00:00:00 2001 From: Aleen Date: Fri, 18 Jan 2019 18:14:29 +0800 Subject: [PATCH 1/3] add support for asynchronous reading file API --- delegate.js | 69 +++++++++++++++++++++++++++++++++++++++++++++ index.js | 81 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 delegate.js diff --git a/delegate.js b/delegate.js new file mode 100644 index 0000000..2ffa1d2 --- /dev/null +++ b/delegate.js @@ -0,0 +1,69 @@ +// based on https://github.com/matt-curtis/MochaJSDelegate + +module.exports = function(selectorHandlerDict) { + var uniqueClassName = 'MochaJSDelegate_DynamicClass_' + NSUUID.UUID().UUIDString() + + var delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_(uniqueClassName, NSObject) + + delegateClassDesc.registerClass() + + // Handler storage + var handlers = {} + + // Define interface + this.setHandlerForSelector = function(selectorString, func) { + var handlerHasBeenSet = (selectorString in handlers) + var selector = NSSelectorFromString(selectorString) + + handlers[selectorString] = func + + if (!handlerHasBeenSet) { + /* + For some reason, Mocha acts weird about arguments: + https://github.com/logancollins/Mocha/issues/28 + We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments. + */ + var dynamicHandler = function() { + var functionToCall = handlers[selectorString] + + if(!functionToCall) return + + return functionToCall.apply(delegateClassDesc, arguments) + } + + var args = [], regex = /:/g + while(match = regex.exec(selectorString)) args.push('arg' + args.length) + + dynamicFunction = eval('(function(' + args.join(',') + '){ return dynamicHandler.apply(this, arguments) })') + + delegateClassDesc.addInstanceMethodWithSelector_function_(selector, dynamicFunction) + } + } + + this.removeHandlerForSelector = function(selectorString) { + delete handlers[selectorString] + } + + this.getHandlerForSelector = function(selectorString) { + return handlers[selectorString] + } + + this.getAllHandlers = function() { + return handlers + } + + this.getClass = function() { + return NSClassFromString(uniqueClassName) + } + + this.getClassInstance = function() { + return NSClassFromString(uniqueClassName).new() + } + + // Convenience + if (typeof selectorHandlerDict === 'object') { + for (var selectorString in selectorHandlerDict) { + this.setHandlerForSelector(selectorString, selectorHandlerDict[selectorString]) + } + } +} diff --git a/index.js b/index.js index 79bbc6d..1fc4f42 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,34 @@ // TODO: async. Should probably be done with NSFileHandle and some notifications // TODO: file descriptor. Needs to be done with NSFileHandle var Buffer = require('buffer').Buffer +var Delegate = require('./delegate') +var notificationCenter = NSNotificationCenter.defaultCenter() + +function dispatchToNSData(dispatchData) { + return NSString.alloc().initWithData_encoding(dispatchData, NSISOLatin1StringEncoding) + .dataUsingEncoding(NSISOLatin1StringEncoding) +} + +function addObserver(name, notificationType, callback) { + var option = {} + var observerInstance + option[name] = function(notification) { + // need to unregister the observer after callback + notificationCenter.removeObserver_name_object(observerInstance, notificationType, nil) + callback && callback(dispatchToNSData(notification.userInfo().NSFileHandleNotificationDataItem)) + } + + observerInstance = new Delegate(option).getClassInstance() + notificationCenter.addObserver_selector_name_object(observerInstance, NSSelectorFromString(name), notificationType, nil) +} + +function fsError(options) { + var ERROR_MESSAGES = { + '-2': 'no such file or directory' + } + + return Object.assign(new Error(options.code + ': ' + ERROR_MESSAGES[options.errno] + ', ' + options.syscall + ' \'' + options.path + '\''), options) +} function encodingFromOptions(options, defaultValue) { return options && options.encoding @@ -145,21 +173,52 @@ module.exports.readdirSync = function(path) { return arr } -module.exports.readFileSync = function(path, options) { - var encoding = encodingFromOptions(options, 'buffer') - var fileManager = NSFileManager.defaultManager() - var data = fileManager.contentsAtPath(path) - var buffer = Buffer.from(data) - - if (encoding === 'buffer') { - return buffer - } else if (encoding === 'NSData') { - return buffer.toNSData() +var fns = ['readFile', 'readFileSync'] +var fn = function(sync, path, options, callback) { + // handle data + var handler = function(data, options) { + var encoding = encodingFromOptions(options, 'buffer') + var buffer = Buffer.from(data) + + if (encoding === 'buffer') { + return buffer + } else if (encoding === 'NSData') { + return buffer.toNSData() + } else { + return buffer.toString(encoding) + } + } + // read data + if (sync) { + var fileManager = NSFileManager.defaultManager() + return handler(fileManager.contentsAtPath(path), options) } else { - return buffer.toString(encoding) + var fileInstance = NSFileHandle.fileHandleForReadingAtPath(path) + if (fileInstance) { + addObserver('onReadCompleted:', NSFileHandleReadCompletionNotification, function (data) { + callback && callback(null, handler(data, options)) + }) + fileInstance.readInBackgroundAndNotify() + } else { + callback && callback(fsError({ + errno: -2, + code: 'ENOENT', + syscall: 'open', + path: path + })) + } } } +for (var i = 0; i < fns.length; i++) { + var name = fns[i] + var isSync = /sync$/i.test(name) + + module.exports[name] = eval('(function(' + ['path', 'options'].concat(isSync ? [] : ['callback']).join(',') + '){' + + 'return fn.apply(this, [' + isSync + '].concat([].slice.call(arguments)))' + + '})') +} + module.exports.readlinkSync = function(path) { var err = MOPointer.alloc().init() var fileManager = NSFileManager.defaultManager() From 0c06e3cd37c0f4d4d044464e18a1059aa46bd74e Mon Sep 17 00:00:00 2001 From: Aleen Date: Mon, 21 Jan 2019 10:20:11 +0800 Subject: [PATCH 2/3] use fileManager to detect some specific errors --- index.js | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 1fc4f42..cf6d55e 100644 --- a/index.js +++ b/index.js @@ -24,10 +24,17 @@ function addObserver(name, notificationType, callback) { function fsError(options) { var ERROR_MESSAGES = { - '-2': 'no such file or directory' + '-2': 'no such file or directory', + '-13': 'permission denied', + '-21': 'illegal operation on a directory' } - return Object.assign(new Error(options.code + ': ' + ERROR_MESSAGES[options.errno] + ', ' + options.syscall + ' \'' + options.path + '\''), options) + return Object.assign(new Error( + options.code + ': ' + + ERROR_MESSAGES[options.errno] + ', ' + + options.syscall + + (options.path ? ' \'' + options.path + '\'' : '') + ), options) } function encodingFromOptions(options, defaultValue) { @@ -189,8 +196,8 @@ var fn = function(sync, path, options, callback) { } } // read data + var fileManager = NSFileManager.defaultManager() if (sync) { - var fileManager = NSFileManager.defaultManager() return handler(fileManager.contentsAtPath(path), options) } else { var fileInstance = NSFileHandle.fileHandleForReadingAtPath(path) @@ -200,12 +207,36 @@ var fn = function(sync, path, options, callback) { }) fileInstance.readInBackgroundAndNotify() } else { - callback && callback(fsError({ - errno: -2, - code: 'ENOENT', - syscall: 'open', - path: path - })) + var isExisted = fileManager.fileExistsAtPath(path) + var isDirectory = fileManager.fileExistsAtPath_isDirectory(path, true) + var isReadable = fileManager.isReadableFileAtPath(path) + // need to use NSFileManager to detect whether it is existed or readable + if (callback) { + if (!isExisted) { + // not existed + callback(fsError({ + errno: -2, + code: 'ENOENT', + syscall: 'open', + path: path + })) + } else if (!isReadable) { + // permission denied + callback(fsError({ + errno: -13, + code: 'EACCES', + syscall: 'open', + path: path + })) + } else if (isDirectory) { + // directory + callback(fsError({ + errno: -21, + code: 'EISDIR', + syscall: 'read' + })) + } + } } } } From 2cf651fa8489e274e507d08da766ba88f97f5cc1 Mon Sep 17 00:00:00 2001 From: Aleen Date: Mon, 21 Jan 2019 11:47:18 +0800 Subject: [PATCH 3/3] specific test cases for asynchronous file reading API --- __tests__/async.test.js | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 __tests__/async.test.js diff --git a/__tests__/async.test.js b/__tests__/async.test.js new file mode 100644 index 0000000..e53abe3 --- /dev/null +++ b/__tests__/async.test.js @@ -0,0 +1,70 @@ +const { readFile, writeFileSync, unlinkSync, mkdirSync, rmdirSync, chmodSync } = require('../') + +function getScriptFolder(context) { + const parts = context.scriptPath.split('/') + parts.pop() + return parts.join('/') +} + +test('failed to read', (context) => { + const targetFile = `${getScriptFolder(context)}/test.js` + const targetDir = `${getScriptFolder(context)}/test` + return Promise.all([ + new Promise((resolve) => { + readFile(targetFile, 'utf8', (err, res) => { + if (err) { + resolve(err) + return + } + resolve(res) + }) + }).then(err => { + expect(err.errno).toEqual(-2) // no such a file + }), + new Promise((resolve) => { + mkdirSync(targetDir) + readFile(targetDir, 'utf8', (err, res) => { + if (err) { + resolve(err) + return + } + resolve(res) + }) + }).then(err => { + expect(err.errno).toEqual(-21) // operations on directories + rmdirSync(targetDir) + }), + new Promise((resolve) => { + writeFileSync(targetFile, 'test') + chmodSync(targetFile, 0o000) + readFile(targetFile, 'utf8', (err, res) => { + if (err) { + resolve(err) + return + } + resolve(res) + }) + }).then(err => { + expect(err.errno).toEqual(-13) // permission denied + chmodSync(targetFile, 0o777) + unlinkSync(targetFile) + }) + ]) +}) + +test('should read a file', (context) => { + const targetFile = `${getScriptFolder(context)}/test.js` + writeFileSync(targetFile, 'test') + return new Promise((resolve, reject) => { + readFile(targetFile, 'utf8', (err, res) => { + if (err) { + reject(err) + return + } + resolve(res) + }) + }).then((res) => { + expect(res).toBe('test') + unlinkSync(targetFile) + }) +})