/** \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 #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" /** 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( 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 char * search_ini( const char *filename, const char *match ) { 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 = 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 = 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. */ static char *get_filename( char *f ) { 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) return result; } else { const char *home; home = getenv ("HOME"); if (home != NULL) { char *guessed_xdg_home; guessed_xdg_home = 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) return result; } } 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; } if (*end_ptr == ':') len = end_ptr - ptr; else len = end_ptr - ptr + 1; dir = my_malloc (len + 1); if( !dir ) return 0; strncpy (dir, ptr, len); dir[len] = '\0'; result = file_exists( dir, f ); free (dir); if (result) return result; ptr = end_ptr; } return 0; } /** 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 = 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; char *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 = my_malloc( sizeof(regex_t)); stop_re = 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 = 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 ) { return 0; } fd = open( fn, O_RDONLY ); // fprintf( stderr, "%s\n", fn ); if( fd == -1 ) { perror( "open" ); error=1; return 0; } if( stat( fn, &st) ) { perror( "stat" ); error=1; return 0; } contents = my_malloc( st.st_size + 1 ); if( !contents ) { return 0; } if( read( fd, contents, st.st_size ) != st.st_size ) { perror( "read" ); error=1; return 0; } /* Don't need to check exit status of close on read-only file descriptors */ close( fd ); free( fn ); 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; char *launcher; char *end; char *mime_filename; char *launcher_str; char *launcher_filename, *launcher_command_str, *launcher_command; char *launcher_full; mime_filename = get_filename( DESKTOP_DEFAULT ); if( !mime_filename ) return 0; launcher_str = search_ini( mime_filename, mimetype ); free( mime_filename ); 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 = 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++; /* Only use first launcher */ end = strchr( launcher, ';' ); if( end ) *end = '\0'; launcher_full = my_malloc( strlen( launcher) + strlen( APPLICATIONS_DIR)+1 ); if( !launcher_full ) { free( launcher_str ); return 0; } strcpy( launcher_full, APPLICATIONS_DIR ); strcat( launcher_full, launcher ); free( launcher_str ); launcher_filename = get_filename( launcher_full ); free( launcher_full ); launcher_command_str = search_ini( launcher_filename, "Exec" ); if( !launcher_command_str ) { fprintf( stderr, _( "%s: Default launcher '%s' does not specify how to start\n"), MIMEDB, launcher_filename ); free( launcher_filename ); return 0; } free( launcher_filename ); launcher_command = strchr( launcher_command_str, '=' ); launcher_command++; res = my_strdup( launcher_command ); free( 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 = 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 char *get_fullfile( char *file ) { char *fullfile; if( file[0] == '/' ) { fullfile = file; } else { char *cwd = my_getcwd(); if( !cwd ) { error = 1; perror( "getcwd" ); return 0; } int l = strlen(cwd); fullfile = my_malloc( l + strlen(file)+2 ); if( !fullfile ) { free(cwd); return 0; } strcpy( fullfile, cwd ); if( cwd[l-1] != '/' ) strcat(fullfile, "/" ); strcat( fullfile, file ); free(cwd); } return fullfile; } /** Write specified file as an URL */ static void write_url( char *file ) { char *fullfile = get_fullfile( file ); 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( fullfile ); } /** Write specified file */ static void write_file( char *file, int print_path ) { char *fullfile; char *str; if( print_path ) { fullfile = get_fullfile( file ); str = fullfile; } else { fullfile = my_strdup( file ); if( !fullfile ) { return; } str = basename( fullfile ); } 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( 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, array_list_t *files, int fileno ) { char *filter_org=filter; int count=0; int launch_again=0; if( al_get_count( files ) <= fileno ) return; launch_pos=0; for( ;*filter && !error; filter++) { if(*filter == '%') { filter++; switch( *filter ) { case 'u': { launch_again = 1; write_url( (char *)al_get( files, fileno ) ); break; } case 'U': { int i; for( i=0; i