f8b4564a96574896f6bb371e7f3d653cd2354917
[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         FileMetadata metadata;
59         if (!getFileMetadata(childPath, metadata, ShouldFollowSymbolicLinks::No))
60             continue;
61         listedChildren.uncheckedAppend(ListedChild { pathGetFileName(childPath), metadata.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::TypeFile:
78             entries.uncheckedAppend(FileSystemFileEntry::create(context, fileSystem, virtualPath));
79             break;
80         case FileMetadata::TypeDirectory:
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     ASSERT(!segment.isEmpty());
100     if (segment == "." || segment == "..")
101         return true;
102
103     for (unsigned i = 0; i < segment.length(); ++i) {
104         if (!isValidPathNameCharacter(segment[i]))
105             return false;
106     }
107     return true;
108 }
109
110 // https://wicg.github.io/entries-api/#relative-path
111 static bool isValidRelativeVirtualPath(StringView virtualPath)
112 {
113     if (virtualPath.isEmpty())
114         return false;
115
116     if (virtualPath[0] == '/')
117         return false;
118
119     auto segments = virtualPath.split('/');
120     for (auto segment : segments) {
121         if (!isValidPathSegment(segment))
122             return false;
123     }
124     return true;
125 }
126
127 // https://wicg.github.io/entries-api/#valid-path
128 static bool isValidVirtualPath(StringView virtualPath)
129 {
130     if (virtualPath.isEmpty())
131         return false;
132     if (virtualPath[0] == '/')
133         return virtualPath.length() == 1 || isValidRelativeVirtualPath(virtualPath.substring(1));
134     return isValidRelativeVirtualPath(virtualPath);
135 }
136
137 DOMFileSystem::DOMFileSystem(Ref<File>&& file)
138     : m_name(createCanonicalUUIDString())
139     , m_file(WTFMove(file))
140     , m_rootPath(directoryName(m_file->path()))
141     , m_workQueue(WorkQueue::create("DOMFileSystem work queue"))
142 {
143     ASSERT(!m_rootPath.endsWith('/'));
144 }
145
146 DOMFileSystem::~DOMFileSystem()
147 {
148 }
149
150 Ref<FileSystemDirectoryEntry> DOMFileSystem::root(ScriptExecutionContext& context)
151 {
152     return FileSystemDirectoryEntry::create(context, *this, ASCIILiteral("/"));
153 }
154
155 Ref<FileSystemEntry> DOMFileSystem::fileAsEntry(ScriptExecutionContext& context)
156 {
157     if (m_file->isDirectory())
158         return FileSystemDirectoryEntry::create(context, *this, "/" + m_file->name());
159     return FileSystemFileEntry::create(context, *this, "/" + m_file->name());
160 }
161
162 static ExceptionOr<String> validatePathIsExpectedType(const String& fullPath, String&& virtualPath, FileMetadata::Type expectedType)
163 {
164     ASSERT(!isMainThread());
165
166     FileMetadata metadata;
167     if (!getFileMetadata(fullPath, metadata, ShouldFollowSymbolicLinks::No))
168         return Exception { NotFoundError, ASCIILiteral("Path does not exist") };
169
170     if (metadata.type != expectedType)
171         return Exception { TypeMismatchError, "Entry at path does not have expected type" };
172
173     return WTFMove(virtualPath);
174 }
175
176 // https://wicg.github.io/entries-api/#resolve-a-relative-path
177 static String resolveRelativeVirtualPath(const String& baseVirtualPath, StringView relativeVirtualPath)
178 {
179     ASSERT(baseVirtualPath[0] == '/');
180     if (relativeVirtualPath[0] == '/')
181         return relativeVirtualPath.length() == 1 ? relativeVirtualPath.toString() : resolveRelativeVirtualPath(ASCIILiteral("/"), relativeVirtualPath.substring(1));
182
183     auto virtualPathSegments = baseVirtualPath.split('/');
184     auto relativePathSegments = relativeVirtualPath.split('/');
185     for (auto segment : relativePathSegments) {
186         ASSERT(!segment.isEmpty());
187         if (segment == ".")
188             continue;
189         if (segment == "..") {
190             if (!virtualPathSegments.isEmpty())
191                 virtualPathSegments.removeLast();
192             continue;
193         }
194         virtualPathSegments.append(segment.toString());
195     }
196
197     if (virtualPathSegments.isEmpty())
198         return ASCIILiteral("/");
199
200     StringBuilder builder;
201     for (auto& segment : virtualPathSegments) {
202         builder.append('/');
203         builder.append(segment);
204     }
205     return builder.toString();
206 }
207
208 // https://wicg.github.io/entries-api/#evaluate-a-path
209 String DOMFileSystem::evaluatePath(const String& virtualPath)
210 {
211     ASSERT(virtualPath[0] == '/');
212     auto components = virtualPath.split('/');
213
214     Vector<String> resolvedComponents;
215     resolvedComponents.reserveInitialCapacity(components.size());
216     for (auto& component : components) {
217         if (component == ".")
218             continue;
219         if (component == "..") {
220             if (!resolvedComponents.isEmpty())
221                 resolvedComponents.removeLast();
222             continue;
223         }
224         resolvedComponents.uncheckedAppend(component);
225     }
226
227     return pathByAppendingComponents(m_rootPath, resolvedComponents);
228 }
229
230 void DOMFileSystem::listDirectory(ScriptExecutionContext& context, FileSystemDirectoryEntry& directory, DirectoryListingCallback&& completionHandler)
231 {
232     ASSERT(&directory.filesystem() == this);
233
234     auto directoryVirtualPath = directory.virtualPath();
235     auto fullPath = evaluatePath(directoryVirtualPath);
236     if (fullPath == m_rootPath) {
237         Vector<Ref<FileSystemEntry>> children;
238         children.append(fileAsEntry(context));
239         completionHandler(WTFMove(children));
240         return;
241     }
242
243     m_workQueue->dispatch([this, context = makeRef(context), completionHandler = WTFMove(completionHandler), fullPath = crossThreadCopy(fullPath), directoryVirtualPath = crossThreadCopy(directoryVirtualPath)]() mutable {
244         auto listedChildren = listDirectoryWithMetadata(fullPath);
245         callOnMainThread([this, context = WTFMove(context), completionHandler = WTFMove(completionHandler), listedChildren = crossThreadCopy(listedChildren), directoryVirtualPath = directoryVirtualPath.isolatedCopy()]() mutable {
246             completionHandler(toFileSystemEntries(context, *this, WTFMove(listedChildren), directoryVirtualPath));
247         });
248     });
249 }
250
251 void DOMFileSystem::getParent(ScriptExecutionContext& context, FileSystemEntry& entry, GetParentCallback&& completionCallback)
252 {
253     ASSERT(&entry.filesystem() == this);
254
255     auto virtualPath = resolveRelativeVirtualPath(entry.virtualPath(), "..");
256     ASSERT(virtualPath[0] == '/');
257     auto fullPath = evaluatePath(virtualPath);
258     m_workQueue->dispatch([this, context = makeRef(context), fullPath = crossThreadCopy(fullPath), virtualPath = crossThreadCopy(virtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
259         auto validatedVirtualPath = validatePathIsExpectedType(fullPath, WTFMove(virtualPath), FileMetadata::TypeDirectory);
260         callOnMainThread([this, context = WTFMove(context), validatedVirtualPath = crossThreadCopy(validatedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
261             if (validatedVirtualPath.hasException())
262                 completionCallback(validatedVirtualPath.releaseException());
263             else
264                 completionCallback(FileSystemDirectoryEntry::create(context, *this, validatedVirtualPath.releaseReturnValue()));
265         });
266     });
267 }
268
269 // https://wicg.github.io/entries-api/#dom-filesystemdirectoryentry-getfile
270 // https://wicg.github.io/entries-api/#dom-filesystemdirectoryentry-getdirectory
271 void DOMFileSystem::getEntry(ScriptExecutionContext& context, FileSystemDirectoryEntry& directory, const String& virtualPath, const FileSystemDirectoryEntry::Flags& flags, GetEntryCallback&& completionCallback)
272 {
273     ASSERT(&directory.filesystem() == this);
274
275     if (!isValidVirtualPath(virtualPath)) {
276         callOnMainThread([completionCallback = WTFMove(completionCallback)] {
277             completionCallback(Exception { TypeMismatchError, ASCIILiteral("Path is invalid") });
278         });
279         return;
280     }
281
282     if (flags.create) {
283         callOnMainThread([completionCallback = WTFMove(completionCallback)] {
284             completionCallback(Exception { SecurityError, ASCIILiteral("create flag cannot be true") });
285         });
286         return;
287     }
288
289     auto resolvedVirtualPath = resolveRelativeVirtualPath(directory.virtualPath(), virtualPath);
290     ASSERT(resolvedVirtualPath[0] == '/');
291     auto fullPath = evaluatePath(resolvedVirtualPath);
292     if (fullPath == m_rootPath) {
293         callOnMainThread([this, context = makeRef(context), completionCallback = WTFMove(completionCallback)]() mutable {
294             completionCallback(Ref<FileSystemEntry> { root(context) });
295         });
296         return;
297     }
298
299     m_workQueue->dispatch([this, context = makeRef(context), fullPath = crossThreadCopy(fullPath), resolvedVirtualPath = crossThreadCopy(resolvedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
300         FileMetadata metadata;
301         getFileMetadata(fullPath, metadata, ShouldFollowSymbolicLinks::No);
302         callOnMainThread([this, context = WTFMove(context), resolvedVirtualPath = crossThreadCopy(resolvedVirtualPath), entryType = metadata.type, completionCallback = WTFMove(completionCallback)]() mutable {
303             switch (entryType) {
304             case FileMetadata::TypeDirectory:
305                 completionCallback(Ref<FileSystemEntry> { FileSystemDirectoryEntry::create(context, *this, resolvedVirtualPath) });
306                 break;
307             case FileMetadata::TypeFile:
308                 completionCallback(Ref<FileSystemEntry> { FileSystemFileEntry::create(context, *this, resolvedVirtualPath) });
309                 break;
310             default:
311                 completionCallback(Exception { NotFoundError, ASCIILiteral("Cannot find entry at given path") });
312                 break;
313             }
314         });
315     });
316 }
317
318 void DOMFileSystem::getFile(ScriptExecutionContext& context, FileSystemFileEntry& fileEntry, GetFileCallback&& completionCallback)
319 {
320     auto virtualPath = fileEntry.virtualPath();
321     auto fullPath = evaluatePath(virtualPath);
322     m_workQueue->dispatch([this, context = makeRef(context), fullPath = crossThreadCopy(fullPath), virtualPath = crossThreadCopy(virtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
323         auto validatedVirtualPath = validatePathIsExpectedType(fullPath, WTFMove(virtualPath), FileMetadata::TypeFile);
324         callOnMainThread([this, context = WTFMove(context), fullPath = crossThreadCopy(fullPath), validatedVirtualPath = crossThreadCopy(validatedVirtualPath), completionCallback = WTFMove(completionCallback)]() mutable {
325             if (validatedVirtualPath.hasException())
326                 completionCallback(validatedVirtualPath.releaseException());
327             else
328                 completionCallback(File::create(fullPath));
329         });
330     });
331 }
332
333 } // namespace WebCore