1//----------------------------------------------------------------------
2//  This software is part of the OpenBeOS distribution and is covered
3//  by the OpenBeOS license.
4//---------------------------------------------------------------------
5/*!
6	\file AssociatedTypes.cpp
7	AssociatedTypes class implementation
8*/
9
10#include "AssociatedTypes.h"
11#include "MimeSnifferAddonManager.h"
12
13#include <Directory.h>
14#include <Entry.h>
15#include <Message.h>
16#include <MimeType.h>
17#include <Path.h>
18#include <String.h>
19#include <mime/database_support.h>
20#include <storage_support.h>
21
22#include <new>
23#include <stdio.h>
24
25#define DBG(x) x
26//#define DBG(x)
27#define OUT printf
28
29namespace BPrivate {
30namespace Storage {
31namespace Mime {
32
33/*!
34	\class AssociatedTypes
35	\brief Information about file extensions and their associated types
36*/
37
38// Constructor
39//! Constructs a new AssociatedTypes object
40AssociatedTypes::AssociatedTypes()
41	: fHaveDoneFullBuild(false)
42{
43}
44
45// Destructor
46//! Destroys the AssociatedTypes object
47AssociatedTypes::~AssociatedTypes()
48{
49}
50
51// GetAssociatedTypes
52/*! \brief Returns a list of mime types associated with the given file
53	extension in the pre-allocated \c BMessage pointed to by \c types.
54
55	See \c BMimeType::GetAssociatedTypes() for more information.
56*/
57status_t
58AssociatedTypes::GetAssociatedTypes(const char *extension, BMessage *types)
59{
60	status_t err = extension && types ? B_OK : B_BAD_VALUE;
61	std::string extStr;
62
63	// See if we need to do our initial build still
64	if (!err && !fHaveDoneFullBuild) {
65		err = BuildAssociatedTypesTable();
66	}
67	// Format the extension
68	if (!err) {
69		extStr = PrepExtension(extension);
70		err = extStr.length() > 0 ? B_OK : B_BAD_VALUE;
71	}
72	// Build the message
73	if (!err) {
74		// Clear the message, as we're just going to add to it
75		types->MakeEmpty();
76
77		// Add the types associated with this extension
78		std::set<std::string> &assTypes = fAssociatedTypes[extStr];
79		std::set<std::string>::const_iterator i;
80		for (i = assTypes.begin(); i != assTypes.end() && !err; i++) {
81			err = types->AddString(kTypesField, i->c_str());
82		}
83	}
84	return err;
85}
86
87// GuessMimeType
88/*! \brief Guesses a MIME type for the given filename based on its extension
89
90	\param filename The filename of interest
91	\param result Pointer to a pre-allocated \c BString object into which
92	              the result is stored. If the function returns a value other
93	              than \c B_OK, \a result will not be modified.
94	\return
95	- \c B_OK: success
96	- \c other error code: failure
97*/
98status_t
99AssociatedTypes::GuessMimeType(const char *filename, BString *result)
100{
101	status_t err = filename && result ? B_OK : B_BAD_VALUE;
102	if (!err && !fHaveDoneFullBuild)
103		err = BuildAssociatedTypesTable();
104
105	// if we have an MimeSnifferAddonManager, let's give it a shot first
106	if (!err) {
107		MimeSnifferAddonManager* manager = MimeSnifferAddonManager::Default();
108		if (manager) {
109			BMimeType mimeType;
110			float priority = manager->GuessMimeType(filename, &mimeType);
111			if (priority >= 0) {
112				*result = mimeType.Type();
113				return B_OK;
114			}
115		}
116	}
117
118	if (!err) {
119		// Extract the extension from the file
120		const char *rawExtension = strrchr(filename, '.');
121
122		// If there was an extension, grab it and look up its associated
123		// type(s). Otherwise, the best guess we can offer is
124		// "application/octect-stream"
125		if (rawExtension && rawExtension[1] != '\0') {
126			std::string extension = PrepExtension(rawExtension + 1);
127
128			/*! \todo I'm just grabbing the first item in the set here. Should we perhaps
129				do something different?
130			*/
131			std::set<std::string> &types = fAssociatedTypes[extension];
132			std::set<std::string>::const_iterator i = types.begin();
133			if (i != types.end())
134				result->SetTo(i->c_str());
135			else
136				err = kMimeGuessFailureError;
137		} else {
138			err = kMimeGuessFailureError;
139		}
140	}
141	return err;
142}
143
144// GuessMimeType
145/*! \brief Guesses a MIME type for the given \c entry_ref based on its filename extension
146
147	\param filename The entry_ref of interest
148	\param result Pointer to a pre-allocated \c BString object into which
149	              the result is stored. If the function returns a value other
150	              than \c B_OK, \a result will not be modified.
151	\return
152	- \c B_OK: success
153	- \c other error code: failure
154*/
155status_t
156AssociatedTypes::GuessMimeType(const entry_ref *ref, BString *result)
157{
158	// Convert the entry_ref to a filename and then do the check
159	if (!ref)
160		return B_BAD_VALUE;
161	BPath path;
162	status_t err = path.SetTo(ref);
163	if (!err)
164		err = GuessMimeType(path.Path(), result);
165	return err;
166}
167
168// SetFileExtensions
169/*! \brief Sets the list of file extensions for the given type and
170	updates the associated types mappings.
171
172	All listed extensions will including the given mime type in
173	their list of associated types following this call.
174
175	All extensions previously but no longer associated with this
176	mime type will no longer list this mime type as an associated
177	type.
178
179	\param app The mime type whose associated file extensions you are setting
180	\param types Pointer to a \c BMessage containing an array of associated
181	             file extensions in its \c Mime::kExtensionsField field.
182*/
183status_t
184AssociatedTypes::SetFileExtensions(const char *type, const BMessage *extensions)
185{
186	status_t err = type && extensions ? B_OK : B_BAD_VALUE;
187	if (!fHaveDoneFullBuild)
188		return err;
189
190	std::set<std::string> oldExtensions;
191	std::set<std::string> &newExtensions = fFileExtensions[type];
192	// Make a copy of the previous extensions
193	if (!err)
194		oldExtensions = newExtensions;
195
196	if (!err) {
197		// Read through the list of new extensions, creating the new
198		// file extensions list and adding the type as an associated type
199		// for each extension
200		newExtensions.clear();
201		const char *extension;
202		for (int32 i = 0;
203			   extensions->FindString(kTypesField, i, &extension) == B_OK;
204			     i++)
205		{
206			newExtensions.insert(extension);
207			AddAssociatedType(extension, type);
208		}
209
210		// Remove any extensions that are still associated from the list
211		// of previously associated extensions
212		for (std::set<std::string>::const_iterator i = newExtensions.begin();
213			   i != newExtensions.end();
214			     i++)
215		{
216			oldExtensions.erase(*i);
217		}
218
219		// Now remove the type as an associated type for any of its previously
220		// but no longer associated extensions
221		for (std::set<std::string>::const_iterator i = oldExtensions.begin();
222			   i != oldExtensions.end();
223			     i++)
224		{
225			RemoveAssociatedType(i->c_str(), type);
226		}
227	}
228	return err;
229}
230
231// DeleteFileExtensions
232/*! \brief Clears the given types's file extensions list and removes the
233	types from each of said extensions' associated types list.
234	\param app The mime type whose file extensions you are clearing
235*/
236status_t
237AssociatedTypes::DeleteFileExtensions(const char *type)
238{
239	BMessage extensions;
240	return SetFileExtensions(type, &extensions);
241}
242
243// PrintToStream
244//! Dumps the associated types mapping to standard output
245void
246AssociatedTypes::PrintToStream() const
247{
248	printf("\n");
249	printf("-----------------\n");
250	printf("Associated Types:\n");
251	printf("-----------------\n");
252
253	for (std::map<std::string, std::set<std::string> >::const_iterator i = fAssociatedTypes.begin();
254		   i != fAssociatedTypes.end();
255		     i++)
256	{
257		printf("%s: ", i->first.c_str());
258		fflush(stdout);
259		bool first = true;
260		for (std::set<std::string>::const_iterator type = i->second.begin();
261			   type != i->second.end();
262			     type++)
263		{
264			if (first)
265				first = false;
266			else
267				printf(", ");
268			printf("%s", type->c_str());
269			fflush(stdout);
270		}
271		printf("\n");
272	}
273}
274
275// AddAssociatedType
276/*! \brief Adds the given mime type to the set of associated types
277	for the given extension.
278
279	\param extension The file extension
280	\param type The associated mime type
281	\return
282	- B_OK: success, even if the type was already in the associated types list
283	- "error code": failure
284*/
285status_t
286AssociatedTypes::AddAssociatedType(const char *extension, const char *type)
287{
288	status_t err = extension && type ? B_OK : B_BAD_VALUE;
289	std::string extStr;
290	if (!err) {
291		extStr = PrepExtension(extension);
292		err = extStr.length() > 0 ? B_OK : B_BAD_VALUE;
293	}
294	if (!err)
295		fAssociatedTypes[extStr].insert(type);
296	return err;
297}
298
299// RemoveAssociatedType
300/*! \brief Removes the given mime type from the set of associated types
301	for the given extension.
302
303	\param extension The file extension
304	\param type The associated mime type
305	\return
306	- B_OK: success, even if the type was not found in the associated types list
307	- "error code": failure
308*/
309status_t
310AssociatedTypes::RemoveAssociatedType(const char *extension, const char *type)
311{
312	status_t err = extension && type ? B_OK : B_BAD_VALUE;
313	std::string extStr;
314	if (!err) {
315		extStr = PrepExtension(extension);
316		err = extStr.length() > 0 ? B_OK : B_BAD_VALUE;
317	}
318	if (!err)
319		fAssociatedTypes[extension].erase(type);
320	return err;
321}
322
323// BuildAssociatedTypesTable
324/*! \brief Crawls the mime database and builds a list of associated types
325	for every associated file extension.
326*/
327status_t
328AssociatedTypes::BuildAssociatedTypesTable()
329{
330	fFileExtensions.clear();
331	fAssociatedTypes.clear();
332
333	BDirectory root;
334	status_t err = root.SetTo(get_database_directory().c_str());
335	if (!err) {
336		root.Rewind();
337		while (true) {
338			BEntry entry;
339			err = root.GetNextEntry(&entry);
340			if (err) {
341				// If we've come to the end of list, it's not an error
342				if (err == B_ENTRY_NOT_FOUND)
343					err = B_OK;
344				break;
345			} else {
346				// Check that this entry is both a directory and a valid MIME string
347				char supertype[B_PATH_NAME_LENGTH];
348				if (entry.IsDirectory()
349				      && entry.GetName(supertype) == B_OK
350				         && BMimeType::IsValid(supertype))
351				{
352					// Make sure the supertype string is all lowercase
353					BPrivate::Storage::to_lower(supertype);
354
355					// First, iterate through this supertype directory and process
356					// all of its subtypes
357					BDirectory dir;
358					if (dir.SetTo(&entry) == B_OK) {
359						dir.Rewind();
360						while (true) {
361							BEntry subEntry;
362							err = dir.GetNextEntry(&subEntry);
363							if (err) {
364								// If we've come to the end of list, it's not an error
365								if (err == B_ENTRY_NOT_FOUND)
366									err = B_OK;
367								break;
368							} else {
369								// Get the subtype's name
370								char subtype[B_PATH_NAME_LENGTH];
371								if (subEntry.GetName(subtype) == B_OK) {
372									BPrivate::Storage::to_lower(subtype);
373
374									char fulltype[B_PATH_NAME_LENGTH];
375									sprintf(fulltype, "%s/%s", supertype, subtype);
376
377									// Process the subtype
378									ProcessType(fulltype);
379								}
380							}
381						}
382					} else {
383						DBG(OUT("Mime::AssociatedTypes::BuildAssociatedTypesTable(): "
384						          "Failed opening supertype directory '%s'\n",
385						            supertype));
386					}
387
388					// Second, process the supertype
389					ProcessType(supertype);
390				}
391			}
392		}
393	} else {
394		DBG(OUT("Mime::AssociatedTypes::BuildAssociatedTypesTable(): "
395		          "Failed opening mime database directory '%s'\n",
396		            get_database_directory().c_str()));
397	}
398	if (!err) {
399		fHaveDoneFullBuild = true;
400//		PrintToStream();
401	} else {
402		DBG(OUT("Mime::AssociatedTypes::BuildAssociatedTypesTable() failed, "
403			"error code == 0x%" B_PRIx32 "\n", err));
404	}
405	return err;
406
407}
408
409// ProcessType
410/*! \brief Handles a portion of the initial associated types table construction for
411	the given mime type.
412
413	\note To be called by BuildAssociatedTypesTable() *ONLY*. :-)
414
415	\param type The mime type of interest. The mime string is expected to be valid
416	            and lowercase. Both "supertype" and "supertype/subtype" mime types
417	            are allowed.
418*/
419status_t
420AssociatedTypes::ProcessType(const char *type)
421{
422	status_t err = type ? B_OK : B_BAD_VALUE;
423	if (!err) {
424		// Read in the list of file extension types
425		BMessage msg;
426		if (read_mime_attr_message(type, kFileExtensionsAttr, &msg) == B_OK) {
427			// Iterate through the file extesions, adding them to the list of
428			// file extensions for the mime type and adding the mime type
429			// to the list of associated types for each file extension
430			const char *extension;
431			std::set<std::string> &fileExtensions = fFileExtensions[type];
432			for (int i = 0; msg.FindString(kExtensionsField, i, &extension) == B_OK; i++) {
433				std::string extStr = PrepExtension(extension);
434				if (extStr.length() > 0) {
435					fileExtensions.insert(extStr);
436					AddAssociatedType(extStr.c_str(), type);
437				}
438			}
439		}
440	}
441	return err;
442}
443
444// PrepExtension
445/*! \brief Strips any leading '.' chars from the given extension and
446	forces all chars to lowercase.
447*/
448std::string
449AssociatedTypes::PrepExtension(const char *extension) const
450{
451	if (extension) {
452		uint i = 0;
453		while (extension[i] == '.')
454			i++;
455		return BPrivate::Storage::to_lower(&(extension[i]));
456	} else {
457		return "";	// This shouldn't ever happen, but if it does, an
458		            // empty string is considered an invalid extension
459	}
460}
461
462} // namespace Mime
463} // namespace Storage
464} // namespace BPrivate
465
466