1/*
2 * Copyright 2017-2023, Andrew Lindesay <apl@lindesay.co.nz>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6#include "StorageUtils.h"
7
8#include <errno.h>
9#include <stdlib.h>
10#include <vector>
11#include <algorithm>
12
13#include <Directory.h>
14#include <File.h>
15#include <FindDirectory.h>
16#include <Entry.h>
17#include <String.h>
18
19#include "HaikuDepotConstants.h"
20#include "Logger.h"
21
22#define FILE_TO_STRING_BUFFER_LEN 64
23
24
25static bool sAreWorkingFilesAvailable = true;
26
27
28class PathWithLastAccessTimestamp {
29public:
30	PathWithLastAccessTimestamp(const BPath path, uint64 lastAccessMillisSinceEpoch)
31		:
32		fPath(path),
33		fLastAccessMillisSinceEpoch(lastAccessMillisSinceEpoch)
34	{
35	}
36
37	~PathWithLastAccessTimestamp()
38	{
39	}
40
41	const BPath& Path() const
42	{
43		return fPath;
44	}
45
46	int64 LastAccessMillisSinceEpoch() const
47	{
48		return fLastAccessMillisSinceEpoch;
49	}
50
51	BString String() const
52	{
53		BString result;
54		result.SetToFormat("%s; @ %" B_PRIu64, fPath.Leaf(), fLastAccessMillisSinceEpoch);
55		return result;
56	}
57
58	bool operator<(const PathWithLastAccessTimestamp& other) const
59    {
60    	return fLastAccessMillisSinceEpoch < other.fLastAccessMillisSinceEpoch &&
61    		strcmp(fPath.Path(), other.fPath.Path()) < 0;
62    }
63
64private:
65	BPath fPath;
66	uint64 fLastAccessMillisSinceEpoch;
67};
68
69
70/*static*/ bool
71StorageUtils::AreWorkingFilesAvailable()
72{
73	return sAreWorkingFilesAvailable;
74}
75
76
77/*static*/ void
78StorageUtils::SetWorkingFilesUnavailable()
79{
80	sAreWorkingFilesAvailable = false;
81}
82
83
84/* This method will append the contents of the file at the supplied path to the
85 * string provided.
86 */
87
88/*static*/ status_t
89StorageUtils::AppendToString(const BPath& path, BString& result)
90{
91	BFile file(path.Path(), O_RDONLY);
92	uint8_t buffer[FILE_TO_STRING_BUFFER_LEN];
93	size_t buffer_read;
94
95	while((buffer_read = file.Read(buffer, FILE_TO_STRING_BUFFER_LEN)) > 0)
96		result.Append((char *) buffer, buffer_read);
97
98	return (status_t) buffer_read;
99}
100
101
102/*static*/ status_t
103StorageUtils::AppendToFile(const BString& input, const BPath& path)
104{
105	BFile file(path.Path(), O_WRONLY | O_CREAT | O_APPEND);
106	const char* cstr = input.String();
107	size_t cstrLen = strlen(cstr);
108	return file.WriteExactly(cstr, cstrLen);
109}
110
111
112/*static*/ status_t
113StorageUtils::RemoveWorkingDirectoryContents()
114{
115	BPath path;
116	status_t result = B_OK;
117
118	if (result == B_OK)
119		result = find_directory(B_USER_CACHE_DIRECTORY, &path);
120	if (result == B_OK)
121		result = path.Append(CACHE_DIRECTORY_APP);
122
123	bool exists;
124	bool isDirectory;
125
126	if (result == B_OK)
127		result = ExistsObject(path, &exists, &isDirectory, NULL);
128
129	if (result == B_OK && exists && !isDirectory) {
130		HDERROR("the working directory at [%s] is not a directory",
131			path.Path());
132		result = B_ERROR;
133	}
134
135	if (result == B_OK && exists)
136		result = RemoveDirectoryContents(path);
137
138	return result;
139}
140
141
142/* This method will traverse the directory structure and will remove all of the
143 * files that are present in the directories as well as the directories
144 * themselves.
145 */
146
147status_t
148StorageUtils::RemoveDirectoryContents(BPath& path)
149{
150	BDirectory directory(path.Path());
151	BEntry directoryEntry;
152	status_t result = B_OK;
153
154	while (result == B_OK &&
155		directory.GetNextEntry(&directoryEntry) != B_ENTRY_NOT_FOUND) {
156
157		bool exists = false;
158		bool isDirectory = false;
159		BPath directoryEntryPath;
160
161		result = directoryEntry.GetPath(&directoryEntryPath);
162
163		if (result == B_OK) {
164			result = ExistsObject(directoryEntryPath, &exists, &isDirectory,
165				NULL);
166		}
167
168		if (result == B_OK) {
169			if (isDirectory)
170				RemoveDirectoryContents(directoryEntryPath);
171
172			if (remove(directoryEntryPath.Path()) == 0) {
173				HDDEBUG("did delete contents under [%s]",
174					directoryEntryPath.Path());
175			} else {
176				HDERROR("unable to delete [%s]", directoryEntryPath.Path());
177				result = B_ERROR;
178			}
179		}
180
181	}
182
183	return result;
184}
185
186
187/*! This function will delete all of the files in a directory except for the most
188	recent `countLatestRetained` files.
189*/
190
191/*static*/ status_t
192StorageUtils::RemoveDirectoryContentsRetainingLatestFiles(BPath& path, uint32 countLatestRetained)
193{
194	std::vector<PathWithLastAccessTimestamp> pathAndTimestampses;
195	BDirectory directory(path.Path());
196	BEntry directoryEntry;
197	status_t result = B_OK;
198	struct stat s;
199
200	while (result == B_OK &&
201		directory.GetNextEntry(&directoryEntry) != B_ENTRY_NOT_FOUND) {
202		BPath directoryEntryPath;
203		result = directoryEntry.GetPath(&directoryEntryPath);
204
205		if (result == B_OK) {
206			if (-1 == stat(directoryEntryPath.Path(), &s))
207				result = B_ERROR;
208		}
209
210		if (result == B_OK) {
211			pathAndTimestampses.push_back(PathWithLastAccessTimestamp(
212				directoryEntryPath,
213				(static_cast<uint64>(s.st_atim.tv_sec) * 1000) + (static_cast<uint64>(s.st_atim.tv_nsec) / 1000)));
214		}
215	}
216
217	if (pathAndTimestampses.size() > countLatestRetained) {
218
219		// sort the list with the oldest files first (smallest fLastAccessMillisSinceEpoch)
220		std::sort(pathAndTimestampses.begin(), pathAndTimestampses.end());
221
222		std::vector<PathWithLastAccessTimestamp>::iterator it;
223
224		if (Logger::IsTraceEnabled()) {
225			for (it = pathAndTimestampses.begin(); it != pathAndTimestampses.end(); it++) {
226				PathWithLastAccessTimestamp pathAndTimestamp = *it;
227				HDTRACE("delete candidate [%s]", pathAndTimestamp.String().String());
228			}
229		}
230
231		for (it = pathAndTimestampses.begin(); it != pathAndTimestampses.end() - countLatestRetained; it++) {
232			PathWithLastAccessTimestamp pathAndTimestamp = *it;
233			const char* pathStr = pathAndTimestamp.Path().Path();
234
235			if (remove(pathStr) == 0)
236				HDDEBUG("did delete [%s]", pathStr);
237			else {
238				HDERROR("unable to delete [%s]", pathStr);
239				result = B_ERROR;
240			}
241		}
242	}
243
244	return result;
245}
246
247
248/* This method checks to see if a file object exists at the path specified.  If
249 * something does exist then the value of the 'exists' pointer is set to true.
250 * If the object is a directory then this value is also set to true.
251 */
252
253status_t
254StorageUtils::ExistsObject(const BPath& path,
255	bool* exists,
256	bool* isDirectory,
257	off_t* size)
258{
259	struct stat s;
260
261	if (exists != NULL)
262		*exists = false;
263
264	if (isDirectory != NULL)
265		*isDirectory = false;
266
267	if (size != NULL)
268		*size = 0;
269
270	if (-1 == stat(path.Path(), &s)) {
271		if (ENOENT != errno)
272			 return B_ERROR;
273	} else {
274		if (exists != NULL)
275			*exists = true;
276
277		if (isDirectory != NULL)
278			*isDirectory = S_ISDIR(s.st_mode);
279
280		if (size != NULL)
281			*size = s.st_size;
282	}
283
284	return B_OK;
285}
286
287
288/*! This method will check that it is possible to write to the specified file.
289    This may create the file, write some data to it and then read that data
290    back again to be sure.  This can be used as an effective safety measure as
291    the application starts up in order to ensure that the storage systems are
292    in place for the application to startup.
293
294    It is assumed here that the directory containing the test file exists.
295*/
296
297/*static*/ status_t
298StorageUtils::CheckCanWriteTo(const BPath& path)
299{
300	status_t result = B_OK;
301	bool exists = false;
302	uint8 buffer[16];
303
304	// create some random latin letters into the buffer to write.
305	for (int i = 0; i < 16; i++)
306		buffer[i] = 65 + (abs(rand()) % 26);
307
308	if (result == B_OK)
309		result = ExistsObject(path, &exists, NULL, NULL);
310
311	if (result == B_OK && exists) {
312		HDTRACE("an object exists at the candidate path "
313			"[%s] - it will be deleted", path.Path());
314
315		if (remove(path.Path()) == 0) {
316			HDTRACE("did delete the candidate file [%s]", path.Path());
317		} else {
318			HDERROR("unable to delete the candidate file [%s]", path.Path());
319			result = B_ERROR;
320		}
321	}
322
323	if (result == B_OK) {
324		BFile file(path.Path(), O_WRONLY | O_CREAT);
325		if (file.Write(buffer, 16) != 16) {
326			HDERROR("unable to write test data to candidate file [%s]",
327				path.Path());
328			result = B_ERROR;
329		}
330	}
331
332	if (result == B_OK) {
333		BFile file(path.Path(), O_RDONLY);
334		uint8 readBuffer[16];
335		if (file.Read(readBuffer, 16) != 16) {
336			HDERROR("unable to read test data from candidate file [%s]",
337				path.Path());
338			result = B_ERROR;
339		}
340
341		for (int i = 0; result == B_OK && i < 16; i++) {
342			if (readBuffer[i] != buffer[i]) {
343				HDERROR("mismatched read..write check on candidate file [%s]",
344					path.Path());
345				result = B_ERROR;
346			}
347		}
348	}
349
350	return result;
351}
352
353
354/*! As the application runs it will need to store some files into the local
355    disk system.  This method, given a leafname, will write into the supplied
356    path variable, a final path where this leafname should be stored.
357*/
358
359/*static*/ status_t
360StorageUtils::LocalWorkingFilesPath(const BString leaf, BPath& path,
361	bool failOnCreateDirectory)
362{
363	BPath resultPath;
364	status_t result = B_OK;
365
366	if (result == B_OK)
367		result = find_directory(B_USER_CACHE_DIRECTORY, &resultPath);
368
369	if (result == B_OK)
370		result = resultPath.Append(CACHE_DIRECTORY_APP);
371
372	if (result == B_OK) {
373		if (failOnCreateDirectory)
374			result = create_directory(resultPath.Path(), 0777);
375		else
376			create_directory(resultPath.Path(), 0777);
377	}
378
379	if (result == B_OK)
380		result = resultPath.Append(leaf);
381
382	if (result == B_OK)
383		path.SetTo(resultPath.Path());
384	else {
385		path.Unset();
386		HDERROR("unable to find the user cache file for "
387			"[%s] data; %s", leaf.String(), strerror(result));
388	}
389
390	return result;
391}
392
393
394/*static*/ status_t
395StorageUtils::LocalWorkingDirectoryPath(const BString leaf, BPath& path,
396	bool failOnCreateDirectory)
397{
398	BPath resultPath;
399	status_t result = B_OK;
400
401	if (result == B_OK)
402		result = find_directory(B_USER_CACHE_DIRECTORY, &resultPath);
403
404	if (result == B_OK)
405		result = resultPath.Append(CACHE_DIRECTORY_APP);
406
407	if (result == B_OK)
408		result = resultPath.Append(leaf);
409
410	if (result == B_OK) {
411		if (failOnCreateDirectory)
412			result = create_directory(resultPath.Path(), 0777);
413		else
414			create_directory(resultPath.Path(), 0777);
415	}
416
417	if (result == B_OK)
418		path.SetTo(resultPath.Path());
419	else {
420		path.Unset();
421		HDERROR("unable to find the user cache directory for "
422			"[%s] data; %s", leaf.String(), strerror(result));
423	}
424
425	return result;
426}
427
428
429/*static*/ status_t
430StorageUtils::SwapExtensionOnPath(BPath& path, const char* extension)
431{
432	BPath parent;
433	status_t result = path.GetParent(&parent);
434	if (result == B_OK) {
435		path.SetTo(parent.Path(),
436			SwapExtensionOnPathComponent(path.Leaf(), extension).String());
437	}
438	return result;
439}
440
441
442/*static*/ BString
443StorageUtils::SwapExtensionOnPathComponent(const char* pathComponent,
444	const char* extension)
445{
446	BString result(pathComponent);
447	int32 lastDot = result.FindLast(".");
448	if (lastDot != B_ERROR) {
449		result.Truncate(lastDot);
450	}
451	result.Append(".");
452	result.Append(extension);
453	return result;
454}
455