1/*
2   uniconv - convert volume encodings
3   Copyright (C) Bjoern Fernhomberg 2004
4
5   This program is free software; you can redistribute it and/or modify
6   it under the terms of the GNU General Public License as published by
7   the Free Software Foundation; either version 2 of the License, or
8   (at your option) any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18*/
19
20
21#ifdef HAVE_CONFIG_H
22#include "config.h"
23#endif /* HAVE_CONFIG_H */
24#include <errno.h>
25#include <stdlib.h>
26#include <stdio.h>
27#include <fcntl.h>
28#include <unistd.h>
29#include <string.h>
30#include <sys/mman.h>
31#include <sys/stat.h>
32#include <sys/time.h>
33#include <ctype.h>
34#include <sys/types.h>
35#include <sys/param.h>
36#include <pwd.h>
37#include <dirent.h>
38#include <atalk/afp.h>
39#include <atalk/unicode.h>
40#include <atalk/util.h>
41#include <atalk/logger.h>
42
43#include "atalk/cnid.h"
44#ifndef MAXPATHLEN
45#define MAXPATHLEN 4096
46#endif
47
48static struct _cnid_db *cdb;
49static char curpath[MAXPATHLEN];
50static cnid_t cdir_id;
51static char db_stamp[ADEDLEN_PRIVSYN];
52
53static charset_t ch_from;
54char* from_charset;
55static charset_t ch_to;
56char* to_charset;
57static charset_t ch_mac;
58char* mac_charset;
59static int usedots = 0;
60static u_int16_t conv_flags = 0;
61static int dry_run = 0;
62static int verbose=0;
63char *cnid_type;
64
65char             Cnid_srv[256] = "localhost";
66int              Cnid_port = 4700;
67
68extern  struct charset_functions charset_iso8859_adapted;
69
70#ifndef MAX
71#define MAX(a,b) (((a)>(b))?(a):(b))
72#endif
73
74
75#define VETO "./../.AppleDB/.AppleDouble/.AppleDesktop/.Parent/"
76
77static int veto(const char *path)
78{
79    int i,j;
80    char *veto_str = VETO;
81
82
83    if ((path == NULL))
84        return 0;
85
86    for(i=0, j=0; veto_str[i] != '\0'; i++) {
87        if (veto_str[i] == '/') {
88            if ((j>0) && (path[j] == '\0'))
89                return 1;
90            j = 0;
91        } else {
92            if (veto_str[i] != path[j]) {
93                while ((veto_str[i] != '/')
94                        && (veto_str[i] != '\0'))
95                    i++;
96                j = 0;
97                continue;
98            }
99            j++;
100        }
101    }
102    return 0;
103}
104
105
106static int do_rename( char* src, char *dst, struct stat *st)
107{
108    char        adsrc[ MAXPATHLEN + 1];
109    struct      stat tmp_st;
110
111    if (!stat(dst, &tmp_st)) {
112	fprintf (stderr, "error: cannot rename %s to %s, destination exists\n", src, dst);
113	return -1;
114    }
115
116    if ( rename( src, dst ) < 0 ) {
117	fprintf (stderr, "error: cannot rename %s to %s, %s\n", src, dst, strerror(errno));
118	return -1;
119    }
120
121    if (S_ISDIR(st->st_mode))
122	return 0;
123
124    strcpy( adsrc, ad_path( src, 0 ));
125
126    if (rename( adsrc, ad_path( dst, 0 )) < 0 ) {
127        struct stat ad_st;
128
129        if (errno == ENOENT) {
130            if (stat(adsrc, &ad_st)) /* source has no ressource fork, */
131		return 0;
132	}
133	else {
134		fprintf (stderr, "failed to rename resource fork, error: %s\n", strerror(errno));
135                return -1;
136 	}
137
138    }
139    return 0;
140}
141
142
143static char *convert_name(char *name, struct stat *st, cnid_t cur_did)
144{
145	static char   buffer[MAXPATHLEN +2];  /* for convert_charset dest_len parameter +2 */
146	size_t outlen = 0;
147	unsigned char *p,*q;
148	int require_conversion = 0;
149    u_int16_t    flags = conv_flags;
150	cnid_t id;
151
152	p = (unsigned char *)name;
153	q = (unsigned char *)buffer;
154
155	/* optimize for ascii case */
156	while (*p != 0) {
157		if ( *p >= 0x80 || *p == ':') {
158			require_conversion = 1;
159			break;
160		}
161		p++;
162	}
163
164	if (!require_conversion) {
165		if (verbose > 1)
166		    fprintf(stdout, "no conversion required\n");
167		return name;
168	}
169
170	/* convert charsets */
171	q=(unsigned char *)buffer;
172	p=(unsigned char *)name;
173
174	outlen = convert_charset(ch_from, ch_to, ch_mac, (char *)p, strlen((char *)p), (char *)q, sizeof(buffer) -2, &flags);
175	if ((size_t)-1 == outlen) {
176  	   if ( ch_to == CH_UTF8) {
177		/* maybe name is already in UTF8? */
178		flags = conv_flags;
179		q = (unsigned char *)buffer;
180		p = (unsigned char *)name;
181		outlen = convert_charset(ch_to, ch_to, ch_mac, (char *)p, strlen((char *)p), (char *)q, sizeof(buffer) -2, &flags);
182		if ((size_t)-1 == outlen) {
183			/* it's not UTF8... */
184        		fprintf(stderr, "ERROR: conversion from '%s' to '%s' for '%s' in DID %u failed!!!\n",
185                  		from_charset, to_charset, name, ntohl(cur_did));
186			return name;
187		}
188
189		if (!strcmp(buffer, name)) {
190	   		return name;
191		}
192           }
193 	   fprintf(stderr, "ERROR: conversion from '%s' to '%s' for '%s' in DID %u failed. Please check this!\n",
194                  	from_charset, to_charset, name, ntohl(cur_did));
195	   return name;
196	}
197
198	if (strcmp (name, buffer)) {
199	    if (dry_run) {
200    		fprintf(stdout, "dry_run: would rename %s to %s.\n", name, buffer);
201	    }
202	    else if (!do_rename(name, buffer, st)) {
203		if (CNID_INVALID != (id = cnid_add(cdb, st, cur_did, buffer, strlen(buffer), 0)))
204       	    		fprintf(stdout, "converted '%s' to '%s' (ID %u, DID %u).\n",
205                                name, buffer, ntohl(id), ntohl(cur_did));
206	    }
207	}
208	else if (verbose > 1)
209	    fprintf(stdout, "no conversion required\n");
210
211	return (buffer);
212}
213
214static int check_dirent(char** name, cnid_t cur_did)
215{
216	struct stat st;
217	int ret = 0;
218
219	if (veto(*name))
220		return 0;
221
222        if (stat(*name, &st) != 0) {
223        	switch (errno) {
224                    case ELOOP:
225                    case ENOENT:
226			return 0;
227                    default:
228			return (-1);
229                }
230        }
231
232	if (S_ISDIR(st.st_mode)){
233		ret = 1;
234	}
235
236	if (verbose > 1)
237	    fprintf(stdout, "Checking: '%s' - ", *name);
238
239	*name = convert_name(*name, &st, cur_did);
240
241	return ret;
242}
243
244static int check_adouble(DIR *curdir, char * path _U_)
245{
246	DIR *adouble;
247	struct dirent* entry;
248	struct dirent* ad_entry;
249	int found = 0;
250
251	strlcat(curpath, "/", sizeof(curpath));
252	strlcat(curpath, ".AppleDouble", sizeof(curpath));
253
254	if (NULL == (adouble = opendir(curpath))) {
255		return(-1);
256	}
257
258	while (NULL != (ad_entry=readdir(adouble)) ) {
259		if (veto(ad_entry->d_name))
260			break;
261		found = 0;
262		rewinddir(curdir);
263		while (NULL != (entry=readdir(curdir)) ) {
264			if (!strcmp(ad_entry->d_name, entry->d_name)) {
265				found = 1;
266				break;
267			}
268		}
269		if (!found) {
270			fprintf (stderr, "found orphaned resource file %s", ad_entry->d_name);
271		}
272	}
273
274	rewinddir(curdir);
275	closedir(adouble);
276	return (0);
277}
278
279static cnid_t add_dir_db(char *name, cnid_t cur_did)
280{
281	cnid_t id, did;
282	struct stat st;
283
284	if (CNID_INVALID != ( id = cnid_get(cdb, cur_did, name, strlen(name))) )
285	    return id;
286
287	if (dry_run) {
288		return 0;
289	}
290
291        did = cur_did;
292	if (stat(name, &st)) {
293             fprintf( stderr, "dir '%s' cannot be stat'ed, error %u\n", name, errno);
294	     return 0;
295        }
296
297        id = cnid_add(cdb, &st, did, name, strlen(name), 0);
298
299	fprintf (stderr, "added '%s' to DID %u as %u\n", name, ntohl(did), ntohl(id));
300	return id;
301}
302
303static int getdir(DIR *curdir, char ***names)
304{
305	struct dirent* entry;
306	char **tmp = NULL, **new=NULL;
307	char *name;
308	int i=0;
309
310        while ((entry=readdir(curdir)) != NULL) {
311		new = (char **) realloc (tmp, (i+1) * sizeof(char*));
312		if (new == NULL) {
313			fprintf(stderr, "out of memory");
314			exit (-1);
315		}
316		tmp = new;
317		name = strdup(entry->d_name);
318		if (name == NULL) {
319			fprintf(stderr, "out of memory");
320			exit (-1);
321		}
322		tmp[i]= (char*) name;
323		i++;
324        };
325
326	*names = tmp;
327	return i;
328}
329
330static int checkdir(DIR *curdir, char *path, cnid_t cur_did)
331{
332	DIR* cdir;
333	int ret = 0;
334	cnid_t id;
335	char *name, *tmp;
336        int n;
337	size_t len=strlen(curpath);
338
339	char **names;
340
341	chdir(path);
342
343	check_adouble(curdir, path);
344	curpath[len] = 0;
345
346	if (verbose)
347	    fprintf( stdout, "\nchecking DIR '%s' with ID %u\n", path, ntohl(cur_did));
348
349	n = getdir(curdir, &names);
350
351	while (n--) {
352		name = names[n];
353		tmp = strdup(name);
354                ret = check_dirent(&name, cur_did);
355		if (ret==1) {
356                    id = add_dir_db(name, cur_did);
357		    if ( id == 0 && !dry_run )
358			continue;  /* skip, no ID */
359		    if ( dry_run )
360			name = tmp;
361                    strlcat(curpath, "/", sizeof(curpath));
362                    strlcat(curpath, name, sizeof(curpath));
363                    cdir = opendir(curpath);
364		    if (cdir == NULL) {
365	    	 	fprintf( stderr, "ERROR: cannot open DIR '%s' with ID %u\n", curpath, ntohl(cur_did));
366			continue;
367		    }
368                    checkdir(cdir, curpath, id);
369                    closedir(cdir);
370                    curpath[len] = 0;
371                    chdir(path);
372		    if (verbose)
373	    	 	fprintf( stdout, "returned to DIR '%s' with ID %u\n", path, ntohl(cur_did));
374                }
375                free(names[n]);
376		free(tmp);
377        }
378	free(names);
379	if (verbose)
380	    fprintf( stdout, "leaving DIR '%s' with ID %u\n\n", path, ntohl(cur_did));
381
382	return 0;
383}
384
385static int init(char* path)
386{
387	DIR* startdir;
388
389    if (NULL == (cdb = cnid_open (path, 0, cnid_type, 0, "localhost", "4700")) ) {
390                fprintf (stderr, "ERROR: cannot open CNID database in '%s'\n", path);
391                fprintf (stderr, "ERROR: check the logs for reasons, aborting\n");
392		return -1;
393        }
394        cnid_getstamp(cdb, db_stamp, sizeof(db_stamp));
395	cdir_id = htonl(2);
396
397	startdir = opendir(path);
398	strlcpy(curpath, path, sizeof(curpath));
399	checkdir (startdir, path, cdir_id);
400	closedir(startdir);
401
402	cnid_close(cdb);
403
404	return (0);
405}
406
407static void usage( char * name )
408{
409    fprintf( stderr, "usage:\t%s [-ndv] -c cnid -f fromcode -t tocode [-m maccode] path\n", name );
410    fprintf( stderr, "Try `%s -h' for more information.\n", name );
411    exit( 1 );
412}
413
414static void print_version (void)
415{
416    fprintf( stderr, "uniconv - Netatalk %s\n", VERSION );
417}
418
419static void help (void)
420{
421    fprintf (stdout, "\nuniconv, a tool to convert between various Netatalk volume encodings\n");
422    fprintf (stdout, "\nUsage:  uniconv [-ndv] -c cnid -f fromcode -t tocode [-m maccode] path\n\n");
423    fprintf (stdout, "Examples:\n");
424    fprintf (stdout, "     uniconv -c dbd -f ASCII -t UTF8 -m MAC_ROMAN /path/to/share\n");
425    fprintf (stdout, "     uniconv -c cdb -f ISO-8859-1 -t UTF8 -m MAC_ROMAN /path/to/share\n");
426    fprintf (stdout, "     uniconv -c cdb -f ISO-8859-ADAPTED -t ASCII -m MAC_ROMAN /path/to/share\n");
427    fprintf (stdout, "     uniconv -f UTF8 -t ASCII -m MAC_ROMAN /path/to/share\n\n");
428    fprintf (stdout, "Options:\n");
429    fprintf (stdout, "\t-f\tencoding to convert from, use ASCII for CAP encoded volumes\n");
430    fprintf (stdout, "\t-t\tvolume encoding to convert to, e.g. UTF8.\n");
431    fprintf (stdout, "\t-m\tMacintosh client codepage, required for CAP encoded volumes.\n");
432    fprintf (stdout, "\t\tDefaults to `MAC_ROMAN'\n");
433    fprintf (stdout, "\t-n\t`dry run', don't change anything.\n");
434    fprintf (stdout, "\t-d\tDon't CAP encode leading dots (:2e).\n");
435    fprintf (stdout, "\t-c\tCNID backend used on this volume, usually cdb or dbd.\n");
436    fprintf (stdout, "\t\tIf not specified, the default cnid backend `%s' is used\n", DEFAULT_CNID_SCHEME);
437    fprintf (stdout, "\t-v\tVerbose output, use twice for maximum logging.\n");
438    fprintf (stdout, "\t-V\tPrint version and exit\n");
439    fprintf (stdout, "\t-h\tThis help screen\n\n");
440    fprintf (stdout, "WARNING:\n");
441    fprintf (stdout, "     Setting the wrong options might render your data unusable!!!\n");
442    fprintf (stdout, "     Make sure you know what you are doing. Always backup your data first.\n\n");
443    fprintf (stdout, "     It is *strongly* recommended to do a `dry run' first and to check the\n");
444    fprintf (stdout, "     output for conversion errors.\n");
445    fprintf (stdout, "     USE AT YOUR OWN RISK!!!\n\n");
446
447}
448
449
450int main(int argc, char *argv[])
451{
452        char path[MAXPATHLEN];
453        int  c;
454
455	path[0]= 0;
456        conv_flags = CONV_UNESCAPEHEX | CONV_ESCAPEHEX | CONV_ESCAPEDOTS;
457
458#ifdef HAVE_SETLINEBUF
459        setlinebuf(stdout);
460#endif
461
462        while ((c = getopt (argc, argv, "f:m:t:c:dnvVh")) != -1)
463        switch (c)
464        {
465	case 'f':
466		from_charset = strdup(optarg);
467		break;
468	case 't':
469		to_charset = strdup(optarg);
470		break;
471	case 'm':
472		mac_charset = strdup(optarg);
473		break;
474	case 'd':
475		conv_flags &= ~CONV_ESCAPEDOTS;
476		usedots = 1;
477		break;
478	case 'n':
479		fprintf (stderr, "doing dry run, volume will *not* be changed\n");
480		dry_run = 1;
481		break;
482	case 'c':
483		cnid_type = strdup(optarg);
484		fprintf (stderr, "CNID backend set to: %s\n", cnid_type);
485		break;
486	case 'v':
487		verbose++;
488		break;
489	case 'V':
490		print_version();
491		exit (1);
492		break;
493        case 'h':
494		help();
495		exit(1);
496                break;
497        default:
498		break;
499        }
500
501        if ( argc - optind != 1 ) {
502            usage( argv[0] );
503            exit( 1 );
504        }
505        set_processname("uniconv");
506
507	if ( from_charset == NULL || to_charset == NULL) {
508		fprintf (stderr, "required charsets not specified\n");
509		exit(-1);
510	}
511
512	if ( mac_charset == NULL )
513		mac_charset = "MAC_ROMAN";
514
515	if ( cnid_type == NULL)
516		cnid_type = DEFAULT_CNID_SCHEME;
517
518
519	/* get path */
520	strlcpy(path, argv[optind], sizeof(path));
521
522	/* deal with relative path */
523	if (chdir(path)) {
524                fprintf (stderr, "ERROR: cannot chdir to '%s'\n", path);
525                return (-1);
526	}
527
528        if (NULL == (getcwd(path, sizeof(path))) ) {
529                fprintf (stderr, "ERROR: getcwd failed\n");
530                return (-1);
531        }
532
533	/* set charsets */
534	atalk_register_charset(&charset_iso8859_adapted);
535
536    	if ( (charset_t) -1 == ( ch_from = add_charset(from_charset)) ) {
537        	fprintf( stderr, "Setting codepage %s as source codepage failed", from_charset);
538		exit (-1);
539    	}
540
541    	if ( (charset_t) -1 == ( ch_to = add_charset(to_charset)) ) {
542        	fprintf( stderr, "Setting codepage %s as destination codepage failed", to_charset);
543		exit (-1);
544    	}
545
546    	if ( (charset_t) -1 == ( ch_mac = add_charset(mac_charset)) ) {
547        	fprintf( stderr, "Setting codepage %s as mac codepage failed", mac_charset);
548		exit (-1);
549    	}
550
551	cnid_init();
552	init(path);
553
554	return (0);
555}
556