Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: build

on:
pull_request:
push:
branches:
- main

jobs:
build:
name: build node v24 ${{ matrix.os }}
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-15-intel

steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm test
- run: npm audit --audit-level=moderate
- run: npm ls --omit=dev --all
- run: npm pack --dry-run
- run: node --check index.js
- run: node --check lib/buddy-allocator.js
- run: node --check lib/ds-store.js
- run: node --check lib/entry.js
- run: node --check lib/partition.js
- run: node --check test/ds-store.js
- run: node --check test/entry.js
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# node-ds-store
# @appdmg/ds-store

.DS_Store manipulation and creation from node.js
`.DS_Store` creation for appdmg packages.

This package is a CommonJS Node.js 24+ modernization of the original
`ds-store` package used by `node-appdmg`. It keeps the appdmg-facing API while
moving the maintained package under the `@appdmg` npm scope.

## Status

Expand All @@ -11,15 +15,30 @@ limitations and also only allows creating new files from scratch.
## Installation

```sh
npm install ds-store
npm install @appdmg/ds-store
```

## Usage

```javscript
var DSStore = require('ds-store');
```javascript
const DSStore = require('@appdmg/ds-store')

async function main () {
const file = new DSStore()

file.setWindowPos(100, 100)
file.setWindowSize(640, 480)
file.setIconSize(128)
file.setIconPos('Example.app', 140, 120)
file.setIconPos('Applications', 420, 120)

await file.write('/Volumes/Example/.DS_Store')
}

