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