Use "= default" to denote default constructor or destructor
[WebKit-https.git] / Source / WebCore / Modules / entriesapi / DOMFileSystem.cpp
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 "FileMetadata.h"
31 #include "FileSystem.h"
32 #include "FileSystemDirectoryEntry.h"
33 #include "FileSystemFileEntry.h"
34 #include "ScriptExecutionContext.h"
35 #include <wtf/CrossThreadCopier.h>
36 #include <wtf/UUID.h>
37 #include <wtf/text/StringBuilder.h>
38
39 namespace WebCore {
40
41 struct ListedChild {
42     String filename;
43     FileMetadata::Type type;
44
45     ListedChild isolatedCopy() const { return { filename.isolatedCopy(), type }; }
46 };
47
48 static ExceptionOr<Vector<ListedChild>> listDirectoryWithMetadata(const String& fullPath)
49 {
50     ASSERT(!isMainThread());
51     if (!fileIsDirectory(fullPath, ShouldFollowSymbolicLinks::No))
52         return Exception { NotFoundError, "Path no longer exists or is no longer a directory" };
53
54     auto childPaths = listDirectory(fullPath, "*");
55     Vector<ListedChild> listedChildren;
56     listedChildren.reserveInitialCapacity(childPaths.size());
57     for (auto& childPath : childPaths) {
58         auto metadata = fileMetadata(childPath);
59         if (!metadata || metadata.value().isHidden)
60             continue;
61         listedChildren.uncheckedAppend(ListedChild { pathGetFileName(childPath), metadata.value().type });
62     }
63     return WTFMove(listedChildren);
64 }
65
66 static ExceptionOr<Vector<Ref<FileSystemEntry>>> toFileSystemEntries(ScriptExecutionContext& context, DOMFileSystem& fileSystem, ExceptionOr<Vector<ListedChild>>&& listedChildren, const String& parentVirtualPath)
67 {
68     ASSERT(isMainThread());
69     if (listedChildren.hasException())
70         return listedChildren.releaseException();
71
72     Vector<Ref<FileSystemEntry>> entries;
73     entries.reserveInitialCapacity(listedChildren.returnValue().size());
74     for (auto& child : listedChildren.returnValue()) {
75         String virtualPath = parentVirtualPath + "/" + child.filename;
76         switch (child.type) {
77         case FileMetadata::Type::File:
78             entries.uncheckedAppend(FileSystemFileEntry::create(context, fileSystem, virtualPath));
79             break;
80         case FileMetadata::Type::Directory:
81             entries.uncheckedAppend(FileSystemDirectoryEntry::create(context, fileSystem, virtualPath));
82             break;
83         default:
84             break;
85         }
86     }
87     return WTFMove(entries);
88 }
89
90 // https://wicg.github.io/entries-api/#name
91 static bool isValidPathNameCharacter(UChar c)
92 {
93     return c != '\0' && c != '/' && c != '\\';
94 }
95
96 // https://wicg.github.io/entries-api/#path-segment
97 static bool isValidPathSegment(StringView segment)
98 {
99     if (segment.isEmpty() || segment == "." || segment == "..")
100         return true;
101
102     for (unsigned i = 0; i < segment.length(); ++i) {
103         if (!isValidPathNameCharacter(segment[i]))
104             return false;
105     }
106     return true;
107 }
108
109 static bool isZeroOrMorePathSegmentsSeparatedBySlashes(StringView string)
110 {
111     auto segments = string.split('/');
112     for (auto segment : segments) {
113         if (!isValidPathSegment(segment))
114             return false;
115     }
116     return true;
117 }
118
119 // https://wicg.github.io/entries-api/#relative-path
120 static bool isValidRelativeVirtualPath(StringView virtualPath)
121 {
122     if (virtualPath.isEmpty())
123         return false;
124
125     if (virtualPath[0] == '/')
126         return false;
127
128     return isZeroOrMorePathSegmentsSeparatedBySlashes(virtualPath);
129 }
130
131 // https://wicg.github.io/entries-api/#valid-path
132 static bool isValidVirtualPath(StringView virtualPath)
133 {
134     if (virtualPath.isEmpty())
135         return true;
136     if (virtualPath[0] == '/') {
137         // An absolute path is a string consisting of '/' (U+002F SOLIDUS) followed by one or more path segments joined by '/' (U+002F SOLIDUS).
138         return isZeroOrMorePathSegmentsSeparatedBySlashes(virtualPath.substring(1));
139     }
140     return isValidRelativeVirtualPath(virtualPath);
141 }
142
143 DOMFileSystem::DOMFileSystem(Ref<File>&& file)
144     : m_name(createCanonicalUUIDString())
145     , m_file(WTFMove(file))
146     , m_rootPath(directoryName(m_file->path()))
147     , m_workQueue(WorkQueue::create("DOMFileSystem work queue"))
148 {
149     ASSERT(!m_rootPath.endsWith('/'));
150 }
151
152 DOMFileSystem::~DOMFileSystem() = default;
153
154 Ref<FileSystemDirectoryEntry> DOMFileSystem::root(ScriptExecutionContext& context)
155 {
156     return FileSystemDirectoryEntry::create(context, *this, ASCIILiteral("/"));
157 }
158
159 Ref<FileSystemEntry> DOMFileSystem::fileAsEntry(ScriptExecutionContext& context)
160 {
161     if (m_file->isDirectory())
162         return FileSystemDirectoryEntry::create(context, *this, "/" + m_file->name());
163     return FileSystemFileEntry::create(context, *this, "/" + m_file->name());
164 }
165
166 static ExceptionOr<String> validatePathIsExpectedType(const String& fullPath, String&& virtualPath, FileMetadata::Type expectedType)
167 {
168     ASSERT(!isMainThread());
169
170     auto metadata = fileMetadata(fullPath);
171     if (!metadata || metadata.value().isHidden)
172         return Exception { NotFoundError, ASCIILiteral("Path does not exist") };
173
174     if (metadata.value().type != expectedType)
175         return Exception { TypeMismatchError, "Entry at path does not have expected type" };
176
177     return WTFMove(virtualPath);
178 }
179
180 static std::optional<FileMetadata::Type> fileType(const String& fullPath)
181 {
182     auto metadata = fileMetadata(fullPath);
183     if (!metadata || metadata.value().isHidden)
184         return std::nullopt;
185     return metadata.value().type;
186 }
187
188 // https://wicg.github.io/entries-api/#resolve-a-relative-path
189 static String resolveRelativeVirtualPath(StringView baseVirtualPath, StringView relativeVirtualPath)
190 {
191     ASSERT(baseVirtualPath[0] == '/');
192     if (!relativeVirtualPath.isEmpty() && relativeVirtualPath[0] == '/')
193         return relativeVirtualPath.length() == 1 ? relativeVirtualPath.toString() : resolveRelativeVirtualPath("/", relativeVirtualPath.substring(1));
194
195     Vector<StringView> virtualPathSegments;
196     for (auto segment : baseVirtualPath.split('/'))
197         virtualPathSegments.append(segment);
198
199     for (auto segment : relativeVirtualPath.split('/')) {
200         ASSERT(!segment.isEmpty());
201         if (segment == ".")
202             continue;
203         if (segment == "..") {
204             if (!virtualPathSegments.isEmpty())
205                 virtualPathSegments.removeLast();
206             continue;
207         }
208         virtualPathSegments.append(segment);
209     }
210
211     if (virtualPathSegments.isEmpty())
212         return ASCIILiteral("/");
213
214     StringBuilder builder;
215     for (auto& segment : virtualPathSegments) {
216         builder.append('/');
217         builder.append(segment);
218     }
219     return builder.toString();
220 }
221
222 // https://wicg.github.io/entries-api/#evaluate-a-path
223 String DOMFileSystem::evaluatePath(StringView virtualPath)
224 {
225     ASSERT(virtualPath[0] == '/');
226
227     Vector<StringView> resolvedComponents;
228     for (auto component : virtualPath.split('/')) {
229         if (component == ".")
230             continue;
231         if (component == "..") {
232             if (!resolvedComponents.isEmpty())
233                 resolvedComponents.removeLast();
234             continue;
235         }
236         resolvedComponents.append(component);
237     }
238
239     return pathByAppendingComponents(m_rootPath, resolvedComponents);
240 }
241
242 void DOMFileSystem::listDirectory(ScriptExecutionContext& context, FileSystemDirectoryEntry& directory, DirectoryListingCallback&& completionHandler)
243 {
244     ASSERT(&directory.filesystem() == this);
245
246     auto directoryVirtualPath = directory.virtualPath();
247     auto fullPath = evaluatePath(directoryVirtualPath);
248     if (fullPath == m_rootPath) {
249         Vector<Ref<FileSystemEntry>> children;
250         children.append(fileAsEntry(context));
251         completionHandler(WTFMove(children));
252         return;
253     }
254
255     m_workQueue->dispatch([this, context = makeRef(context), completionHandler = WTFMove(completionHandler), fullPath = crossThreadCopy(fullPath), directoryVirtualPath = crossThreadCopy(directoryVirtualPath)]() mutable {
256         auto listedChildren = listDirectoryWithMetadata(fullPath);
257         callOnMainThread([this, context = WTFMove(context), completionHandler = WTFMove(completionHandler), listedChildren = crossThreadCopy(listedChildren), directoryVirtualPath = directoryVirtualPath.isolatedCopy()]() mutable {
258             completionHandler(toFileSystemEntries(context, *this, WTFMove(listedChildren), directoryVirtualPath));
259         });
260     });
261 }
262
263 void DOMFileSystem::getParent(ScriptExecutionContext& context, FileSystemEntry& entry, GetParentCallback&& completionCallback)
264 {
265     ASSERT(&entry.filesystem() == this);
266
267     auto virtualPath = resolveRelativeVirtualPath(entry.virtualPath(), "..");
268     ASSERT(virtualPath[0] == '/');
269     auto fullPath = evaluatePath(virtualPath);
270     m_workQueue->dispatch([this, context = makeRef(context), fullPath = crossThreadCopy(fullPath), virtualPath = crossThreadCopy(virtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
271         auto validatedVirtualPath = validatePathIsExpectedType(fullPath, WTFMove(virtualPath), FileMetadata::Type::Directory);
272         callOnMainThread([this, context = WTFMove(context), validatedVirtualPath = crossThreadCopy(validatedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
273             if (validatedVirtualPath.hasException())
274                 completionCallback(validatedVirtualPath.releaseException());
275             else
276                 completionCallback(FileSystemDirectoryEntry::create(context, *this, validatedVirtualPath.releaseReturnValue()));
277         });
278     });
279 }
280
281 // https://wicg.github.io/entries-api/#dom-filesystemdirectoryentry-getfile
282 // https://wicg.github.io/entries-api/#dom-filesystemdirectoryentry-getdirectory
283 void DOMFileSystem::getEntry(ScriptExecutionContext& context, FileSystemDirectoryEntry& directory, const String& virtualPath, const FileSystemDirectoryEntry::Flags& flags, GetEntryCallback&& completionCallback)
284 {
285     ASSERT(&directory.filesystem() == this);
286
287     if (!isValidVirtualPath(virtualPath)) {
288         callOnMainThread([completionCallback = WTFMove(completionCallback)] {
289             completionCallback(Exception { TypeMismatchError, ASCIILiteral("Path is invalid") });
290         });
291         return;
292     }
293
294     if (flags.create) {
295         callOnMainThread([completionCallback = WTFMove(completionCallback)] {
296             completionCallback(Exception { SecurityError, ASCIILiteral("create flag cannot be true") });
297         });
298         return;
299     }
300
301     auto resolvedVirtualPath = resolveRelativeVirtualPath(directory.virtualPath(), virtualPath);
302     ASSERT(resolvedVirtualPath[0] == '/');
303     auto fullPath = evaluatePath(resolvedVirtualPath);
304     if (fullPath == m_rootPath) {
305         callOnMainThread([this, context = makeRef(context), completionCallback = WTFMove(completionCallback)]() mutable {
306             completionCallback(Ref<FileSystemEntry> { root(context) });
307         });
308         return;
309     }
310
311     m_workQueue->dispatch([this, context = makeRef(context), fullPath = crossThreadCopy(fullPath), resolvedVirtualPath = crossThreadCopy(resolvedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
312         auto entryType = fileType(fullPath);
313         callOnMainThread([this, context = WTFMove(context), resolvedVirtualPath = crossThreadCopy(resolvedVirtualPath), entryType, completionCallback = WTFMove(completionCallback)]() mutable {
314             if (!entryType) {
315                 completionCallback(Exception { NotFoundError, ASCIILiteral("Cannot find entry at given path") });
316                 return;
317             }
318             switch (entryType.value()) {
319             case FileMetadata::Type::Directory:
320                 completionCallback(Ref<FileSystemEntry> { FileSystemDirectoryEntry::create(context, *this, resolvedVirtualPath) });
321                 break;
322             case FileMetadata::Type::File:
323                 completionCallback(Ref<FileSystemEntry> { FileSystemFileEntry::create(context, *this, resolvedVirtualPath) });
324                 break;
325             default:
326                 completionCallback(Exception { NotFoundError, ASCIILiteral("Cannot find entry at given path") });
327                 break;
328             }
329         });
330     });
331 }
332
333 void DOMFileSystem::getFile(ScriptExecutionContext& context, FileSystemFileEntry& fileEntry, GetFileCallback&& completionCallback)
334 {
335     auto virtualPath = fileEntry.virtualPath();
336     auto fullPath = evaluatePath(virtualPath);
337     m_workQueue->dispatch([context = makeRef(context), fullPath = crossThreadCopy(fullPath), virtualPath = crossThreadCopy(virtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
338         auto validatedVirtualPath = validatePathIsExpectedType(fullPath, WTFMove(virtualPath), FileMetadata::Type::File);
339         callOnMainThread([context = WTFMove(context), fullPath = crossThreadCopy(fullPath), validatedVirtualPath = crossThreadCopy(validatedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
340             if (validatedVirtualPath.hasException())
341                 completionCallback(validatedVirtualPath.releaseException());
342             else
343                 completionCallback(File::create(fullPath));
344         });
345     });
346 }
347
348 } // namespace WebCore