var file = new DSStore();
main().catch((err) => {
console.error(err)
process.exitCode = 1
})
```

## API
Expand All @@ -28,6 +47,10 @@ var file = new DSStore();

Set the background image to file specified by `path`.

On non-Darwin platforms, pass `{ volumeName: 'Volume Name' }` as the second
argument when using background image aliases. The native macOS volume lookup is
not available there.

### file.setBackgroundColor(red, green, blue)

Set the background color to the color specified by three floats between 0 and 1.
Expand Down Expand Up @@ -60,6 +83,12 @@ Write the `.DS_Store` information to file at `path`.

`cb` will get called with `err` upon file creation.

### await file.write(path)

Promise-based equivalent of `file.write(path, cb)`.

The callback form remains supported for existing appdmg callers.

## Future

I have started work on a Buddy Allocator and B-Tree implementation,
Expand Down
18 changes: 18 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
declare class DSStore {
setBackgroundPath(path: string, options?: { volumeName?: string }): void;
setBackgroundColor(red: number, green: number, blue: number): void;
setIconSize(size: number): void;
setIconPos(name: string, x: number, y: number): void;
setWindowPos(x: number, y: number): void;
setWindowSize(width: number, height: number): void;
vSrn(value: 0 | 1): void;
write(path: string): Promise<void>;
write(path: string, callback: (error: Error | null) => void): void;

/**
* @deprecated Use setBackgroundPath instead.
*/
setBackground(path: string, options?: { volumeName?: string }): void;
}

export = DSStore;
45 changes: 31 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
var assert = require('assert')
var alias = require('macos-alias')
var util = require('util')
'use strict'

var Entry = require('./lib/entry')
var DSStore = require('./lib/ds-store')
const assert = require('node:assert')
const util = require('node:util')
const alias = require('@appdmg/macos-alias')

const Entry = require('./lib/entry')
const DSStore = require('./lib/ds-store')

function Helper () {
this.file = new DSStore()
Expand All @@ -12,8 +14,9 @@ function Helper () {
}
}

Helper.prototype.setBackgroundPath = function (path) {
Helper.prototype.setBackgroundPath = function (path, options) {
this.opts.backgroundPath = path
this.opts.backgroundAliasOptions = options
}

Helper.prototype.setBackgroundColor = function (red, green, blue) {
Expand Down Expand Up @@ -44,20 +47,34 @@ Helper.prototype.vSrn = function (value) {
}

Helper.prototype.write = function (path, cb) {
var rawAlias, colorComponents
const promise = writeHelper(this, path)

if (typeof cb === 'function') {
promise.then(function () {
cb(null)
}, cb)
return
}

return promise
}

async function writeHelper (helper, path) {
let rawAlias
let colorComponents

if (this.opts.backgroundPath) {
rawAlias = alias.create(this.opts.backgroundPath)
if (helper.opts.backgroundPath) {
rawAlias = alias.create(helper.opts.backgroundPath, helper.opts.backgroundAliasOptions || {})
}

if (this.opts.backgroundColor) {
colorComponents = this.opts.backgroundColor
if (helper.opts.backgroundColor) {
colorComponents = helper.opts.backgroundColor
}

this.file.push(Entry.construct('.', 'bwsp', this.opts.window))
this.file.push(Entry.construct('.', 'icvp', { iconSize: this.opts.iconSize, rawAlias: rawAlias, colorComponents: colorComponents }))
helper.file.push(Entry.construct('.', 'bwsp', helper.opts.window))
helper.file.push(Entry.construct('.', 'icvp', { iconSize: helper.opts.iconSize, rawAlias: rawAlias, colorComponents: colorComponents }))

this.file.write(path, cb)
await helper.file.write(path)
}

/* Backwards compatibility */
Expand Down
26 changes: 15 additions & 11 deletions lib/buddy-allocator.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
var assert = require('assert')
'use strict'

const assert = require('node:assert')

function BuddyAllocator () {
this.offsets = new Array(0)
this.freelist = new Array(32)

for (var i = 0; i < 32; i++) {
for (let i = 0; i < 32; i++) {
this.freelist[i] = []
}

this.freelist[31].push(0)

var head = this._alloc(5)
const head = this._alloc(5)
assert(head === 0)
}

BuddyAllocator.prototype._alloc = function (width) {
assert(width < 32)

var list = this.freelist[width]
const list = this.freelist[width]

if (list.length > 0) {
// There is a block of the desired size; return it.
Expand All @@ -27,19 +29,19 @@ BuddyAllocator.prototype._alloc = function (width) {
// Allocate a block of the next larger size; split
// it and put the other half on the free list.

var offset = this._alloc(width + 1)
var buddy = offset ^ Math.pow(2, width)
const offset = this._alloc(width + 1)
const buddy = offset ^ Math.pow(2, width)

this._free(buddy, width)
return offset
}
}

BuddyAllocator.prototype._free = function (offset, width) {
var list = this.freelist[width]
var buddy = offset ^ Math.pow(2, width)
const list = this.freelist[width]
const buddy = offset ^ Math.pow(2, width)

var idx = list.indexOf(buddy)
const idx = list.indexOf(buddy)

if (~idx) {
// Our buddy is free. Coalesce, and
Expand All @@ -64,12 +66,14 @@ BuddyAllocator.prototype.allocate = function (bytes, blocknum) {
}
}

var wantwidth = 5
let wantwidth = 5
while (bytes > (1 << wantwidth)) {
wantwidth += 1
}

var blockaddr, blockwidth, blockoffset
let blockaddr
let blockwidth
let blockoffset
if (this.offsets[blocknum]) {
blockaddr = this.offsets[blocknum]
blockwidth = blockaddr & 0x1F
Expand Down
60 changes: 35 additions & 25 deletions lib/ds-store.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
var fs = require('fs')
var path = require('path')
'use strict'

const fs = require('node:fs/promises')
const path = require('node:path')
// var assert = require('assert')

var Entry = require('./entry')
const Entry = require('./entry')
// var partition = require('./partition')
// var BuddyAllocator = require('./buddy-allocator')

const CLEAN_STORE_PATH = path.join(__dirname, '..', 'assets', 'DSStore-clean')

function DSStore () {
this.entries = []
// this.store = new BuddyAllocator()
Expand Down Expand Up @@ -260,37 +264,43 @@ DSStore.prototype.push = function (entry) {
// }

DSStore.prototype.write = function (filePath, cb) {
var entries = this.entries.sort(Entry.sort)

fs.readFile(path.join(__dirname, '/../assets/DSStore-clean'), function (err, buf) {
if (err) return cb(err)
const promise = writeStore(this, filePath)

var modified = new Buffer(3840)
if (typeof cb === 'function') {
promise.then(function () {
cb(null)
}, cb)
return
}

modified.fill(0)
return promise
}

var currentPos = 0
async function writeStore (store, filePath) {
const entries = store.entries.sort(Entry.sort)

var P = 0
var count = entries.length
const buf = await fs.readFile(CLEAN_STORE_PATH)
const modified = Buffer.alloc(3840)

modified.writeUInt32BE(P, currentPos)
modified.writeUInt32BE(count, currentPos + 4)
currentPos += 8
let currentPos = 0

entries.forEach(function (e, i) {
var b = e.buffer
b.copy(modified, currentPos)
currentPos += b.length
})
const P = 0
const count = entries.length

buf.writeUInt32BE(entries.length, 76)
modified.copy(buf, 4100)
modified.writeUInt32BE(P, currentPos)
modified.writeUInt32BE(count, currentPos + 4)
currentPos += 8

fs.writeFile(filePath, buf, function (err) {
cb(err)
})
entries.forEach(function (entry) {
const entryBuffer = entry.buffer
entryBuffer.copy(modified, currentPos)
currentPos += entryBuffer.length
})

buf.writeUInt32BE(entries.length, 76)
modified.copy(buf, 4100)

await fs.writeFile(filePath, buf)
}

module.exports = exports = DSStore
Loading