1/*
2 * Copyright 2020-2021, Andrew Lindesay <apl@lindesay.co.nz>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5#include "PackageIconTarRepository.h"
6
7#include <Autolock.h>
8#include <AutoDeleter.h>
9#include <File.h>
10#include <StopWatch.h>
11
12#include "Logger.h"
13#include "TarArchiveService.h"
14
15
16#define LIMIT_ICON_CACHE 50
17
18
19BitmapRef
20PackageIconTarRepository::sDefaultIcon(new(std::nothrow) SharedBitmap(
21	"application/x-vnd.haiku-package"), true);
22
23
24/*!	An instance of this class can be provided to the TarArchiveService to
25	be called each time a tar entry is found.  This way it is able to capture
26	the offsets of the icons in the tar file against the package names.
27*/
28
29class IconTarPtrEntryListener : public TarEntryListener
30{
31public:
32								IconTarPtrEntryListener(
33									PackageIconTarRepository*
34									fPackageIconTarRepository);
35	virtual						~IconTarPtrEntryListener();
36
37	virtual status_t			Handle(
38									const TarArchiveHeader& header,
39									size_t offset,
40									BDataIO *data);
41private:
42			status_t			_LeafNameToBitmapSize(BString& leafName,
43									BitmapSize* bitmapSize);
44
45private:
46			PackageIconTarRepository*
47								fPackageIconTarRepository;
48};
49
50
51IconTarPtrEntryListener::IconTarPtrEntryListener(
52	PackageIconTarRepository* packageIconTarRepository)
53	:
54	fPackageIconTarRepository(packageIconTarRepository)
55{
56}
57
58
59IconTarPtrEntryListener::~IconTarPtrEntryListener()
60{
61}
62
63
64/*!	The format of the filenames in the archive are of the format;
65	\code
66	hicn/ardino/icon.hvif
67	\endcode
68	The leafname (the last part of the path) determines the type of the file as
69	it could be either in HVIF format or a PNG of various sizes.
70*/
71
72status_t
73IconTarPtrEntryListener::Handle(const TarArchiveHeader& header,
74	size_t offset, BDataIO *data)
75{
76	if (header.FileType() != TAR_FILE_TYPE_NORMAL)
77		return B_OK;
78
79	BString fileName = header.FileName();
80	if (!fileName.StartsWith("hicn/"))
81		return B_OK;
82
83	int32 secondSlashIdx = fileName.FindFirst("/", 5);
84	if (secondSlashIdx == B_ERROR || secondSlashIdx == 5)
85		return B_OK;
86
87	BString packageName;
88	BString leafName;
89	fileName.CopyInto(packageName, 5, secondSlashIdx - 5);
90	fileName.CopyInto(leafName, secondSlashIdx + 1,
91		fileName.Length() - (secondSlashIdx + 1));
92	BitmapSize bitmapSize;
93
94	if (_LeafNameToBitmapSize(leafName, &bitmapSize) == B_OK) {
95		fPackageIconTarRepository->AddIconTarPtr(packageName, bitmapSize,
96			offset);
97	}
98
99	return B_OK;
100}
101
102
103status_t
104IconTarPtrEntryListener::_LeafNameToBitmapSize(BString& leafName,
105	BitmapSize* bitmapSize)
106{
107	if (leafName == "icon.hvif") {
108		*bitmapSize = BITMAP_SIZE_ANY;
109		return B_OK;
110	}
111	if (leafName == "64.png") {
112		*bitmapSize = BITMAP_SIZE_64;
113		return B_OK;
114	}
115	if (leafName == "32.png") {
116		*bitmapSize = BITMAP_SIZE_32;
117		return B_OK;
118	}
119	if (leafName == "16.png") {
120		*bitmapSize = BITMAP_SIZE_16;
121		return B_OK;
122	}
123	return B_BAD_VALUE;
124}
125
126
127PackageIconTarRepository::PackageIconTarRepository()
128	:
129	fTarIo(NULL),
130	fIconCache(LIMIT_ICON_CACHE)
131{
132}
133
134
135PackageIconTarRepository::~PackageIconTarRepository()
136{
137}
138
139
140void
141PackageIconTarRepository::Clear() {
142	BAutolock locker(&fLock);
143	fIconCache.Clear();
144}
145
146
147/*!	This method will reconfigure itself using the data in the tar file supplied.
148	Any existing data will be flushed and the new tar will be scanned for
149	offsets to usable files.
150*/
151
152status_t
153PackageIconTarRepository::Init(BPath& tarPath)
154{
155	BAutolock locker(&fLock);
156	_Close();
157	status_t result = B_OK;
158
159	if (tarPath.Path() == NULL) {
160		HDINFO("empty path to tar-ball");
161		result = B_BAD_VALUE;
162	}
163
164	BFile *tarIo = NULL;
165
166	if (result == B_OK) {
167		HDINFO("will init icon model from tar [%s]", tarPath.Path());
168		tarIo = new BFile(tarPath.Path(), O_RDONLY);
169
170		if (!tarIo->IsReadable()) {
171			HDERROR("unable to read the tar [%s]", tarPath.Path());
172			result = B_IO_ERROR;
173		}
174	}
175
176	// will fill the model up with records from the tar-ball.
177
178	if (result == B_OK) {
179		BStopWatch watch("PackageIconTarRepository::Init", true);
180		HDINFO("will read [%s] and collect the tar pointers", tarPath.Path());
181
182		IconTarPtrEntryListener* listener = new IconTarPtrEntryListener(this);
183		ObjectDeleter<IconTarPtrEntryListener> listenerDeleter(listener);
184		TarArchiveService::ForEachEntry(*tarIo, listener);
185
186		double secs = watch.ElapsedTime() / 1000000.0;
187		HDINFO("did collect %" B_PRIi32 " tar pointers (%6.3g secs)",
188			fIconTarPtrs.Size(), secs);
189	}
190
191	if (result == B_OK)
192		fTarIo = tarIo;
193	else
194		delete tarIo;
195
196	return result;
197}
198
199
200void
201PackageIconTarRepository::_Close()
202{
203	fIconCache.Clear();
204	delete fTarIo;
205	fTarIo = NULL;
206	fIconTarPtrs.Clear();
207}
208
209
210/*!	This method should be treated private and only called from a situation
211	in which the class's lock is acquired.  It is used to populate data from
212	the parsing of the tar headers.  It is called from the listener above.
213*/
214
215void
216PackageIconTarRepository::AddIconTarPtr(const BString& packageName,
217	BitmapSize bitmapSize, off_t offset)
218{
219	IconTarPtrRef tarPtrRef = _GetOrCreateIconTarPtr(packageName);
220	tarPtrRef->SetOffset(bitmapSize, offset);
221}
222
223
224bool
225PackageIconTarRepository::HasAnyIcon(const BString& pkgName)
226{
227	BAutolock locker(&fLock);
228	HashString key(pkgName);
229	return fIconTarPtrs.ContainsKey(key);
230}
231
232
233status_t
234PackageIconTarRepository::GetIcon(const BString& pkgName, BitmapSize size,
235	BitmapRef& bitmap)
236{
237	BAutolock locker(&fLock);
238	status_t result = B_OK;
239	BitmapSize actualSize;
240	off_t iconDataTarOffset = -1;
241	const IconTarPtrRef tarPtrRef = _GetIconTarPtr(pkgName);
242
243	if (tarPtrRef.IsSet()) {
244		iconDataTarOffset = _OffsetToBestRepresentation(tarPtrRef, size,
245			&actualSize);
246	}
247
248	if (iconDataTarOffset < 0)
249		bitmap.SetTo(sDefaultIcon);
250	else {
251		HashString key = _ToIconCacheKey(pkgName, actualSize);
252
253		if (!fIconCache.ContainsKey(key)) {
254			result = _CreateIconFromTarOffset(iconDataTarOffset, bitmap);
255			if (result == B_OK)
256				fIconCache.Put(key, bitmap);
257			else {
258				HDERROR("failure to read image for package [%s] at offset %"
259					B_PRIdOFF, pkgName.String(), iconDataTarOffset);
260				fIconCache.Put(key, sDefaultIcon);
261			}
262		}
263		bitmap.SetTo(fIconCache.Get(key).Get());
264	}
265	return result;
266}
267
268
269IconTarPtrRef
270PackageIconTarRepository::_GetIconTarPtr(const BString& pkgName) const
271{
272	return fIconTarPtrs.Get(HashString(pkgName));
273}
274
275
276const char*
277PackageIconTarRepository::_ToIconCacheKeySuffix(BitmapSize size)
278{
279	switch (size)
280	{
281		case BITMAP_SIZE_16:
282			return "16";
283		// note that size 22 is not supported.
284		case BITMAP_SIZE_32:
285			return "32";
286		case BITMAP_SIZE_64:
287			return "64";
288		case BITMAP_SIZE_ANY:
289			return "any";
290		default:
291			HDFATAL("unsupported bitmap size");
292			break;
293	}
294}
295
296
297const HashString
298PackageIconTarRepository::_ToIconCacheKey(const BString& pkgName,
299	BitmapSize size)
300{
301	return HashString(BString(pkgName) << "__x" << _ToIconCacheKeySuffix(size));
302}
303
304
305status_t
306PackageIconTarRepository::_CreateIconFromTarOffset(off_t offset,
307	BitmapRef& bitmap)
308{
309	fTarIo->Seek(offset, SEEK_SET);
310	TarArchiveHeader header;
311	status_t result = TarArchiveService::GetEntry(*fTarIo, header);
312
313	if (result == B_OK && header.Length() <= 0)
314		result = B_BAD_DATA;
315
316	if (result == B_OK)
317		bitmap.SetTo(new(std::nothrow)SharedBitmap(*fTarIo, header.Length()));
318
319	return result;
320}
321
322
323/*!	If there is a vector representation (HVIF) then this will be the best
324	option.  If there are only bitmap images to choose from then consider what
325	the target size is and choose the best image to match.
326*/
327
328/*static*/ off_t
329PackageIconTarRepository::_OffsetToBestRepresentation(
330	const IconTarPtrRef iconTarPtrRef, BitmapSize desiredSize,
331	BitmapSize* actualSize)
332{
333	if (iconTarPtrRef->HasOffset(BITMAP_SIZE_ANY)) {
334		*actualSize = BITMAP_SIZE_ANY;
335		return iconTarPtrRef->Offset(BITMAP_SIZE_ANY);
336	}
337	if (iconTarPtrRef->HasOffset(BITMAP_SIZE_64)
338			&& desiredSize >= BITMAP_SIZE_64) {
339		*actualSize = BITMAP_SIZE_64;
340		return iconTarPtrRef->Offset(BITMAP_SIZE_64);
341	}
342	if (iconTarPtrRef->HasOffset(BITMAP_SIZE_32)
343			&& desiredSize >= BITMAP_SIZE_32) {
344		*actualSize = BITMAP_SIZE_32;
345		return iconTarPtrRef->Offset(BITMAP_SIZE_32);
346	}
347	if (iconTarPtrRef->HasOffset(BITMAP_SIZE_22)
348			&& desiredSize >= BITMAP_SIZE_22) {
349		*actualSize = BITMAP_SIZE_22;
350		return iconTarPtrRef->Offset(BITMAP_SIZE_22);
351	}
352	if (iconTarPtrRef->HasOffset(BITMAP_SIZE_16)) {
353		*actualSize = BITMAP_SIZE_16;
354		return iconTarPtrRef->Offset(BITMAP_SIZE_16);
355	}
356	return -1;
357}
358
359
360IconTarPtrRef
361PackageIconTarRepository::_GetOrCreateIconTarPtr(const BString& pkgName)
362{
363	BAutolock locker(&fLock);
364	HashString key(pkgName);
365	if (!fIconTarPtrs.ContainsKey(key)) {
366		IconTarPtrRef value(new IconTarPtr(pkgName));
367		fIconTarPtrs.Put(key, value);
368		return value;
369	}
370	return fIconTarPtrs.Get(key);
371}
372
373
374/*static*/ void
375PackageIconTarRepository::CleanupDefaultIcon()
376{
377	sDefaultIcon.Unset();
378}
379