@md-safeedit/core 0.1.0-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/diff/diff.d.ts +4 -0
- package/dist/diff/diff.js +99 -0
- package/dist/diff/diff.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/snapshot/snapshot.d.ts +47 -0
- package/dist/snapshot/snapshot.js +169 -0
- package/dist/snapshot/snapshot.js.map +1 -0
- package/dist/transaction/planner.d.ts +24 -0
- package/dist/transaction/planner.js +75 -0
- package/dist/transaction/planner.js.map +1 -0
- package/dist/writer/atomic.d.ts +12 -0
- package/dist/writer/atomic.js +90 -0
- package/dist/writer/atomic.js.map +1 -0
- package/package.json +19 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a unified diff between oldStr and newStr.
|
|
3
|
+
*/
|
|
4
|
+
export function generateUnifiedDiff(oldStr, newStr, filePath) {
|
|
5
|
+
if (oldStr === newStr) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
const oldLines = oldStr.split(/\r?\n/);
|
|
9
|
+
const newLines = newStr.split(/\r?\n/);
|
|
10
|
+
const m = oldLines.length;
|
|
11
|
+
const n = newLines.length;
|
|
12
|
+
// LCS Dynamic Programming
|
|
13
|
+
const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
|
|
14
|
+
for (let i = 1; i <= m; i++) {
|
|
15
|
+
for (let j = 1; j <= n; j++) {
|
|
16
|
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
17
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const ops = [];
|
|
25
|
+
let i = m;
|
|
26
|
+
let j = n;
|
|
27
|
+
while (i > 0 || j > 0) {
|
|
28
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
29
|
+
ops.push({ type: 'same', line: oldLines[i - 1], oldIdx: i - 1, newIdx: j - 1 });
|
|
30
|
+
i--;
|
|
31
|
+
j--;
|
|
32
|
+
}
|
|
33
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
34
|
+
ops.push({ type: 'added', line: newLines[j - 1], oldIdx: -1, newIdx: j - 1 });
|
|
35
|
+
j--;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
ops.push({ type: 'removed', line: oldLines[i - 1], oldIdx: i - 1, newIdx: -1 });
|
|
39
|
+
i--;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
ops.reverse();
|
|
43
|
+
const contextSize = 3;
|
|
44
|
+
let diffText = `--- a/${filePath}\n+++ b/${filePath}\n`;
|
|
45
|
+
for (let k = 0; k < ops.length; k++) {
|
|
46
|
+
const op = ops[k];
|
|
47
|
+
if (op.type !== 'same') {
|
|
48
|
+
const start = Math.max(0, k - contextSize);
|
|
49
|
+
let end = k;
|
|
50
|
+
let consecutiveSames = 0;
|
|
51
|
+
while (end < ops.length) {
|
|
52
|
+
if (ops[end].type === 'same') {
|
|
53
|
+
consecutiveSames++;
|
|
54
|
+
if (consecutiveSames > contextSize * 2) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
consecutiveSames = 0;
|
|
60
|
+
}
|
|
61
|
+
end++;
|
|
62
|
+
}
|
|
63
|
+
const hunk = ops.slice(start, end);
|
|
64
|
+
const oldStartOp = hunk.find(o => o.oldIdx !== -1);
|
|
65
|
+
const newStartOp = hunk.find(o => o.newIdx !== -1);
|
|
66
|
+
const oldStartLine = oldStartOp ? oldStartOp.oldIdx + 1 : 1;
|
|
67
|
+
const newStartLine = newStartOp ? newStartOp.newIdx + 1 : 1;
|
|
68
|
+
let oldLen = 0;
|
|
69
|
+
let newLen = 0;
|
|
70
|
+
hunk.forEach(o => {
|
|
71
|
+
if (o.type === 'same') {
|
|
72
|
+
oldLen++;
|
|
73
|
+
newLen++;
|
|
74
|
+
}
|
|
75
|
+
else if (o.type === 'removed') {
|
|
76
|
+
oldLen++;
|
|
77
|
+
}
|
|
78
|
+
else if (o.type === 'added') {
|
|
79
|
+
newLen++;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
diffText += `@@ -${oldStartLine},${oldLen} +${newStartLine},${newLen} @@\n`;
|
|
83
|
+
hunk.forEach(o => {
|
|
84
|
+
if (o.type === 'same') {
|
|
85
|
+
diffText += ` ${o.line}\n`;
|
|
86
|
+
}
|
|
87
|
+
else if (o.type === 'removed') {
|
|
88
|
+
diffText += `-${o.line}\n`;
|
|
89
|
+
}
|
|
90
|
+
else if (o.type === 'added') {
|
|
91
|
+
diffText += `+${o.line}\n`;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
k = end - 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return diffText;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/diff/diff.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type LineEnding = 'lf' | 'crlf' | 'mixed';
|
|
2
|
+
export type Encoding = 'utf-8' | 'utf-8-bom';
|
|
3
|
+
export interface ByteRange {
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
export interface FileIdentity {
|
|
8
|
+
device: number;
|
|
9
|
+
inode: number;
|
|
10
|
+
}
|
|
11
|
+
export interface DocumentSnapshot {
|
|
12
|
+
canonicalPath: string;
|
|
13
|
+
fileIdentity?: FileIdentity;
|
|
14
|
+
bytes: Uint8Array;
|
|
15
|
+
content: string;
|
|
16
|
+
revision: string;
|
|
17
|
+
encoding: Encoding;
|
|
18
|
+
lineEnding: LineEnding;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolves symlinks and ensures the path is canonical and resides within the configured allowed roots.
|
|
23
|
+
* Throws an error if path traversal is detected.
|
|
24
|
+
*/
|
|
25
|
+
export declare function authorizeAndCanonicalizePath(targetPath: string, allowedRoots: string[]): string;
|
|
26
|
+
/**
|
|
27
|
+
* Detects encoding (UTF-8 vs UTF-8-BOM) and line endings (LF vs CRLF vs mixed) from content bytes.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectFileMetadata(bytes: Uint8Array): {
|
|
30
|
+
encoding: Encoding;
|
|
31
|
+
lineEnding: LineEnding;
|
|
32
|
+
cleanContent: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Creates a DocumentSnapshot from a file path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSnapshot(targetPath: string, allowedRoots: string[]): DocumentSnapshot;
|
|
38
|
+
/**
|
|
39
|
+
* Helper class to map between JS character offsets and UTF-8 byte offsets.
|
|
40
|
+
*/
|
|
41
|
+
export declare class OffsetMapper {
|
|
42
|
+
private charToByte;
|
|
43
|
+
private byteToChar;
|
|
44
|
+
constructor(bytes: Uint8Array, str: string);
|
|
45
|
+
toByteOffset(charOffset: number): number;
|
|
46
|
+
toCharOffset(byteOffset: number): number;
|
|
47
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves symlinks and ensures the path is canonical and resides within the configured allowed roots.
|
|
6
|
+
* Throws an error if path traversal is detected.
|
|
7
|
+
*/
|
|
8
|
+
export function authorizeAndCanonicalizePath(targetPath, allowedRoots) {
|
|
9
|
+
if (allowedRoots.length === 0) {
|
|
10
|
+
throw new Error('No allowed roots configured for path authorization.');
|
|
11
|
+
}
|
|
12
|
+
let canonical;
|
|
13
|
+
try {
|
|
14
|
+
canonical = fs.realpathSync(targetPath);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
// If the file doesn't exist yet (e.g. for creation), canonicalize the parent directory
|
|
18
|
+
const resolvedParent = fs.realpathSync(path.dirname(targetPath));
|
|
19
|
+
canonical = path.join(resolvedParent, path.basename(targetPath));
|
|
20
|
+
}
|
|
21
|
+
const isAllowed = allowedRoots.some(root => {
|
|
22
|
+
try {
|
|
23
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
24
|
+
const relative = path.relative(canonicalRoot, canonical);
|
|
25
|
+
// If relative starts with '..' or is absolute, it's outside the root
|
|
26
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (!isAllowed) {
|
|
33
|
+
throw new Error(`Access Denied: Path "${targetPath}" resolves to "${canonical}" which is outside the authorized roots.`);
|
|
34
|
+
}
|
|
35
|
+
return canonical;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Detects encoding (UTF-8 vs UTF-8-BOM) and line endings (LF vs CRLF vs mixed) from content bytes.
|
|
39
|
+
*/
|
|
40
|
+
export function detectFileMetadata(bytes) {
|
|
41
|
+
let encoding = 'utf-8';
|
|
42
|
+
let startOffset = 0;
|
|
43
|
+
// Detect UTF-8 BOM: EF BB BF
|
|
44
|
+
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
45
|
+
encoding = 'utf-8-bom';
|
|
46
|
+
startOffset = 3;
|
|
47
|
+
}
|
|
48
|
+
const decoder = new TextDecoder('utf-8');
|
|
49
|
+
const cleanContent = decoder.decode(bytes.subarray(startOffset));
|
|
50
|
+
let lineEnding = 'lf';
|
|
51
|
+
const hasLF = cleanContent.includes('\n');
|
|
52
|
+
const hasCRLF = cleanContent.includes('\r\n');
|
|
53
|
+
if (hasCRLF) {
|
|
54
|
+
// Check if there are also standalone LF endings (i.e. LF not preceded by CR)
|
|
55
|
+
// Replace CRLF with empty, then check if there is still LF
|
|
56
|
+
const withoutCRLF = cleanContent.replace(/\r\n/g, '');
|
|
57
|
+
if (withoutCRLF.includes('\n')) {
|
|
58
|
+
lineEnding = 'mixed';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lineEnding = 'crlf';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (hasLF) {
|
|
65
|
+
lineEnding = 'lf';
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// No line endings, default to lf
|
|
69
|
+
lineEnding = 'lf';
|
|
70
|
+
}
|
|
71
|
+
return { encoding, lineEnding, cleanContent };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Creates a DocumentSnapshot from a file path.
|
|
75
|
+
*/
|
|
76
|
+
export function createSnapshot(targetPath, allowedRoots) {
|
|
77
|
+
const canonicalPath = authorizeAndCanonicalizePath(targetPath, allowedRoots);
|
|
78
|
+
const stat = fs.statSync(canonicalPath);
|
|
79
|
+
const fileIdentity = {
|
|
80
|
+
device: stat.dev,
|
|
81
|
+
inode: stat.ino
|
|
82
|
+
};
|
|
83
|
+
const bytes = fs.readFileSync(canonicalPath);
|
|
84
|
+
const { encoding, lineEnding, cleanContent } = detectFileMetadata(bytes);
|
|
85
|
+
const hash = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
86
|
+
const revision = `sha256:${hash}`;
|
|
87
|
+
return {
|
|
88
|
+
canonicalPath,
|
|
89
|
+
fileIdentity,
|
|
90
|
+
bytes,
|
|
91
|
+
content: cleanContent,
|
|
92
|
+
revision,
|
|
93
|
+
encoding,
|
|
94
|
+
lineEnding,
|
|
95
|
+
createdAt: new Date().toISOString()
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Helper class to map between JS character offsets and UTF-8 byte offsets.
|
|
100
|
+
*/
|
|
101
|
+
export class OffsetMapper {
|
|
102
|
+
charToByte;
|
|
103
|
+
byteToChar;
|
|
104
|
+
constructor(bytes, str) {
|
|
105
|
+
// Account for potential BOM offset in raw bytes relative to decoded string
|
|
106
|
+
const bomOffset = bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0;
|
|
107
|
+
this.charToByte = new Int32Array(str.length + 1);
|
|
108
|
+
this.byteToChar = new Int32Array(bytes.length + 1 - bomOffset);
|
|
109
|
+
let charIdx = 0;
|
|
110
|
+
let byteIdx = 0;
|
|
111
|
+
while (charIdx < str.length) {
|
|
112
|
+
this.charToByte[charIdx] = byteIdx;
|
|
113
|
+
this.byteToChar[byteIdx] = charIdx;
|
|
114
|
+
const codePoint = str.codePointAt(charIdx);
|
|
115
|
+
if (codePoint === undefined)
|
|
116
|
+
break;
|
|
117
|
+
let utf8Len = 0;
|
|
118
|
+
if (codePoint <= 0x7f)
|
|
119
|
+
utf8Len = 1;
|
|
120
|
+
else if (codePoint <= 0x7ff)
|
|
121
|
+
utf8Len = 2;
|
|
122
|
+
else if (codePoint <= 0xffff)
|
|
123
|
+
utf8Len = 3;
|
|
124
|
+
else
|
|
125
|
+
utf8Len = 4;
|
|
126
|
+
const utf16Len = codePoint > 0xffff ? 2 : 1;
|
|
127
|
+
for (let b = 0; b < utf8Len; b++) {
|
|
128
|
+
this.byteToChar[byteIdx + b] = charIdx;
|
|
129
|
+
}
|
|
130
|
+
for (let c = 0; c < utf16Len; c++) {
|
|
131
|
+
this.charToByte[charIdx + c] = byteIdx;
|
|
132
|
+
}
|
|
133
|
+
byteIdx += utf8Len;
|
|
134
|
+
charIdx += utf16Len;
|
|
135
|
+
}
|
|
136
|
+
this.charToByte[str.length] = byteIdx;
|
|
137
|
+
this.byteToChar[byteIdx] = str.length;
|
|
138
|
+
// Shift everything in charToByte by BOM offset to match raw byte offsets
|
|
139
|
+
if (bomOffset > 0) {
|
|
140
|
+
for (let i = 0; i < this.charToByte.length; i++) {
|
|
141
|
+
this.charToByte[i] += bomOffset;
|
|
142
|
+
}
|
|
143
|
+
// Re-initialize byteToChar to include BOM
|
|
144
|
+
const newByteToChar = new Int32Array(bytes.length + 1);
|
|
145
|
+
for (let b = 0; b < bomOffset; b++) {
|
|
146
|
+
newByteToChar[b] = 0;
|
|
147
|
+
}
|
|
148
|
+
for (let b = 0; b < this.byteToChar.length; b++) {
|
|
149
|
+
newByteToChar[b + bomOffset] = this.byteToChar[b];
|
|
150
|
+
}
|
|
151
|
+
this.byteToChar = newByteToChar;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
toByteOffset(charOffset) {
|
|
155
|
+
if (charOffset < 0)
|
|
156
|
+
return this.charToByte[0];
|
|
157
|
+
if (charOffset >= this.charToByte.length)
|
|
158
|
+
return this.charToByte[this.charToByte.length - 1];
|
|
159
|
+
return this.charToByte[charOffset];
|
|
160
|
+
}
|
|
161
|
+
toCharOffset(byteOffset) {
|
|
162
|
+
if (byteOffset < 0)
|
|
163
|
+
return 0;
|
|
164
|
+
if (byteOffset >= this.byteToChar.length)
|
|
165
|
+
return this.byteToChar[this.byteToChar.length - 1];
|
|
166
|
+
return this.byteToChar[byteOffset];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/snapshot/snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ByteEdit {
|
|
2
|
+
offset: number;
|
|
3
|
+
length: number;
|
|
4
|
+
replacement: Uint8Array;
|
|
5
|
+
index: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Checks if any edits overlap.
|
|
9
|
+
* Intersecting ranges are forbidden.
|
|
10
|
+
* An insert is allowed at the boundary of a replace, but not strictly inside it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hasOverlaps(edits: ByteEdit[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Sorts edits from highest offset to lowest offset for safe application.
|
|
15
|
+
* Deterministic rules for same-offset edits:
|
|
16
|
+
* - Replaces (length > 0) are applied first (sorted before inserts).
|
|
17
|
+
* - Inserts at the same offset are applied in request order (sorted by index descending for right-to-left application).
|
|
18
|
+
*/
|
|
19
|
+
export declare function sortEdits(edits: ByteEdit[]): ByteEdit[];
|
|
20
|
+
/**
|
|
21
|
+
* Applies a list of byte edits to a source Uint8Array.
|
|
22
|
+
* Throws an error if overlaps are detected.
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyEdits(source: Uint8Array, edits: ByteEdit[]): Uint8Array;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if any edits overlap.
|
|
3
|
+
* Intersecting ranges are forbidden.
|
|
4
|
+
* An insert is allowed at the boundary of a replace, but not strictly inside it.
|
|
5
|
+
*/
|
|
6
|
+
export function hasOverlaps(edits) {
|
|
7
|
+
for (let i = 0; i < edits.length; i++) {
|
|
8
|
+
const a = edits[i];
|
|
9
|
+
const startA = a.offset;
|
|
10
|
+
const endA = a.offset + a.length;
|
|
11
|
+
for (let j = i + 1; j < edits.length; j++) {
|
|
12
|
+
const b = edits[j];
|
|
13
|
+
const startB = b.offset;
|
|
14
|
+
const endB = b.offset + b.length;
|
|
15
|
+
// Check standard range overlap: max(startA, startB) < min(endA, endB)
|
|
16
|
+
// This detects if two non-empty ranges intersect, or if an insert is inside a replace.
|
|
17
|
+
const hasOverlap = Math.max(startA, startB) < Math.min(endA, endB);
|
|
18
|
+
if (hasOverlap)
|
|
19
|
+
return true;
|
|
20
|
+
// Check if one is insert (length 0) and strictly inside another's non-empty range
|
|
21
|
+
if (a.length > 0 && b.length === 0 && startB > startA && startB < endA)
|
|
22
|
+
return true;
|
|
23
|
+
if (b.length > 0 && a.length === 0 && startA > startB && startA < endB)
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sorts edits from highest offset to lowest offset for safe application.
|
|
31
|
+
* Deterministic rules for same-offset edits:
|
|
32
|
+
* - Replaces (length > 0) are applied first (sorted before inserts).
|
|
33
|
+
* - Inserts at the same offset are applied in request order (sorted by index descending for right-to-left application).
|
|
34
|
+
*/
|
|
35
|
+
export function sortEdits(edits) {
|
|
36
|
+
return [...edits].sort((a, b) => {
|
|
37
|
+
if (a.offset !== b.offset) {
|
|
38
|
+
return b.offset - a.offset; // Higher offsets first
|
|
39
|
+
}
|
|
40
|
+
// Same offset: Replace before insert
|
|
41
|
+
if (a.length !== b.length) {
|
|
42
|
+
return b.length - a.length; // Replaces (longer lengths) first
|
|
43
|
+
}
|
|
44
|
+
// Same offset and same length (e.g. multiple inserts): preserve request order
|
|
45
|
+
// Since we apply right-to-left, higher request index must be applied first
|
|
46
|
+
return b.index - a.index;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Applies a list of byte edits to a source Uint8Array.
|
|
51
|
+
* Throws an error if overlaps are detected.
|
|
52
|
+
*/
|
|
53
|
+
export function applyEdits(source, edits) {
|
|
54
|
+
if (hasOverlaps(edits)) {
|
|
55
|
+
throw new Error('OVERLAPPING_OPERATIONS');
|
|
56
|
+
}
|
|
57
|
+
const sorted = sortEdits(edits);
|
|
58
|
+
// To avoid multiple allocations, we can rebuild the buffer by slicing and concatenating
|
|
59
|
+
// since we process from right to left.
|
|
60
|
+
let currentBytes = source;
|
|
61
|
+
for (const edit of sorted) {
|
|
62
|
+
if (edit.offset < 0 || edit.offset + edit.length > currentBytes.length) {
|
|
63
|
+
throw new Error(`INVALID_REPLACEMENT: Edit offset ${edit.offset} or length ${edit.length} is out of bounds for content size ${currentBytes.length}.`);
|
|
64
|
+
}
|
|
65
|
+
const before = currentBytes.subarray(0, edit.offset);
|
|
66
|
+
const after = currentBytes.subarray(edit.offset + edit.length);
|
|
67
|
+
const nextBytes = new Uint8Array(before.length + edit.replacement.length + after.length);
|
|
68
|
+
nextBytes.set(before, 0);
|
|
69
|
+
nextBytes.set(edit.replacement, before.length);
|
|
70
|
+
nextBytes.set(after, before.length + edit.replacement.length);
|
|
71
|
+
currentBytes = nextBytes;
|
|
72
|
+
}
|
|
73
|
+
return currentBytes;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=planner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.js","sourceRoot":"","sources":["../../src/transaction/planner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely and atomically replaces targetFilePath with newBytes.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Checks if original file exists. If it does, record its permissions.
|
|
6
|
+
* 2. Writes newBytes to a temporary file in the same directory.
|
|
7
|
+
* 3. Applies the original file permissions to the temporary file.
|
|
8
|
+
* 4. Checks the current revision of the target file. If it doesn't match expectedRevision, aborts with COMMIT_RACE.
|
|
9
|
+
* 5. Renames the temporary file to the target path (atomic replace).
|
|
10
|
+
* 6. Cleans up temporary files on error.
|
|
11
|
+
*/
|
|
12
|
+
export declare function atomicWriteFile(targetFilePath: string, newBytes: Uint8Array, expectedRevision: string): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
/**
|
|
5
|
+
* Computes the SHA-256 revision hash of a file's bytes.
|
|
6
|
+
*/
|
|
7
|
+
function getFileRevision(filePath) {
|
|
8
|
+
const bytes = fs.readFileSync(filePath);
|
|
9
|
+
const hash = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
return `sha256:${hash}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Safely and atomically replaces targetFilePath with newBytes.
|
|
14
|
+
*
|
|
15
|
+
* Flow:
|
|
16
|
+
* 1. Checks if original file exists. If it does, record its permissions.
|
|
17
|
+
* 2. Writes newBytes to a temporary file in the same directory.
|
|
18
|
+
* 3. Applies the original file permissions to the temporary file.
|
|
19
|
+
* 4. Checks the current revision of the target file. If it doesn't match expectedRevision, aborts with COMMIT_RACE.
|
|
20
|
+
* 5. Renames the temporary file to the target path (atomic replace).
|
|
21
|
+
* 6. Cleans up temporary files on error.
|
|
22
|
+
*/
|
|
23
|
+
export function atomicWriteFile(targetFilePath, newBytes, expectedRevision) {
|
|
24
|
+
const targetDir = path.dirname(targetFilePath);
|
|
25
|
+
const targetName = path.basename(targetFilePath);
|
|
26
|
+
const tempFilePath = path.join(targetDir, `.tmp-${targetName}-${crypto.randomBytes(6).toString('hex')}`);
|
|
27
|
+
let originalMode;
|
|
28
|
+
let exists = false;
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs.statSync(targetFilePath);
|
|
31
|
+
originalMode = stat.mode;
|
|
32
|
+
exists = true;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err.code !== 'ENOENT') {
|
|
36
|
+
throw new Error(`IO_ERROR: Failed to stat target file: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Write new content to temporary file
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(tempFilePath, newBytes);
|
|
42
|
+
if (originalMode !== undefined) {
|
|
43
|
+
fs.chmodSync(tempFilePath, originalMode);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
// Cleanup temporary file
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(tempFilePath)) {
|
|
50
|
+
fs.unlinkSync(tempFilePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
throw new Error(`IO_ERROR: Failed to write temporary file: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
// Commit phase
|
|
57
|
+
try {
|
|
58
|
+
if (exists) {
|
|
59
|
+
// Re-read current file to verify no concurrent modification occurred (CAS check)
|
|
60
|
+
let currentRevision;
|
|
61
|
+
try {
|
|
62
|
+
currentRevision = getFileRevision(targetFilePath);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new Error(`IO_ERROR: Failed to read target file for revision check: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
if (currentRevision !== expectedRevision) {
|
|
68
|
+
throw new Error('COMMIT_RACE');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Atomic rename
|
|
72
|
+
fs.renameSync(tempFilePath, targetFilePath);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// Cleanup
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(tempFilePath)) {
|
|
78
|
+
fs.unlinkSync(tempFilePath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
if (err.message === 'COMMIT_RACE') {
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`IO_ERROR: Failed to rename temporary file: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
// Calculate new revision
|
|
88
|
+
return getFileRevision(targetFilePath);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=atomic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic.js","sourceRoot":"","sources":["../../src/writer/atomic.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@md-safeedit/core",
|
|
3
|
+
"version": "0.1.0-dev",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc"
|
|
18
|
+
}
|
|
19
|
+
}
|