/** \file mimedb.c mimedb is a program for checking the mimetype, description and default action associated with a file or mimetype. It uses the xdgmime library written by the fine folks at freedesktop.org. There does not seem to be any standard way for the user to change the preferred application yet. The first implementation of mimedb used xml_grep to parse the xml file for the mime entry to determine the description. This was abandoned because of the performance implications of parsing xml. The current version only does a simple string search, which is much, much faster but it might fall on it's head. This code is Copyright 2005-2008 Axel Liljencrantz. It is released under the GPL. The xdgmime library is dual licensed under LGPL/artistic license. Read the source code of the library for more information. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_GETOPT_H #include #endif #if HAVE_LIBINTL_H #include #endif #include "xdgmime.h" #include "fallback.h" #include "util.h" #include "print_help.h" typedef std::vector string_list_t; /** Location of the applications .desktop file, relative to a base mime directory */ #define APPLICATIONS_DIR "applications/" /** Location of the mime xml database, relative to a base mime directory */ #define MIME_DIR "mime/" /** Filename suffix for XML files */ #define MIME_SUFFIX ".xml" /** Start tag for langauge-specific comment */ #define START_TAG "" /** End tab for comment */ #define STOP_TAG "" /** File contains cached list of mime actions */ #define DESKTOP_DEFAULT "applications/defaults.list" /** Size for temporary string buffer used to make a regex for language specific descriptions */ #define BUFF_SIZE 1024 /** Program name */ #define MIMEDB "mimedb" /** Getopt short switches for mimedb */ #define GETOPT_STRING "tfimdalhv" /** Error message if system call goes wrong. */ #define ERROR_SYSTEM "%s: Could not execute command \"%s\"\n" /** Exit code if system call goes wrong. */ #define STATUS_ERROR_SYSTEM 1 /** All types of input and output possible */ enum { FILEDATA, FILENAME, MIMETYPE, DESCRIPTION, ACTION, LAUNCH } ; /** Regular expression variable used to find start tag of description */ static regex_t *start_re=0; /** Regular expression variable used to find end tag of description */ static regex_t *stop_re=0; /** Error flag. Non-zero if something bad happened. */ static int error = 0; /** String of characters to send to system() to launch a file */ static char *launch_buff=0; /** Length of the launch_buff buffer */ static int launch_len=0; /** Current position in the launch_buff buffer */ static int launch_pos=0; /** gettext alias */ #ifdef USE_GETTEXT #define _(string) gettext(string) #else #define _(string) (string) #endif /** Call malloc, set error flag and print message on failure */ void *my_malloc( size_t s ) { void *res = malloc( s ); if( !s ) { error=1; fprintf( stderr, _("%s: Out of memory\n"), MIMEDB ); } return res; } /** Duplicate string, set error flag and print message on failure */ char *my_strdup( const char *s ) { char *res = strdup( s ); if( !s ) { error=1; fprintf( stderr, _("%s: Out of memory\n"), MIMEDB ); } return res; } /** Search the file \c filename for the first line starting with \c match, which is returned in a newly allocated string. */ static const char * search_ini( const char *filename, const char *match ) { /* OK to not use CLO_EXEC here because mimedb is single threaded */ FILE *f = fopen( filename, "r" ); char buf[4096]; int len=strlen(match); int done = 0; if(!f ) { perror( "fopen" ); error=1; return 0; } while( !done ) { if( !fgets( buf, 4096, f ) ) { if( !feof( f ) ) { perror( "fgets" ); error=1; } buf[0]=0; done = 1; } else if( strncmp( buf, match, len ) == 0 && buf[len] == '=' ) { done=1; } } fclose( f ); if( buf[0] ) { char *res=strdup(buf); if( res ) { if(res[strlen(res)-1]=='\n' ) res[strlen(res)-1]='\0'; } return res; } else return (char *)0; } /** Test if the specified file exists. If it does not, also try replacing dashes with slashes in \c in. */ static char *file_exists( const char *dir, const char *in ) { int dir_len = strlen( dir ); int need_sep = dir[dir_len - 1] != '/'; char *filename = (char *)my_malloc( dir_len + need_sep + strlen( in ) + 1 ); char *replaceme; struct stat buf; // fprintf( stderr, "Check %s%s\n", dir, in ); if( !filename ) { return 0; } strcpy( filename, dir ); if ( need_sep ) filename[dir_len++] = '/'; strcpy( filename + dir_len, in ); if( !stat( filename, &buf ) ) return filename; free( filename ); /* DOH! File does not exist. But all is not lost. KDE sometimes uses a slash in the name as a directory separator. We try to replace a dash with a slash and try again. */ replaceme = const_cast(strchr( in, '-' )); if( replaceme ) { char *res; *replaceme = '/'; res = file_exists( dir, in ); *replaceme = '-'; return res; } /* OK, no more slashes left. We really are screwed. Nothing to to but admit defeat and go home. */ return 0; } /** Try to find the specified file in any of the possible directories where mime files can be located. This code is shamelessly stolen from xdg_run_command_on_dirs. \param list Full file paths will be appended to this list. \param f The relative filename search for the the data directories. \param all If zero, then stop after the first filename. \return The number of filenames added to the list. */ static int append_filenames( string_list_t &list, const char *f, int all ) { size_t prev_count = list.size(); char *result; const char *xdg_data_home; const char *xdg_data_dirs; const char *ptr; xdg_data_home = getenv ("XDG_DATA_HOME"); if (xdg_data_home) { result = file_exists( xdg_data_home, f ); if (result) { list.push_back(result); if ( !all ) return 1; } } else { const char *home; home = getenv ("HOME"); if (home != NULL) { char *guessed_xdg_home; guessed_xdg_home = (char *)my_malloc (strlen (home) + strlen ("/.local/share") + 1); if( !guessed_xdg_home ) return 0; strcpy (guessed_xdg_home, home); strcat (guessed_xdg_home, "/.local/share"); result = file_exists( guessed_xdg_home, f ); free (guessed_xdg_home); if (result) { list.push_back(result); if ( !all ) return 1; } } } xdg_data_dirs = getenv ("XDG_DATA_DIRS"); if (xdg_data_dirs == NULL) xdg_data_dirs = "/usr/local/share:/usr/share"; ptr = xdg_data_dirs; while (*ptr != '\000') { const char *end_ptr; char *dir; int len; end_ptr = ptr; while (*end_ptr != ':' && *end_ptr != '\000') end_ptr ++; if (end_ptr == ptr) { ptr++; continue; } len = end_ptr - ptr; dir = (char *)my_malloc (len + 1); if( !dir ) return 0; strncpy (dir, ptr, len); dir[len] = '\0'; result = file_exists( dir, f ); free (dir); if (result) { list.push_back(result); if ( !all ) { return 1; } } ptr = end_ptr; } return list.size() - prev_count; } /** Find at most one file relative to the XDG data directories; returns the empty string on failure */ static std::string get_filename( char *f ) { string_list_t list; append_filenames( list, f, 0 ); if (list.empty()) { return ""; } else { return list.back(); } } /** Remove excessive whitespace from string. Replaces arbitrary sequence of whitespace with a single space. Also removes any leading and trailing whitespace */ static char *munge( char *in ) { char *out = (char *)my_malloc( strlen( in )+1 ); char *p=out; int had_whitespace = 0; int printed = 0; if( !out ) { return 0; } while( 1 ) { // fprintf( stderr, "%c\n", *in ); switch( *in ) { case ' ': case '\n': case '\t': case '\r': { had_whitespace = 1; break; } case '\0': *p = '\0'; return out; default: { if( printed && had_whitespace ) { *(p++)=' '; } printed=1; had_whitespace=0; *(p++)=*in; break; } } in++; } fprintf( stderr, _( "%s: Unknown error in munge()\n"), MIMEDB ); error=1; return 0; } /** Return a regular expression that matches all strings specifying the current locale */ static char *get_lang_re() { static char buff[BUFF_SIZE]; const char *lang = setlocale( LC_MESSAGES, 0 ); int close=0; char *out=buff; if( (1+strlen(lang)*4) >= BUFF_SIZE ) { fprintf( stderr, _( "%s: Locale string too long\n"), MIMEDB ); error = 1; return 0; } for( ; *lang; lang++ ) { switch( *lang ) { case '@': case '.': case '_': if( close ) { *out++ = ')'; *out++ = '?'; } close=1; *out++ = '('; *out++ = *lang; break; default: *out++ = *lang; } } if( close ) { *out++ = ')'; *out++ = '?'; } *out++=0; return buff; } /** Get description for a specified mimetype. */ static char *get_description( const char *mimetype ) { char *fn_part; std::string fn; int fd; struct stat st; char *contents; char *start=0, *stop=0, *best_start=0; if( !start_re ) { char *lang; char buff[BUFF_SIZE]; lang = get_lang_re(); if( !lang ) return 0; snprintf( buff, BUFF_SIZE, START_TAG, lang, lang ); start_re = (regex_t *)my_malloc( sizeof(regex_t)); stop_re = (regex_t *)my_malloc( sizeof(regex_t)); int reg_status; if( ( reg_status = regcomp( start_re, buff, REG_EXTENDED ) ) ) { char regerrbuf[BUFF_SIZE]; regerror(reg_status, start_re, regerrbuf, BUFF_SIZE); fprintf( stderr, _( "%s: Could not compile regular expressions %s with error %s\n"), MIMEDB, buff, regerrbuf); error=1; } else if ( ( reg_status = regcomp( stop_re, STOP_TAG, REG_EXTENDED ) ) ) { char regerrbuf[BUFF_SIZE]; regerror(reg_status, stop_re, regerrbuf, BUFF_SIZE); fprintf( stderr, _( "%s: Could not compile regular expressions %s with error %s\n"), MIMEDB, buff, regerrbuf); error=1; } if( error ) { free( start_re ); free( stop_re ); start_re = stop_re = 0; return 0; } } fn_part = (char *)my_malloc( strlen(MIME_DIR) + strlen( mimetype) + strlen(MIME_SUFFIX) + 1 ); if( !fn_part ) { return 0; } strcpy( fn_part, MIME_DIR ); strcat( fn_part, mimetype ); strcat( fn_part, MIME_SUFFIX ); fn = get_filename(fn_part); //malloc( strlen(MIME_DIR) +strlen( MIME_SUFFIX)+ strlen( mimetype ) + 1 ); free(fn_part ); if( fn.empty() ) { return 0; } /* OK to not use CLO_EXEC here because mimedb is single threaded */ fd = open( fn.c_str(), O_RDONLY ); // fprintf( stderr, "%s\n", fn ); if( fd == -1 ) { perror( "open" ); error=1; return 0; } if( stat( fn.c_str(), &st) ) { perror( "stat" ); error=1; return 0; } contents = (char *)my_malloc( st.st_size + 1 ); if( !contents ) { return 0; } if( read( fd, contents, st.st_size ) != st.st_size ) { perror( "read" ); error=1; free((void *)contents); return 0; } /* Don't need to check exit status of close on read-only file descriptors */ close( fd ); contents[st.st_size]=0; regmatch_t match[1]; int w = -1; start=contents; /* On multiple matches, use the longest match, should be a pretty good heuristic for best match... */ while( !regexec(start_re, start, 1, match, 0) ) { int new_w = match[0].rm_eo - match[0].rm_so; start += match[0].rm_eo; if( new_w > w ) { /* New match is for a longer match then the previous match, so we use the new match */ w=new_w; best_start = start; } } if( w != -1 ) { start = best_start; if( !regexec(stop_re, start, 1, match, 0) ) { /* We've found the beginning and the end of a suitable description */ char *res; stop = start + match[0].rm_so; *stop = '\0'; res = munge( start ); free( contents ); return res; } } free( contents ); fprintf( stderr, _( "%s: No description for type %s\n"), MIMEDB, mimetype ); error=1; return 0; } /** Get default action for a specified mimetype. */ static char *get_action( const char *mimetype ) { char *res=0; const char *launcher, *end; string_list_t mime_filenames; const char *launcher_str = NULL; const char *launcher_command_str, *launcher_command; char *launcher_full; if( !append_filenames( mime_filenames, DESKTOP_DEFAULT, 1 ) ) { return 0; } for ( size_t i = 0; i < mime_filenames.size(); i++ ) { launcher_str = search_ini( mime_filenames.at(i).c_str(), mimetype ); if ( launcher_str ) break; } if( !launcher_str ) { /* This type does not have a launcher. Try the supertype! */ // fprintf( stderr, "mimedb: %s does not have launcher, try supertype\n", mimetype ); const char ** parents = xdg_mime_get_mime_parents(mimetype); const char **p; if( parents ) { for( p=parents; *p; p++ ) { char *a = get_action(*p); if( a != 0 ) return a; } } /* Just in case subclassing doesn't work, (It doesn't on Fedora Core 3) we also test some common subclassings. */ if( strncmp( mimetype, "text/plain", 10) != 0 && strncmp( mimetype, "text/", 5 ) == 0 ) return get_action( "text/plain" ); return 0; } // fprintf( stderr, "WOOT %s\n", launcher_str ); launcher = const_cast(strchr( launcher_str, '=' )); if( !launcher ) { fprintf( stderr, _("%s: Could not parse launcher string '%s'\n"), MIMEDB, launcher_str ); error=1; return 0; } /* Skip the = */ launcher++; /* Make one we can change */ std::string mut_launcher = launcher; /* Only use first launcher */ end = strchr( launcher, ';' ); if( end ) mut_launcher.resize(end - launcher); launcher_full = (char *)my_malloc( mut_launcher.size() + strlen( APPLICATIONS_DIR)+1 ); if( !launcher_full ) { free( (void *)launcher_str ); return 0; } strcpy( launcher_full, APPLICATIONS_DIR ); strcat( launcher_full, mut_launcher.c_str() ); free( (void *)launcher_str ); std::string launcher_filename = get_filename( launcher_full ); free( launcher_full ); launcher_command_str = search_ini( launcher_filename.c_str(), "Exec" ); if( !launcher_command_str ) { fprintf( stderr, _( "%s: Default launcher '%s' does not specify how to start\n"), MIMEDB, launcher_filename.c_str() ); return 0; } launcher_command = strchr( launcher_command_str, '=' ); launcher_command++; res = my_strdup( launcher_command ); free( (void *)launcher_command_str ); return res; } /** Helper function for launch. Write the specified byte to the string we will execute */ static void writer( char c ) { if( launch_len == -1 ) return; if( launch_len <= launch_pos ) { int new_len = launch_len?2*launch_len:256; char *new_buff = (char *)realloc( launch_buff, new_len ); if( !new_buff ) { free( launch_buff ); launch_len = -1; error=1; return; } launch_buff = new_buff; launch_len = new_len; } launch_buff[launch_pos++]=c; } /** Write out the specified byte in hex */ static void writer_hex( int num ) { int a, b; a = num /16; b = num %16; writer( a>9?('A'+a-10):('0'+a)); writer( b>9?('A'+b-10):('0'+b)); } /** Return current directory in newly allocated string */ static char *my_getcwd () { size_t size = 100; while (1) { char *buffer = (char *) malloc (size); if (getcwd (buffer, size) == buffer) return buffer; free (buffer); if (errno != ERANGE) return 0; size *= 2; } } /** Return absolute filename of specified file */ static const char *get_fullfile( const char *file ) { const char *fullfile; if( file[0] == '/' ) { fullfile = file; } else { char *cwd = my_getcwd(); if( !cwd ) { error = 1; perror( "getcwd" ); return 0; } int l = strlen(cwd); char *tmp = (char *)my_malloc( l + strlen(file)+2 ); if( !tmp ) { free(cwd); return 0; } strcpy( tmp, cwd ); if( cwd[l-1] != '/' ) strcat(tmp, "/" ); strcat( tmp, file ); free(cwd); fullfile = tmp; } return fullfile; } /** Write specified file as an URL */ static void write_url( const char *file ) { const char *fullfile = get_fullfile( file ); const char *str = fullfile; if( str == 0 ) { launch_len = -1; return; } writer( 'f'); writer( 'i'); writer( 'l'); writer( 'e'); writer( ':'); writer( '/'); writer( '/'); while( *str ) { if( ((*str >= 'a') && (*str <='z')) || ((*str >= 'A') && (*str <='Z')) || ((*str >= '0') && (*str <='9')) || (strchr( "-_.~/",*str) != 0) ) { writer(*str); } else if(strchr( "()?&=",*str) != 0) { writer('\\'); writer(*str); } else { writer( '%' ); writer_hex( (unsigned char)*str ); } str++; } if( fullfile != file ) free( (void *)fullfile ); } /** Write specified file */ static void write_file( const char *file, int print_path ) { const char *fullfile; const char *str; if( print_path ) { fullfile = get_fullfile( file ); str = fullfile; } else { char *tmp = my_strdup( file ); if( !tmp ) { return; } str = basename( tmp ); fullfile = tmp; } if( !str ) { error = 1; return; } while( *str ) { switch(*str ) { case ')': case '(': case '-': case '#': case '$': case '}': case '{': case ']': case '[': case '*': case '?': case ' ': case '|': case '<': case '>': case '^': case '&': case '\\': case '`': case '\'': case '\"': writer('\\'); writer(*str); break; case '\n': writer('\\'); writer('n'); break; case '\r': writer('\\'); writer('r'); break; case '\t': writer('\\'); writer('t'); break; case '\b': writer('\\'); writer('b'); break; case '\v': writer('\\'); writer('v'); break; default: writer(*str); break; } str++; } if( fullfile != file ) free( (void *)fullfile ); } /** Use the specified launch filter to launch all the files in the specified list. \param filter the action to take \param files the list of files for which to perform the action \param fileno an internal value. Should always be set to zero. */ static void launch( char *filter, const string_list_t &files, int fileno ) { char *filter_org=filter; int count=0; int launch_again=0; if( (int)files.size() <= fileno ) return; launch_pos=0; for( ;*filter && !error; filter++) { if(*filter == '%') { filter++; switch( *filter ) { case 'u': { launch_again = 1; write_url( files.at(fileno).c_str() ); break; } case 'U': { for( size_t i=0; i launch_hash_t; launch_hash_t launch_hash; locale_init(); /* Parse options */ while( 1 ) { static struct option long_options[] = { { "input-file-data", no_argument, 0, 't' } , { "input-filename", no_argument, 0, 'f' } , { "input-mime", no_argument, 0, 'i' } , { "output-mime", no_argument, 0, 'm' } , { "output-description", no_argument, 0, 'd' } , { "output-action", no_argument, 0, 'a' } , { "help", no_argument, 0, 'h' } , { "version", no_argument, 0, 'v' } , { "launch", no_argument, 0, 'l' } , { 0, 0, 0, 0 } } ; int opt_index = 0; int opt = getopt_long( argc, argv, GETOPT_STRING, long_options, &opt_index ); if( opt == -1 ) break; switch( opt ) { case 0: break; case 't': input_type=FILEDATA; break; case 'f': input_type=FILENAME; break; case 'i': input_type=MIMETYPE; break; case 'm': output_type=MIMETYPE; break; case 'd': output_type=DESCRIPTION; break; case 'a': output_type=ACTION; break; case 'l': output_type=LAUNCH; break; case 'h': print_help( argv[0], 1 ); exit(0); case 'v': printf( _("%s, version %s\n"), MIMEDB, PACKAGE_VERSION ); exit( 0 ); case '?': return 1; } } if( ( output_type == LAUNCH )&&(input_type==MIMETYPE)) { fprintf( stderr, _("%s: Can not launch a mimetype\n"), MIMEDB ); print_help( argv[0], 2 ); exit(1); } /* Loop over all non option arguments and do the specified lookup */ //fprintf( stderr, "Input %d, output %d\n", input_type, output_type ); for (i = optind; (i < argc)&&(!error); i++) { /* Convert from filename to mimetype, if needed */ if( input_type == FILENAME ) { mimetype = xdg_mime_get_mime_type_from_file_name(argv[i]); } else if( input_type == FILEDATA ) { mimetype = xdg_mime_get_mime_type_for_file(argv[i]); } else mimetype = xdg_mime_is_valid_mime_type(argv[i])?argv[i]:0; mimetype = xdg_mime_unalias_mime_type (mimetype); if( !mimetype ) { fprintf( stderr, _( "%s: Could not parse mimetype from argument '%s'\n"), MIMEDB, argv[i] ); error=1; return 1; } /* Convert from mimetype to whatever, if needed */ switch( output_type ) { case MIMETYPE: { output = (char *)mimetype; break; } case DESCRIPTION: { output = get_description( mimetype ); if( !output ) output = strdup( _("Unknown") ); break; } case ACTION: { output = get_action( mimetype ); break; } case LAUNCH: { /* There may be more files using the same launcher, we add them all up in little array_list_ts and launched them together after all the arguments have been parsed. */ output = 0; string_list_t &l = launch_hash[mimetype]; l.push_back(argv[i]); } } /* Print the glorious result */ if( output ) { printf( "%s\n", output ); if( output != mimetype ) free( output ); } output = 0; } /* Perform the actual launching */ if( output_type == LAUNCH && !error ) { for( launch_hash_t::iterator iter = launch_hash.begin(); iter != launch_hash.end(); ++iter) { const char *mimetype = iter->first.c_str(); string_list_t &files = iter->second; char *launcher = get_action( mimetype ); if( launcher ) { launch( launcher, files, 0 ); free( launcher ); } } } if( launch_buff ) free( launch_buff ); if( start_re ) { regfree( start_re ); regfree( stop_re ); free( start_re ); free( stop_re ); } xdg_mime_shutdown(); return error; }