1/*
2 * Copyright (C) 2017 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "DOMFileSystem.h"
28
29#include "File.h"
30#include "FileSystemDirectoryEntry.h"
31#include "FileSystemFileEntry.h"
32#include "ScriptExecutionContext.h"
33#include <wtf/CrossThreadCopier.h>
34#include <wtf/FileMetadata.h>
35#include <wtf/FileSystem.h>
36#include <wtf/IsoMallocInlines.h>
37#include <wtf/UUID.h>
38#include <wtf/text/StringBuilder.h>
39
40namespace WebCore {
41
42WTF_MAKE_ISO_ALLOCATED_IMPL(DOMFileSystem);
43
44struct ListedChild {
45 String filename;
46 FileMetadata::Type type;
47
48 ListedChild isolatedCopy() const { return { filename.isolatedCopy(), type }; }
49};
50
51static ExceptionOr<Vector<ListedChild>> listDirectoryWithMetadata(const String& fullPath)
52{
53 ASSERT(!isMainThread());
54 if (!FileSystem::fileIsDirectory(fullPath, FileSystem::ShouldFollowSymbolicLinks::No))
55 return Exception { NotFoundError, "Path no longer exists or is no longer a directory" };
56
57 auto childPaths = FileSystem::listDirectory(fullPath, "*");
58 Vector<ListedChild> listedChildren;
59 listedChildren.reserveInitialCapacity(childPaths.size());
60 for (auto& childPath : childPaths) {
61 auto metadata = FileSystem::fileMetadata(childPath);
62 if (!metadata || metadata.value().isHidden)
63 continue;
64 listedChildren.uncheckedAppend(ListedChild { FileSystem::pathGetFileName(childPath), metadata.value().type });
65 }
66 return listedChildren;
67}
68
69static ExceptionOr<Vector<Ref<FileSystemEntry>>> toFileSystemEntries(ScriptExecutionContext& context, DOMFileSystem& fileSystem, ExceptionOr<Vector<ListedChild>>&& listedChildren, const String& parentVirtualPath)
70{
71 ASSERT(isMainThread());
72 if (listedChildren.hasException())
73 return listedChildren.releaseException();
74
75 Vector<Ref<FileSystemEntry>> entries;
76 entries.reserveInitialCapacity(listedChildren.returnValue().size());
77 for (auto& child : listedChildren.returnValue()) {
78 String virtualPath = parentVirtualPath + "/" + child.filename;
79 switch (child.type) {
80 case FileMetadata::Type::File:
81 entries.uncheckedAppend(FileSystemFileEntry::create(context, fileSystem, virtualPath));
82 break;
83 case FileMetadata::Type::Directory:
84 entries.uncheckedAppend(FileSystemDirectoryEntry::create(context, fileSystem, virtualPath));
85 break;
86 default:
87 break;
88 }
89 }
90 return entries;
91}
92
93// https://wicg.github.io/entries-api/#name
94static bool isValidPathNameCharacter(UChar c)
95{
96 return c != '\0' && c != '/' && c != '\\';
97}
98
99// https://wicg.github.io/entries-api/#path-segment
100static bool isValidPathSegment(StringView segment)
101{
102 if (segment.isEmpty() || segment == "." || segment == "..")
103 return true;
104
105 for (unsigned i = 0; i < segment.length(); ++i) {
106 if (!isValidPathNameCharacter(segment[i]))
107 return false;
108 }
109 return true;
110}
111
112static bool isZeroOrMorePathSegmentsSeparatedBySlashes(StringView string)
113{
114 auto segments = string.split('/');
115 for (auto segment : segments) {
116 if (!isValidPathSegment(segment))
117 return false;
118 }
119 return true;
120}
121
122// https://wicg.github.io/entries-api/#relative-path
123static bool isValidRelativeVirtualPath(StringView virtualPath)
124{
125 if (virtualPath.isEmpty())
126 return false;
127
128 if (virtualPath[0] == '/')
129 return false;
130
131 return isZeroOrMorePathSegmentsSeparatedBySlashes(virtualPath);
132}
133
134// https://wicg.github.io/entries-api/#valid-path
135static bool isValidVirtualPath(StringView virtualPath)
136{
137 if (virtualPath.isEmpty())
138 return true;
139 if (virtualPath[0] == '/') {
140 // An absolute path is a string consisting of '/' (U+002F SOLIDUS) followed by one or more path segments joined by '/' (U+002F SOLIDUS).
141 return isZeroOrMorePathSegmentsSeparatedBySlashes(virtualPath.substring(1));
142 }
143 return isValidRelativeVirtualPath(virtualPath);
144}
145
146DOMFileSystem::DOMFileSystem(Ref<File>&& file)
147 : m_name(createCanonicalUUIDString())
148 , m_file(WTFMove(file))
149 , m_rootPath(FileSystem::directoryName(m_file->path()))
150 , m_workQueue(WorkQueue::create("DOMFileSystem work queue"))
151{
152 ASSERT(!m_rootPath.endsWith('/'));
153}
154
155DOMFileSystem::~DOMFileSystem() = default;
156
157Ref<FileSystemDirectoryEntry> DOMFileSystem::root(ScriptExecutionContext& context)
158{
159 return FileSystemDirectoryEntry::create(context, *this, "/"_s);
160}
161
162Ref<FileSystemEntry> DOMFileSystem::fileAsEntry(ScriptExecutionContext& context)
163{
164 if (m_file->isDirectory())
165 return FileSystemDirectoryEntry::create(context, *this, "/" + m_file->name());
166 return FileSystemFileEntry::create(context, *this, "/" + m_file->name());
167}
168
169static ExceptionOr<String> validatePathIsExpectedType(const String& fullPath, String&& virtualPath, FileMetadata::Type expectedType)
170{
171 ASSERT(!isMainThread());
172
173 auto metadata = FileSystem::fileMetadata(fullPath);
174 if (!metadata || metadata.value().isHidden)
175 return Exception { NotFoundError, "Path does not exist"_s };
176
177 if (metadata.value().type != expectedType)
178 return Exception { TypeMismatchError, "Entry at path does not have expected type" };
179
180 return WTFMove(virtualPath);
181}
182
183static Optional<FileMetadata::Type> fileType(const String& fullPath)
184{
185 auto metadata = FileSystem::fileMetadata(fullPath);
186 if (!metadata || metadata.value().isHidden)
187 return WTF::nullopt;
188 return metadata.value().type;
189}
190
191// https://wicg.github.io/entries-api/#resolve-a-relative-path
192static String resolveRelativeVirtualPath(StringView baseVirtualPath, StringView relativeVirtualPath)
193{
194 ASSERT(baseVirtualPath[0] == '/');
195 if (!relativeVirtualPath.isEmpty() && relativeVirtualPath[0] == '/')
196 return relativeVirtualPath.length() == 1 ? relativeVirtualPath.toString() : resolveRelativeVirtualPath("/", relativeVirtualPath.substring(1));
197
198 Vector<StringView> virtualPathSegments;
199 for (auto segment : baseVirtualPath.split('/'))
200 virtualPathSegments.append(segment);
201
202 for (auto segment : relativeVirtualPath.split('/')) {
203 ASSERT(!segment.isEmpty());
204 if (segment == ".")
205 continue;
206 if (segment == "..") {
207 if (!virtualPathSegments.isEmpty())
208 virtualPathSegments.removeLast();
209 continue;
210 }
211 virtualPathSegments.append(segment);
212 }
213
214 if (virtualPathSegments.isEmpty())
215 return "/"_s;
216
217 StringBuilder builder;
218 for (auto& segment : virtualPathSegments) {
219 builder.append('/');
220 builder.append(segment);
221 }
222 return builder.toString();
223}
224
225// https://wicg.github.io/entries-api/#evaluate-a-path
226String DOMFileSystem::evaluatePath(StringView virtualPath)
227{
228 ASSERT(virtualPath[0] == '/');
229
230 Vector<StringView> resolvedComponents;
231 for (auto component : virtualPath.split('/')) {
232 if (component == ".")
233 continue;
234 if (component == "..") {
235 if (!resolvedComponents.isEmpty())
236 resolvedComponents.removeLast();
237 continue;
238 }
239 resolvedComponents.append(component);
240 }
241
242 return FileSystem::pathByAppendingComponents(m_rootPath, resolvedComponents);
243}
244
245void DOMFileSystem::listDirectory(ScriptExecutionContext& context, FileSystemDirectoryEntry& directory, DirectoryListingCallback&& completionHandler)
246{
247 ASSERT(&directory.filesystem() == this);
248
249 auto directoryVirtualPath = directory.virtualPath();
250 auto fullPath = evaluatePath(directoryVirtualPath);
251 if (fullPath == m_rootPath) {
252 Vector<Ref<FileSystemEntry>> children;
253 children.append(fileAsEntry(context));
254 completionHandler(WTFMove(children));
255 return;
256 }
257
258 m_workQueue->dispatch([this, context = makeRef(context), completionHandler = WTFMove(completionHandler), fullPath = crossThreadCopy(fullPath), directoryVirtualPath = crossThreadCopy(directoryVirtualPath)]() mutable {
259 auto listedChildren = listDirectoryWithMetadata(fullPath);
260 callOnMainThread([this, context = WTFMove(context), completionHandler = WTFMove(completionHandler), listedChildren = crossThreadCopy(listedChildren), directoryVirtualPath = directoryVirtualPath.isolatedCopy()]() mutable {
261 completionHandler(toFileSystemEntries(context, *this, WTFMove(listedChildren), directoryVirtualPath));
262 });
263 });
264}
265
266void DOMFileSystem::getParent(ScriptExecutionContext& context, FileSystemEntry& entry, GetParentCallback&& completionCallback)
267{
268 ASSERT(&entry.filesystem() == this);
269
270 auto virtualPath = resolveRelativeVirtualPath(entry.virtualPath(), "..");
271 ASSERT(virtualPath[0] == '/');
272 auto fullPath = evaluatePath(virtualPath);
273 m_workQueue->dispatch([this, context = makeRef(context), fullPath = crossThreadCopy(fullPath), virtualPath = crossThreadCopy(virtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
274 auto validatedVirtualPath = validatePathIsExpectedType(fullPath, WTFMove(virtualPath), FileMetadata::Type::Directory);
275 callOnMainThread([this, context = WTFMove(context), validatedVirtualPath = crossThreadCopy(validatedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
276 if (validatedVirtualPath.hasException())
277 completionCallback(validatedVirtualPath.releaseException());
278 else
279 completionCallback(FileSystemDirectoryEntry::create(context, *this, validatedVirtualPath.releaseReturnValue()));
280 });
281 });
282}
283
284// https://wicg.github.io/entries-api/#dom-filesystemdirectoryentry-getfile
285// https://wicg.github.io/entries-api/#dom-filesystemdirectoryentry-getdirectory
286void DOMFileSystem::getEntry(ScriptExecutionContext& context, FileSystemDirectoryEntry& directory, const String& virtualPath, const FileSystemDirectoryEntry::Flags& flags, GetEntryCallback&& completionCallback)
287{
288 ASSERT(&directory.filesystem() == this);
289
290 if (!isValidVirtualPath(virtualPath)) {
291 callOnMainThread([completionCallback = WTFMove(completionCallback)] {
292 completionCallback(Exception { TypeMismatchError, "Path is invalid"_s });
293 });
294 return;
295 }
296
297 if (flags.create) {
298 callOnMainThread([completionCallback = WTFMove(completionCallback)] {
299 completionCallback(Exception { SecurityError, "create flag cannot be true"_s });
300 });
301 return;
302 }
303
304 auto resolvedVirtualPath = resolveRelativeVirtualPath(directory.virtualPath(), virtualPath);
305 ASSERT(resolvedVirtualPath[0] == '/');
306 auto fullPath = evaluatePath(resolvedVirtualPath);
307 if (fullPath == m_rootPath) {
308 callOnMainThread([this, context = makeRef(context), completionCallback = WTFMove(completionCallback)]() mutable {
309 completionCallback(Ref<FileSystemEntry> { root(context) });
310 });
311 return;
312 }
313
314 m_workQueue->dispatch([this, context = makeRef(context), fullPath = crossThreadCopy(fullPath), resolvedVirtualPath = crossThreadCopy(resolvedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
315 auto entryType = fileType(fullPath);
316 callOnMainThread([this, context = WTFMove(context), resolvedVirtualPath = crossThreadCopy(resolvedVirtualPath), entryType, completionCallback = WTFMove(completionCallback)]() mutable {
317 if (!entryType) {
318 completionCallback(Exception { NotFoundError, "Cannot find entry at given path"_s });
319 return;
320 }
321 switch (entryType.value()) {
322 case FileMetadata::Type::Directory:
323 completionCallback(Ref<FileSystemEntry> { FileSystemDirectoryEntry::create(context, *this, resolvedVirtualPath) });
324 break;
325 case FileMetadata::Type::File:
326 completionCallback(Ref<FileSystemEntry> { FileSystemFileEntry::create(context, *this, resolvedVirtualPath) });
327 break;
328 default:
329 completionCallback(Exception { NotFoundError, "Cannot find entry at given path"_s });
330 break;
331 }
332 });
333 });
334}
335
336void DOMFileSystem::getFile(ScriptExecutionContext& context, FileSystemFileEntry& fileEntry, GetFileCallback&& completionCallback)
337{
338 auto virtualPath = fileEntry.virtualPath();
339 auto fullPath = evaluatePath(virtualPath);
340 m_workQueue->dispatch([context = makeRef(context), fullPath = crossThreadCopy(fullPath), virtualPath = crossThreadCopy(virtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
341 auto validatedVirtualPath = validatePathIsExpectedType(fullPath, WTFMove(virtualPath), FileMetadata::Type::File);
342 callOnMainThread([context = WTFMove(context), fullPath = crossThreadCopy(fullPath), validatedVirtualPath = crossThreadCopy(validatedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
343 if (validatedVirtualPath.hasException())
344 completionCallback(validatedVirtualPath.releaseException());
345 else
346 completionCallback(File::create(fullPath));
347 });
348 });
349}
350
351} // namespace WebCore
352