2012-04-17

Static code analyzer for Torque 3D BitStreams

Some very crude code that finds out of balance objects in terms of server/client bit stream communication in the Torque 3D game engine. I know, exciting, right?!



Of course, this does not happen in a vanilla Torque 3D environment, but heavily and often changing the source can leave you with a network issue that is very hard to find and might pop up only months after the bug had been planted.

So run this php script on your source directory, but set the value of $sourcedir with a trailing slash (/) first, and then wait for it to execute. It will generate an unbalanced.txt file for you in the source root which you can check for a report.

The output will look something like this:

1
2
3
// 1 Bitcount mismatch between packData [3532] and unpackData [3539] in X:/Xenocell/engine/source/T3D/fx/explosion.cpp.
// 2 Bitcount mismatch between pack [72] and unpack [68] in X:/Xenocell/engine/source/T3D/fx/lightning.cpp.
// 3 Bitcount mismatch between packUpdate [268] and unpackUpdate [267] in X:/Xenocell/engine/source/T3D/item.cpp.

The following is an excerpt from my resource post over at GarageGames.com:

Like all static code analyzers, this is not 100% exact, but to my defense, it will almost surely find it if bit counts mismatch in ie. a packUpdate and an unpackUpdate. The downside is that it reports a few false positives. I had to leave these in to not decrease the chance of it finding a real problem.
The script goes through all .cpp files in your source directory and checks whether they have pack/unpack, packData/unpackData, packUpdate/unpackUpdate and readPacketData/writePacketData methods, and if they do, it will compare the stream i/o commands used. It tries to find the number of bits used in each operation where possible, or assign some realistic value.
The downside if that it does not currently compare the order of these commands, but being able to find an issue where you write one bit more than you read is a pretty good thing in itself if I may say so.
False positives are usually cases, where the program structures to write and to read bits into the bitstream are different. (Ie. using a writeFlag(true) and a writeFlag(false) in two branches of a condition which is read by a readFlag()) There are currently about 6 such reports in vanilla Torque 3D 1.2 and 6+1 in AFX for Torque 3D 1.2. So if you get mismatches over these values, be very suspicious! :)
If you intend to use this tool to check on your code from time to time, you might want to rewrite the parts where false positives are reported, so that they don't mislead you the next time.
And the PHP source code. Disclaimer: it was written quickly out of an urgent necessity, but it works :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
<?php
//--------------------------------------------------------------------------------------
// Static Network BitStream Analyzer for Torque 3D (c) 2012 Bitgap Games by Konrad Kiss
//--------------------------------------------------------------------------------------
 
$sourcedir = "X:/Xenocell/engine/source/";
 
error_reporting(E_ALL);
function errorhandler($errno, $errstr, $errfile, $errline) {
   echo($errstr." in ".$errfile." on line ".$errline);
   exit;
//   return true;
}
$old_error_handler = set_error_handler("errorhandler");
 
