Skip to content

Commit

Permalink
Add getLabel() to retrieve filesystem label
Browse files Browse the repository at this point in the history
Change-type: minor
Signed-off-by: Ken Bannister <[email protected]>
  • Loading branch information
kb2ma committed Jan 15, 2025
1 parent 18fff02 commit 66623b9
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 1 deletion.
77 changes: 77 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,80 @@ export async function interact<T>(
throw new Error('image must be a String (file path) or a Disk instance');
}
}

/**
* @summary Return the label encoded in the filesystem for a given partition,
* or an empty string if one can't be found.
*
* @example
*
* await filedisk.withOpenFile('/foo/bar.img', 'r', async (handle) => {
* const disk = new filedisk.FileDisk(handle);
* const info = partitioninfo.getPartitions(disk);
* for (const partition of info.partitions) {
* const label = await getLabel(disk, partition);
* console.log(`${partition.index}: ${label}`);
* }
* }
*/
export async function getLabel(
disk: Disk,
partition: partitioninfo.GPTPartition | partitioninfo.MBRPartition,
): Promise<string> {
// If GPT, can we just read the protective MBR?
// Is there a more Typescript native way to determine partition table type?
// const isGpt = 'guid' in partition;

// Linux native FS, expecting ext2+, so skip to superblock for metadata.
let metadataOffset = 0;
if (partition.type === 0x83) {
metadataOffset += 0x400;
}

// Read filesystem metadata
let buf = Buffer.alloc(0x100);
await disk.read(buf, 0, buf.length, partition.offset + metadataOffset);

let labelOffset = 0;
let maxLength = 0;
// First verify magic signature to determine metadata layout for label offset.
const fatTypes = [0xB, 0xC, 0xE];
if (fatTypes.some(ptype => partition.type === ptype)) {
maxLength = 11;
// FAT16
if (buf.readUInt8(0x26) === 0x29) {
labelOffset = 0x2B;
// FAT32
} else if (buf.readUInt8(0x42) === 0x29) {
labelOffset = 0x47;
} else {
return '';
}
} else if (partition.type === 0x83) {
maxLength = 16;
if (buf.readUInt16LE(0x38) === 0xEF53) {
labelOffset = 0x78;
} else {
return '';
}
// Unexpected partition type
} else {
return '';
}

// Identify and exclude trailing /0 bytes to stringify.
let i = 0;
for (; i <= maxLength; i++) {
// If label fills available space, no need to test. We just need i
// to have the expected value for Buffer.toString() call below.
if (i == maxLength) {
break;
}
if (buf.readUInt8(labelOffset + i) == 0) {
break;
}
}

const label = buf.toString('utf8', labelOffset, labelOffset + i).trim();
return label;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"lint-fix": "balena-lint --fix --typescript lib tests",
"clean": "rm -rf build",
"build": "npm run clean && tsc",
"test": "npm run lint && mocha -r ts-node/register tests/e2e.ts",
"test": "npm run lint && mocha -r ts-node/register tests/*.ts",
"readme": "jsdoc2md --template doc/README.hbs build/index.js > README.md",
"prepublish": "npm run test && npm run build && npm run readme"
},
Expand Down
117 changes: 117 additions & 0 deletions tests/fsLabel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { deepEqual } from 'assert';
import { FileDisk, withOpenFile } from 'file-disk';
import * as Fs from 'fs';
import * as Path from 'path';
import * as tmp from 'tmp';
import * as partitioninfo from 'partitioninfo';

import * as imagefs from '../lib';

const RASPBERRYPI = Path.join(__dirname, 'images', 'raspberrypi.img');
const MBR_FAT32 = Path.join(__dirname, 'images', 'mbr-fat32.img');

async function tmpFile(): Promise<{ path: string; cleanup: () => void }> {
return await new Promise((resolve, reject) => {
tmp.file(
{ discardDescriptor: true },
(error: Error | null, path: string, _fd: number, cleanup: () => void) => {
if (error != null) {
reject(error);
} else {
resolve({ path, cleanup });
}
},
);
});
}

async function withFileCopy<T>(
filePath: string,
fn: (tmpFilePath: string) => Promise<T>,
): Promise<T> {
const { path, cleanup } = await tmpFile();
await Fs.promises.copyFile(filePath, path);
try {
return await fn(path);
} finally {
cleanup();
}
}

function testWithFileCopy(
title: string,
file: string,
fn: (fileCopy: string) => Promise<void>,
) {
it(title, async () => {
await withFileCopy(file, async (fileCopy: string) => {
await fn(fileCopy);
});
});
}

function testFileDisk(
title: string,
image: { image: string; partition: number },
fn: (
disk: FileDisk,
partition: partitioninfo.GPTPartition | partitioninfo.MBRPartition,
) => Promise<void>,
) {
testWithFileCopy(
`${title} (filedisk)`,
image.image,
async (fileCopy: string) => {
await withOpenFile(fileCopy, 'r+', async (handle) => {
const disk = new FileDisk(handle);
const partition = await partitioninfo.get(disk, image.partition);
await fn(disk, partition);
});
},
);
}

testFileDisk(
'should find label in MBR with FAT16 (0xB) partition',
{
image: RASPBERRYPI,
partition: 1,
},
async (
disk: FileDisk,
partition: partitioninfo.GPTPartition | partitioninfo.MBRPartition,
) => {
const label = await imagefs.getLabel(disk, partition);
deepEqual(label, 'RESIN-BOOT');
},
);

testFileDisk(
'should find label in MBR with FAT32 (0xC) partition',
{
image: MBR_FAT32,
partition: 1,
},
async (
disk: FileDisk,
partition: partitioninfo.GPTPartition | partitioninfo.MBRPartition,
) => {
const label = await imagefs.getLabel(disk, partition);
deepEqual(label, 'resin-boot');
},
);

testFileDisk(
'should find label in MBR with ext4 (0x83) partition',
{
image: MBR_FAT32,
partition: 6,
},
async (
disk: FileDisk,
partition: partitioninfo.GPTPartition | partitioninfo.MBRPartition,
) => {
const label = await imagefs.getLabel(disk, partition);
deepEqual(label, 'resin-data');
},
);
Binary file added tests/images/mbr-fat32.img
Binary file not shown.

0 comments on commit 66623b9

Please sign in to comment.