Skip to content

Commit fab29b6

Browse files
KoenkkNerivecCopilot
authored
fix: Improve backup corruption message (Koenkk#1538)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 8fc0a6f commit fab29b6

File tree

13 files changed

+75
-93
lines changed

13 files changed

+75
-93
lines changed

src/adapter/deconz/adapter/deconzAdapter.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import {existsSync, readFileSync} from "node:fs";
55
import {dirname} from "node:path";
66
import type * as Models from "../../../models";
7-
import type {Backup, UnifiedBackupStorage} from "../../../models";
7+
import type {Backup} from "../../../models";
88
import {BackupUtils, Waitress} from "../../../utils";
99
import {logger} from "../../../utils/logger";
1010
import * as ZSpec from "../../../zspec";
@@ -15,6 +15,7 @@ import type * as ZdoTypes from "../../../zspec/zdo/definition/tstypes";
1515
import Adapter from "../../adapter";
1616
import type * as Events from "../../events";
1717
import type {AdapterOptions, CoordinatorVersion, NetworkOptions, NetworkParameters, SerialPortOptions, StartResult} from "../../tstype";
18+
import {readBackup} from "../../utils";
1819
import PARAM, {
1920
ApsAddressMode,
2021
type ApsDataRequest,
@@ -558,19 +559,10 @@ export class DeconzAdapter extends Adapter {
558559
* Loads currently stored backup and returns it in internal backup model.
559560
*/
560561
private getStoredBackup(): Backup | undefined {
561-
if (!existsSync(this.backupPath)) {
562-
return undefined;
563-
}
564-
565-
let data: UnifiedBackupStorage;
566-
567-
try {
568-
data = JSON.parse(readFileSync(this.backupPath).toString());
569-
} catch (error) {
570-
throw new Error(`[BACKUP] Coordinator backup is corrupted. (${(error as Error).stack})`);
571-
}
562+
const data = readBackup(this.backupPath);
563+
if (!data) return undefined;
572564

573-
if (data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
565+
if ("metadata" in data && data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
574566
if (data.metadata?.version !== 1) {
575567
throw new Error(`[BACKUP] Unsupported open coordinator backup version (version=${data.metadata?.version}).`);
576568
}

src/adapter/deconz/driver/driver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {Buffalo} from "../../../buffalo";
88
import type {Backup} from "../../../models";
99
import {logger} from "../../../utils/logger";
1010
import {SerialPort} from "../../serialPort";
11-
import SocketPortUtils from "../../socketPortUtils";
1211
import type {NetworkOptions, SerialPortOptions} from "../../tstype";
12+
import {isTcpPath, parseTcpPath} from "../../utils";
1313
import PARAM, {
1414
ApsAddressMode,
1515
type ApsDataRequest,
@@ -362,7 +362,7 @@ class Driver extends events.EventEmitter {
362362
}
363363

364364
let prom: Promise<void> | undefined;
365-
if (SocketPortUtils.isTcpPath(this.serialPortOptions.path)) {
365+
if (isTcpPath(this.serialPortOptions.path)) {
366366
prom = this.openSocketPort();
367367
} else if (baudrate) {
368368
prom = this.openSerialPort(baudrate);
@@ -893,7 +893,7 @@ class Driver extends events.EventEmitter {
893893
throw new Error("No serial port TCP path specified");
894894
}
895895

896-
const info = SocketPortUtils.parseTcpPath(this.serialPortOptions.path);
896+
const info = parseTcpPath(this.serialPortOptions.path);
897897
logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS);
898898
this.socketPort = new net.Socket();
899899
this.socketPort.setNoDelay(true);

src/adapter/ember/adapter/emberAdapter.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {randomBytes} from "node:crypto";
2-
import {existsSync, readFileSync, renameSync} from "node:fs";
2+
import {readFileSync, renameSync} from "node:fs";
33
import path from "node:path";
44

55
import equals from "fast-deep-equal/es6";
6-
import type {Backup, UnifiedBackupStorage} from "../../../models";
6+
import type {Backup} from "../../../models";
77
import {BackupUtils, Queue, wait} from "../../../utils";
88
import {logger} from "../../../utils/logger";
99
import * as ZSpec from "../../../zspec";
@@ -14,6 +14,7 @@ import type * as ZdoTypes from "../../../zspec/zdo/definition/tstypes";
1414
import {Adapter, type TsType} from "../..";
1515
import {WORKAROUND_JOIN_MANUF_IEEE_PREFIX_TO_CODE} from "../../const";
1616
import type {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from "../../events";
17+
import {readBackup} from "../../utils";
1718
import {
1819
EMBER_HIGH_RAM_CONCENTRATOR,
1920
EMBER_LOW_RAM_CONCENTRATOR,
@@ -1141,19 +1142,10 @@ export class EmberAdapter extends Adapter {
11411142
* Loads currently stored backup and returns it in internal backup model.
11421143
*/
11431144
private getStoredBackup(): Backup | undefined {
1144-
if (!existsSync(this.backupPath)) {
1145-
return undefined;
1146-
}
1147-
1148-
let data: UnifiedBackupStorage;
1149-
1150-
try {
1151-
data = JSON.parse(readFileSync(this.backupPath).toString());
1152-
} catch (error) {
1153-
throw new Error(`[BACKUP] Coordinator backup is corrupted. (${(error as Error).stack})`);
1154-
}
1145+
const data = readBackup(this.backupPath);
1146+
if (!data) return undefined;
11551147

1156-
if (data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
1148+
if ("metadata" in data && data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
11571149
if (data.metadata?.version !== 1) {
11581150
throw new Error(`[BACKUP] Unsupported open coordinator backup version (version=${data.metadata?.version}).`);
11591151
}

src/adapter/ember/uart/ash.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {Socket} from "node:net";
66
import {wait} from "../../../utils";
77
import {logger} from "../../../utils/logger";
88
import {SerialPort} from "../../serialPort";
9-
import SocketPortUtils from "../../socketPortUtils";
109
import type {SerialPortOptions} from "../../tstype";
10+
import {isTcpPath, parseTcpPath} from "../../utils";
1111
import {EzspStatus} from "../enums";
1212
import {halCommonCrc16, inc8, mod8, withinRange} from "../utils/math";
1313
import {
@@ -410,7 +410,7 @@ export class UartAsh extends EventEmitter<UartAshEventMap> {
410410
}
411411

412412
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
413-
if (SocketPortUtils.isTcpPath(this.portOptions.path!)) {
413+
if (isTcpPath(this.portOptions.path!)) {
414414
return this.socketPort ? !this.socketPort.closed : false;
415415
}
416416

@@ -463,7 +463,7 @@ export class UartAsh extends EventEmitter<UartAshEventMap> {
463463
await this.closePort(); // will do nothing if nothing's open
464464

465465
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
466-
if (!SocketPortUtils.isTcpPath(this.portOptions.path!)) {
466+
if (!isTcpPath(this.portOptions.path!)) {
467467
const serialOpts = {
468468
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
469469
path: this.portOptions.path!,
@@ -509,7 +509,7 @@ export class UartAsh extends EventEmitter<UartAshEventMap> {
509509
}
510510
} else {
511511
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
512-
const info = SocketPortUtils.parseTcpPath(this.portOptions.path!);
512+
const info = parseTcpPath(this.portOptions.path!);
513513
logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS);
514514

515515
this.socketPort = new Socket();

src/adapter/ezsp/adapter/backup.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/* v8 ignore start */
22

3-
import * as fs from "node:fs";
4-
53
import type * as Models from "../../../models";
64
import {BackupUtils} from "../../../utils";
75
import {logger} from "../../../utils/logger";
86
import {uint32MaskToChannels} from "../../../zspec/utils";
7+
import {readBackup} from "../../utils";
98
import type {Driver} from "../driver";
109
import {
1110
type EmberKeyData,
@@ -83,18 +82,10 @@ export class EZSPAdapterBackup {
8382
* Loads currently stored backup and returns it in internal backup model.
8483
*/
8584
public getStoredBackup(): Models.Backup | undefined {
86-
try {
87-
fs.accessSync(this.defaultPath);
88-
} catch {
89-
return undefined;
90-
}
91-
let data: Models.UnifiedBackupStorage;
92-
try {
93-
data = JSON.parse(fs.readFileSync(this.defaultPath).toString());
94-
} catch (error) {
95-
throw new Error(`Coordinator backup is corrupted (${(error as Error).stack})`);
96-
}
97-
if (data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
85+
const data = readBackup(this.defaultPath);
86+
if (!data) return undefined;
87+
88+
if ("metadata" in data && data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
9889
if (data.metadata?.version !== 1) {
9990
throw new Error(`Unsupported open coordinator backup version (version=${data.metadata?.version})`);
10091
}

src/adapter/ezsp/driver/uart.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import net from "node:net";
66
import {Queue, Waitress, wait} from "../../../utils";
77
import {logger} from "../../../utils/logger";
88
import {SerialPort} from "../../serialPort";
9-
import SocketPortUtils from "../../socketPortUtils";
109
import type {SerialPortOptions} from "../../tstype";
10+
import {isTcpPath, parseTcpPath} from "../../utils";
1111
import {FrameType, Frame as NpiFrame} from "./frame";
1212
import {Parser} from "./parser";
1313
import {Writer} from "./writer";
@@ -58,7 +58,7 @@ export class SerialDriver extends EventEmitter {
5858

5959
async connect(options: SerialPortOptions): Promise<void> {
6060
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
61-
if (SocketPortUtils.isTcpPath(options.path!)) {
61+
if (isTcpPath(options.path!)) {
6262
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
6363
await this.openSocketPort(options.path!);
6464
} else {
@@ -118,7 +118,7 @@ export class SerialDriver extends EventEmitter {
118118
}
119119

120120
private async openSocketPort(path: string): Promise<void> {
121-
const info = SocketPortUtils.parseTcpPath(path);
121+
const info = parseTcpPath(path);
122122
logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS);
123123

124124
this.socketPort = new net.Socket();

src/adapter/socketPortUtils.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/adapter/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {existsSync, readFileSync} from "node:fs";
2+
import type * as Models from "../models";
3+
4+
export function isTcpPath(path: string): boolean {
5+
// tcp path must be:
6+
// tcp://<host>:<port>
7+
const regex = /^(?:tcp:\/\/)[\w.-]+[:][\d]+$/gm;
8+
return regex.test(path);
9+
}
10+
11+
export function parseTcpPath(path: string): {host: string; port: number} {
12+
// built-in extra validation
13+
const info = new URL(path);
14+
15+
return {
16+
host: info.hostname,
17+
port: Number(info.port),
18+
};
19+
}
20+
21+
export function readBackup(path: string): Models.UnifiedBackupStorage | Models.LegacyBackupStorage | undefined {
22+
if (!existsSync(path)) {
23+
return undefined;
24+
}
25+
26+
try {
27+
return JSON.parse(readFileSync(path).toString());
28+
} catch (error) {
29+
throw new Error(
30+
`[BACKUP] Coordinator backup is corrupted. This can happen due to filesystem corruption. To re-create the backup, delete '${path}' and start again. (${(error as Error).stack})`,
31+
);
32+
}
33+
}

src/adapter/z-stack/adapter/adapter-backup.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import assert from "node:assert";
2-
import * as fs from "node:fs";
3-
42
import type * as Models from "../../../models";
53
import {BackupUtils} from "../../../utils";
64
import {logger} from "../../../utils/logger";
75
import {NULL_NODE_ID, Utils as ZSpecUtils} from "../../../zspec";
6+
import {readBackup} from "../../utils";
87
import {NvItemsIds, NvSystemIds} from "../constants/common";
98
import * as Structs from "../structs";
109
import {AddressManagerUser, SecurityManagerAuthenticationOption} from "../structs";
@@ -34,27 +33,18 @@ export class AdapterBackup {
3433
* Loads currently stored backup and returns it in internal backup model.
3534
*/
3635
public getStoredBackup(): Models.Backup | undefined {
37-
try {
38-
fs.accessSync(this.defaultPath);
39-
} catch {
40-
return undefined;
41-
}
42-
let data: Models.UnifiedBackupStorage | Models.LegacyBackupStorage;
43-
try {
44-
data = JSON.parse(fs.readFileSync(this.defaultPath).toString());
45-
} catch (error) {
46-
throw new Error(`Coordinator backup is corrupted (${error})`);
47-
}
36+
const data = readBackup(this.defaultPath);
37+
if (!data) return undefined;
4838

4939
if ("adapterType" in data) {
50-
return BackupUtils.fromLegacyBackup(data as Models.LegacyBackupStorage);
40+
return BackupUtils.fromLegacyBackup(data);
5141
}
5242

5343
if (data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
5444
if (data.metadata?.version !== 1) {
5545
throw new Error(`Unsupported open coordinator backup version (version=${data.metadata?.version})`);
5646
}
57-
return BackupUtils.fromUnifiedBackup(data as Models.UnifiedBackupStorage);
47+
return BackupUtils.fromUnifiedBackup(data);
5848
}
5949

6050
throw new Error("Unknown backup format");

src/adapter/z-stack/znp/znp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Queue, Waitress, wait} from "../../../utils";
66
import {logger} from "../../../utils/logger";
77
import {ClusterId as ZdoClusterId} from "../../../zspec/zdo";
88
import {SerialPort} from "../../serialPort";
9-
import SocketPortUtils from "../../socketPortUtils";
9+
import {isTcpPath, parseTcpPath} from "../../utils";
1010
import * as Constants from "../constants";
1111
import {Frame as UnpiFrame, Parser as UnpiParser, Writer as UnpiWriter} from "../unpi";
1212
import {Subsystem, Type} from "../unpi/constants";
@@ -91,7 +91,7 @@ export class Znp extends events.EventEmitter {
9191
}
9292

9393
public async open(): Promise<void> {
94-
return SocketPortUtils.isTcpPath(this.path) ? await this.openSocketPort() : await this.openSerialPort();
94+
return isTcpPath(this.path) ? await this.openSocketPort() : await this.openSerialPort();
9595
}
9696

9797
private async openSerialPort(): Promise<void> {
@@ -126,7 +126,7 @@ export class Znp extends events.EventEmitter {
126126
}
127127

128128
private async openSocketPort(): Promise<void> {
129-
const info = SocketPortUtils.parseTcpPath(this.path);
129+
const info = parseTcpPath(this.path);
130130
logger.info(`Opening TCP socket with ${info.host}:${info.port}`, NS);
131131

132132
this.socketPort = new Socket();

0 commit comments

Comments
 (0)