From f45ad3b25fa94a898be46382284b77fdbdafe8a2 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:06:04 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20=EC=98=B5=EC=85=98=20=EC=B9=B8=20?= =?UTF-8?q?=ED=8B=88=20=EC=85=8B=20(=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8C=90=EB=8B=A8=2010/20,=2028=20=ED=99=95=EC=9D=B8):=20?= =?UTF-8?q?=EA=B0=92=20=EC=9E=90=EB=A6=AC=EA=B0=80=20=EC=95=84=EB=8B=8C?= =?UTF-8?q?=EB=8D=B0=20-=20=EB=A1=9C=20=EC=8B=9C=EC=9E=91=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=82=B1=EB=A7=90=20("?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EC=9D=BD=ED=9E=99=EB=8B=88?= =?UTF-8?q?=EB=8B=A4")=20=C2=B7=20=ED=95=98=EC=9D=B4=ED=94=88=20=EC=85=8B?= =?UTF-8?q?=20=EC=9D=B4=EC=83=81=20=C2=B7=20=EB=81=9D=EC=97=90=20=EA=B0=92?= =?UTF-8?q?=20=EC=97=86=EB=8A=94=20=EC=98=B5=EC=85=98=20("--exclude=20?= =?UTF-8?q?=EC=9D=98=20=EA=B0=92=EC=9D=B4=20=EC=97=86=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4")=20=EB=8F=84=20=EC=95=8C=EC=95=84=EB=B3=BC=20?= =?UTF-8?q?=EC=88=98=20=EC=97=86=EC=9D=8C=EC=9C=BC=EB=A1=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=EC=9D=84=20=EB=A7=89=EC=9D=8C=20-=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20rsync=20=EB=8A=94=20=EC=9B=90=EB=B3=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EB=A1=9C=20=EB=A8=B9=EC=96=B4=20--delete=20=EA=B0=80?= =?UTF-8?q?=20=EB=B9=A0=EC=A7=80=EA=B1=B0=EB=82=98=20=EC=95=84=EB=AC=B4?= =?UTF-8?q?=EA=B2=83=EB=8F=84=20=EB=B3=B4=EB=82=B4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=8C=20(=EC=8B=9C=ED=97=98=EC=9C=BC=EB=A1=9C=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8)=20(=EA=B2=80=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 2 ++ assets/l10n/ja.json | 2 ++ assets/l10n/zh-Hans.json | 2 ++ lib/core/sync_tools.dart | 22 +++++++++++++++------ test/option_paste_test.dart | 39 ++++++++++++++++++++++++++++++++++--- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 577c051..c886a29 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -526,6 +526,7 @@ "{0} 을(를) 설치했습니다.": "Installed {0}.", "{0} 을(를) 지울까요?": "Delete {0}?", "{0} 의 SHA256 이 다릅니다 (손상되었거나 다른 파일). 설치를 멈췄습니다.": "The SHA256 of {0} does not match (damaged or a different file). Installation stopped.", + "{0} 의 값이 없습니다": "{0} has no value", "{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} is empty (insert a card, USB drive or disc and tap again).", "{0} 이상": "{0} or larger", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg does not have the {0} encoder.", @@ -725,6 +726,7 @@ "경로 입력 · 네트워크 서버": "Enter path · Network servers", "경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).", "경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently", + "경로로 읽힙니다: {0}": "Read as a path: {0}", "경로를 복사했습니다: {0}": "Copied path: {0}", "계산 안 함": "Don't calculate", "계속": "Always", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index c7c8c5f..7c674ed 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -526,6 +526,7 @@ "{0} 을(를) 설치했습니다.": "{0} をインストールしました。", "{0} 을(를) 지울까요?": "{0} を削除しますか?", "{0} 의 SHA256 이 다릅니다 (손상되었거나 다른 파일). 설치를 멈췄습니다.": "{0} の SHA256 が一致しません (破損しているか別のファイル)。インストールを中止しました。", + "{0} 의 값이 없습니다": "{0} の値がありません", "{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} は空です (カード・USB・ディスクを入れてからもう一度押してください)。", "{0} 이상": "{0} 以上", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg に {0} エンコーダーがありません。", @@ -725,6 +726,7 @@ "경로 입력 · 네트워크 서버": "パス入力・ネットワークサーバー", "경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。", "경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます", + "경로로 읽힙니다: {0}": "パスとして読まれます: {0}", "경로를 복사했습니다: {0}": "パスをコピーしました: {0}", "계산 안 함": "計算しない", "계속": "常時", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 1a16d1a..663d1e7 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -526,6 +526,7 @@ "{0} 을(를) 설치했습니다.": "已安装 {0}。", "{0} 을(를) 지울까요?": "要删除 {0} 吗?", "{0} 의 SHA256 이 다릅니다 (손상되었거나 다른 파일). 설치를 멈췄습니다.": "{0} 的 SHA256 不一致(已损坏或是其他文件)。已停止安装。", + "{0} 의 값이 없습니다": "{0} 缺少值", "{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).": "{0} 为空 (插入存储卡、U 盘或光盘后再点按)。", "{0} 이상": "{0} 及以上", "{0} 인코더가 FFmpeg 에 없습니다.": "FFmpeg 中没有 {0} 编码器。", @@ -725,6 +726,7 @@ "경로 입력 · 네트워크 서버": "输入路径 · 网络服务器", "경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。", "경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除", + "경로로 읽힙니다: {0}": "会被当作路径: {0}", "경로를 복사했습니다: {0}": "已复制路径: {0}", "계산 안 함": "不计算", "계속": "持续", diff --git a/lib/core/sync_tools.dart b/lib/core/sync_tools.dart index aa87a49..3751c43 100644 --- a/lib/core/sync_tools.dart +++ b/lib/core/sync_tools.dart @@ -192,29 +192,39 @@ const _rsyncLongWithValue = { '--write-batch', '--only-write-batch', '--read-batch', '--protocol', '--checksum-seed', '--copy-as', '--early-input', }; -/// 관리자 지시 (10/20, 28 확인): [splitOptions] 로 고쳐 읽은 뒤에도 옵션 자리에 ASCII 밖 글자로 시작하는 낱말이 남으면 -/// (rsync 는 그것을 원본 경로로 읽어 옵션이 몰래 빠진다) 그 낱말들. 있으면 확인 창 · 실시간 동기화에 빨갛게 알리고 실행을 막는다. -/// 옵션의 값 자리 (--exclude 한글.txt 의 한글.txt, -f 뒤) 는 세지 않는다 +/// 관리자 지시 (10/20, 28 · 15 확인): [splitOptions] 로 고쳐 읽은 뒤에도 rsync 가 다르게 읽을 것 - 있으면 확인 창 · 실시간 동기화에 +/// 빨갛게 알리고 실행을 막는다: 닫히지 않은 따옴표, 옵션 자리의 - 로 시작하지 않는 낱말 (rsync 는 원본 경로로 읽어 옵션이 몰래 빠짐), +/// 하이픈 셋 이상, 끝에 값 없이 남은 값 받는 옵션. 옵션의 값 자리 (--exclude 한글.txt 의 한글.txt, -f 뒤) 는 세지 않는다 List unreadableOptions(String options) { final parsed = _parseOptions(options); // 15 확인 F1: 닫히지 않은 따옴표 - 뒤의 옵션이 다르게 읽힐 수 있다 final bad = [for (final u in parsed.unclosed) trf('따옴표가 닫히지 않았습니다: {0}', [u])]; var value = false; + var last = ''; for (final t in parsed.words) { if (value) { value = false; continue; } - if (t.startsWith('--')) { + if (t.startsWith('---')) { + // 관리자 판단 (10/20, 28 확인): 하이픈이 셋 이상 (---delete) 은 rsync 가 알아보지 못한다 + bad.add(t); + } else if (t.startsWith('--')) { value = !t.contains('=') && _rsyncLongWithValue.contains(t); + last = t; } else if (t.length > 1 && t.startsWith('-')) { // 짧은 옵션 묶음: 값을 받는 글자 (B · e · f · M · T · @) 가 끝이면 다음 낱말이 값 (rsync_filter 와 같은 규칙) final k = t.indexOf(RegExp('[BefMT@]'), 1); value = k == t.length - 1; - } else if (t.isNotEmpty && t.codeUnitAt(0) > 0x7F) { - bad.add(t); + last = t; + } else { + // 관리자 판단 (10/20, 28 확인): 옵션 칸에는 경로가 들어갈 자리가 없다 (원본 · 대상은 앱이 넣음) - 값 자리가 아닌데 + // - 로 시작하지 않는 낱말 (-a delete 의 delete, 혼자 쓴 -) 은 rsync 가 원본 경로로 읽어 옵션이 몰래 빠진다 + bad.add(trf('경로로 읽힙니다: {0}', [t])); } } + // 값이 필요한 옵션이 끝에 값 없이 (--exclude) - rsync 가 원본 경로를 그 값으로 먹는다 + if (value) bad.add(trf('{0} 의 값이 없습니다', [last])); return bad; } diff --git a/test/option_paste_test.dart b/test/option_paste_test.dart index 6fd8ac3..3d51bcd 100644 --- a/test/option_paste_test.dart +++ b/test/option_paste_test.dart @@ -90,12 +90,21 @@ void main() { }); test('고쳐 읽어도 알아볼 수 없는 옵션 (ASCII 밖 글자로 시작) 은 알린다 - 옵션의 값 자리는 세지 않음', () { - expect(unreadableOptions('-a \u00ABdelete\u00BB'), ['\u00ABdelete\u00BB']); - expect(unreadableOptions('-a \u2026delete'), ['\u2026delete']); + expect(unreadableOptions('-a \u00ABdelete\u00BB'), ['경로로 읽힙니다: \u00ABdelete\u00BB']); + expect(unreadableOptions('-a \u2026delete'), ['경로로 읽힙니다: \u2026delete']); expect(unreadableOptions('--exclude 한글.txt -a'), isEmpty, reason: '--exclude 의 값'); expect(unreadableOptions('-f 한글.txt'), isEmpty, reason: '-f 의 값'); - expect(unreadableOptions('-a 한글'), ['한글'], reason: '옵션 자리 - rsync 는 원본 경로로 읽는다'); + expect(unreadableOptions('-a 한글'), ['경로로 읽힙니다: 한글'], reason: '옵션 자리 - rsync 는 원본 경로로 읽는다'); expect(unreadableOptions('-avPog --delete'), isEmpty); + // 관리자 판단 (10/20, 28 확인): - 로 시작하지 않는 낱말 · 하이픈 셋 이상 · 끝에 값 없는 옵션 + expect(unreadableOptions('-a delete'), ['경로로 읽힙니다: delete']); + expect(unreadableOptions('-a - --delete'), ['경로로 읽힙니다: -']); + expect(unreadableOptions('-a ---delete'), ['---delete']); + expect(unreadableOptions('-a --exclude'), ['--exclude 의 값이 없습니다']); + expect(unreadableOptions('-a -f'), ['-f 의 값이 없습니다']); + expect(unreadableOptions('-a --exclude *.log --delete'), isEmpty, reason: '값이 있음'); + expect(unreadableOptions('-a --exclude=*.log'), isEmpty); + expect(unreadableOptions(''), isEmpty); }); testWidgets('알아볼 수 없는 옵션 알림 (빨간 글, 옵션 글은 줄바꿈 없이)', (tester) async { @@ -103,6 +112,30 @@ void main() { expect(find.textContaining('옵션을 알아볼 수 없습니다'), findsOneWidget); }); + // 28 확인 (10/20): 막기 전 실제 rsync 는 이런 낱말을 원본 경로로 먹는다 - 그래서 [unreadableOptions] 로 막는다 + test('실제 rsync: -a delete 의 delete · 끝의 --exclude 는 원본 경로로 먹혀 의도와 다르게 돈다', () async { + final rsync = sceneRsync; + if (rsync == null) return markTestSkipped('앱에 든 rsync 없음 (Windows · third_party)'); + final tmp = Directory.systemTemp.createTempSync('jj_optpath_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + final s = p.join(tmp.path, 's'), d = p.join(tmp.path, 'd'); + Future run(String opts) { + writeScene(s, d, srcFiles: const ['a.txt'], dstFiles: const ['old.txt']); + return Process.run(rsync, [...splitOptions(opts), '${toCygwinPath(s)}/', '${toCygwinPath(d)}/'], workingDirectory: tmp.path); + } + + // "delete" 를 원본으로 찾다 실패 (exit 23) - --delete 를 뜻했지만 old.txt 는 그대로 + final r1 = await run('-r delete'); + expect(r1.exitCode, isNot(0)); + expect(File(p.join(d, 'old.txt')).existsSync(), isTrue); + // --exclude 가 원본 폴더를 값으로 먹고 받는 쪽 하나만 남아 목록만 보이고 끝 (아무것도 보내지 않음) + final r2 = await run('-r --exclude'); + expect(r2.exitCode, 0); + expect(File(p.join(d, 'a.txt')).existsSync(), isFalse, reason: '보내지 않음'); + expect(unreadableOptions('-r delete'), isNotEmpty); + expect(unreadableOptions('-r --exclude'), isNotEmpty); + }); + // 실제 rsync (Windows, 앱에 든 것): 붙여 넣은 글로 맞춘 결과 = 곧은 글자로 맞춘 결과 (28 의 장면: 받는 쪽 keep.log · old.txt) group('실제 rsync 결과가 같다', () { for (final (name, pasted, straight) in cases) {