1+ <?php
2+ /**
3+ * Class for reading in NBT-format files.
4+ *
5+ * @author Justin Martin <frozenfire@thefrozenfire.com>
6+ * @version 1.0
7+ *
8+ * Dependencies:
9+ * PHP 4.3+ (5.3+ recommended)
10+ * GMP Extension
11+ */
12+
13+ if (PHP_INT_SIZE < 8 ) {
14+ /*
15+ * GMP isn't required for 64-bit machines as we're handling signed ints. We can use native math instead.
16+ * We still need to use GMP for 32-bit builds of PHP though.
17+ */
18+ extension_loaded ("gmp " ) or trigger_error (
19+ "The NBT class requires the GMP extension for 64-bit number handling on 32-bit PHP builds. " .
20+ "Execution will continue, but will halt if a 64-bit number is handled. " , E_USER_NOTICE
21+ );
22+ }
23+
24+ class NBT {
25+ public $ root = array ();
26+
27+ public $ verbose = false ;
28+
29+ const TAG_END = 0 ;
30+ const TAG_BYTE = 1 ;
31+ const TAG_SHORT = 2 ;
32+ const TAG_INT = 3 ;
33+ const TAG_LONG = 4 ;
34+ const TAG_FLOAT = 5 ;
35+ const TAG_DOUBLE = 6 ;
36+ const TAG_BYTE_ARRAY = 7 ;
37+ const TAG_STRING = 8 ;
38+ const TAG_LIST = 9 ;
39+ const TAG_COMPOUND = 10 ;
40+ const TAG_INT_ARRAY = 11 ;
41+ const TAG_LONG_ARRAY = 12 ;
42+
43+ public function __construct ($ filename = null , $ wrapper = "compress.zlib:// " ) {
44+ // PHP 5 constructor (just in case PHP 4-style constructors are ever deprecated)
45+ if (!is_null ($ filename )) $ this ->loadFile ($ filename , $ wrapper );
46+ }
47+
48+ public function NBT ($ filename = null , $ wrapper = "compress.zlib:// " ) {
49+ // PHP 4 constructor
50+ if (!is_null ($ filename )) $ this ->loadFile ($ filename , $ wrapper );
51+ }
52+
53+ public function loadFile ($ filename , $ wrapper = "compress.zlib:// " ) {
54+ if (is_string ($ wrapper ) && is_file ($ filename )) {
55+ if ($ this ->verbose ) trigger_error ("Loading file \"{$ filename }\" with stream wrapper \"{$ wrapper }\". " , E_USER_NOTICE );
56+ $ fp = fopen ("{$ wrapper }{$ filename }" , "rb " );
57+ } elseif (is_null ($ wrapper ) && is_resource ($ filename )) {
58+ if ($ this ->verbose ) trigger_error ("Loading file from existing resource. " , E_USER_NOTICE );
59+ $ fp = $ filename ;
60+ } else {
61+ trigger_error ("First parameter must be a filename or a resource. " , E_USER_WARNING );
62+ return false ;
63+ }
64+ if ($ this ->verbose ) trigger_error ("Traversing first tag in file. " , E_USER_NOTICE );
65+ $ this ->traverseTag ($ fp , $ this ->root );
66+ if ($ this ->verbose ) trigger_error ("Encountered end tag for first tag; finished. " , E_USER_NOTICE );
67+ return end ($ this ->root );
68+ }
69+
70+ public function writeFile ($ filename , $ wrapper = "compress.zlib:// " ) {
71+ if (is_string ($ wrapper )) {
72+ if ($ this ->verbose ) trigger_error ("Writing file \"{$ filename }\" with stream wrapper \"{$ wrapper }\". " , E_USER_NOTICE );
73+ $ fp = fopen ("{$ wrapper }{$ filename }" , "wb " );
74+ } elseif (is_null ($ wrapper ) && is_resource ($ fp )) {
75+ if ($ this ->verbose ) trigger_error ("Writing file to existing resource. " , E_USER_NOTICE );
76+ $ fp = $ filename ;
77+ } else {
78+ trigger_error ("First parameter must be a filename or a resource. " , E_USER_WARNING );
79+ return false ;
80+ }
81+ if ($ this ->verbose ) trigger_error ("Writing " .count ($ this ->root )." root tag(s) to file/resource. " , E_USER_NOTICE );
82+ foreach ($ this ->root as $ rootNum => $ rootTag ) if (!$ this ->writeTag ($ fp , $ rootTag )) trigger_error ("Failed to write root tag # {$ rootNum } to file/resource. " , E_USER_WARNING );
83+ return true ;
84+ }
85+
86+ public function purge () {
87+ if ($ this ->verbose ) trigger_error ("Purging all loaded data " , E_USER_ERROR );
88+ $ this ->root = array ();
89+ }
90+
91+ public function traverseTag ($ fp , &$ tree ) {
92+ if (feof ($ fp )) {
93+ if ($ this ->verbose ) trigger_error ("Reached end of file/resource. " , E_USER_NOTICE );
94+ return false ;
95+ }
96+ $ tagType = $ this ->readType ($ fp , self ::TAG_BYTE ); // Read type byte.
97+ if ($ tagType == self ::TAG_END ) {
98+ return false ;
99+ } else {
100+ if ($ this ->verbose ) $ position = ftell ($ fp );
101+ $ tagName = $ this ->readType ($ fp , self ::TAG_STRING );
102+ if ($ this ->verbose ) trigger_error ("Reading tag \"{$ tagName }\" at offset {$ position }. " , E_USER_NOTICE );
103+ $ tagData = $ this ->readType ($ fp , $ tagType );
104+ // $tree[] = array("type"=>$tagType, "name"=>$tagName, "value"=>$tagData);
105+ $ tree [$ tagName ] = $ tagData ;
106+ return true ;
107+ }
108+ }
109+
110+ public function writeTag ($ fp , $ tag ) {
111+ if ($ this ->verbose ) {
112+ $ position = ftell ($ fp );
113+ trigger_error ("Writing tag \"{$ tag ["name " ]}\" of type {$ tag ["type " ]} at offset {$ position }. " , E_USER_NOTICE );
114+ }
115+ return $ this ->writeType ($ fp , self ::TAG_BYTE , $ tag ["type " ]) && $ this ->writeType ($ fp , self ::TAG_STRING , $ tag ["name " ]) && $ this ->writeType ($ fp , $ tag ["type " ], $ tag ["value " ]);
116+ }
117+
118+ public function readType ($ fp , $ tagType ) {
119+ switch ($ tagType ) {
120+ case self ::TAG_BYTE : // Signed byte (8 bit)
121+ list (,$ unpacked ) = unpack ("c " , fread ($ fp , 1 ));
122+ return $ unpacked ;
123+ case self ::TAG_SHORT : // Signed short (16 bit, big endian)
124+ list (,$ unpacked ) = unpack ("n " , fread ($ fp , 2 ));
125+ if ($ unpacked >= pow (2 , 15 )) $ unpacked -= pow (2 , 16 ); // Convert unsigned short to signed short.
126+ return $ unpacked ;
127+ case self ::TAG_INT : // Signed integer (32 bit, big endian)
128+ list (,$ unpacked ) = unpack ("N " , fread ($ fp , 4 ));
129+ if ($ unpacked >= pow (2 , 31 )) $ unpacked -= pow (2 , 32 ); // Convert unsigned int to signed int
130+ return $ unpacked ;
131+ case self ::TAG_LONG : // Signed long (64 bit, big endian)
132+ list (,$ firstHalf , $ secondHalf ) = unpack ("N* " , fread ($ fp , 8 ));
133+ if (PHP_INT_SIZE >=8 ) {
134+ // Workaround for PHP bug #47564 in 64-bit PHP<=5.2.9
135+ $ firstHalf &= 0xFFFFFFFF ; $ secondHalf &= 0xFFFFFFFF ;
136+
137+ $ value = ($ firstHalf << 32 ) | $ secondHalf ;
138+ if ($ value >pow (2 ,63 )) $ value -= pow (2 ,64 );
139+ } else {
140+ extension_loaded ("gmp " ) or trigger_error (
141+ "This file contains a 64-bit number and execution cannot continue. " .
142+ "Please install the GMP extension for 64-bit number handling. " , E_USER_ERROR
143+ );
144+
145+ // Fix values >= 2^31 (same fix as above, but this time because it's > PHP_INT_MAX)
146+ $ firstHalf = gmp_and ($ firstHalf , "0xFFFFFFFF " );
147+ $ secondHalf = gmp_and ($ secondHalf , "0xFFFFFFFF " );
148+
149+ $ value = gmp_add ($ secondHalf , gmp_mul ($ firstHalf , "4294967296 " ));
150+ if (gmp_cmp ($ value , gmp_pow (2 , 63 )) >= 0 ) $ value = gmp_sub ($ value , gmp_pow (2 , 64 ));
151+ $ value =gmp_strval ($ value );
152+ }
153+ return $ value ;
154+ case self ::TAG_FLOAT : // Floating point value (32 bit, big endian, IEEE 754-2008)
155+ list (,$ value ) = (pack ('d ' , 1 ) == "\77\360\0\0\0\0\0\0" )?unpack ('f ' , fread ($ fp , 4 )):unpack ('f ' , strrev (fread ($ fp , 4 )));
156+ return $ value ;
157+ case self ::TAG_DOUBLE : // Double value (64 bit, big endian, IEEE 754-2008)
158+ list (,$ value ) = (pack ('d ' , 1 ) == "\77\360\0\0\0\0\0\0" )?unpack ('d ' , fread ($ fp , 8 )):unpack ('d ' , strrev (fread ($ fp , 8 )));
159+ return $ value ;
160+ case self ::TAG_BYTE_ARRAY : // Byte array
161+ $ arrayLength = $ this ->readType ($ fp , self ::TAG_INT );
162+ $ array = array_values (unpack ("c* " , fread ($ fp , $ arrayLength )));
163+ return $ array ;
164+ case self ::TAG_STRING : // String
165+ if (!$ stringLength = $ this ->readType ($ fp , self ::TAG_SHORT )) return "" ;
166+ $ string = utf8_decode (fread ($ fp , $ stringLength )); // Read in number of bytes specified by string length, and decode from utf8.
167+ return $ string ;
168+ case self ::TAG_LIST : // List
169+ $ tagID = $ this ->readType ($ fp , self ::TAG_BYTE );
170+ $ listLength = $ this ->readType ($ fp , self ::TAG_INT );
171+ if ($ this ->verbose ) trigger_error ("Reading in list of {$ listLength } tags of type {$ tagID }. " , E_USER_NOTICE );
172+ $ list = array ("type " =>$ tagID , "value " =>array ());
173+ for ($ i = 0 ; $ i < $ listLength ; $ i ++) {
174+ if (feof ($ fp )) break ;
175+ $ list ["value " ][] = $ this ->readType ($ fp , $ tagID );
176+ }
177+ return $ list ;
178+ case self ::TAG_COMPOUND : // Compound
179+ $ tree = array ();
180+ while ($ this ->traverseTag ($ fp , $ tree ));
181+ return $ tree ;
182+ case self ::TAG_INT_ARRAY :
183+ $ arrayLength = $ this ->readType ($ fp , self ::TAG_INT );
184+ $ array = array_values (unpack ("N* " , fread ($ fp , $ arrayLength * 4 )));
185+ return $ array ;
186+ case self ::TAG_LONG_ARRAY :
187+ $ arrayLength = $ this ->readType ($ fp , self ::TAG_INT );
188+ $ array = array_values (unpack ("J* " , fread ($ fp , $ arrayLength * 8 )));
189+ return $ array ;
190+ }
191+ }
192+
193+ public function writeType ($ fp , $ tagType , $ value ) {
194+ switch ($ tagType ) {
195+ case self ::TAG_BYTE : // Signed byte (8 bit)
196+ return is_int (fwrite ($ fp , pack ("c " , $ value )));
197+ case self ::TAG_SHORT : // Signed short (16 bit, big endian)
198+ if ($ value < 0 ) $ value += pow (2 , 16 ); // Convert signed short to unsigned short
199+ return is_int (fwrite ($ fp , pack ("n " , $ value )));
200+ case self ::TAG_INT : // Signed integer (32 bit, big endian)
201+ if ($ value < 0 ) $ value += pow (2 , 32 ); // Convert signed int to unsigned int
202+ return is_int (fwrite ($ fp , pack ("N " , $ value )));
203+ case self ::TAG_LONG : // Signed long (64 bit, big endian)
204+ if (PHP_INT_SIZE >=8 ) {
205+ $ firstHalf = ($ value & 0xFFFFFFFF00000000 ) >> 32 ;
206+ $ secondHalf = $ value & 0xFFFFFFFF ;
207+
208+ $ wResult = is_int (fwrite ($ fp , pack ("NN " , $ firstHalf , $ secondHalf )));
209+ } else {
210+ extension_loaded ("gmp " ) or trigger_error (
211+ "This file contains a 64-bit number and execution cannot continue. " .
212+ "Please install the GMP extension for 64-bit number handling. " , E_USER_ERROR
213+ );
214+
215+ // 32-bit values seem to be too long for pack() on 32-bit machines. Split into 4x16-bit instead.
216+ $ quarters [0 ] = gmp_div (gmp_and ($ value , "0xFFFF000000000000 " ), gmp_pow (2 ,48 ));
217+ $ quarters [1 ] = gmp_div (gmp_and ($ value , "0x0000FFFF00000000 " ), gmp_pow (2 ,32 ));
218+ $ quarters [2 ] = gmp_div (gmp_and ($ value , "0x00000000FFFF0000 " ), gmp_pow (2 ,16 ));
219+ $ quarters [3 ] = gmp_and ($ value , "0xFFFF " );
220+
221+ $ wResult = is_int (fwrite ($ fp , pack ("nnnn " , gmp_intval ($ quarters [0 ]), gmp_intval ($ quarters [1 ]), gmp_intval ($ quarters [2 ]), gmp_intval ($ quarters [3 ]))));
222+ }
223+ return $ wResult ;
224+ case self ::TAG_FLOAT : // Floating point value (32 bit, big endian, IEEE 754-2008)
225+ return is_int (fwrite ($ fp , (pack ('d ' , 1 ) == "\77\360\0\0\0\0\0\0" )?pack ('f ' , $ value ):strrev (pack ('f ' , $ value ))));
226+ case self ::TAG_DOUBLE : // Double value (64 bit, big endian, IEEE 754-2008)
227+ return is_int (fwrite ($ fp , (pack ('d ' , 1 ) == "\77\360\0\0\0\0\0\0" )?pack ('d ' , $ value ):strrev (pack ('d ' , $ value ))));
228+ case self ::TAG_BYTE_ARRAY : // Byte array
229+ return $ this ->writeType ($ fp , self ::TAG_INT , count ($ value )) && is_int (fwrite ($ fp , call_user_func_array ("pack " , array_merge (array ("c " .count ($ value )), $ value ))));
230+ case self ::TAG_STRING : // String
231+ $ value = utf8_encode ($ value );
232+ return $ this ->writeType ($ fp , self ::TAG_SHORT , strlen ($ value )) && is_int (fwrite ($ fp , $ value ));
233+ case self ::TAG_LIST : // List
234+ if ($ this ->verbose ) trigger_error ("Writing list of " .count ($ value ["value " ])." tags of type {$ value ["type " ]}. " , E_USER_NOTICE );
235+ if (!($ this ->writeType ($ fp , self ::TAG_BYTE , $ value ["type " ]) && $ this ->writeType ($ fp , self ::TAG_INT , count ($ value ["value " ])))) return false ;
236+ foreach ($ value ["value " ] as $ listItem ) if (!$ this ->writeType ($ fp , $ value ["type " ], $ listItem )) return false ;
237+ return true ;
238+ case self ::TAG_COMPOUND : // Compound
239+ foreach ($ value as $ listItem ) if (!$ this ->writeTag ($ fp , $ listItem )) return false ;
240+ if (!is_int (fwrite ($ fp , "\0" ))) return false ;
241+ return true ;
242+ case self ::TAG_INT_ARRAY : // Byte array
243+ return $ this ->writeType ($ fp , self ::TAG_INT , count ($ value )) && is_int (fwrite ($ fp , call_user_func_array ("pack " , array_merge (array ("N " .count ($ value )), $ value ))));
244+ case self ::TAG_LONG_ARRAY : // Byte array
245+ return $ this ->writeType ($ fp , self ::TAG_LONG , count ($ value )) && is_long (fwrite ($ fp , call_user_func_array ("pack " , array_merge (array ("J " .count ($ value )), $ value ))));
246+ }
247+ }
248+ }
249+ ?>
0 commit comments