Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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이 일어나던 문제(디스크 실제명 기준으로 해소).
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ nfd2nfc -v 보고서.pdf 자료.xlsx # 여러 파일 + 변경 내역 출

---

## <img src="https://api.iconify.design/ph/info-bold.svg?color=%236E7DF2&width=24" width="24" /> 바꾸는 범위 — 알아두면 좋은 점

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`는 정규형을 구분하는 일부 외장·네트워크 볼륨에서만 의미가 있고, 그곳에선 같은 이름의 **다른 파일을 영구히 덮어쓰므로**(복구 불가) 주의하세요. 기본값(충돌 시 건너뜀)을 권장합니다.

---

## <img src="https://api.iconify.design/ph/hard-drives-bold.svg?color=%236E7DF2&width=24" width="24" /> 근본 해결 (서버를 직접 운영한다면)

받는 서버를 직접 운영한다면, 업로드 시점에 서버에서 정규화하는 것이 가장 완전합니다.
Expand Down
11 changes: 8 additions & 3 deletions nfd2nfc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading