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) + }) +}) 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..cf6d55e 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,41 @@ // 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', + '-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.path + '\'' : '') + ), options) +} function encodingFromOptions(options, defaultValue) { return options && options.encoding @@ -145,21 +180,76 @@ module.exports.readdirSync = function(path) { return arr } -module.exports.readFileSync = function(path, options) { - var encoding = encodingFromOptions(options, 'buffer') +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 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() + if (sync) { + 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 { + 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' + })) + } + } + } } } +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()