[Storage] Preserve OPFS UTF-8 child names
Linux의 POSIX/C locale에서 OPFS의 비 ASCII 디렉터리 이름이 손실되어 자식 핸들이 부모 디렉터리를 가리키거나, 디렉터리 열거 결과의 이름이 비어 버리던 문제를 수정했습니다. 자식 URL 생성부터 디렉터리 열거까지 OPFS 이름이 보존되도록 개선했습니다.
문제 설명
비개발자 관점에서는 웹 애플리케이션이 "テスト用の本" 같은 이름으로 폴더를 만들었을 때, 성공한 것처럼 보이지만 실제로는 부모 폴더를 돌려줄 수 있는 문제였습니다. 이 상태에서 파일을 쓰면 예상한 하위 폴더가 아니라 부모 폴더에 저장될 수 있었습니다.
기술적으로는 content/browser/file_system_access의 OPFS 경로 처리 문제였습니다. OPFS의 가상 경로 component가 process locale에 의존하는 네이티브 경로 변환을 거치면서 손실됐습니다. 최종 변경에서는 OPFS 자식 URL과 열거 결과의 이름 모두 storage path representation을 보존하도록 했습니다.
Origin은 웹 페이지를 구분하는 보안·스토리지 경계이며, OPFS(Origin Private File System)는 각 origin에 제공되는 브라우저 내부 전용 파일시스템입니다. 사용자의 로컬 파일시스템과 달리 OPFS 경로는 sandboxed virtual filesystem의 논리적 component입니다.
이 File System Access 경로에서 OPFS URL은 storage::kFileSystemTypeTemporary로 표현됩니다. 기존 GetChildURL()은 OPFS를 포함한 non-Android filesystem 이름을 다음과 같이 처리했습니다.
parent.virtual_path().Append(
base::FilePath::FromUTF8Unsafe(basename));FromUTF8Unsafe()는 SYSTEM_NATIVE_UTF8이 아닌 POSIX 환경에서 UTF-8 문자열을 현재 locale의 native multibyte encoding으로 변환합니다. C locale은 일본어 같은 문자를 표현하지 못하므로 변환 결과에서 이름이 손실될 수 있었습니다.
OPFS child name
→ locale-dependent native conversion
→ C locale에서 non-ASCII 이름 손실
→ parent.virtual_path().Append("")
→ child path == parent path
→ child URL이 parent URL과 alias이름의 안전성 검사는 변환 전에 통과하므로 API 호출은 오류 없이 성공할 수 있습니다. 이슈의 재현 결과에서는 반환된 핸들의 이름은 요청값처럼 보였지만 isSameEntry(parent)가 true였고, 하위 디렉터리에 쓰려던 파일이 부모 디렉터리에 생성됐습니다. UTF-8 locale과 ASCII 이름에서는 정상적으로 동작했습니다.
Root Cause
문제는 서로 다른 경로 의미론을 동일한 변환으로 처리한 데 있었습니다.
- OPFS는 sandboxed virtual path component의 storage representation을 사용합니다.
- local/external 등 다른 filesystem type은 기존 native path 변환 동작을 유지해야 합니다.
- Android는 content URI를 포함한 별도의 path-construction 분기를 사용하므로 이번 변경 대상이 아닙니다.
따라서 모든 filesystem 이름을 일괄적으로 바꾸지 않고, OPFS에 해당하는 temporary filesystem 분기만 storage 변환 helper를 사용하도록 범위를 제한했습니다.
해결 내용
자식 URL의 경로 보존
GetChildURL()에서 OPFS 이름은 storage::StringToFilePath()로 변환해 가상 경로에 추가하도록 변경했습니다.
base::FilePath child_path =
parent.type() == storage::kFileSystemTypeTemporary
? parent.virtual_path().Append(
storage::StringToFilePath(basename))
: parent.virtual_path().Append(
base::FilePath::FromUTF8Unsafe(basename));StringToFilePath()는 POSIX에서 문자열 바이트를 그대로 FilePath에 보존하므로 locale-dependent conversion을 거치지 않습니다. 다른 filesystem type의 기존 변환과 Android의 별도 분기는 유지했습니다.
초기 패치에서는 AppendUTF8()을 사용했지만, 리뷰에서 모든 GetChildURL() call site가 해당 함수의 UTF-8 precondition을 보장하는지 질문이 제기됐습니다. 디렉터리 열거 경로의 basename은 FilePathToString()에서 유래하므로, storage 경로의 왕복 표현을 위한 StringToFilePath()가 더 적절하다고 판단했습니다.
디렉터리 열거 이름 보존
리뷰어가 요청한 열거 테스트를 작성하면서 두 번째 locale-dependent conversion 지점도 발견했습니다. OPFS 열거 경로에서는 별도의 display_name이 제공되지 않아 CreateEntry()가 basename.AsUTF8Unsafe()로 fallback했고, C locale에서는 반환되는 entry->name이 빈 문자열이 될 수 있었습니다.
OPFS에 한해 반대 방향의 storage helper를 사용하도록 수정했습니다.
std::string name = display_name;
if (name.empty()) {
name = url.type() == storage::kFileSystemTypeTemporary
? storage::FilePathToString(basename.path())
: basename.AsUTF8Unsafe();
}최종 변경은 기존 implementation과 unittest 두 파일에만 한정했습니다. Mojo validation, base::FilePath, storage subsystem, Android 및 다른 filesystem type의 동작은 변경하지 않았습니다.
테스트 방법
두 Linux 회귀 테스트는 다음 조건을 결합해 locale-dependent failure를 재현합니다.
C locale + non-ASCII OPFS child
→ 기존 native conversion: FAIL
→ storage path representation 보존: PASSGetChildURL_NonAsciiNameInCLocale
base::ScopedLocale("C")에서 "テスト用の本"의 child URL을 직접 생성합니다. 기존 구현에서는 child path가 "probe"로 남아 parent와 같아졌지만, 수정 후에는 "probe/テスト用の本"이 보존됩니다.
GetEntries_PreservesNonAsciiOPFSName
C++ content_unittests에서 실제 temporary filesystem root를 구성하고 다음 흐름을 실행합니다.
GetDirectory(create=true)
→ GetEntries()
→ 반환된 entry의 type과 name 확인이는 renderer까지 포함하는 browser E2E 테스트는 아니지만, content와 storage 경로를 연결해 사용자에게 반환되는 결과까지 확인하는 통합형 회귀 단위 테스트입니다. 테스트 작성 과정에서 PS5 구현은 entry를 정상적으로 찾으면서도 이름을 ""으로 반환한다는 사실을 확인했고, PS6의 CreateEntry() 수정으로 해결했습니다.
PS6 구현 검증 당시 다음 항목을 확인했습니다.
- 추가한 회귀 테스트: 2/2 PASS
- 관련
FileSystemAccessDirectoryHandleImpl테스트: 47/47 PASS content_unittests빌드 성공git diff --checkgit cl format --dry-run --diffgit cl presubmit
최종적으로 Chromium LUCI CQ의 full run이 성공하여 변경이 main에 제출됐습니다.
리뷰 및 Patch Set 변화
| Patch Set | 주요 변화 | 판단 근거 |
|---|---|---|
| PS1–2 | OPFS 자식 URL 수정과 직접 회귀 테스트 추가, locale scope 축소 | Linux/C-locale alias failure를 최소 단위로 재현 |
| PS3–4 | OPFS에 특별 처리가 필요한 이유를 인접 주석으로 설명 | Mingyu Lei의 리뷰 요청 반영 |
| PS5 | AppendUTF8()을 StringToFilePath()로 교체 |
Ming-Ying Chung이 제기한 UTF-8 precondition 보장 문제 대응 |
| PS6 | GetEntries() 회귀 테스트와 CreateEntry() 보완 |
열거 결과에서도 원래 이름을 보존하라는 리뷰 요청 반영 |
| PS7–8 | 최종 구현을 반영하도록 CL description 수정 | CreateEntry() 변경이 기존 설명에서 누락됐다는 피드백 대응 |
| PS9 | CQ가 최신 main 위로 rebase 후 제출 | 최종 upstream commit 생성 |
PS2의 CQ Dry Run은 성공했지만 이후 코드가 변경되면서 당시 vote는 outdated 처리됐습니다. 최종 코드가 완성된 PS6에는 Ming-Ying Chung과 Arthur Sonzogni가 각각 Code-Review +1을 부여했습니다.
이후 Ming-Ying Chung이 CreateEntry()와 열거 동작을 포함하도록 CL description 갱신을 요청했습니다. PS8에서 commit message를 수정했으며 소스 파일에는 변화가 없었습니다. Ming-Ying Chung이 PS8에 Commit-Queue +2를 부여했고, Chromium LUCI CQ가 full run을 완료한 뒤 PS9로 rebase하여 제출했습니다. 최종 unresolved reviewer comment는 0개입니다.
AI 활용
AI는 다음 작업에서 코드베이스 탐색 및 검증 도구로 활용했습니다.
GetChildURL()의 call site와 입력 경로 조사FromUTF8Unsafe(),AppendUTF8(), storage path helper의 contract 비교- directory enumeration과
CreateEntry()fallback 추적 - 기존 test fixture를 활용한 재현 방법 탐색
- reviewer feedback의 기술적 의도 분석
- 최종 diff의 플랫폼·보안·scope regression 검토
AI가 후보와 구현안을 제안할 수 있지만, 변경 범위와 설계 선택, 테스트의 적절성, 실제 diff의 수용 여부는 contributor가 코드와 실행 결과를 확인한 뒤 결정했습니다.
배운 점
- 같은 문자열이라도 filesystem abstraction에 따라 virtual component와 native path라는 서로 다른 의미를 가질 수 있습니다.
- locale-dependent 문제는
ScopedLocale("C")처럼 재현 조건을 테스트 안에 고정해야 결정적으로 검증할 수 있습니다. - 직접 함수 테스트가 통과하더라도 사용자에게 반환되는 열거 결과까지 정상이라는 보장은 없으므로 인접 흐름을 함께 확인해야 합니다.
- 리뷰 질문은 문구 수정에 그치지 않고 숨은 API precondition과 미검증 경로를 발견하는 계기가 될 수 있습니다.
- 동일 원인의 문제는 보완하되 Mojo나 Unicode validation 정책까지 변경하지 않도록 CL 범위를 통제해야 합니다.
최종 결과
CL 8262447은 2026-08-26 Chromium main에 병합됐습니다.
- 최종 Patch Set: PS9
- 최종 commit:
ca80cf53038f6e8d3141257071d478b2d5ffca34 - Cr-Commit-Position:
refs/heads/main@{#1686165} - Submitter: Chromium LUCI CQ
- Issue 545336271 상태: Fixed
- 최종 변경량: 2 files changed, 90 insertions, 3 deletions
- PS6 이후 최종 제출까지 소스 파일 변경 없음
이슈는 현재 Fixed이며 아직 Fixed (Verified) 상태는 아닙니다.
핵심 요약
- Linux C locale에서 OPFS 비 ASCII 자식 이름이 손실되던 문제를 수정했습니다.
- 자식 URL과 디렉터리 열거 결과 모두 storage path representation을 보존합니다.
- 직접 회귀 테스트와
GetDirectory()→GetEntries()통합형 테스트를 추가했습니다. - 리뷰 과정에서 UTF-8 precondition과
CreateEntry()fallback 문제를 추가로 확인했습니다. - CL은 PS9,
main@{#1686165}로 Chromium main에 병합됐습니다.