$cmdCosts = array(
   // write fn => array(regexp, costParamPos (1..) or 0, defaultCost)
   "writeInt" => array("stream\-\>writeInt[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 32),
   "readInt" => array("stream\-\>readInt[\W]*\(([^)\n]+)\)", 1, 32),
   "mathWrite" => array("mathWrite[\W]*\([\W]*\*[\W]*stream[\W]*\,([^)\n]+)\)", 0, 64),
   "mathRead" => array("mathRead[\W]*\([\W]*\*[\W]*stream[\W]*\,([^)\n]+)\)", 0, 64),
   "writeCussedU32" => array("stream\-\>writeCussedU32[\W]*\(([^)\n]+)\)", 0, 37),
   "readCussedU32" => array("stream\-\>readCussedU32[\W]*\(([^)\n]*)\)", 0, 37),
   "writeSignedInt" => array("stream\-\>writeSignedInt[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 32),
   "readSignedInt" => array("stream\-\>readSignedInt[\W]*\(([^)\n]+)\)", 1, 32),
   "writeRangedU32" => array("stream\-\>writeRangedU32[\W]*\(([^,\n]+)\,([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "readRangedU32" => array("stream\-\>readRangedU32[\W]*\(([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "writeRangedS32" => array("stream\-\>writeRangedS32[\W]*\(([^,\n]+)\,([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "readRangedS32" => array("stream\-\>readRangedS32[\W]*\(([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "writeFloat" => array("stream\-\>writeFloat[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 32),
   "readFloat" => array("stream\-\>readFloat[\W]*\(([^)\n]+)\)", 1, 32),
   "writeSignedFloat" => array("stream\-\>writeSignedFloat[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 32),
   "readSignedFloat" => array("stream\-\>readSignedFloat[\W]*\(([^)\n]+)\)", 1, 32),
   "writeRangedF32" => array("stream\-\>writeRangedF32[\W]*\(([^,\n]+)\,([^,\n]+)\,([^,\n]+)\,([^)\n]+)\)", 4, 32),
   "readRangedF32" => array("stream\-\>readRangedF32[\W]*\(([^,\n]+)\,([^,\n]+)\,([^)\n]+)\)", 3, 32),
   "writeClassId" => array("stream\-\>writeClassId[\W]*\(([^,\n]+)\,([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "readClassId" => array("stream\-\>readClassId[\W]*\(([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "writeNormalVector" => array("stream\-\>writeNormalVector[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 96),
   "readNormalVector" => array("stream\-\>readNormalVector[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 96),
   "writeCompressedPoint" => array("stream\-\>writeCompressedPoint[\W]*\(([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "readCompressedPoint" => array("stream\-\>readCompressedPoint[\W]*\(([^,\n]+)\,([^)\n]+)\)", 0, 32),
   "writeVector" => array("stream\-\>writeVector[\W]*\(([^,\n]+)\,([^,\n]+)\,([^,\n]+)\,([^)\n]+)\)", 4, 32),
   "readVector" => array("stream\-\>readVector[\W]*\(([^,\n]+)\,([^,\n]+)\,([^,\n]+)\,([^)\n]+)\)", 4, 32),
   "writeAffineTransform" => array("stream\-\>writeAffineTransform[\W]*\(([^)\n]+)\)", 0, 96),
   "readAffineTransform" => array("stream\-\>readAffineTransform[\W]*\(([^)\n]+)\)", 0, 96),
   "writeQuat" => array("stream\-\>writeQuat[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 9),
   "readQuat" => array("stream\-\>readQuat[\W]*\(([^,\n]+)\,([^)\n]+)\)", 2, 9),
   "writeQuat2" => array("stream\-\>writeQuat[\W]*\(([^,\n]+)\)", 0, 9),
   "readQuat2" => array("stream\-\>readQuat[\W]*\(([^,\n]+)\)", 0, 9),
   "writeBits" => array("stream\-\>writeBits[\W]*\(([^,\n]+)\,([^)\n]+)\)", 1, 1),
   "readBits" => array("stream\-\>readBits[\W]*\(([^,\n]+)\,([^)\n]+)\)", 1, 1),
   "writeBits2" => array("stream\-\>writeBits[\W]*\(([^)\n]+)\)", 0, 1),
   "readBits2" => array("stream\-\>readBits[\W]*\(([^)\n]+)\)", 0, 1),
//   "writeFlagTrue" => array("stream\-\>writeFlag[\W]*\([ ]*true[ ]*\)", 0, 0), // trick to not have to parse conditions
//   "writeFlagFalse" => array("stream\-\>writeFlag[\W]*\([ ]*false[ ]*\)", 0, 0), // trick to not have to parse conditions
   "writeFlag" => array("stream\-\>writeFlag[\W]*\(([^)\n]+)\)", 0, 1),
   "readFlag" => array("stream\-\>readFlag[\W]*\(([^)\n]*)\)", 0, 1),
   "readFlag2" => array("_readDirtyFlag[\W]*\([\W]*stream[\W]*\,([^)\n]+)\)", 0, 1), // sfxEmitter exotics
   "writeString" => array("stream\-\>writeString[\W]*\(([^,\n]+)\,([^)\n]+)\)", 1, 256*8),
   "writeString2" => array("stream\-\>writeString[\W]*\(([^)\n]+)\)", 0, 256*8),
   "readString" => array("stream\-\>readString[\W]*\(([^)\n]+)\)", 0, 256*8),
   "readSTString" => array("stream\-\>readSTString[\W]*\(([^)\n]*)\)", 0, 256*8),
   "write" => array("stream\-\>write[\W]*\(([^)\n]+)\)", 0, 64),
   "read" => array("stream\-\>read[\W]*\(([^)\n]+)\)", 0, 64),
);
 
$errorcount = 0;
$currentFile = "";
$output = "";
$summary = "";
 
uecho("// ****************************************************************\n");
find_files($sourcedir, "/cpp$/", "examine");
file_put_contents($sourcedir."unbalanced.txt", "// SUMMARY\n\n\n".$summary."\n\n\n\n// DETAILED OUTPUT".$output);
uecho("// ****************************************************************\n");
 
//------------------------------------------------------------------------------
function fn_countbits($source, $bitstreamName)
//------------------------------------------------------------------------------
{
   global $cmdCosts, $currentFile;
   $lines = explode("\n", $source);
   $count = 0;
   $cmdlist = "";
   for ($l=0;$l<count($lines);$l++) {
      //uecho(".");
      $line = $lines[$l];
      // strip comments
      foreach ($cmdCosts as $cmd => $data) {
         $pattern = "/".str_replace("stream", $bitstreamName, $data[0])."/s";
         preg_match($pattern, $line, $params);
         if (count($params)>0)
         {
            if ($data[1]>0) {
               // there is a parameter that explicitly defines the number of used bits for this operation
               if (is_numeric($params[$data[1]])) {
                  // the cost is a clear numeric bit count
                  $cost = $params[$data[1]];
               } else {
                  // the cost could be a variable or a function perhaps - we'll default to a cost of 1 here
                  $cost = 1;
               }
            } else {
               // no paremeters are used to define bitcount... we can then use a default bitcount for comparison
               // it could be 0, which means that the command should not be used to compare values
               // It is used in the often case when writing a flag is broken up into the two branches of a condition while
               // only one command is reading the other side.
               $cost = $data[2];
            }
            //uecho($cmd." : ".$cost."\n");//." [".$data[1]." / ".$params[$data[1]]." / ".$data[2]."]\n");
            $count += $cost;
            $cmdlist .= $cmd." - ".$cost." @ line ".$l."\n";
            if ($cmd=="writeFlagTrue" || $cmd=="writeFlagFalse")
               break;
         }
      }
       
      //uecho(trim($lines[$l]," \t\n\r")."\n");
   }
   //$cmdcount = substr_count($source, $bitstreamName."->");
   return array($count, $cmdlist);
}
 
//------------------------------------------------------------------------------
function compare($filepath, $fn1, $fn2)
//------------------------------------------------------------------------------
{
   global $errorcount;
 
   $content = file_get_contents($filepath);
 
   // remove comments (basic, but mostly does the job)
   $content = preg_replace('/(\/\/[^\n]*\n)/sm', "\n", $content);
   $content = preg_replace('/(\/\*.*?\*\/)/sm', "", $content);
 
   // get rid of all return characters
   $content = str_replace("\r", "", $content);
 
   // replace multiple new lines with a single new line
   $content = preg_replace('/([\n]+)/sm', "\n", $content);
 
   // remove irrelevant lines
   $wasif = true;
   $wassec = true;
   $wasfn = true;
   $wasrw = true;
   $wassd = false;
   $lvl = 0;
   $modified = true;
   $newcontent = "";
   foreach (explode("\n", $content) as $linenum => $line)
   {
      //uecho("*");
      // replace multiple tabs and spaces with single spaces
      //$line = preg_replace('/([\t ]+)/sm', " ", $line);
      //$line = trim($line, " \t");
      $line = rtrim($line, " \t");
 
      // remove strings between ""
      //$line = preg_replace('/(\")[^\"]*(\")/', "", $line);
 
      if ($line=="") {
         continue;
      }
 
      $keep = false;
 
      $if0 = (strpos($line, "if (") !== false ||
               strpos($line, "if(") !== false );
      $if1 = (strpos($line, "else") !== false );
      $if = ($if0 || $if1);
 
      $sec =(strpos($line, "{") !== false ||
               strpos($line, "}") !== false );
      $fn = (strpos($line, "::".$fn1) !== false ||
               strpos($line, "::".$fn2) !== false );
      $rw = (strpos($line, "->write") !== false ||
               strpos($line, "->read") !== false ||
               strpos($line, "mathRead") !== false ||
               strpos($line, "mathWrite") !== false );
 
      $sd = strpos($line, ";") !== false;
      $sp = strpos($line, "(") !== false;
      $ep = strpos($line, ")") !== false;
 
      $separator = "\n";
 
      if (!$wassd && !$wassec && !$if && !$rw) {
         $separator = " ";
         $line = trim($line, " \t");
         $modified = true;
      }
 
      $newcontent .= $separator.$line;
 
      $wasif = $if;
      $wassec = $sec;
      $wasfn = $fn;
      $wasrw = $rw;
      $wassd = $sd;
   }
   $content = $newcontent;
 
   // remove empty {}-s
   //$content = preg_replace('/(\{[ \t\n\r]*\})/sm', "", $content);
 
   $fnone = fn_contents($content, $fn1);
   $fntwo = fn_contents($content, $fn2);
 
   if ($fnone === false && $fntwo == false)
      return;
 
   if (($fnone === false || $fntwo === false)) {
      $errorcount++;
      uecho("\n\n\n");
      uecho("// ".$errorcount." Missing either a(n) ".$fn1." or a(n) ".$fn2." method in ".$filepath.".\n", true);
      return;
   }
 
   $one = fn_countbits($fnone[1], $fnone[0]);
   $two = fn_countbits($fntwo[1], $fntwo[0]);
 
   if ($one[0] != $two[0]) {
      $errorcount++;
      uecho("\n\n\n");
      uecho("// ".$errorcount." Bitcount mismatch between ".$fn1." [".$one[0]."] and ".$fn2." [".$two[0]."] in ".$filepath.".\n", true);
      uecho("\n// ".str_pad($fn1." (".$fnone[0]." - ".$one[0].") ", 66, "+")."\n");
      uecho($fnone[1]);
      uecho("\n\n".$one[1]);
      uecho("\n// ".str_pad($fn2." (".$fntwo[0]." - ".$two[0].") ", 66, "+")."\n");
      uecho($two[1]."\n\n");
      uecho($fntwo[1]);
      uecho("\n// ".str_pad("", 66, "+")."\n");
   }
}
 
//------------------------------------------------------------------------------
function examine($filepath)
//------------------------------------------------------------------------------
{
   global $currentFile;
   $currentFile = $filepath;
   compare($filepath, "pack", "unpack");
   compare($filepath, "packData", "unpackData");
   compare($filepath, "packUpdate", "unpackUpdate");
   compare($filepath, "readPacketData", "writePacketData");
}
 
//------------------------------------------------------------------------------
function fn_contents($content, $functionName)
//------------------------------------------------------------------------------
{
   $fnofs = strpos($content, "::".$functionName);
   if ($fnofs === false)
      return;
 
   // find the last enter before the function
   $lastenterpos = strrpos(substr($content, 0, $fnofs), "\n");
   $content = substr($content, $lastenterpos+1);
   $content = str_replace("\"@", "[at]", $content);   // we need the @ and ` characters to replace } and {
                                                      // the at sign is only found in doc references
   $end = 0;
   $lvl = 0;
   $l = strlen($content);
   for ($p=0;$p<$l;$p++) {
      $ch = substr($content, $p, 1);
      if ($ch=="{") {
         if ($lvl>0)
            $content = substr_replace($content, "`", $p, 1);
         $lvl++;
      } else if ($ch=="}") {
         if ($lvl>1)
            $content = substr_replace($content, "@", $p, 1);
         if ($lvl>0) {
            $lvl--;
            if ($lvl==0) {
               $end = $p;
               break;
            }
         }
      }
   }
   $content = substr($content, 0, $end+1);
 
   $pattern = "/(\w+) \b(\w+)::".$functionName."\(([^{]+)\{(.+?)}/s";
   preg_match($pattern, $content, $matches);
   if (count($matches)>0)
   {
      $bitstreamName = trim(substr($matches[3], strpos($matches[3], "BitStream")+9), " *)\n\t\r");
      $par = strpos($bitstreamName, ")");
      if ($par) $bitstreamName = substr($bitstreamName, 0, $par-1);
      $com = strpos($bitstreamName, ",");
      if ($com) $bitstreamName = substr($bitstreamName, 0, $com-1);
 
      $fnsource = str_replace("`", "{", str_replace("@", "}", $matches[4]));
      $fnsource = str_replace("[at]", "@", $fnsource);
   } else {
      return false;
   }
 
   $res = array();
   $res[0] = $bitstreamName;
   $res[1] = $fnsource;
 
   return $res;
}
 
//------------------------------------------------------------------------------
function find_files($path, $pattern, $callback)
//------------------------------------------------------------------------------
{
   $path = rtrim(str_replace("\\", "/", $path), '/') . '/*';
   foreach (glob ($path) as $fullname) {
      //uecho("%");
      if (basename($fullname)=="_svn" || basename($fullname)==".svn")
         continue;
      if (is_dir($fullname)) {
         //uecho("\n".$fullname.":\n", true);
         find_files($fullname, $pattern, $callback);
      } else if (preg_match($pattern, $fullname)) {
         //uecho("        ".$fullname." (" . round(filesize($fullname)/1024, 2) . "kBytes\n");
         call_user_func($callback, $fullname);
      }
   }
}
 
//------------------------------------------------------------------------------
function uecho($str, $important=false)
//------------------------------------------------------------------------------
{
   global $output, $summary;
   if ($important==true) {
      $summary .= $str;
   }
   $output .= $str;
 echo($str);
}
 
?>

Try changing the bitcount of a single writeInt for example, and see if it catches it!

No comments:

Post a Comment