@xcanwin/manyoyo 6.2.17 → 6.2.19

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.
@@ -9,6 +9,18 @@
9
9
  .replace(/'/g, ''');
10
10
  }
11
11
 
12
+ // 双向控制符(RTLO 等)/ 零宽字符可以让文件名视觉上和真实字节完全不一致
13
+ // (例如 "cod" + U+202E + "exe.txt" 会被浏览器渲染成 "codtxt.exe"),
14
+ // 展示前统一替换成可见的 \uXXXX 记法,破坏其排版效果并让人一眼看出异常。
15
+ // 范围:U+200B-U+200F(零宽字符/LRM/RLM)、U+202A-U+202E(嵌入/覆盖方向控制符)、
16
+ // U+2060-U+2069(零宽连接符/方向隔离符)、U+FEFF(BOM/零宽不换行空格)、U+061C(阿拉伯字母标记)。
17
+ const SUSPICIOUS_UNICODE_PATTERN = /[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u061C]/g;
18
+ function sanitizeDisplayText(value) {
19
+ return String(value == null ? '' : value).replace(SUSPICIOUS_UNICODE_PATTERN, function (ch) {
20
+ return '\\u' + ch.codePointAt(0).toString(16).toUpperCase().padStart(4, '0');
21
+ });
22
+ }
23
+
12
24
  function formatBytes(size) {
13
25
  const value = Number(size || 0);
14
26
  if (!Number.isFinite(value) || value <= 0) {
@@ -46,6 +58,8 @@
46
58
  const parts = [];
47
59
  if (entry && entry.kind === 'directory') {
48
60
  parts.push('目录');
61
+ } else if (entry && entry.kind === 'symlink') {
62
+ parts.push('符号链接');
49
63
  } else {
50
64
  parts.push(formatBytes(entry && entry.size));
51
65
  }
@@ -55,6 +69,16 @@
55
69
  return parts.join(' · ');
56
70
  }
57
71
 
72
+ function buildEntryTitle(entry) {
73
+ const basePath = sanitizeDisplayText(entry && (entry.path || entry.name) || '');
74
+ if (entry && entry.kind === 'symlink') {
75
+ return entry.symlinkTarget
76
+ ? `${basePath} → ${sanitizeDisplayText(entry.symlinkTarget)}`
77
+ : `${basePath}(符号链接目标无法解析,可能已损坏)`;
78
+ }
79
+ return basePath;
80
+ }
81
+
58
82
  function inferLanguageFromPath(filePath) {
59
83
  const text = String(filePath || '').toLowerCase();
60
84
  if (text.endsWith('.md') || text.endsWith('.markdown')) return 'markdown';
@@ -237,7 +261,7 @@
237
261
  state.previewReadOnly = true;
238
262
  state.previewDirty = false;
239
263
  if (previewTitleNode) {
240
- previewTitleNode.textContent = title;
264
+ previewTitleNode.textContent = sanitizeDisplayText(title);
241
265
  }
242
266
  if (previewMetaNode) {
243
267
  previewMetaNode.textContent = description;
@@ -282,7 +306,7 @@
282
306
  return;
283
307
  }
284
308
  if (previewTitleNode) {
285
- previewTitleNode.textContent = payload.path || '未命名文件';
309
+ previewTitleNode.textContent = sanitizeDisplayText(payload.path) || '未命名文件';
286
310
  }
287
311
  updatePreviewMeta();
288
312
  if (!previewBodyNode) {
@@ -366,6 +390,23 @@
366
390
  syncSaveButton();
367
391
  }
368
392
 
393
+ function openSymlinkEntry(entry) {
394
+ const safeName = sanitizeDisplayText(entry.name);
395
+ const message = entry.symlinkTarget
396
+ ? `"${safeName}" 是符号链接,实际指向:\n${sanitizeDisplayText(entry.symlinkTarget)}\n\n是否继续访问?`
397
+ : `"${safeName}" 是一个无法解析的符号链接(可能已损坏),是否仍要尝试访问?`;
398
+ confirmFn(message).then(function (proceed) {
399
+ if (!proceed) {
400
+ return;
401
+ }
402
+ if (entry.symlinkTargetKind === 'directory') {
403
+ loadDirectory(entry.path);
404
+ return;
405
+ }
406
+ loadFile(entry.path, entry);
407
+ });
408
+ }
409
+
369
410
  function renderList() {
370
411
  if (pathNode) {
371
412
  pathNode.value = state.pathDraft || state.currentPath || state.containerPath || '/';
@@ -408,7 +449,7 @@
408
449
  const parentButton = document.createElement('button');
409
450
  parentButton.type = 'button';
410
451
  parentButton.className = 'files-entry files-entry-parent';
411
- parentButton.title = state.parentPath;
452
+ parentButton.title = sanitizeDisplayText(state.parentPath);
412
453
  parentButton.addEventListener('click', function () {
413
454
  loadDirectory(state.parentPath);
414
455
  });
@@ -430,8 +471,12 @@
430
471
  const button = document.createElement('button');
431
472
  button.type = 'button';
432
473
  button.className = 'files-entry' + (state.selectedPath === entry.path ? ' is-active' : '');
433
- button.title = String(entry.path || entry.name || '');
474
+ button.title = buildEntryTitle(entry);
434
475
  button.addEventListener('click', function () {
476
+ if (entry.kind === 'symlink') {
477
+ openSymlinkEntry(entry);
478
+ return;
479
+ }
435
480
  if (entry.kind === 'directory') {
436
481
  loadDirectory(entry.path);
437
482
  return;
@@ -440,7 +485,7 @@
440
485
  });
441
486
  button.innerHTML = `
442
487
  <span class="files-entry-name">
443
- <span class="files-entry-title">${escapeHtml(entry.name || entry.path || '未命名')}</span>
488
+ <span class="files-entry-title">${escapeHtml(sanitizeDisplayText(entry.name || entry.path || '未命名'))}</span>
444
489
  </span>
445
490
  <span class="files-entry-meta">${escapeHtml(buildEntryMeta(entry))}</span>
446
491
  `;
@@ -749,6 +794,7 @@
749
794
  }
750
795
 
751
796
  window.ManyoyoFileBrowser = {
752
- create
797
+ create,
798
+ sanitizeDisplayText
753
799
  };
754
800
  }());
package/lib/web/server.js CHANGED
@@ -3315,19 +3315,32 @@ try {
3315
3315
  itemStat = null;
3316
3316
  }
3317
3317
  let kind = 'other';
3318
+ let symlinkTarget = null;
3319
+ let symlinkTargetKind = null;
3318
3320
  if (entry.isDirectory()) {
3319
3321
  kind = 'directory';
3320
3322
  } else if (entry.isFile()) {
3321
3323
  kind = 'file';
3322
3324
  } else if (entry.isSymbolicLink()) {
3323
3325
  kind = 'symlink';
3326
+ try {
3327
+ const resolvedPath = fs.realpathSync(fullPath);
3328
+ const resolvedStat = fs.statSync(resolvedPath);
3329
+ symlinkTarget = resolvedPath;
3330
+ symlinkTargetKind = resolvedStat.isDirectory() ? 'directory' : 'file';
3331
+ } catch (e) {
3332
+ symlinkTarget = null;
3333
+ symlinkTargetKind = null;
3334
+ }
3324
3335
  }
3325
3336
  return {
3326
3337
  name: entry.name,
3327
3338
  path: fullPath,
3328
3339
  kind,
3329
3340
  size: itemStat && typeof itemStat.size === 'number' ? itemStat.size : 0,
3330
- mtimeMs: itemStat && typeof itemStat.mtimeMs === 'number' ? Math.floor(itemStat.mtimeMs) : 0
3341
+ mtimeMs: itemStat && typeof itemStat.mtimeMs === 'number' ? Math.floor(itemStat.mtimeMs) : 0,
3342
+ symlinkTarget,
3343
+ symlinkTargetKind
3331
3344
  };
3332
3345
  })
3333
3346
  .sort((a, b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "6.2.17",
3
+ "version": "6.2.19",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.18",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",