Line data Source code
1 : /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* This Source Code Form is subject to the terms of the Mozilla Public
3 : * License, v. 2.0. If a copy of the MPL was not distributed with this
4 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 :
6 : #include "plhash.h"
7 : #include "nsDirectoryServiceUtils.h"
8 : #include "nsDirectoryServiceDefs.h"
9 : #include "nsAppDirectoryServiceDefs.h"
10 : #include "GMPParent.h"
11 : #include "gmp-storage.h"
12 : #include "mozilla/Unused.h"
13 : #include "mozilla/EndianUtils.h"
14 : #include "nsClassHashtable.h"
15 : #include "prio.h"
16 : #include "mozIGeckoMediaPluginService.h"
17 : #include "nsContentCID.h"
18 : #include "nsServiceManagerUtils.h"
19 : #include "nsISimpleEnumerator.h"
20 :
21 : namespace mozilla {
22 :
23 : #ifdef LOG
24 : #undef LOG
25 : #endif
26 :
27 : extern LogModule* GetGMPLog();
28 :
29 : #define LOGD(msg) MOZ_LOG(GetGMPLog(), mozilla::LogLevel::Debug, msg)
30 : #define LOG(level, msg) MOZ_LOG(GetGMPLog(), (level), msg)
31 :
32 : namespace gmp {
33 :
34 : // We store the records for a given GMP as files in the profile dir.
35 : // $profileDir/gmp/$platform/$gmpName/storage/$nodeId/
36 : static nsresult
37 0 : GetGMPStorageDir(nsIFile** aTempDir,
38 : const nsString& aGMPName,
39 : const nsCString& aNodeId)
40 : {
41 0 : if (NS_WARN_IF(!aTempDir)) {
42 0 : return NS_ERROR_INVALID_ARG;
43 : }
44 :
45 : nsCOMPtr<mozIGeckoMediaPluginChromeService> mps =
46 0 : do_GetService("@mozilla.org/gecko-media-plugin-service;1");
47 0 : if (NS_WARN_IF(!mps)) {
48 0 : return NS_ERROR_FAILURE;
49 : }
50 :
51 0 : nsCOMPtr<nsIFile> tmpFile;
52 0 : nsresult rv = mps->GetStorageDir(getter_AddRefs(tmpFile));
53 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
54 0 : return rv;
55 : }
56 :
57 0 : rv = tmpFile->Append(aGMPName);
58 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
59 0 : return rv;
60 : }
61 :
62 0 : rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
63 0 : if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
64 0 : return rv;
65 : }
66 :
67 0 : rv = tmpFile->AppendNative(NS_LITERAL_CSTRING("storage"));
68 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
69 0 : return rv;
70 : }
71 :
72 0 : rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
73 0 : if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
74 0 : return rv;
75 : }
76 :
77 0 : rv = tmpFile->AppendNative(aNodeId);
78 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
79 0 : return rv;
80 : }
81 :
82 0 : rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
83 0 : if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
84 0 : return rv;
85 : }
86 :
87 0 : tmpFile.forget(aTempDir);
88 :
89 0 : return NS_OK;
90 : }
91 :
92 : // Disk-backed GMP storage. Records are stored in files on disk in
93 : // the profile directory. The record name is a hash of the filename,
94 : // and we resolve hash collisions by just adding 1 to the hash code.
95 : // The format of records on disk is:
96 : // 4 byte, uint32_t $recordNameLength, in little-endian byte order,
97 : // record name (i.e. $recordNameLength bytes, no null terminator)
98 : // record bytes (entire remainder of file)
99 : class GMPDiskStorage : public GMPStorage {
100 : public:
101 0 : explicit GMPDiskStorage(const nsCString& aNodeId,
102 : const nsString& aGMPName)
103 0 : : mNodeId(aNodeId)
104 0 : , mGMPName(aGMPName)
105 : {
106 0 : }
107 :
108 0 : ~GMPDiskStorage() {
109 : // Close all open file handles.
110 0 : for (auto iter = mRecords.ConstIter(); !iter.Done(); iter.Next()) {
111 0 : Record* record = iter.UserData();
112 0 : if (record->mFileDesc) {
113 0 : PR_Close(record->mFileDesc);
114 0 : record->mFileDesc = nullptr;
115 : }
116 : }
117 0 : }
118 :
119 0 : nsresult Init() {
120 : // Build our index of records on disk.
121 0 : nsCOMPtr<nsIFile> storageDir;
122 0 : nsresult rv = GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId);
123 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
124 0 : return NS_ERROR_FAILURE;
125 : }
126 :
127 0 : DirectoryEnumerator iter(storageDir, DirectoryEnumerator::FilesAndDirs);
128 0 : for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) {
129 0 : PRFileDesc* fd = nullptr;
130 0 : if (NS_FAILED(dirEntry->OpenNSPRFileDesc(PR_RDONLY, 0, &fd))) {
131 0 : continue;
132 : }
133 0 : int32_t recordLength = 0;
134 0 : nsCString recordName;
135 0 : nsresult err = ReadRecordMetadata(fd, recordLength, recordName);
136 0 : PR_Close(fd);
137 0 : if (NS_FAILED(err)) {
138 : // File is not a valid storage file. Don't index it. Delete the file,
139 : // to make our indexing faster in future.
140 0 : dirEntry->Remove(false);
141 0 : continue;
142 : }
143 :
144 0 : nsAutoString filename;
145 0 : rv = dirEntry->GetLeafName(filename);
146 0 : if (NS_FAILED(rv)) {
147 0 : continue;
148 : }
149 :
150 0 : mRecords.Put(recordName, new Record(filename, recordName));
151 : }
152 :
153 0 : return NS_OK;
154 : }
155 :
156 0 : GMPErr Open(const nsCString& aRecordName) override
157 : {
158 0 : MOZ_ASSERT(!IsOpen(aRecordName));
159 : nsresult rv;
160 0 : Record* record = nullptr;
161 0 : if (!mRecords.Get(aRecordName, &record)) {
162 : // New file.
163 0 : nsAutoString filename;
164 0 : rv = GetUnusedFilename(aRecordName, filename);
165 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
166 0 : return GMPGenericErr;
167 : }
168 0 : record = new Record(filename, aRecordName);
169 0 : mRecords.Put(aRecordName, record);
170 : }
171 :
172 0 : MOZ_ASSERT(record);
173 0 : if (record->mFileDesc) {
174 0 : NS_WARNING("Tried to open already open record");
175 0 : return GMPRecordInUse;
176 : }
177 :
178 0 : rv = OpenStorageFile(record->mFilename, ReadWrite, &record->mFileDesc);
179 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
180 0 : return GMPGenericErr;
181 : }
182 :
183 0 : MOZ_ASSERT(IsOpen(aRecordName));
184 :
185 0 : return GMPNoErr;
186 : }
187 :
188 0 : bool IsOpen(const nsCString& aRecordName) const override {
189 : // We are open if we have a record indexed, and it has a valid
190 : // file descriptor.
191 0 : const Record* record = mRecords.Get(aRecordName);
192 0 : return record && !!record->mFileDesc;
193 : }
194 :
195 0 : GMPErr Read(const nsCString& aRecordName,
196 : nsTArray<uint8_t>& aOutBytes) override
197 : {
198 0 : if (!IsOpen(aRecordName)) {
199 0 : return GMPClosedErr;
200 : }
201 :
202 0 : Record* record = nullptr;
203 0 : mRecords.Get(aRecordName, &record);
204 0 : MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this.
205 :
206 : // Our error strategy is to report records with invalid contents as
207 : // containing 0 bytes. Zero length records are considered "deleted" by
208 : // the GMPStorage API.
209 0 : aOutBytes.SetLength(0);
210 :
211 0 : int32_t recordLength = 0;
212 0 : nsCString recordName;
213 0 : nsresult err = ReadRecordMetadata(record->mFileDesc,
214 : recordLength,
215 0 : recordName);
216 0 : if (NS_FAILED(err) || recordLength == 0) {
217 : // We failed to read the record metadata. Or the record is 0 length.
218 : // Treat damaged records as empty.
219 : // ReadRecordMetadata() could fail if the GMP opened a new record and
220 : // tried to read it before anything was written to it..
221 0 : return GMPNoErr;
222 : }
223 :
224 0 : if (!aRecordName.Equals(recordName)) {
225 0 : NS_WARNING("Record file contains some other record's contents!");
226 0 : return GMPRecordCorrupted;
227 : }
228 :
229 : // After calling ReadRecordMetadata, we should be ready to read the
230 : // record data.
231 0 : if (PR_Available(record->mFileDesc) != recordLength) {
232 0 : NS_WARNING("Record file length mismatch!");
233 0 : return GMPRecordCorrupted;
234 : }
235 :
236 0 : aOutBytes.SetLength(recordLength);
237 0 : int32_t bytesRead = PR_Read(record->mFileDesc, aOutBytes.Elements(), recordLength);
238 0 : return (bytesRead == recordLength) ? GMPNoErr : GMPRecordCorrupted;
239 : }
240 :
241 0 : GMPErr Write(const nsCString& aRecordName,
242 : const nsTArray<uint8_t>& aBytes) override
243 : {
244 0 : if (!IsOpen(aRecordName)) {
245 0 : return GMPClosedErr;
246 : }
247 :
248 0 : Record* record = nullptr;
249 0 : mRecords.Get(aRecordName, &record);
250 0 : MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this.
251 :
252 : // Write operations overwrite the entire record. So close it now.
253 0 : PR_Close(record->mFileDesc);
254 0 : record->mFileDesc = nullptr;
255 :
256 : // Writing 0 bytes means removing (deleting) the file.
257 0 : if (aBytes.Length() == 0) {
258 0 : nsresult rv = RemoveStorageFile(record->mFilename);
259 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
260 : // Could not delete file -> Continue with trying to erase the contents.
261 : } else {
262 0 : return GMPNoErr;
263 : }
264 : }
265 :
266 : // Write operations overwrite the entire record. So re-open the file
267 : // in truncate mode, to clear its contents.
268 0 : if (NS_FAILED(OpenStorageFile(record->mFilename,
269 : Truncate,
270 : &record->mFileDesc))) {
271 0 : return GMPGenericErr;
272 : }
273 :
274 : // Store the length of the record name followed by the record name
275 : // at the start of the file.
276 0 : int32_t bytesWritten = 0;
277 0 : char buf[sizeof(uint32_t)] = {0};
278 0 : LittleEndian::writeUint32(buf, aRecordName.Length());
279 0 : bytesWritten = PR_Write(record->mFileDesc, buf, MOZ_ARRAY_LENGTH(buf));
280 0 : if (bytesWritten != MOZ_ARRAY_LENGTH(buf)) {
281 0 : NS_WARNING("Failed to write GMPStorage record name length.");
282 0 : return GMPRecordCorrupted;
283 : }
284 0 : bytesWritten = PR_Write(record->mFileDesc,
285 0 : aRecordName.get(),
286 0 : aRecordName.Length());
287 0 : if (bytesWritten != (int32_t)aRecordName.Length()) {
288 0 : NS_WARNING("Failed to write GMPStorage record name.");
289 0 : return GMPRecordCorrupted;
290 : }
291 :
292 0 : bytesWritten = PR_Write(record->mFileDesc, aBytes.Elements(), aBytes.Length());
293 0 : if (bytesWritten != (int32_t)aBytes.Length()) {
294 0 : NS_WARNING("Failed to write GMPStorage record data.");
295 0 : return GMPRecordCorrupted;
296 : }
297 :
298 : // Try to sync the file to disk, so that in the event of a crash,
299 : // the record is less likely to be corrupted.
300 0 : PR_Sync(record->mFileDesc);
301 :
302 0 : return GMPNoErr;
303 : }
304 :
305 0 : void Close(const nsCString& aRecordName) override
306 : {
307 0 : Record* record = nullptr;
308 0 : mRecords.Get(aRecordName, &record);
309 0 : if (record && !!record->mFileDesc) {
310 0 : PR_Close(record->mFileDesc);
311 0 : record->mFileDesc = nullptr;
312 : }
313 0 : MOZ_ASSERT(!IsOpen(aRecordName));
314 0 : }
315 :
316 : private:
317 :
318 : // We store records in a file which is a hash of the record name.
319 : // If there is a hash collision, we just keep adding 1 to the hash
320 : // code, until we find a free slot.
321 0 : nsresult GetUnusedFilename(const nsACString& aRecordName,
322 : nsString& aOutFilename)
323 : {
324 0 : nsCOMPtr<nsIFile> storageDir;
325 0 : nsresult rv = GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId);
326 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
327 0 : return rv;
328 : }
329 :
330 0 : uint64_t recordNameHash = HashString(PromiseFlatCString(aRecordName).get());
331 0 : for (int i = 0; i < 1000000; i++) {
332 0 : nsCOMPtr<nsIFile> f;
333 0 : rv = storageDir->Clone(getter_AddRefs(f));
334 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
335 0 : return rv;
336 : }
337 0 : nsAutoString hashStr;
338 0 : hashStr.AppendInt(recordNameHash);
339 0 : rv = f->Append(hashStr);
340 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
341 0 : return rv;
342 : }
343 0 : bool exists = false;
344 0 : f->Exists(&exists);
345 0 : if (!exists) {
346 : // Filename not in use, we can write into this file.
347 0 : aOutFilename = hashStr;
348 0 : return NS_OK;
349 : } else {
350 : // Hash collision; just increment the hash name and try that again.
351 0 : ++recordNameHash;
352 0 : continue;
353 : }
354 : }
355 : // Somehow, we've managed to completely fail to find a vacant file name.
356 : // Give up.
357 0 : NS_WARNING("GetUnusedFilename had extreme hash collision!");
358 0 : return NS_ERROR_FAILURE;
359 : }
360 :
361 : enum OpenFileMode { ReadWrite, Truncate };
362 :
363 0 : nsresult OpenStorageFile(const nsAString& aFileLeafName,
364 : const OpenFileMode aMode,
365 : PRFileDesc** aOutFD)
366 : {
367 0 : MOZ_ASSERT(aOutFD);
368 :
369 0 : nsCOMPtr<nsIFile> f;
370 0 : nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId);
371 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
372 0 : return rv;
373 : }
374 0 : f->Append(aFileLeafName);
375 :
376 0 : auto mode = PR_RDWR | PR_CREATE_FILE;
377 0 : if (aMode == Truncate) {
378 0 : mode |= PR_TRUNCATE;
379 : }
380 :
381 0 : return f->OpenNSPRFileDesc(mode, PR_IRWXU, aOutFD);
382 : }
383 :
384 0 : nsresult ReadRecordMetadata(PRFileDesc* aFd,
385 : int32_t& aOutRecordLength,
386 : nsACString& aOutRecordName)
387 : {
388 0 : int32_t offset = PR_Seek(aFd, 0, PR_SEEK_END);
389 0 : PR_Seek(aFd, 0, PR_SEEK_SET);
390 :
391 0 : if (offset < 0 || offset > GMP_MAX_RECORD_SIZE) {
392 : // Refuse to read big records, or records where we can't get a length.
393 0 : return NS_ERROR_FAILURE;
394 : }
395 0 : const uint32_t fileLength = static_cast<uint32_t>(offset);
396 :
397 : // At the start of the file the length of the record name is stored in a
398 : // uint32_t (little endian byte order) followed by the record name at the
399 : // start of the file. The record name is not null terminated. The remainder
400 : // of the file is the record's data.
401 :
402 0 : if (fileLength < sizeof(uint32_t)) {
403 : // Record file doesn't have enough contents to store the record name
404 : // length. Fail.
405 0 : return NS_ERROR_FAILURE;
406 : }
407 :
408 : // Read length, and convert to host byte order.
409 0 : uint32_t recordNameLength = 0;
410 0 : char buf[sizeof(recordNameLength)] = { 0 };
411 0 : int32_t bytesRead = PR_Read(aFd, &buf, sizeof(recordNameLength));
412 0 : recordNameLength = LittleEndian::readUint32(buf);
413 0 : if (sizeof(recordNameLength) != bytesRead ||
414 0 : recordNameLength == 0 ||
415 0 : recordNameLength + sizeof(recordNameLength) > fileLength ||
416 : recordNameLength > GMP_MAX_RECORD_NAME_SIZE) {
417 : // Record file has invalid contents. Fail.
418 0 : return NS_ERROR_FAILURE;
419 : }
420 :
421 0 : nsCString recordName;
422 0 : recordName.SetLength(recordNameLength);
423 0 : bytesRead = PR_Read(aFd, recordName.BeginWriting(), recordNameLength);
424 0 : if ((uint32_t)bytesRead != recordNameLength) {
425 : // Read failed.
426 0 : return NS_ERROR_FAILURE;
427 : }
428 :
429 0 : MOZ_ASSERT(fileLength >= sizeof(recordNameLength) + recordNameLength);
430 0 : int32_t recordLength = fileLength - (sizeof(recordNameLength) + recordNameLength);
431 :
432 0 : aOutRecordLength = recordLength;
433 0 : aOutRecordName = recordName;
434 :
435 : // Read cursor should be positioned after the record name, before the record contents.
436 0 : if (PR_Seek(aFd, 0, PR_SEEK_CUR) != (int32_t)(sizeof(recordNameLength) + recordNameLength)) {
437 0 : NS_WARNING("Read cursor mismatch after ReadRecordMetadata()");
438 0 : return NS_ERROR_FAILURE;
439 : }
440 :
441 0 : return NS_OK;
442 : }
443 :
444 0 : nsresult RemoveStorageFile(const nsString& aFilename)
445 : {
446 0 : nsCOMPtr<nsIFile> f;
447 0 : nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId);
448 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
449 0 : return rv;
450 : }
451 0 : rv = f->Append(aFilename);
452 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
453 0 : return rv;
454 : }
455 0 : return f->Remove(/* bool recursive= */ false);
456 : }
457 :
458 : struct Record {
459 0 : Record(const nsAString& aFilename,
460 : const nsACString& aRecordName)
461 0 : : mFilename(aFilename)
462 : , mRecordName(aRecordName)
463 0 : , mFileDesc(0)
464 0 : {}
465 0 : ~Record() {
466 0 : MOZ_ASSERT(!mFileDesc);
467 0 : }
468 : nsString mFilename;
469 : nsCString mRecordName;
470 : PRFileDesc* mFileDesc;
471 : };
472 :
473 : // Hash record name to record data.
474 : nsClassHashtable<nsCStringHashKey, Record> mRecords;
475 : const nsCString mNodeId;
476 : const nsString mGMPName;
477 : };
478 :
479 0 : already_AddRefed<GMPStorage> CreateGMPDiskStorage(const nsCString& aNodeId,
480 : const nsString& aGMPName)
481 : {
482 0 : RefPtr<GMPDiskStorage> storage(new GMPDiskStorage(aNodeId, aGMPName));
483 0 : if (NS_FAILED(storage->Init())) {
484 0 : NS_WARNING("Failed to initialize on disk GMP storage");
485 0 : return nullptr;
486 : }
487 0 : return storage.forget();
488 : }
489 :
490 : } // namespace gmp
491 : } // namespace mozilla
|