* monoresgen.cs: Clean up resources when resource file cannot be
[mono.git] / mcs / tools / resgen / monoresgen.cs
1 /*
2  * resgen: convert between the resource formats (.txt, .resources, .resx).
3  *
4  * Copyright (c) 2002 Ximian, Inc
5  *
6  * Authors:
7  *      Paolo Molaro (lupus@ximian.com)
8  *      Gonzalo Paniagua Javier (gonzalo@ximian.com)
9  */
10
11 using System;
12 using System.Globalization;
13 using System.Text;
14 using System.IO;
15 using System.Collections;
16 using System.Resources;
17 using System.Reflection;
18
19 class ResGen {
20
21         static Assembly swf;
22         static Type resxr;
23         static Type resxw;
24
25         /*
26          * We load the ResX format stuff on demand, since the classes are in 
27          * System.Windows.Forms (!!!) and we can't depend on that assembly in mono, yet.
28          */
29         static void LoadResX () {
30                 if (swf != null)
31                         return;
32                 try {
33                         swf = Assembly.Load (Consts.AssemblySystem_Windows_Forms);
34                         resxr = swf.GetType ("System.Resources.ResXResourceReader");
35                         resxw = swf.GetType ("System.Resources.ResXResourceWriter");
36                 } catch (Exception e) {
37                         throw new Exception ("Cannot load support for ResX format: " + e.Message);
38                 }
39         }
40
41         static void Usage () {
42                 string Usage = @"Mono Resource Generator version 0.1
43 Usage:
44                 resgen source.ext [dest.ext]";
45 #if NET_2_0
46                 Usage += @"
47                 resgen [options] /compile source.ext[,dest.resources] [...]";
48 #else
49                 Usage += @"
50                 resgen /compile source.ext[,dest.resources] [...]";
51 #endif
52                 Usage += @"
53
54 Convert a resource file from one format to another.
55 The currently supported formats are: '.txt' '.resources' '.resx' '.po'.
56 If the destination file is not specified, source.resources will be used.";
57 #if NET_2_0
58                 Usage += @"
59 Options:
60 /compile
61         takes a list of .resX or .txt files to convert to .resources files
62         in one bulk operation, replacing .ext with .resources for the 
63         output file name (if not set).
64 /useSourcePath
65         to resolve relative file paths, use the directory of the resource 
66         file as current directory.";
67 #else
68                 Usage += @"
69 The /compile option takes a list of .resX or .txt files to convert to
70 .resources files in one bulk operation, replacing .ext with .resources for
71 the output file name (if not set).";
72 #endif
73                 Usage += @"
74 ";
75                 Console.WriteLine( Usage );
76         }
77         
78         static IResourceReader GetReader (Stream stream, string name, bool useSourcePath) {
79                 string format = Path.GetExtension (name);
80                 switch (format.ToLower (System.Globalization.CultureInfo.InvariantCulture)) {
81                 case ".po":
82                         return new PoResourceReader (stream);
83                 case ".txt":
84                 case ".text":
85                         return new TxtResourceReader (stream);
86                 case ".resources":
87                         return new ResourceReader (stream);
88                 case ".resx":
89                         LoadResX ();
90                         IResourceReader reader = (IResourceReader) Activator.CreateInstance (
91                                 resxr, new object[] {stream});
92                         if (useSourcePath) { // only possible on 2.0 profile, or higher
93                                 PropertyInfo p = reader.GetType ().GetProperty ("BasePath",
94                                         BindingFlags.Public | BindingFlags.Instance);
95                                 if (p != null && p.CanWrite) {
96                                         p.SetValue (reader, Path.GetDirectoryName (name), null);
97                                 }
98                         }
99                         return reader;
100                 default:
101                         throw new Exception ("Unknown format in file " + name);
102                 }
103         }
104         
105         static IResourceWriter GetWriter (Stream stream, string name) {
106                 string format = Path.GetExtension (name);
107                 switch (format.ToLower ()) {
108                 case ".po":
109                         return new PoResourceWriter (stream);
110                 case ".txt":
111                 case ".text":
112                         return new TxtResourceWriter (stream);
113                 case ".resources":
114                         return new ResourceWriter (stream);
115                 case ".resx":
116                         LoadResX ();
117                         return (IResourceWriter)Activator.CreateInstance (resxw, new object[] {stream});
118                 default:
119                         throw new Exception ("Unknown format in file " + name);
120                 }
121         }
122         
123         static int CompileResourceFile (string sname, string dname, bool useSourcePath) {
124                 FileStream source = null;
125                 FileStream dest = null;
126                 IResourceReader reader = null;
127                 IResourceWriter writer = null;
128                 bool success;
129
130                 try {
131                         source = new FileStream (sname, FileMode.Open, FileAccess.Read);
132                         reader = GetReader (source, sname, useSourcePath);
133
134                         dest = new FileStream (dname, FileMode.Create, FileAccess.Write);
135                         writer = GetWriter (dest, dname);
136
137                         int rescount = 0;
138                         foreach (DictionaryEntry e in reader) {
139                                 rescount++;
140                                 object val = e.Value;
141                                 if (val is string)
142                                         writer.AddResource ((string)e.Key, (string)e.Value);
143                                 else
144                                         writer.AddResource ((string)e.Key, e.Value);
145                         }
146                         Console.WriteLine( "Read in {0} resources from '{1}'", rescount, sname );
147
148                         reader.Close ();
149                         writer.Close ();
150                         Console.WriteLine("Writing resource file...  Done.");
151                         success = true;
152                 } catch (Exception e) {
153                         Console.WriteLine ("Error: {0}", e.Message);
154                         Exception inner = e.InnerException;
155                         if (inner is TargetInvocationException && inner.InnerException != null)
156                                 inner = inner.InnerException;
157                         if (inner != null)
158                                 Console.WriteLine ("Inner exception: {0}", inner.Message);
159
160                         if (reader != null)
161                                 reader.Dispose ();
162                         if (source != null)
163                                 source.Close ();
164                         if (writer != null)
165                                 writer.Dispose ();
166                         if (dest != null)
167                                 dest.Close ();
168
169                         // since we're not first reading all entries in source, we may get a
170                         // read failure after we're started writing to the destination file
171                         // and leave behind a broken resources file, so remove it here
172                         try {
173                                 File.Delete (dname);
174                         } catch {
175                         }
176                         return 1;
177                 }
178                 return 0;
179         }
180         
181         static int Main (string[] args) {
182                 bool compileMultiple = false;
183                 bool useSourcePath = false;
184                 ArrayList inputFiles = new ArrayList ();
185
186                 for (int i = 0; i < args.Length; i++) {
187                         switch (args [i].ToLower ()) {
188                         case "-h":
189                         case "/h":
190                         case "-?":
191                         case "/?":
192                                 Usage ();
193                                 return 1;
194                         case "/compile":
195                         case "-compile":
196                                 if (inputFiles.Count > 0) {
197                                         // the /compile option should be specified before any files
198                                         Usage ();
199                                         return 1;
200                                 }
201                                 compileMultiple = true;
202                                 break;
203 #if NET_2_0
204                         case "/usesourcepath":
205                         case "-usesourcepath":
206                                 if (compileMultiple) {
207                                         // the /usesourcepath option should not appear after the
208                                         // /compile switch on the command-line
209                                         Console.WriteLine ("ResGen : error RG0000: Invalid "
210                                                 + "command line syntax.  Switch: \"/compile\"  Bad value: "
211                                                 + args [i] + ".  Use ResGen /? for usage information.");
212                                         return 1;
213                                 }
214                                 useSourcePath = true;
215                                 break;
216 #endif
217                         default:
218                                 if (!IsFileArgument (args [i])) {
219                                         Usage ();
220                                         return 1;
221                                 }
222
223                                 ResourceInfo resInf = new ResourceInfo ();
224                                 if (compileMultiple) {
225                                         string [] pair = args [i].Split (',');
226                                         switch (pair.Length) {
227                                         case 1:
228                                                 resInf.InputFile = Path.GetFullPath (pair [0]);
229                                                 resInf.OutputFile = Path.ChangeExtension (resInf.InputFile,
230                                                         "resources");
231                                                 break;
232                                         case 2:
233                                                 if (pair [1].Length == 0) {
234                                                         Console.WriteLine (@"error: You must specify an input & outfile file name like this:");
235                                                         Console.WriteLine ("inFile.txt,outFile.resources.");
236                                                         Console.WriteLine ("You passed in '{0}'.", args [i]);
237                                                         return 1;
238                                                 }
239                                                 resInf.InputFile = Path.GetFullPath (pair [0]);
240                                                 resInf.OutputFile = Path.GetFullPath (pair [1]);
241                                                 break;
242                                         default:
243                                                 Usage ();
244                                                 return 1;
245                                         }
246                                 } else {
247                                         if ((i + 1) < args.Length) {
248                                                 resInf.InputFile = Path.GetFullPath (args [i]);
249                                                 // move to next arg, since we assume that one holds
250                                                 // the name of the output file
251                                                 i++;
252                                                 resInf.OutputFile = Path.GetFullPath (args [i]);
253                                         } else {
254                                                 resInf.InputFile = Path.GetFullPath (args [i]);
255                                                 resInf.OutputFile = Path.ChangeExtension (resInf.InputFile,
256                                                         "resources");
257                                         }
258                                 }
259                                 inputFiles.Add (resInf);
260                                 break;
261                         }
262                 }
263
264                 if (inputFiles.Count == 0) {
265                         Usage ();
266                         return 1;
267                 }
268
269                 foreach (ResourceInfo res in inputFiles) {
270                         int ret = CompileResourceFile (res.InputFile, res.OutputFile, useSourcePath);
271                         if (ret != 0 )
272                                 return ret;
273                 }
274                 return 0;
275         }
276
277         private static bool RunningOnUnix {
278                 get {
279                         // check for Unix platforms - see FAQ for more details
280                         // http://www.mono-project.com/FAQ:_Technical#How_to_detect_the_execution_platform_.3F
281                         int platform = (int) Environment.OSVersion.Platform;
282                         return ((platform == 4) || (platform == 128));
283                 }
284         }
285
286         private static bool IsFileArgument (string arg)
287         {
288                 if ((arg [0] != '-') && (arg [0] != '/'))
289                         return true;
290
291                 // cope with absolute filenames for resx files on unix, as
292                 // they also match the option pattern
293                 //
294                 // `/home/test.resx' is considered as a resx file, however
295                 // '/test.resx' is considered as error
296                 return (RunningOnUnix && arg.Length > 2 && arg.IndexOf ('/', 2) != -1);
297         }
298 }
299
300 class TxtResourceWriter : IResourceWriter {
301         StreamWriter s;
302         
303         public TxtResourceWriter (Stream stream) {
304                 s = new StreamWriter (stream);
305         }
306         
307         public void AddResource (string name, byte[] value) {
308                 throw new Exception ("Binary data not valid in a text resource file");
309         }
310         
311         public void AddResource (string name, object value) {
312                 if (value is string) {
313                         AddResource (name, (string)value);
314                         return;
315                 }
316                 throw new Exception ("Objects not valid in a text resource file");
317         }
318         
319         public void AddResource (string name, string value) {
320                 s.WriteLine ("{0}={1}", name, Escape (value));
321         }
322
323         // \n -> \\n ...
324         static string Escape (string value)
325         {
326                 StringBuilder b = new StringBuilder ();
327                 for (int i = 0; i < value.Length; i++) {
328                         switch (value [i]) {
329                         case '\n':
330                                 b.Append ("\\n");
331                                 break;
332                         case '\r':
333                                 b.Append ("\\r");
334                                 break;
335                         case '\t':
336                                 b.Append ("\\t");
337                                 break;
338                         case '\\':
339                                 b.Append ("\\\\");
340                                 break;
341                         default:
342                                 b.Append (value [i]);
343                                 break;
344                         }
345                 }
346                 return b.ToString ();
347         }
348         
349         public void Close () {
350                 s.Close ();
351         }
352         
353         public void Dispose () {}
354         
355         public void Generate () {}
356 }
357
358 class TxtResourceReader : IResourceReader {
359         Hashtable data;
360         Stream s;
361         
362         public TxtResourceReader (Stream stream) {
363                 data = new Hashtable ();
364                 s = stream;
365                 Load ();
366         }
367         
368         public virtual void Close () {
369         }
370         
371         public IDictionaryEnumerator GetEnumerator() {
372                 return data.GetEnumerator ();
373         }
374         
375         void Load () {
376                 StreamReader reader = new StreamReader (s);
377                 string line, key, val;
378                 int epos, line_num = 0;
379                 while ((line = reader.ReadLine ()) != null) {
380                         line_num++;
381                         line = line.Trim ();
382                         if (line.Length == 0 || line [0] == '#' ||
383                             line [0] == ';')
384                                 continue;
385                         epos = line.IndexOf ('=');
386                         if (epos < 0) 
387                                 throw new Exception ("Invalid format at line " + line_num);
388                         key = line.Substring (0, epos);
389                         val = line.Substring (epos + 1);
390                         key = key.Trim ();
391                         val = val.Trim ();
392                         if (key.Length == 0) 
393                                 throw new Exception ("Key is empty at line " + line_num);
394
395                         val = Unescape (val);
396                         if (val == null)
397                                 throw new Exception (String.Format ("Unsupported escape character in value of key '{0}'.", key));
398
399
400                         data.Add (key, val);
401                 }
402         }
403
404         // \\n -> \n ...
405         static string Unescape (string value)
406         {
407                 StringBuilder b = new StringBuilder ();
408
409                 for (int i = 0; i < value.Length; i++) {
410                         if (value [i] == '\\') {
411                                 if (i == value.Length - 1)
412                                         return null;
413
414                                 i++;
415                                 switch (value [i]) {
416                                 case 'n':
417                                         b.Append ('\n');
418                                         break;
419                                 case 'r':
420                                         b.Append ('\r');
421                                         break;
422                                 case 't':
423                                         b.Append ('\t');
424                                         break;
425 #if NET_2_0
426                                 case 'u':
427                                         int ch = int.Parse (value.Substring (++i, 4), NumberStyles.HexNumber);
428                                         b.Append (char.ConvertFromUtf32 (ch));
429                                         i += 3;
430                                         break;
431 #endif
432                                 case '\\':
433                                         b.Append ('\\');
434                                         break;
435                                 default:
436                                         return null;
437                                 }
438
439                         } else {
440                                 b.Append (value [i]);
441                         }
442                 }
443
444                 return b.ToString ();
445         }
446         
447         IEnumerator IEnumerable.GetEnumerator () {
448                 return ((IResourceReader) this).GetEnumerator();
449         }
450
451         void IDisposable.Dispose () {}
452 }
453
454 class PoResourceReader : IResourceReader {
455         Hashtable data;
456         Stream s;
457         int line_num;
458         
459         public PoResourceReader (Stream stream)
460         {
461                 data = new Hashtable ();
462                 s = stream;
463                 Load ();
464         }
465         
466         public virtual void Close ()
467         {
468                 s.Close ();
469         }
470         
471         public IDictionaryEnumerator GetEnumerator()
472         {
473                 return data.GetEnumerator ();
474         }
475         
476         string GetValue (string line)
477         {
478                 int begin = line.IndexOf ('"');
479                 if (begin == -1)
480                         throw new FormatException (String.Format ("No begin quote at line {0}: {1}", line_num, line));
481
482                 int end = line.LastIndexOf ('"');
483                 if (end == -1)
484                         throw new FormatException (String.Format ("No closing quote at line {0}: {1}", line_num, line));
485
486                 return line.Substring (begin + 1, end - begin - 1);
487         }
488         
489         void Load ()
490         {
491                 StreamReader reader = new StreamReader (s);
492                 string line;
493                 string msgid = null;
494                 string msgstr = null;
495                 bool ignoreNext = false;
496
497                 while ((line = reader.ReadLine ()) != null) {
498                         line_num++;
499                         line = line.Trim ();
500                         if (line.Length == 0)
501                                 continue;
502                                 
503                         if (line [0] == '#') {
504                                 if (line.Length == 1 || line [1] != ',')
505                                         continue;
506
507                                 if (line.IndexOf ("fuzzy") != -1) {
508                                         ignoreNext = true;
509                                         if (msgid != null) {
510                                                 if (msgstr == null)
511                                                         throw new FormatException ("Error. Line: " + line_num);
512                                                 data.Add (msgid, msgstr);
513                                                 msgid = null;
514                                                 msgstr = null;
515                                         }
516                                 }
517                                 continue;
518                         }
519                         
520                         if (line.StartsWith ("msgid ")) {
521                                 if (msgid == null && msgstr != null)
522                                         throw new FormatException ("Found 2 consecutive msgid. Line: " + line_num);
523
524                                 if (msgstr != null) {
525                                         if (!ignoreNext)
526                                                 data.Add (msgid, msgstr);
527
528                                         ignoreNext = false;
529                                         msgid = null;
530                                         msgstr = null;
531                                 }
532
533                                 msgid = GetValue (line);
534                                 continue;
535                         }
536
537                         if (line.StartsWith ("msgstr ")) {
538                                 if (msgid == null)
539                                         throw new FormatException ("msgstr with no msgid. Line: " + line_num);
540
541                                 msgstr = GetValue (line);
542                                 continue;
543                         }
544
545                         if (line [0] == '"') {
546                                 if (msgid == null || msgstr == null)
547                                         throw new FormatException ("Invalid format. Line: " + line_num);
548
549                                 msgstr += GetValue (line);
550                                 continue;
551                         }
552
553                         throw new FormatException ("Unexpected data. Line: " + line_num);
554                 }
555
556                 if (msgid != null) {
557                         if (msgstr == null)
558                                 throw new FormatException ("Expecting msgstr. Line: " + line_num);
559
560                         if (!ignoreNext)
561                                 data.Add (msgid, msgstr);
562                 }
563         }
564         
565         IEnumerator IEnumerable.GetEnumerator ()
566         {
567                 return GetEnumerator();
568         }
569
570         void IDisposable.Dispose ()
571         {
572                 if (data != null)
573                         data = null;
574
575                 if (s != null) {
576                         s.Close ();
577                         s = null;
578                 }
579         }
580 }
581
582 class PoResourceWriter : IResourceWriter
583 {
584         TextWriter s;
585         bool headerWritten;
586         
587         public PoResourceWriter (Stream stream)
588         {
589                 s = new StreamWriter (stream);
590         }
591         
592         public void AddResource (string name, byte [] value)
593         {
594                 throw new InvalidOperationException ("Binary data not valid in a po resource file");
595         }
596         
597         public void AddResource (string name, object value)
598         {
599                 if (value is string) {
600                         AddResource (name, (string) value);
601                         return;
602                 }
603                 throw new InvalidOperationException ("Objects not valid in a po resource file");
604         }
605
606         StringBuilder ebuilder = new StringBuilder ();
607         
608         public string Escape (string ns)
609         {
610                 ebuilder.Length = 0;
611
612                 foreach (char c in ns){
613                         switch (c){
614                         case '"':
615                         case '\\':
616                                 ebuilder.Append ('\\');
617                                 ebuilder.Append (c);
618                                 break;
619                         case '\a':
620                                 ebuilder.Append ("\\a");
621                                 break;
622                         case '\n':
623                                 ebuilder.Append ("\\n");
624                                 break;
625                         case '\r':
626                                 ebuilder.Append ("\\r");
627                                 break;
628                         default:
629                                 ebuilder.Append (c);
630                                 break;
631                         }
632                 }
633                 return ebuilder.ToString ();
634         }
635         
636         public void AddResource (string name, string value)
637         {
638                 if (!headerWritten) {
639                         headerWritten = true;
640                         WriteHeader ();
641                 }
642                 
643                 s.WriteLine ("msgid \"{0}\"", Escape (name));
644                 s.WriteLine ("msgstr \"{0}\"", Escape (value));
645                 s.WriteLine ("");
646         }
647         
648         void WriteHeader ()
649         {
650                 s.WriteLine ("msgid \"\"");
651                 s.WriteLine ("msgstr \"\"");
652                 s.WriteLine ("\"MIME-Version: 1.0\\n\"");
653                 s.WriteLine ("\"Content-Type: text/plain; charset=UTF-8\\n\"");
654                 s.WriteLine ("\"Content-Transfer-Encoding: 8bit\\n\"");
655                 s.WriteLine ("\"X-Generator: Mono resgen 0.1\\n\"");
656                 s.WriteLine ("#\"Project-Id-Version: FILLME\\n\"");
657                 s.WriteLine ("#\"POT-Creation-Date: yyyy-MM-dd HH:MM+zzzz\\n\"");
658                 s.WriteLine ("#\"PO-Revision-Date: yyyy-MM-dd HH:MM+zzzz\\n\"");
659                 s.WriteLine ("#\"Last-Translator: FILLME\\n\"");
660                 s.WriteLine ("#\"Language-Team: FILLME\\n\"");
661                 s.WriteLine ("#\"Report-Msgid-Bugs-To: \\n\"");
662                 s.WriteLine ();
663         }
664
665         public void Close ()
666         {
667                 s.Close ();
668         }
669         
670         public void Dispose () { }
671         
672         public void Generate () {}
673 }
674
675 class ResourceInfo
676 {
677         public string InputFile;
678         public string OutputFile;
679 }