diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdbd52..b56e116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ ## [Unreleased] +### Changed (형식/FS 검토 후속) +- `--force`가 실제로 **다른 항목을 덮어쓸 때 경고**를 출력(`덮어씀(기존 항목 영구 삭제)`) — loud opt-in. 충돌은 정규화-구분 FS에서만 발생하므로 macOS 기본 볼륨에선 무영향. +- README에 **"바꾸는 범위" 섹션** 추가 — 파일 내용·형식 무관, **압축 파일 내부 엔트리명은 스코프 밖**(zip/tar/hwpx/docx), `.app` 번들 서명 주의, `--force` 데이터 손실은 정규화-구분 볼륨 한정임을 명시. +- 통합 테스트: 압축 파일 내부 엔트리명·내용 불변(스코프 한계 회귀 가드). + ### Fixed (안정성/스트레스 테스트 후속) - **셸 glob/탭완성이 인자를 NFC로 정규화하면 변환이 누락되던 문제** — macOS 기본 셸 zsh는 `nfd2nfc *.hwp`에서 파일명을 NFC로 넘겨, 디스크가 NFD인데도 "이미 정규화됨"으로 스킵했다. 최상위 인자를 부모 readdir로 **디스크 실제 저장명**으로 바꿔(`disk_real_path`) 셸 정규화와 무관하게 동작. - 같은 디스크 항목을 NFD/NFC **다른 철자로 중복 지정**하면 `%seen` 키가 raw 바이트라 dedup이 실패해 카운트 부풀림·이중 rename이 일어나던 문제(디스크 실제명 기준으로 해소). diff --git a/README.md b/README.md index 2fcd3d6..3e59eef 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,17 @@ nfd2nfc -v 보고서.pdf 자료.xlsx # 여러 파일 + 변경 내역 출 --- +## 바꾸는 범위 — 알아두면 좋은 점 + +nfd2nfc는 **파일·폴더 이름만** NFC로 바꿉니다. 그래서: + +- **파일 내용은 건드리지 않습니다.** 확장자·형식(`png`·`jpg`·`pdf`·`hwp`·`docx`·`xlsx` 등)과 무관하게, 이름만 정규화하고 내용·형식은 그대로 둡니다. +- **압축 파일 안의 이름은 바꾸지 않습니다.** `zip`·`tar`, 그리고 내부가 압축인 `hwpx`·`docx` 같은 파일은 **파일 자체 이름만** 정규화됩니다. 압축 안에 든 한글 파일명이 NFD라면 다른 OS에서 풀 때 여전히 깨집니다 — 내부까지 고치려면 **macOS에서 풀어 정규화한 뒤 다시 압축**하세요. +- **앱 번들(`.app`)은 피하세요.** macOS는 `.app`을 폴더로 다뤄, 통째로 정리하면 내부까지 들어갑니다. 보통은 무해하지만 **코드서명된 앱은 서명이 무효화**될 수 있습니다. +- **`--force`는 거의 쓸 일이 없습니다.** macOS 기본 볼륨(APFS·HFS+)에선 NFD와 NFC가 같은 파일이라 이름 충돌이 생기지 않습니다. `--force`는 정규형을 구분하는 일부 외장·네트워크 볼륨에서만 의미가 있고, 그곳에선 같은 이름의 **다른 파일을 영구히 덮어쓰므로**(복구 불가) 주의하세요. 기본값(충돌 시 건너뜀)을 권장합니다. + +--- + ## 근본 해결 (서버를 직접 운영한다면) 받는 서버를 직접 운영한다면, 업로드 시점에 서버에서 정규화하는 것이 가장 완전합니다. diff --git a/nfd2nfc b/nfd2nfc index 5469648..bf7c7ce 100755 --- a/nfd2nfc +++ b/nfd2nfc @@ -185,15 +185,20 @@ for my $p (@sorted) { # macOS 파일시스템(APFS·HFS+)은 정규화 비구분(normalization-insensitive)이라 # NFD로 저장된 파일도 NFC 이름으로 -e/lstat 검사하면 "존재"로 잡힌다. # 즉 단순 -e 검사는 자기 자신을 충돌로 오인해 전부 건너뛴다. - # 따라서 '진짜 다른 파일'(다른 inode)이 그 이름을 차지한 경우에만 건너뛴다. - # --force면 충돌 검사를 우회하고 rename을 강행(기존 파일을 덮어씀). + # 따라서 '진짜 다른 파일'(다른 inode)이 그 이름을 차지한 경우에만 충돌로 본다. + # (이 충돌은 정규화-구분 FS에서만 발생 — APFS/HFS+에선 NFD/NFC가 같은 inode라 안 생긴다.) my @cur = lstat($p); my @tgt = lstat($new); - if (!$force && @tgt && (!@cur || $tgt[0] != $cur[0] || $tgt[1] != $cur[1])) { + my $collision = @tgt && (!@cur || $tgt[0] != $cur[0] || $tgt[1] != $cur[1]); + if ($collision && !$force) { warn "건너뜀(같은 이름의 다른 항목 존재): $p\n"; $skipped++; next; } + # --force는 충돌 검사를 우회한다. 실제로 다른 항목을 덮어쓸 때는 영구 삭제임을 알린다. + if ($collision && $force && !$dry) { + warn "덮어씀(기존 항목 영구 삭제): $new\n"; + } if ($dry) { print "[미리보기] $p → $new\n" unless $quiet; diff --git a/test.sh b/test.sh index 881b0e6..f4c7f87 100755 --- a/test.sh +++ b/test.sh @@ -203,6 +203,24 @@ base15=$(/usr/bin/perl "$NFD2NFC" -n "$D15" 2>&1 | tail -1) dup15=$(/usr/bin/perl "$NFD2NFC" -n "$D15" "$nfc_dir" 2>&1 | tail -1) if [ "$base15" = "$dup15" ]; then ok "NFD/NFC 철자 중복 입력 dedup(카운트 안 부풀림)"; else ng "dedup 실패: base=[$base15] dup=[$dup15]"; fi +# [16] 압축 파일의 '내부' 엔트리명은 스코프 밖 — 외부 파일명만 정규화되고 내부·내용은 불변(한계 회귀 가드) +D16="$TMP/t16"; mkdir -p "$D16" +( cd "$D16" || exit + inner=$(/usr/bin/perl -e 'use utf8;use Unicode::Normalize qw(NFD);use Encode qw(encode_utf8);print encode_utf8(NFD("안.txt"))') + printf x > "$inner" + arc=$(/usr/bin/perl -e 'use utf8;use Unicode::Normalize qw(NFD);use Encode qw(encode_utf8);print encode_utf8(NFD("자료.zip"))') + zip -q "$arc" "$inner" && rm -f "$inner" ) +entry_bytes() { /usr/bin/python3 -c ' +import zipfile,glob,sys +z=glob.glob(sys.argv[1]+"/*.zip")[0] +i=zipfile.ZipFile(z).infolist()[0] +raw=i.filename.encode("utf-8") if (i.flag_bits & 0x800) else i.filename.encode("cp437") +print(raw.hex())' "$1"; } +before16=$(entry_bytes "$D16"); md5b16=$(md5 -q "$D16"/*.zip) +/usr/bin/perl "$NFD2NFC" -q "$D16" +after16=$(entry_bytes "$D16"); md5a16=$(md5 -q "$D16"/*.zip) +if [ "$before16" = "$after16" ] && [ "$md5b16" = "$md5a16" ]; then ok "압축 내부 엔트리명·내용 불변(스코프 밖)"; else ng "압축 내부 변함: name $before16→$after16"; fi + # [버전] --version 출력 형식 ver_out=$(/usr/bin/perl "$NFD2NFC" --version 2>&1) if echo "$ver_out" | grep -Eq '^nfd2nfc [0-9]+\.[0-9]+\.[0-9]+$'; then ok "--version 출력 형식"; else ng "--version 형식 이상: $ver_out"; fi