1/*
2 * Copyright 2015, TigerKid001.
3 * Copyright 2020-2022, Andrew Lindesay <apl@lindesay.co.nz>
4 * All rights reserved. Distributed under the terms of the MIT License.
5 */
6
7#include "PackageContentsView.h"
8
9#include <algorithm>
10#include <stdio.h>
11
12#include <Autolock.h>
13#include <Catalog.h>
14#include <FindDirectory.h>
15#include <LayoutBuilder.h>
16#include <LayoutUtils.h>
17#include <OutlineListView.h>
18#include <Path.h>
19#include <ScrollBar.h>
20#include <ScrollView.h>
21#include <StringFormat.h>
22#include <StringItem.h>
23
24#include "GeneralContentScrollView.h"
25#include "Logger.h"
26#include "PackageUtils.h"
27
28#include <package/PackageDefs.h>
29#include <package/hpkg/NoErrorOutput.h>
30#include <package/hpkg/PackageContentHandler.h>
31#include <package/hpkg/PackageEntry.h>
32#include <package/hpkg/PackageReader.h>
33
34using namespace BPackageKit;
35
36using BPackageKit::BHPKG::BNoErrorOutput;
37using BPackageKit::BHPKG::BPackageContentHandler;
38using BPackageKit::BHPKG::BPackageEntry;
39using BPackageKit::BHPKG::BPackageEntryAttribute;
40using BPackageKit::BHPKG::BPackageInfoAttributeValue;
41using BPackageKit::BHPKG::BPackageReader;
42
43#undef B_TRANSLATION_CONTEXT
44#define B_TRANSLATION_CONTEXT "PackageContentsView"
45
46
47// #pragma mark - PackageEntryItem
48
49
50class PackageEntryItem : public BStringItem {
51public:
52	PackageEntryItem(const BPackageEntry* entry, const BString& path)
53		:
54		BStringItem(entry->Name()),
55		fPath(path)
56	{
57		if (fPath.Length() > 0)
58			fPath.Append("/");
59		fPath.Append(entry->Name());
60	}
61
62	inline const BString& EntryPath() const
63	{
64		return fPath;
65	}
66
67private:
68	BString fPath;
69};
70
71
72// #pragma mark - PackageContentOutliner
73
74
75class PackageContentOutliner : public BPackageContentHandler {
76public:
77	PackageContentOutliner(BOutlineListView* listView,
78			const PackageInfo* packageInfo,
79			BLocker& packageLock, PackageInfoRef& packageInfoRef)
80		:
81		fListView(listView),
82		fLastParentEntry(NULL),
83		fLastParentItem(NULL),
84		fLastEntry(NULL),
85		fLastItem(NULL),
86
87		fPackageInfoToPopulate(packageInfo),
88		fPackageLock(packageLock),
89		fPackageInfoRef(packageInfoRef)
90	{
91	}
92
93	virtual status_t HandleEntry(BPackageEntry* entry)
94	{
95		if (fListView->LockLooperWithTimeout(1000000) != B_OK)
96			return B_ERROR;
97
98		// Check if we are still supposed to popuplate the list
99		if (fPackageInfoRef.Get() != fPackageInfoToPopulate) {
100			fListView->UnlockLooper();
101			return B_ERROR;
102		}
103
104		BString path;
105		const BPackageEntry* parent = entry->Parent();
106		while (parent != NULL) {
107			if (path.Length() > 0)
108				path.Prepend("/");
109			path.Prepend(parent->Name());
110			parent = parent->Parent();
111		}
112
113		PackageEntryItem* item = new PackageEntryItem(entry, path);
114
115		if (entry->Parent() == NULL) {
116			fListView->AddItem(item);
117			fLastParentEntry = NULL;
118			fLastParentItem = NULL;
119		} else if (entry->Parent() == fLastEntry) {
120			fListView->AddUnder(item, fLastItem);
121			fLastParentEntry = fLastEntry;
122			fLastParentItem = fLastItem;
123		} else if (entry->Parent() == fLastParentEntry) {
124			fListView->AddUnder(item, fLastParentItem);
125		} else {
126			// Not the last parent entry, need to search for the parent
127			// among the already added list items.
128			bool foundParent = false;
129			for (int32 i = 0; i < fListView->FullListCountItems(); i++) {
130				PackageEntryItem* listItem
131					= dynamic_cast<PackageEntryItem*>(
132						fListView->FullListItemAt(i));
133				if (listItem == NULL)
134					continue;
135				if (listItem->EntryPath() == path) {
136					fLastParentEntry = entry->Parent();
137					fLastParentItem = listItem;
138					fListView->AddUnder(item, listItem);
139					foundParent = true;
140					break;
141				}
142			}
143			if (!foundParent) {
144				// NOTE: Should not happen. Just add this entry at the
145				// root level.
146				fListView->AddItem(item);
147				fLastParentEntry = NULL;
148				fLastParentItem = NULL;
149			}
150		}
151
152		fLastEntry = entry;
153		fLastItem = item;
154
155		fListView->UnlockLooper();
156
157		return B_OK;
158	}
159
160	virtual status_t HandleEntryAttribute(BPackageEntry* entry,
161		BPackageEntryAttribute* attribute)
162	{
163		return B_OK;
164	}
165
166	virtual status_t HandleEntryDone(BPackageEntry* entry)
167	{
168		return B_OK;
169	}
170
171	virtual status_t HandlePackageAttribute(
172		const BPackageInfoAttributeValue& value)
173	{
174		return B_OK;
175	}
176
177	virtual void HandleErrorOccurred()
178	{
179	}
180
181private:
182	BOutlineListView*		fListView;
183
184	const BPackageEntry*	fLastParentEntry;
185	PackageEntryItem*		fLastParentItem;
186
187	const BPackageEntry*	fLastEntry;
188	PackageEntryItem*		fLastItem;
189
190	const PackageInfo*		fPackageInfoToPopulate;
191	BLocker&				fPackageLock;
192	PackageInfoRef&			fPackageInfoRef;
193};
194
195
196// #pragma mark - PackageContentView
197
198
199PackageContentsView::PackageContentsView(const char* name)
200	:
201	BView("package_contents_view", B_WILL_DRAW),
202	fPackageLock("package contents populator lock"),
203	fLastPackageState(NONE)
204{
205	fContentListView = new BOutlineListView("content list view",
206		B_SINGLE_SELECTION_LIST);
207
208	BScrollView* scrollView = new GeneralContentScrollView(
209		"contents scroll view", fContentListView);
210
211	BLayoutBuilder::Group<>(this)
212		.Add(scrollView, 1.0f)
213		.SetInsets(0.0f, -1.0f, -1.0f, -1.0f)
214	;
215
216	_InitContentPopulator();
217}
218
219
220PackageContentsView::~PackageContentsView()
221{
222	Clear();
223
224	delete_sem(fContentPopulatorSem);
225	if (fContentPopulator >= 0)
226		wait_for_thread(fContentPopulator, NULL);
227}
228
229
230void
231PackageContentsView::AttachedToWindow()
232{
233	BView::AttachedToWindow();
234}
235
236
237void
238PackageContentsView::AllAttached()
239{
240	BView::AllAttached();
241}
242
243
244void
245PackageContentsView::SetPackage(const PackageInfoRef& package)
246{
247	// When getting a ref to the same package, don't return when the
248	// package state has changed, since in that case, we may now be able
249	// to read contents where we previously could not. (For example, the
250	// package has been installed.)
251	if (fPackage == package
252		&& (!package.IsSet() || package->State() == fLastPackageState)) {
253		return;
254	}
255
256	Clear();
257
258	{
259		BAutolock lock(&fPackageLock);
260		fPackage = package;
261		fLastPackageState = package.IsSet() ? package->State() : NONE;
262	}
263
264	// if the package is not installed and is not a local file on disk then
265	// there is no point in attempting to populate data for it.
266
267	if (package.IsSet()
268			&& (package->State() == ACTIVATED || package->IsLocalFile())) {
269		release_sem_etc(fContentPopulatorSem, 1, 0);
270	}
271}
272
273
274void
275PackageContentsView::Clear()
276{
277	{
278		BAutolock lock(&fPackageLock);
279		fPackage.Unset();
280	}
281
282	fContentListView->MakeEmpty();
283}
284
285
286// #pragma mark - private
287
288
289void
290PackageContentsView::_InitContentPopulator()
291{
292	fContentPopulatorSem = create_sem(0, "PopulatePackageContents");
293	if (fContentPopulatorSem >= 0) {
294		fContentPopulator = spawn_thread(&_ContentPopulatorThread,
295			"Package Contents Populator", B_NORMAL_PRIORITY, this);
296		if (fContentPopulator >= 0)
297			resume_thread(fContentPopulator);
298	} else
299		fContentPopulator = -1;
300}
301
302
303/*static*/ int32
304PackageContentsView::_ContentPopulatorThread(void* arg)
305{
306	PackageContentsView* view = reinterpret_cast<PackageContentsView*>(arg);
307
308	while (acquire_sem(view->fContentPopulatorSem) == B_OK) {
309		PackageInfoRef package;
310		{
311			BAutolock lock(&view->fPackageLock);
312			package = view->fPackage;
313		}
314
315		if (package.IsSet()) {
316			if (!view->_PopulatePackageContents(*package.Get())) {
317				if (view->LockLooperWithTimeout(1000000) == B_OK) {
318					view->fContentListView->AddItem(
319						new BStringItem(B_TRANSLATE("<Package contents not "
320							"available for remote packages>")));
321					view->UnlockLooper();
322				}
323			}
324		}
325	}
326
327	return 0;
328}
329
330
331bool
332PackageContentsView::_PopulatePackageContents(const PackageInfo& package)
333{
334	BPath packagePath;
335
336	if (PackageUtils::DeriveLocalFilePath(&package, packagePath) != B_OK) {
337		HDDEBUG("unable to obtain local file path");
338		return false;
339	}
340
341	// Setup a BPackageReader
342	BNoErrorOutput errorOutput;
343	BPackageReader reader(&errorOutput);
344
345	status_t status = reader.Init(packagePath.Path());
346	if (status != B_OK) {
347		HDINFO("PackageContentsView::_PopulatePackageContents(): "
348			"failed to init BPackageReader(%s): %s",
349			packagePath.Path(), strerror(status));
350		return false;
351	}
352
353	// Scan package contents and populate list
354	PackageContentOutliner contentHandler(fContentListView, &package,
355		fPackageLock, fPackage);
356	status = reader.ParseContent(&contentHandler);
357	if (status != B_OK) {
358		HDINFO("PackageContentsView::_PopulatePackageContents(): "
359			"failed parse package contents: %s", strerror(status));
360		// NOTE: Do not return false, since it taken to mean this
361		// is a remote package, but is it not, we simply want to stop
362		// populating the contents early.
363	}
364	return true;
365}
366