1/*
2 * Copyright (C) 2010 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 "DiskImageCacheIOS.h"
28
29#if ENABLE(DISK_IMAGE_CACHE) && PLATFORM(IOS)
30
31#include "FileSystemIOS.h"
32#include "Logging.h"
33#include "WebCoreThread.h"
34#include "WebCoreThreadRun.h"
35#include <errno.h>
36#include <sys/mman.h>
37#include <wtf/NeverDestroyed.h>
38#include <wtf/text/CString.h>
39#include <wtf/text/WTFString.h>
40
41namespace WebCore {
42
43DiskImageCache& diskImageCache()
44{
45    static NeverDestroyed<DiskImageCache> cache;
46    return cache;
47}
48
49DiskImageCache::Entry::Entry(SharedBuffer* buffer, DiskImageCacheId id)
50    : m_buffer(buffer)
51    , m_id(id)
52    , m_size(0)
53    , m_mapping(nullptr)
54{
55    ASSERT(WebThreadIsCurrent());
56    ASSERT(WebThreadIsLocked());
57    ASSERT(m_buffer);
58    m_buffer->ref();
59}
60
61DiskImageCache::Entry::~Entry()
62{
63    ASSERT(WebThreadIsCurrent());
64    ASSERT(WebThreadIsLocked());
65    ASSERT(!m_buffer);
66    ASSERT(!m_mapping);
67}
68
69bool DiskImageCache::Entry::mapInternal(const String& path)
70{
71    ASSERT(m_buffer);
72    ASSERT(!m_mapping);
73
74    m_path = path;
75    m_size = m_buffer->size();
76
77    // Open the file for reading and writing.
78    PlatformFileHandle handle = open(m_path.utf8().data(), O_CREAT | O_RDWR | O_TRUNC, static_cast<mode_t>(0600));
79    if (!isHandleValid(handle))
80        return false;
81
82    // Write the data to the file.
83    if (writeToFileInternal(handle) == -1) {
84        closeFile(handle);
85        deleteFile(m_path);
86        return false;
87    }
88
89    // Seek back to the beginning.
90    if (seekFile(handle, 0, SeekFromBeginning) == -1) {
91        closeFile(handle);
92        deleteFile(m_path);
93        return false;
94    }
95
96    // Perform memory mapping for reading.
97    // NOTE: This must not conflict with the open() above, which must also open for reading.
98    m_mapping = mmap(nullptr, m_size, PROT_READ, MAP_SHARED, handle, 0);
99    closeFile(handle);
100    if (m_mapping == MAP_FAILED) {
101        LOG(DiskImageCache, "DiskImageCache: mapping failed (%d): (%s)", errno, strerror(errno));
102        m_mapping = nullptr;
103        deleteFile(m_path);
104        return false;
105    }
106
107    return true;
108}
109
110void DiskImageCache::Entry::map(const String& path)
111{
112    ASSERT(m_buffer);
113    ASSERT(!m_mapping);
114    DiskImageCache::Entry* thisEntry = this;
115
116    bool fileMapped = mapInternal(path);
117    if (!fileMapped) {
118        // Notify the buffer in the case of a failed mapping.
119        WebThreadRun(^{
120            m_buffer->failedMemoryMap();
121            m_buffer->deref();
122            m_buffer = 0;
123            thisEntry->deref();
124        });
125        return;
126    }
127
128    // Notify the buffer in the case of a successful mapping.
129    // This should happen on the WebThread because this is being run
130    // asynchronously inside a dispatch queue.
131    WebThreadRun(^{
132        m_buffer->markAsMemoryMapped();
133        m_buffer->deref();
134        m_buffer = 0;
135        thisEntry->deref();
136    });
137}
138
139void DiskImageCache::Entry::unmap()
140{
141    if (!m_mapping) {
142        ASSERT(!m_size);
143        return;
144    }
145
146    if (munmap(m_mapping, m_size) == -1)
147        LOG_ERROR("DiskImageCache: Could not munmap a memory mapped file with id (%d)", m_id);
148
149    m_mapping = nullptr;
150    m_size = 0;
151}
152
153void DiskImageCache::Entry::removeFile()
154{
155    ASSERT(!m_mapping);
156    ASSERT(!m_size);
157
158    if (!deleteFile(m_path))
159        LOG_ERROR("DiskImageCache: Could not delete memory mapped file (%s)", m_path.utf8().data());
160}
161
162void DiskImageCache::Entry::clearDataWithoutMapping()
163{
164    ASSERT(!m_mapping);
165    ASSERT(m_buffer);
166    m_buffer->deref();
167    m_buffer = 0;
168}
169
170int DiskImageCache::Entry::writeToFileInternal(PlatformFileHandle handle)
171{
172    ASSERT(m_buffer);
173    int totalBytesWritten = 0;
174
175    const char* segment = nullptr;
176    unsigned position = 0;
177    while (unsigned length = m_buffer->getSomeData(segment, position)) {
178        int bytesWritten = writeToFile(handle, segment, length);
179        if (bytesWritten == -1)
180            return -1;
181
182        totalBytesWritten += bytesWritten;
183        position += length;
184    }
185
186    return totalBytesWritten;
187}
188
189
190DiskImageCache::DiskImageCache()
191    : m_enabled(false)
192    , m_size(0)
193    , m_maximumCacheSize(100 * 1024 * 1024)
194    , m_minimumImageSize(100 * 1024)
195    , m_nextAvailableId(DiskImageCache::invalidDiskCacheId + 1)
196{
197}
198
199DiskImageCacheId DiskImageCache::writeItem(PassRefPtr<SharedBuffer> item)
200{
201    if (!isEnabled() || !createDirectoryIfNeeded())
202        return DiskImageCache::invalidDiskCacheId;
203
204    // We are already full, cannot add anything until something is removed.
205    if (isFull()) {
206        LOG(DiskImageCache, "DiskImageCache: could not process an item because the cache was full at (%d). The \"max\" being (%d)", m_size, m_maximumCacheSize);
207        return DiskImageCache::invalidDiskCacheId;
208    }
209
210    // Create an entry.
211    DiskImageCacheId id = nextAvailableId();
212    RefPtr<SharedBuffer> buffer = item;
213    RefPtr<DiskImageCache::Entry> entry = DiskImageCache::Entry::create(buffer.get(), id);
214    m_table.add(id, entry);
215
216    // Create a temporary file path.
217    String path = temporaryFile();
218    LOG(DiskImageCache, "DiskImageCache: creating entry (%d) at (%s)", id, path.utf8().data());
219    if (path.isNull())
220        return DiskImageCache::invalidDiskCacheId;
221
222    // The lifetime of the Entry is handled on the WebThread.
223    // Before we send to a dispatch queue we need to ref so
224    // that we are sure the object still exists. This call
225    // is balanced in the WebThreadRun inside of Entry::map.
226    // or the early return in this dispatch.
227    DiskImageCache::Entry* localEntryForBlock = entry.get();
228    localEntryForBlock->ref();
229
230    // Map to disk asynchronously.
231    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
232        // The cache became full since the time we were added to the queue. Don't map.
233        if (diskImageCache().isFull()) {
234            WebThreadRun(^{
235                localEntryForBlock->clearDataWithoutMapping();
236                localEntryForBlock->deref();
237            });
238            return;
239        }
240
241        localEntryForBlock->map(path);
242
243        // Update the size on a successful mapping.
244        if (localEntryForBlock->isMapped())
245            diskImageCache().updateSize(localEntryForBlock->size());
246    });
247
248    return id;
249}
250
251void DiskImageCache::updateSize(unsigned delta)
252{
253    MutexLocker lock(m_mutex);
254    m_size += delta;
255}
256
257void DiskImageCache::removeItem(DiskImageCacheId id)
258{
259    LOG(DiskImageCache, "DiskImageCache: removeItem (%d)", id);
260    RefPtr<DiskImageCache::Entry> entry = m_table.get(id);
261    m_table.remove(id);
262    if (!entry->isMapped())
263        return;
264
265    updateSize(-(entry->size()));
266
267    DiskImageCache::Entry *localEntryForBlock = entry.get();
268    localEntryForBlock->ref();
269    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
270        localEntryForBlock->unmap();
271        localEntryForBlock->removeFile();
272        WebThreadRun(^{ localEntryForBlock->deref(); });
273    });
274}
275
276void* DiskImageCache::dataForItem(DiskImageCacheId id)
277{
278    ASSERT(id);
279
280    RefPtr<DiskImageCache::Entry> entry = m_table.get(id);
281    ASSERT(entry->isMapped());
282    return entry->data();
283}
284
285bool DiskImageCache::createDirectoryIfNeeded()
286{
287    if (!m_cacheDirectory.isNull())
288        return true;
289
290    m_cacheDirectory = temporaryDirectory();
291    LOG(DiskImageCache, "DiskImageCache: Created temporary directory (%s)", m_cacheDirectory.utf8().data());
292    if (m_cacheDirectory.isNull()) {
293        LOG_ERROR("DiskImageCache: could not create cache directory");
294        return false;
295    }
296
297    if (m_client)
298        m_client->didCreateDiskImageCacheDirectory(m_cacheDirectory);
299
300    return true;
301}
302
303DiskImageCacheId DiskImageCache::nextAvailableId()
304{
305    return m_nextAvailableId++;
306}
307
308String DiskImageCache::temporaryDirectory()
309{
310    NSString *tempDiskCacheDirectory = createTemporaryDirectory(@"DiskImageCache");
311    if (!tempDiskCacheDirectory)
312        LOG_ERROR("DiskImageCache: Could not create a temporary directory.");
313
314    return tempDiskCacheDirectory;
315}
316
317String DiskImageCache::temporaryFile()
318{
319    NSString *tempFile = createTemporaryFile(m_cacheDirectory, @"tmp");
320    if (!tempFile)
321        LOG_ERROR("DiskImageCache: Could not create a temporary file.");
322
323    return tempFile;
324}
325
326} // namespace WebCore
327
328#endif // ENABLE(DISK_IMAGE_CACHE) && PLATFORM(IOS)
329