Skip to content

Instantly share code, notes, and snippets.

@Eugeny
Created April 23, 2021 12:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Eugeny/63069c4221f7dc6671f9f4591b6f5b1a to your computer and use it in GitHub Desktop.
Save Eugeny/63069c4221f7dc6671f9f4591b6f5b1a to your computer and use it in GitHub Desktop.
macOS finder favourites parser
const bplist = require('bplist-parser');
const { isObject } = require('rxjs/internal-compatibility');
(async () => {
let content = await bplist.parseFile(process.env.HOME + '/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.FavoriteItems.sfl2')
content = content[0];
let root = content.$top.root
function expandObject (obj) {
if (obj.UID !== undefined) {
obj = content.$objects[obj.UID]
}
if (obj.$class) {
// obj.$class = expandObject(obj.$class)
delete obj.$class
}
if (obj['NS.keys']) {
let result = {}
for (let i in obj['NS.keys']) {
// console.log('key', i, obj['NS.objects'][i], expandObject(obj['NS.objects'][i]))
result[expandObject(obj['NS.keys'][i])] = expandObject(obj['NS.objects'][i])
}
obj = result
} else if (obj['NS.objects']) {
let result = []
for (let i in obj['NS.objects']) {
result[i] = expandObject(obj['NS.objects'][i])
}
obj = result
} else {
for (let k of Object.keys(obj)) {
if (obj[k].UID !== undefined) {
obj[k] = expandObject(obj[k])
}
}
}
return obj
}
function parseBookmark (data) {
const fieldNames = {
4100: 'filePath', // pathComponents
4101: 'fileInodePath', // fileIDs
4112: 'resourceProps',
4160: 'fileCreationDate', // creationDate
8192: 'volInfoDepths',
8194: 'volPath',
8197: 'volURL',
8208: 'volName',
8209: 'volUUID',
8210: 'volSize', // volCapacity
8211: 'volCreationDate',
8224: 'volProps',
8240: 'volWasBoot',
8272: 'volMountURL',
49153: 'volDepthCountHome', // Vol Home Dir Relative Path Component Count
49169: 'username',
49170: 'userUID',
53264: 'creationOptions',
61463: 'displayName',
61473: 'iconRef', // Effective Flattened Icon Ref
61488: 'bookmarkCreationDate', // Bookmark Creation Time
61568: 'sandboxInfo' // Sandbox RW Extension
}
const fieldTypes = {
257: 'string',
513: 'binary',
771: 'uint32',
772: 'uint8',
774: 'doubleLE',
1024: 'doubleBE',
1281: 'true',
1537: 'pointer array',
2305: 'CFURL',
}
let buffer = Buffer.from(data)
const signature = buffer.slice(0, 4)
const dataLength = buffer.readUInt32LE()
const dataPayloadOffset = buffer.readUInt32LE(12)
let tocOffset = buffer.readUInt32LE(dataPayloadOffset) + dataPayloadOffset
while (true) {
const tocDataLen = buffer.readUInt32LE(tocOffset)
const tocType = buffer.readUInt16LE(tocOffset + 4)
const tocLevel = buffer.readInt32LE(tocOffset + 8)
const nextTocOffset = buffer.readInt32LE(tocOffset + 12)
const nRecords = buffer.readInt32LE(tocOffset + 16)
console.log('\nTOC type', tocType)
for (let i = 0; i < nRecords; i++) {
const recordType = buffer.readUInt16LE(tocOffset + 20 + 12 * i)
const recordFlags = buffer.readUInt16LE(tocOffset + 20 + 12 * i + 2)
const recordDataOffset = buffer.readUInt32LE(tocOffset + 20 + 12 * i + 4)
function parseDataItem (dataOffset) {
const recordData = buffer.slice(dataPayloadOffset + dataOffset)
const dataLength = recordData.readUInt16LE(0)
const dataType = recordData.readUInt16LE(4)
let data = recordData.slice(8, dataLength + 8)
const fieldType = fieldTypes[dataType] || `[${dataType}]`
if (fieldType == 'uint32') {
data = data.readUInt32LE()
} else if (fieldType == 'uint8') {
data = data.readUInt8()
} else if (fieldType == 'string' || fieldType == 'CFURL') {
data = data.toString()
} else if (fieldType == 'doubleLE') {
data = data.readDoubleLE()
} else if (fieldType == 'doubleBE') {
data = data.readDoubleBE()
} else if (fieldType == 'pointer array') {
let items = []
for (let i = 0; i < dataLength / 4; i++) {
items.push(parseDataItem(data.readUInt32LE(i * 4)).data)
}
data = items
} else if (fieldType == 'true') {
data = true
}
return { data, fieldType }
}
let {fieldType, data} = parseDataItem(recordDataOffset)
const fieldName = fieldNames[recordType] || `[${recordType}]`
console.log('rec', fieldName, fieldType, dataLength, data)
}
if (!nextTocOffset)
break
tocOffset = nextTocOffset + dataPayloadOffset
}
if (signature.equals(Buffer.from('book'))) {
} else {
console.warn('unknown sig', signature)
}
}
root = expandObject(root)
const replacer = (k, v) => {
if (v.type == 'Buffer') {
console.log(k, v, typeof v)
return Buffer.from(v.data).toString('hex')
}
return v
}
console.log(JSON.stringify(content, replacer, 2))
for (const item of root.items) {
console.log(`\n\n* Item`)
if (item.URL)
console.log(` URL ${item.URL["NS.relative"]}`)
console.log(` Item ${JSON.stringify(item)}`)
console.log(` Bookmark content ${parseBookmark(item.Bookmark)}`)
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment