001/**
002 * This software is provided under the terms of the Minecraft Forge Public
003 * License v1.0.
004 */
005
006package net.minecraftforge.common;
007
008import java.io.*;
009import java.text.DateFormat;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.Date;
014import java.util.Locale;
015import java.util.Map;
016import java.util.TreeMap;
017import java.util.regex.Matcher;
018import java.util.regex.Pattern;
019
020import com.google.common.base.CharMatcher;
021import com.google.common.base.Splitter;
022import com.google.common.collect.Maps;
023
024import cpw.mods.fml.common.FMLCommonHandler;
025import cpw.mods.fml.common.FMLLog;
026import cpw.mods.fml.common.Loader;
027import cpw.mods.fml.relauncher.FMLInjectionData;
028
029import net.minecraft.block.Block;
030import net.minecraft.item.Item;
031import static net.minecraftforge.common.Property.Type.*;
032
033/**
034 * This class offers advanced configurations capabilities, allowing to provide
035 * various categories for configuration variables.
036 */
037public class Configuration
038{
039    private static boolean[] configMarkers = new boolean[Item.itemsList.length];
040    private static final int ITEM_SHIFT = 256;
041    private static final int MAX_BLOCKS = 4096;
042
043    public static final String CATEGORY_GENERAL = "general";
044    public static final String CATEGORY_BLOCK   = "block";
045    public static final String CATEGORY_ITEM    = "item";
046    public static final String ALLOWED_CHARS = "._-";
047    public static final String DEFAULT_ENCODING = "UTF-8";
048    public static final String CATEGORY_SPLITTER = ".";
049    public static final String NEW_LINE;
050    private static final Pattern CONFIG_START = Pattern.compile("START: \"([^\\\"]+)\"");
051    private static final Pattern CONFIG_END = Pattern.compile("END: \"([^\\\"]+)\"");
052    public static final CharMatcher allowedProperties = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.anyOf(ALLOWED_CHARS));
053    private static Configuration PARENT = null;
054
055    File file;
056
057    public Map<String, ConfigCategory> categories = new TreeMap<String, ConfigCategory>();
058    private Map<String, Configuration> children = new TreeMap<String, Configuration>();
059
060    private boolean caseSensitiveCustomCategories;
061    public String defaultEncoding = DEFAULT_ENCODING;
062    private String fileName = null;
063    public boolean isChild = false;
064
065    static
066    {
067        Arrays.fill(configMarkers, false);
068        NEW_LINE = System.getProperty("line.separator");
069    }
070
071    public Configuration(){}
072
073    /**
074     * Create a configuration file for the file given in parameter.
075     */
076    public Configuration(File file)
077    {
078        this.file = file;
079        String basePath = ((File)(FMLInjectionData.data()[6])).getAbsolutePath().replace(File.separatorChar, '/').replace("/.", "");
080        String path = file.getAbsolutePath().replace(File.separatorChar, '/').replace("/./", "/").replace(basePath, "");
081        if (PARENT != null)
082        {
083            PARENT.setChild(path, this);
084            isChild = true;
085        }
086        else
087        {
088            fileName = path;
089            load();
090        }
091    }
092
093    public Configuration(File file, boolean caseSensitiveCustomCategories)
094    {
095        this(file);
096        this.caseSensitiveCustomCategories = caseSensitiveCustomCategories;
097    }
098
099    /**
100     * Gets or create a block id property. If the block id property key is
101     * already in the configuration, then it will be used. Otherwise,
102     * defaultId will be used, except if already taken, in which case this
103     * will try to determine a free default id.
104     */
105    public Property getBlock(String key, int defaultID) { return getBlock(CATEGORY_BLOCK, key, defaultID, null); }
106    public Property getBlock(String key, int defaultID, String comment) { return getBlock(CATEGORY_BLOCK, key, defaultID, comment); }
107    public Property getBlock(String category, String key, int defaultID) { return getBlockInternal(category, key, defaultID, null, 256, Block.blocksList.length); }
108    public Property getBlock(String category, String key, int defaultID, String comment) { return getBlockInternal(category, key, defaultID, comment, 256, Block.blocksList.length); }
109
110    /**
111     * Special version of getBlock to be used when you want to garentee the ID you get is below 256
112     * This should ONLY be used by mods who do low level terrain generation, or ones that add new
113     * biomes.
114     * EXA: ExtraBiomesXL
115     * 
116     * Specifically, if your block is used BEFORE the Chunk is created, and placed in the terrain byte array directly.
117     * If you add a new biome and you set the top/filler block, they need to be <256, nothing else.
118     * 
119     * If you're adding a new ore, DON'T call this function.
120     * 
121     * Normal mods such as '50 new ores' do not need to be below 256 so should use the normal getBlock
122     */
123    public Property getTerrainBlock(String category, String key, int defaultID, String comment)
124    {
125        return getBlockInternal(category, key, defaultID, comment, 0, 256); 
126    }
127
128    private Property getBlockInternal(String category, String key, int defaultID, String comment, int lower, int upper)
129    {
130        Property prop = get(category, key, -1, comment);
131
132        if (prop.getInt() != -1)
133        {
134            configMarkers[prop.getInt()] = true;
135            return prop;
136        }
137        else
138        {
139            if (defaultID < lower)
140            {
141                FMLLog.warning(
142                    "Mod attempted to get a block ID with a default in the Terrain Generation section, " +
143                    "mod authors should make sure there defaults are above 256 unless explicitly needed " +
144                    "for terrain generation. Most ores do not need to be below 256.");
145                FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID);
146                defaultID = upper - 1;
147            }
148
149            if (Block.blocksList[defaultID] == null && !configMarkers[defaultID])
150            {
151                prop.value = Integer.toString(defaultID);
152                configMarkers[defaultID] = true;
153                return prop;
154            }
155            else
156            {
157                for (int j = upper - 1; j > 0; j--)
158                {
159                    if (Block.blocksList[j] == null && !configMarkers[j])
160                    {
161                        prop.value = Integer.toString(j);
162                        configMarkers[j] = true;
163                        return prop;
164                    }
165                }
166
167                throw new RuntimeException("No more block ids available for " + key);
168            }
169        }
170    }
171
172    public Property getItem(String key, int defaultID) { return getItem(CATEGORY_ITEM, key, defaultID, null); }
173    public Property getItem(String key, int defaultID, String comment) { return getItem(CATEGORY_ITEM, key, defaultID, comment); }
174    public Property getItem(String category, String key, int defaultID) { return getItem(category, key, defaultID, null); }
175
176    public Property getItem(String category, String key, int defaultID, String comment)
177    {
178        Property prop = get(category, key, -1, comment);
179        int defaultShift = defaultID + ITEM_SHIFT;
180
181        if (prop.getInt() != -1)
182        {
183            configMarkers[prop.getInt() + ITEM_SHIFT] = true;
184            return prop;
185        }
186        else
187        {
188            if (defaultID < MAX_BLOCKS - ITEM_SHIFT)
189            {
190                FMLLog.warning(
191                    "Mod attempted to get a item ID with a default value in the block ID section, " +
192                    "mod authors should make sure there defaults are above %d unless explicitly needed " +
193                    "so that all block ids are free to store blocks.", MAX_BLOCKS - ITEM_SHIFT);
194                FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID);
195            }
196
197            if (Item.itemsList[defaultShift] == null && !configMarkers[defaultShift] && defaultShift >= Block.blocksList.length)
198            {
199                prop.value = Integer.toString(defaultID);
200                configMarkers[defaultShift] = true;
201                return prop;
202            }
203            else
204            {
205                for (int x = Item.itemsList.length - 1; x >= ITEM_SHIFT; x--)
206                {
207                    if (Item.itemsList[x] == null && !configMarkers[x])
208                    {
209                        prop.value = Integer.toString(x - ITEM_SHIFT);
210                        configMarkers[x] = true;
211                        return prop;
212                    }
213                }
214
215                throw new RuntimeException("No more item ids available for " + key);
216            }
217        }
218    }
219
220    public Property get(String category, String key, int defaultValue)
221    {
222        return get(category, key, defaultValue, null);
223    }
224
225    public Property get(String category, String key, int defaultValue, String comment)
226    {
227        Property prop = get(category, key, Integer.toString(defaultValue), comment, INTEGER);
228        if (!prop.isIntValue())
229        {
230            prop.value = Integer.toString(defaultValue);
231        }
232        return prop;
233    }
234
235    public Property get(String category, String key, boolean defaultValue)
236    {
237        return get(category, key, defaultValue, null);
238    }
239
240    public Property get(String category, String key, boolean defaultValue, String comment)
241    {
242        Property prop = get(category, key, Boolean.toString(defaultValue), comment, BOOLEAN);
243        if (!prop.isBooleanValue())
244        {
245            prop.value = Boolean.toString(defaultValue);
246        }
247        return prop;
248    }
249
250    public Property get(String category, String key, double defaultValue)
251    {
252        return get(category, key, defaultValue, null);
253    }
254
255    public Property get(String category, String key, double defaultValue, String comment)
256    {
257        Property prop = get(category, key, Double.toString(defaultValue), comment, DOUBLE);
258        if (!prop.isDoubleValue())
259        {
260            prop.value = Double.toString(defaultValue);
261        }
262        return prop;
263    }
264
265    public Property get(String category, String key, String defaultValue)
266    {
267        return get(category, key, defaultValue, null);
268    }
269
270    public Property get(String category, String key, String defaultValue, String comment)
271    {
272        return get(category, key, defaultValue, comment, STRING);
273    }
274
275    public Property get(String category, String key, String[] defaultValue)
276    {
277        return get(category, key, defaultValue, null);
278    }
279
280    public Property get(String category, String key, String[] defaultValue, String comment)
281    {
282        return get(category, key, defaultValue, comment, STRING);
283    }
284
285    public Property get(String category, String key, int[] defaultValue)
286    {
287        return get(category, key, defaultValue, null);
288    }
289
290    public Property get(String category, String key, int[] defaultValue, String comment)
291    {
292        String[] values = new String[defaultValue.length];
293        for (int i = 0; i < defaultValue.length; i++)
294        {
295            values[i] = Integer.toString(defaultValue[i]);
296        }
297
298        Property prop =  get(category, key, values, comment, INTEGER);
299        if (!prop.isIntList())
300        {
301            prop.valueList = values;
302        }
303
304        return prop;
305    }
306
307    public Property get(String category, String key, double[] defaultValue)
308    {
309        return get(category, key, defaultValue, null);
310    }
311
312    public Property get(String category, String key, double[] defaultValue, String comment)
313    {
314        String[] values = new String[defaultValue.length];
315        for (int i = 0; i < defaultValue.length; i++)
316        {
317            values[i] = Double.toString(defaultValue[i]);
318        }
319
320        Property prop =  get(category, key, values, comment, DOUBLE);
321        
322        if (!prop.isDoubleList())
323        {
324            prop.valueList = values;
325        }
326
327        return prop;
328    }
329
330    public Property get(String category, String key, boolean[] defaultValue)
331    {
332        return get(category, key, defaultValue, null);
333    }
334
335    public Property get(String category, String key, boolean[] defaultValue, String comment)
336    {
337        String[] values = new String[defaultValue.length];
338        for (int i = 0; i < defaultValue.length; i++)
339        {
340            values[i] = Boolean.toString(defaultValue[i]);
341        }
342
343        Property prop =  get(category, key, values, comment, BOOLEAN);
344        
345        if (!prop.isBooleanList())
346        {
347            prop.valueList = values;
348        }
349
350        return prop;
351    }
352
353    public Property get(String category, String key, String defaultValue, String comment, Property.Type type)
354    {
355        if (!caseSensitiveCustomCategories)
356        {
357            category = category.toLowerCase(Locale.ENGLISH);
358        }
359
360        ConfigCategory cat = getCategory(category);
361
362        if (cat.containsKey(key))
363        {
364            Property prop = cat.get(key);
365
366            if (prop.getType() == null)
367            {
368                prop = new Property(prop.getName(), prop.value, type);
369                cat.set(key, prop);
370            }
371
372            prop.comment = comment;
373            return prop;
374        }
375        else if (defaultValue != null)
376        {
377            Property prop = new Property(key, defaultValue, type);
378            cat.set(key, prop);
379            prop.comment = comment;
380            return prop;
381        }
382        else
383        {
384            return null;
385        }
386    }
387
388    public Property get(String category, String key, String[] defaultValue, String comment, Property.Type type)
389    {
390        if (!caseSensitiveCustomCategories)
391        {
392            category = category.toLowerCase(Locale.ENGLISH);
393        }
394
395        ConfigCategory cat = getCategory(category);
396
397        if (cat.containsKey(key))
398        {
399            Property prop = cat.get(key);
400
401            if (prop.getType() == null)
402            {
403                prop = new Property(prop.getName(), prop.value, type);
404                cat.set(key, prop);
405            }
406
407            prop.comment = comment;
408
409            return prop;
410        }
411        else if (defaultValue != null)
412        {
413            Property prop = new Property(key, defaultValue, type);
414            prop.comment = comment;
415            cat.set(key, prop);
416            return prop;
417        }
418        else
419        {
420            return null;
421        }
422    }
423
424    public boolean hasCategory(String category)
425    {
426        return categories.get(category) != null;
427    }
428
429    public boolean hasKey(String category, String key)
430    {
431        ConfigCategory cat = categories.get(category);
432        return cat != null && cat.containsKey(key);
433    }
434
435    public void load()
436    {
437        if (PARENT != null && PARENT != this)
438        {
439            return;
440        }
441
442        BufferedReader buffer = null;
443        try
444        {
445            if (file.getParentFile() != null)
446            {
447                file.getParentFile().mkdirs();
448            }
449
450            if (!file.exists() && !file.createNewFile())
451            {
452                return;
453            }
454
455            if (file.canRead())
456            {
457                UnicodeInputStreamReader input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding);
458                defaultEncoding = input.getEncoding();
459                buffer = new BufferedReader(input);
460
461                String line;
462                ConfigCategory currentCat = null;
463                Property.Type type = null;
464                ArrayList<String> tmpList = null;
465                int lineNum = 0;
466                String name = null;
467
468                while (true)
469                {
470                    lineNum++;
471                    line = buffer.readLine();
472
473                    if (line == null)
474                    {
475                        break;
476                    }
477
478                    Matcher start = CONFIG_START.matcher(line);
479                    Matcher end = CONFIG_END.matcher(line);
480
481                    if (start.matches())
482                    {
483                        fileName = start.group(1);
484                        categories = new TreeMap<String, ConfigCategory>();
485                        continue;
486                    }
487                    else if (end.matches())
488                    {
489                        fileName = end.group(1);
490                        Configuration child = new Configuration();
491                        child.categories = categories;
492                        this.children.put(fileName, child);
493                        continue;
494                    }
495
496                    int nameStart = -1, nameEnd = -1;
497                    boolean skip = false;
498                    boolean quoted = false;
499
500                    for (int i = 0; i < line.length() && !skip; ++i)
501                    {
502                        if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"'))
503                        {
504                            if (nameStart == -1)
505                            {
506                                nameStart = i;
507                            }
508
509                            nameEnd = i;
510                        }
511                        else if (Character.isWhitespace(line.charAt(i)))
512                        {
513                            // ignore space charaters
514                        }
515                        else
516                        {
517                            switch (line.charAt(i))
518                            {
519                                case '#':
520                                    skip = true;
521                                    continue;
522
523                                case '"':
524                                    if (quoted)
525                                    {
526                                        quoted = false;
527                                    }
528                                    if (!quoted && nameStart == -1)
529                                    {
530                                        quoted = true;
531                                    }
532                                    break;
533
534                                case '{':
535                                    name = line.substring(nameStart, nameEnd + 1);
536                                    String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat);
537
538                                    ConfigCategory cat = categories.get(qualifiedName);
539                                    if (cat == null)
540                                    {
541                                        currentCat = new ConfigCategory(name, currentCat);
542                                        categories.put(qualifiedName, currentCat);
543                                    }
544                                    else
545                                    {
546                                        currentCat = cat;
547                                    }
548                                    name = null;
549
550                                    break;
551
552                                case '}':
553                                    if (currentCat == null)
554                                    {
555                                        throw new RuntimeException(String.format("Config file corrupt, attepted to close to many categories '%s:%d'", fileName, lineNum));
556                                    }
557                                    currentCat = currentCat.parent;
558                                    break;
559
560                                case '=':
561                                    name = line.substring(nameStart, nameEnd + 1);
562
563                                    if (currentCat == null)
564                                    {
565                                        throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
566                                    }
567
568                                    Property prop = new Property(name, line.substring(i + 1), type, true);
569                                    i = line.length();
570
571                                    currentCat.set(name, prop);
572
573                                    break;
574
575                                case ':':
576                                    type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0));
577                                    nameStart = nameEnd = -1;
578                                    break;
579
580                                case '<':
581                                    if (tmpList != null)
582                                    {
583                                        throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
584                                    }
585
586                                    name = line.substring(nameStart, nameEnd + 1);
587
588                                    if (currentCat == null)
589                                    {
590                                        throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
591                                    }
592
593                                    tmpList = new ArrayList<String>();
594                                    
595                                    skip = true;
596                                    
597                                    break;
598
599                                case '>':
600                                    if (tmpList == null)
601                                    {
602                                        throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
603                                    }
604
605                                    currentCat.set(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type));
606                                    name = null;
607                                    tmpList = null;
608                                    type = null;
609                                    break;
610
611                                default:
612                                    throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum));
613                            }
614                        }
615                    }
616
617                    if (quoted)
618                    {
619                        throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum));
620                    }
621                    else if (tmpList != null && !skip)
622                    {
623                        tmpList.add(line.trim());
624                    }
625                }
626            }
627        }
628        catch (IOException e)
629        {
630            e.printStackTrace();
631        }
632        finally
633        {
634            if (buffer != null)
635            {
636                try
637                {
638                    buffer.close();
639                } catch (IOException e){}
640            }
641        }
642    }
643
644    public void save()
645    {
646        if (PARENT != null && PARENT != this)
647        {
648            PARENT.save();
649            return;
650        }
651
652        try
653        {
654            if (file.getParentFile() != null)
655            {
656                file.getParentFile().mkdirs();
657            }
658
659            if (!file.exists() && !file.createNewFile())
660            {
661                return;
662            }
663
664            if (file.canWrite())
665            {
666                FileOutputStream fos = new FileOutputStream(file);
667                BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding));
668
669                buffer.write("# Configuration file" + NEW_LINE);
670                buffer.write("# Generated on " + DateFormat.getInstance().format(new Date()) + NEW_LINE + NEW_LINE);
671
672                if (children.isEmpty())
673                {
674                    save(buffer);
675                }
676                else
677                {
678                    for (Map.Entry<String, Configuration> entry : children.entrySet())
679                    {
680                        buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE);
681                        entry.getValue().save(buffer);
682                        buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE);
683                    }
684                }
685
686                buffer.close();
687                fos.close();
688            }
689        }
690        catch (IOException e)
691        {
692            e.printStackTrace();
693        }
694    }
695
696    private void save(BufferedWriter out) throws IOException
697    {
698        //For compatiblitties sake just in case, Thanks Atomic, to be removed next MC version
699        //TO-DO: Remove next MC version
700        Object[] categoryArray = categories.values().toArray();
701        for (Object o : categoryArray)
702        {
703            if (o instanceof TreeMap)
704            {
705                TreeMap treeMap = (TreeMap)o;
706                ConfigCategory converted = new ConfigCategory(file.getName());
707                FMLLog.warning("Forge found a Treemap saved for Configuration file " + file.getName() + ", this is deprecated behaviour!");
708                
709                for (Object key : treeMap.keySet())
710                {
711                    FMLLog.warning("Converting Treemap to ConfigCategory, key: " + key + ", property value: " + ((Property)treeMap.get(key)).value);
712                    converted.set((String)key, (Property)treeMap.get(key));
713                }
714                
715                categories.values().remove(o);
716                categories.put(file.getName(), converted);
717            }
718        }
719        
720        for (ConfigCategory cat : categories.values())
721        {
722            if (!cat.isChild())
723            {
724                cat.write(out, 0);
725                out.newLine();
726            }
727        }
728    }
729
730    public ConfigCategory getCategory(String category)
731    {
732        ConfigCategory ret = categories.get(category);
733
734        if (ret == null)
735        {
736            if (category.contains(CATEGORY_SPLITTER))
737            {
738                String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER);
739                ConfigCategory parent = categories.get(hierarchy[0]);
740
741                if (parent == null)
742                {
743                    parent = new ConfigCategory(hierarchy[0]);
744                    categories.put(parent.getQualifiedName(), parent);
745                }
746
747                for (int i = 1; i < hierarchy.length; i++)
748                {
749                    String name = ConfigCategory.getQualifiedName(hierarchy[i], parent);
750                    ConfigCategory child = categories.get(name);
751
752                    if (child == null)
753                    {
754                        child = new ConfigCategory(hierarchy[i], parent);
755                        categories.put(name, child);
756                    }
757
758                    ret = child;
759                    parent = child;
760                }
761            }
762            else
763            {
764                ret = new ConfigCategory(category);
765                categories.put(category, ret);
766            }
767        }
768
769        return ret;
770    }
771
772    public void addCustomCategoryComment(String category, String comment)
773    {
774        if (!caseSensitiveCustomCategories)
775            category = category.toLowerCase(Locale.ENGLISH);
776        getCategory(category).setComment(comment);
777    }
778
779    private void setChild(String name, Configuration child)
780    {
781        if (!children.containsKey(name))
782        {
783            children.put(name, child);
784        }
785        else
786        {
787            Configuration old = children.get(name);
788            child.categories = old.categories;
789            child.fileName = old.fileName;
790        }
791    }
792
793    public static void enableGlobalConfig()
794    {
795        PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
796        PARENT.load();
797    }
798
799    public static class UnicodeInputStreamReader extends Reader
800    {
801        private final InputStreamReader input;
802        private final String defaultEnc;
803
804        public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
805        {
806            defaultEnc = encoding;
807            String enc = encoding;
808            byte[] data = new byte[4];
809
810            PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
811            int read = pbStream.read(data, 0, data.length);
812            int size = 0;
813
814            int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
815            int bom24 = bom16 << 8 | (data[2] & 0xFF);
816            int bom32 = bom24 << 8 | (data[3] & 0xFF);
817
818            if (bom24 == 0xEFBBBF)
819            {
820                enc = "UTF-8";
821                size = 3;
822            }
823            else if (bom16 == 0xFEFF)
824            {
825                enc = "UTF-16BE";
826                size = 2;
827            }
828            else if (bom16 == 0xFFFE)
829            {
830                enc = "UTF-16LE";
831                size = 2;
832            }
833            else if (bom32 == 0x0000FEFF)
834            {
835                enc = "UTF-32BE";
836                size = 4;
837            }
838            else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
839            {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
840                enc = "UTF-32LE";
841                size = 4;
842            }
843
844            if (size < read)
845            {
846                pbStream.unread(data, size, read - size);
847            }
848
849            this.input = new InputStreamReader(pbStream, enc);
850        }
851
852        public String getEncoding()
853        {
854            return input.getEncoding();
855        }
856
857        @Override
858        public int read(char[] cbuf, int off, int len) throws IOException
859        {
860            return input.read(cbuf, off, len);
861        }
862
863        @Override
864        public void close() throws IOException
865        {
866            input.close();
867        }
868    }
869}