├── .gitignore ├── LICENSE ├── README.md ├── binding.gyp ├── index.js ├── lib ├── create.js ├── decode.js ├── encode.js ├── is-alias.js └── values.js ├── package.json ├── src ├── impl-apple-cheetah.cc ├── impl-apple-lion.cc ├── impl-apple.cc └── volume.cc └── test ├── addon.js └── basics.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | build 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Linus Unnebäck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-alias 2 | 3 | Mac OS aliases creation and reading from node.js 4 | 5 | ## Attention 6 | 7 | This library does currently not handle the `book\0\0\0\0mark\0\0\0\0`-header. It only does manipulation on the raw alias data. 8 | 9 | I intend to add something like `alias.write(buf, path)` and `alias.read(path)`. 10 | 11 | ## Installation 12 | 13 | ```sh 14 | npm install macos-alias 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```javascript 20 | var alias = require('macos-alias'); 21 | ``` 22 | 23 | ## API 24 | 25 | ### alias.create(target) 26 | 27 | Create a new alias pointing to `target`, returns a buffer. 28 | 29 | (This function performs blocking fs interaction) 30 | 31 | ### alias.decode(buf) 32 | 33 | Decodes buffer `buf` and returns an object with info about the alias. 34 | 35 | ### alias.encode(info) 36 | 37 | Encodes the `info`-object into an alias, returns a buffer. 38 | 39 | ### alias.isAlias(path) 40 | 41 | Check if the file at `path` is an alias, returns a boolean. 42 | 43 | (This function performs blocking fs interaction) 44 | 45 | ## Hacking 46 | 47 | Clone the repo and start making changes, run `node-gyp` to build the project. 48 | 49 | ```sh 50 | node-gyp rebuild 51 | ``` 52 | 53 | ## Tests 54 | 55 | ```sh 56 | mocha 57 | ``` 58 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "volume", 5 | "sources": [ "src/volume.cc" ], 6 | "include_dirs" : [ 7 | "= 0 && volType <= 5, 'Volume type is valid') 35 | info.volume.type = values.volumeType[volType] 36 | 37 | var dirId = buf.readUInt32BE(46) 38 | info.parent.id = dirId 39 | 40 | var fileNameLength = buf.readUInt8(50) 41 | assert(fileNameLength <= 63, 'File name is not longer than 63 chars') 42 | info.target.filename = buf.toString('utf8', 51, 51 + fileNameLength) 43 | 44 | var fileId = buf.readUInt32BE(114) 45 | info.target.id = fileId 46 | 47 | var fileCreateDate = buf.readUInt32BE(118) 48 | info.target.created = appleDate(fileCreateDate) 49 | 50 | // var fileTypeName = buf.toString('ascii', 122, 126) 51 | // var fileCreatorName = buf.toString('ascii', 126, 130) 52 | // I have only encountered 00 00 00 00 53 | 54 | // var nlvlFrom = buf.readInt16BE(130) 55 | // var nlvlTo = buf.readInt16BE(132) 56 | // I have only encountered -1 57 | 58 | // var volAttributes = buf.readUInt32BE(134) 59 | // I have only encountered 00 00 0D 02 60 | 61 | // var volFSId = buf.readInt16BE(138) 62 | // I have only encountered 00 00 63 | 64 | var reserved = buf.slice(140, 150) 65 | assert(reserved[0] === 0 && reserved[1] === 0, 'Reserved is zero-filled') 66 | assert(reserved[2] === 0 && reserved[3] === 0, 'Reserved is zero-filled') 67 | assert(reserved[4] === 0 && reserved[5] === 0, 'Reserved is zero-filled') 68 | assert(reserved[6] === 0 && reserved[7] === 0, 'Reserved is zero-filled') 69 | assert(reserved[8] === 0 && reserved[9] === 0, 'Reserved is zero-filled') 70 | 71 | var pos = 150 72 | 73 | while (pos < buf.length) { 74 | var partType = buf.readInt16BE(pos) 75 | var length = buf.readUInt16BE(pos + 2) 76 | var data = buf.slice(pos + 4, pos + 4 + length) 77 | pos += 4 + length 78 | 79 | if (partType === -1) { 80 | assert.equal(length, 0) 81 | break 82 | } 83 | 84 | if (length % 2 === 1) { 85 | var padding = buf.readUInt8(pos) 86 | assert.equal(padding, 0) 87 | pos += 1 88 | } 89 | 90 | info.extra.push({ type: partType, length: length, data: data }) 91 | 92 | switch (partType) { 93 | case 0: 94 | info.parent.name = data.toString('utf8') 95 | break 96 | case 1: 97 | assert.equal(info.parent.id, data.readUInt32BE(0)) 98 | break 99 | case 2: 100 | var parts = data.toString('utf8').split('\0') 101 | info.target.path = parts[0] 102 | assert.equal(info.target.filename, parts[1]) 103 | break 104 | case 14: 105 | // FIXME 106 | // Target: name as (16-bit length), (length char utf16be) 107 | break 108 | case 15: 109 | // FIXME 110 | // Volume: name as (16-bit length), (length char utf16be) 111 | break 112 | case 18: 113 | info.target.abspath = data.toString('utf8') 114 | break 115 | case 19: 116 | info.volume.abspath = data.toString('utf8') 117 | break 118 | } 119 | } 120 | 121 | return info 122 | } 123 | -------------------------------------------------------------------------------- /lib/encode.js: -------------------------------------------------------------------------------- 1 | 2 | var util = require('util') 3 | var assert = require('assert') 4 | var values = require('./values') 5 | 6 | var appleEpoch = Date.UTC(1904, 0, 1) 7 | var appleDate = function (value) { 8 | if (!(value instanceof Date)) { 9 | // value = new Date(value); 10 | throw new TypeError('Not a date: ' + value) 11 | } 12 | 13 | return Math.round((value.getTime() - appleEpoch) / 1000) 14 | } 15 | 16 | module.exports = exports = function (info) { 17 | assert.equal(info.version, 2) 18 | 19 | var baseLength = 150 20 | var extraLength = (info.extra || []).reduce(function (p, c) { 21 | assert.equal(c.data.length, c.length) 22 | var padding = (c.length % 2) 23 | return p + 4 + c.length + padding 24 | }, 0) 25 | var trailerLength = 4 26 | 27 | var buf = new Buffer(baseLength + extraLength + trailerLength) 28 | 29 | buf.writeUInt32BE(0, 0) 30 | 31 | buf.writeUInt16BE(buf.length, 4) 32 | buf.writeUInt16BE(info.version, 6) 33 | 34 | var type = values.type.indexOf(info.target.type) 35 | assert(type === 0 || type === 1, 'Type is valid') 36 | buf.writeUInt16BE(type, 8) 37 | 38 | var volNameLength = info.volume.name.length 39 | assert(volNameLength <= 27, 'Volume name is not longer than 27 chars') 40 | buf.writeUInt8(volNameLength, 10) 41 | buf.fill(0, 11, 11 + 27) 42 | buf.write(info.volume.name, 11, 'utf8') 43 | 44 | var volCreateDate = appleDate(info.volume.created) 45 | buf.writeUInt32BE(volCreateDate, 38) 46 | 47 | var volSig = info.volume.signature 48 | assert(volSig === 'BD' || volSig === 'H+' || volSig === 'HX', 'Volume signature is valid') 49 | buf.write(volSig, 42, 'ascii') 50 | 51 | var volType = values.volumeType.indexOf(info.volume.type) 52 | assert(volType >= 0 && volType <= 5, 'Volume type is valid') 53 | buf.writeUInt16BE(volType, 44) 54 | 55 | buf.writeUInt32BE(info.parent.id, 46) 56 | 57 | var fileNameLength = info.target.filename.length 58 | assert(fileNameLength <= 63, 'File name is not longer than 63 chars') 59 | buf.writeUInt8(fileNameLength, 50) 60 | buf.fill(0, 51, 51 + 63) 61 | buf.write(info.target.filename, 51, 'utf8') 62 | 63 | buf.writeUInt32BE(info.target.id, 114) 64 | 65 | var fileCreateDate = appleDate(info.target.created) 66 | buf.writeUInt32BE(fileCreateDate, 118) 67 | 68 | var fileTypeName = '\0\0\0\0' 69 | var fileCreatorName = '\0\0\0\0' 70 | // I have only encountered 00 00 00 00 71 | buf.write(fileTypeName, 122, 'binary') 72 | buf.write(fileCreatorName, 126, 'binary') 73 | 74 | var nlvlFrom = -1 75 | var nlvlTo = -1 76 | // I have only encountered -1 77 | buf.writeInt16BE(nlvlFrom, 130) 78 | buf.writeInt16BE(nlvlTo, 132) 79 | 80 | var volAttributes = 0x00000D02 81 | // I have only encountered 00 00 0D 02 82 | buf.writeUInt32BE(volAttributes, 134) 83 | 84 | var volFSId = 0x0000 85 | // I have only encountered 00 00 86 | buf.writeUInt16BE(volFSId, 138) 87 | 88 | // Reserved space 89 | buf.fill(0, 140, 150) 90 | 91 | var pos = 150 92 | 93 | for (var i = 0; i < info.extra.length; i++) { 94 | var e = info.extra[i] 95 | assert(e.type >= 0, 'Type is valid') 96 | 97 | buf.writeInt16BE(e.type, pos) 98 | buf.writeUInt16BE(e.length, pos + 2) 99 | e.data.copy(buf, pos + 4) 100 | pos += 4 + e.length 101 | 102 | if (e.length % 2 === 1) { 103 | buf.writeUInt8(0, pos) 104 | pos += 1 105 | } 106 | } 107 | 108 | buf.writeInt16BE(-1, pos) 109 | buf.writeUInt16BE(0, pos + 2) 110 | pos += 4 111 | 112 | assert.equal(pos, buf.length) 113 | 114 | return buf 115 | } 116 | -------------------------------------------------------------------------------- /lib/is-alias.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs') 3 | 4 | module.exports = function isAlias (path) { 5 | var read 6 | var fd = fs.openSync(path, 'r') 7 | 8 | try { 9 | read = new Buffer(16) 10 | fs.readSync(fd, read, 0, 16, 0) 11 | } finally { 12 | fs.closeSync(fd) 13 | } 14 | 15 | var expected = '626f6f6b000000006d61726b00000000' 16 | var actual = read.toString('hex') 17 | 18 | return (actual === expected) 19 | } 20 | -------------------------------------------------------------------------------- /lib/values.js: -------------------------------------------------------------------------------- 1 | 2 | exports.type = ['file', 'directory'] 3 | exports.volumeType = ['local', 'network', 'floppy-400', 'floppy-800', 'floppy-1400', 'other'] 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "macos-alias", 3 | "version": "0.2.12", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "author": "Linus Unnebäck ", 7 | "contributors": [ 8 | "Linus Unnebäck ", 9 | "Davide Liessi ", 10 | "Joshua Warner " 11 | ], 12 | "os": [ 13 | "darwin" 14 | ], 15 | "devDependencies": { 16 | "fs-temp": "^1.1.1", 17 | "mocha": "^3.1.0", 18 | "standard": "^8.3.0" 19 | }, 20 | "scripts": { 21 | "test": "standard && mocha" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/LinusU/node-alias.git" 26 | }, 27 | "dependencies": { 28 | "nan": "^2.4.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/impl-apple-cheetah.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | const char* OSErrDescription(OSErr err) { 7 | switch (err) { 8 | case nsvErr: return "Volume not found"; 9 | case ioErr: return "I/O error."; 10 | case bdNamErr: return "Bad filename or volume name."; 11 | case mFulErr: return "Memory full (open) or file won't fit (load)"; 12 | case tmfoErr: return "Too many files open."; 13 | case fnfErr: return "File or directory not found; incomplete pathname."; 14 | case volOffLinErr: return "Volume is offline."; 15 | case nsDrvErr: return "No such drive."; 16 | case dirNFErr: return "Directory not found or incomplete pathname."; 17 | case tmwdoErr: return "Too many working directories open."; 18 | } 19 | 20 | return "Could not get volume name"; 21 | } 22 | 23 | NAN_METHOD(MethodGetVolumeName) { 24 | Nan::Utf8String aPath(info[0]); 25 | 26 | CFStringRef volumePath = CFStringCreateWithCString(NULL, *aPath, kCFStringEncodingUTF8); 27 | CFURLRef url = CFURLCreateWithFileSystemPath(NULL, volumePath, kCFURLPOSIXPathStyle, true); 28 | 29 | OSErr err; 30 | FSRef urlFS; 31 | FSCatalogInfo urlInfo; 32 | HFSUniStr255 outString; 33 | 34 | if (CFURLGetFSRef(url, &urlFS) == false) { 35 | return Nan::ThrowError("Failed to convert URL to file or directory object"); 36 | } 37 | 38 | if ((err = FSGetCatalogInfo(&urlFS, kFSCatInfoVolume, &urlInfo, NULL, NULL, NULL)) != noErr) { 39 | return Nan::ThrowError(OSErrDescription(err)); 40 | } 41 | 42 | if ((err = FSGetVolumeInfo(urlInfo.volume, 0, NULL, kFSVolInfoNone, NULL, &outString, NULL)) != noErr) { 43 | return Nan::ThrowError(OSErrDescription(err)); 44 | } 45 | 46 | info.GetReturnValue().Set(Nan::New(outString.unicode, outString.length).ToLocalChecked()); 47 | } 48 | -------------------------------------------------------------------------------- /src/impl-apple-lion.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | using v8::String; 6 | using v8::Exception; 7 | using v8::Local; 8 | using v8::Value; 9 | 10 | Local MYCFStringGetV8String(CFStringRef aString) { 11 | if (aString == NULL) { 12 | return Nan::EmptyString(); 13 | } 14 | 15 | CFIndex length = CFStringGetLength(aString); 16 | CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8); 17 | char *buffer = (char *) malloc(maxSize); 18 | 19 | if (!CFStringGetCString(aString, buffer, maxSize, kCFStringEncodingUTF8)) { 20 | return Nan::EmptyString(); 21 | } 22 | 23 | Local result = Nan::New(buffer).ToLocalChecked(); 24 | free(buffer); 25 | 26 | return result; 27 | } 28 | 29 | NAN_METHOD(MethodGetVolumeName) { 30 | Nan::Utf8String aPath(info[0]); 31 | 32 | CFStringRef out; 33 | CFErrorRef error; 34 | 35 | CFStringRef volumePath = CFStringCreateWithCString(NULL, *aPath, kCFStringEncodingUTF8); 36 | CFURLRef url = CFURLCreateWithFileSystemPath(NULL, volumePath, kCFURLPOSIXPathStyle, true); 37 | 38 | if(!CFURLCopyResourcePropertyForKey(url, kCFURLVolumeNameKey, &out, &error)) { 39 | return Nan::ThrowError(MYCFStringGetV8String(CFErrorCopyDescription(error))); 40 | } 41 | 42 | info.GetReturnValue().Set(MYCFStringGetV8String(out)); 43 | } 44 | -------------------------------------------------------------------------------- /src/impl-apple.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #ifdef __MAC_10_7 5 | #include "impl-apple-lion.cc" 6 | #else 7 | #include "impl-apple-cheetah.cc" 8 | #endif 9 | -------------------------------------------------------------------------------- /src/volume.cc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #ifdef __APPLE__ 7 | #include "impl-apple.cc" 8 | #else 9 | #error This platform is not implemented yet 10 | #endif 11 | 12 | using v8::FunctionTemplate; 13 | 14 | NAN_MODULE_INIT(Initialize) { 15 | Nan::Set(target, Nan::New("getVolumeName").ToLocalChecked(), 16 | Nan::GetFunction(Nan::New(MethodGetVolumeName)).ToLocalChecked()); 17 | } 18 | 19 | NODE_MODULE(volume, Initialize) 20 | -------------------------------------------------------------------------------- /test/addon.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var assert = require('assert') 4 | var addon = require('../build/Release/volume.node') 5 | 6 | describe('addon', function () { 7 | it('should find the volume name of /', function () { 8 | assert.equal(addon.getVolumeName('/'), 'Macintosh HD') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/basics.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var lib = require('../') 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var temp = require('fs-temp') 8 | var assert = require('assert') 9 | 10 | var rawData = new Buffer( 11 | 'AAAAAAEqAAIAAApUZXN0IFRpdGxlAAAAAAAAAAAAAAAAAAAAAADO615USCsA' + 12 | 'BQAAABMMVGVzdEJrZy50aWZmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + 13 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFM7rXlgAAAAAAAAAAP////8A' + 14 | 'AA0CAAAAAAAAAAAAAAAAAAAACy5iYWNrZ3JvdW5kAAABAAQAAAATAAIAJFRl' + 15 | 'c3QgVGl0bGU6LmJhY2tncm91bmQ6AFRlc3RCa2cudGlmZgAPABYACgBUAGUA' + 16 | 'cwB0ACAAVABpAHQAbABlABIAGS8uYmFja2dyb3VuZC9UZXN0QmtnLnRpZmYA' + 17 | 'ABMAEy9Wb2x1bWVzL1Rlc3QgVGl0bGUA//8AAA==', 'base64' 18 | ) 19 | 20 | describe('decode', function () { 21 | it('should parse a simple alias', function () { 22 | var info = lib.decode(rawData) 23 | 24 | assert.equal(info.version, 2) 25 | 26 | assert.deepEqual(info.volume, { 27 | name: 'Test Title', 28 | created: new Date('2014-01-02T18:20:04.000Z'), 29 | signature: 'H+', 30 | type: 'other', 31 | abspath: '/Volumes/Test Title' 32 | }) 33 | 34 | assert.deepEqual(info.parent, { 35 | id: 19, 36 | name: '.background' 37 | }) 38 | 39 | assert.deepEqual(info.target, { 40 | type: 'file', 41 | filename: 'TestBkg.tiff', 42 | id: 20, 43 | created: new Date('2014-01-02T18:20:08.000Z'), 44 | path: 'Test Title:.background:', 45 | abspath: '/.background/TestBkg.tiff' 46 | }) 47 | }) 48 | }) 49 | 50 | describe('encode', function () { 51 | it('should encode a simple alias', function () { 52 | var info = lib.decode(rawData) 53 | var buf = lib.encode(info) 54 | 55 | assert.deepEqual(rawData, buf) 56 | }) 57 | }) 58 | 59 | describe('create', function () { 60 | it('should create a simple alias', function () { 61 | var buf = lib.create(path.join(__dirname, 'basics.js')) 62 | var info = lib.decode(buf) 63 | 64 | assert.equal('file', info.target.type) 65 | assert.equal('basics.js', info.target.filename) 66 | }) 67 | }) 68 | 69 | describe('isAlias', function () { 70 | var aliasFile, garbageFile 71 | 72 | before(function () { 73 | aliasFile = temp.writeFileSync(new Buffer('626f6f6b000000006d61726b00000000', 'hex')) 74 | garbageFile = temp.writeFileSync(new Buffer('Hello my name is Linus!')) 75 | }) 76 | 77 | after(function () { 78 | fs.unlinkSync(aliasFile) 79 | fs.unlinkSync(garbageFile) 80 | }) 81 | 82 | it('should identify alias', function () { 83 | assert.equal(lib.isAlias(aliasFile), true) 84 | }) 85 | 86 | it('should identify non-alias', function () { 87 | assert.equal(lib.isAlias(garbageFile), false) 88 | }) 89 | }) 90 | --------------------------------------------------------------------------------