#!/usr/bin/perl -w

# pragmas
use strict;

# id
use vars qw( $NAME $VERSION );
$NAME = 'cobfuse';
$VERSION = '1.01';

# modules
use Getopt::Std;

# run
main();

sub main
{
	# process command line arguments
	my %opts = ();
	if( ! getopts( 'o:V', \%opts ) ) {
		die( "Usage: $NAME [-o <output file>] [-V] [<input file>]\n" );
	}
	elsif( defined( $opts{ 'V' } ) ) {
		die( "$NAME $VERSION\n" );
	}

	# load text to obfuscate
	my @text = ();
	my $input_file = ( defined( $ARGV[ 0 ] ) ) ? $ARGV[ 0 ] : undef;
	if( defined( $input_file ) ) {
		if( ! -e $input_file ) {
			die( "$NAME: File does not exist: $input_file\n" );
		}
		elsif( ! -f _ || ! -T _ ) {
			die( "$NAME: Not a plain text file: $input_file\n" );
		}
		elsif( ! open( INPUT, "< $input_file" ) ) {
			die( "$NAME: File could not be read: $input_file\n" );
		}

		@text = <INPUT>;
		close( INPUT );
	}
	else {
		@text = <STDIN>;
	}

	# init
	my $header = "";
	my ( $in_comment, $in_pre, $in_esql, $in_esql_decl ) = ( 0, 0, 0, 0 );
	my ( %number_map, %string_map, %keyword_map ) = ( (), (), () );
	my @keywords = (
		'asm', 'auto',
		'bool', 'break',
		'case', 'catch', 'char', 'class', 'const', 'const_cast', 'continue',
		'default', 'delete', 'do', 'double', 'dynamic_cast',
		'else', 'enum', 'explicit', 'export', 'extern',
		'false', 'float', 'for', 'friend',
		'goto',
		'if', 'inline', 'int',
		'long',
		'mutable',
		'namespace', 'new',
		'operator',
		'private', 'protected', 'public',
		'register', 'reinterpret_cast', 'return',
		'short', 'signed', 'sizeof', 'static', 'static_cast', 'struct', 'switch',
		'template', 'this', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename',
		'union', 'unsigned', 'using',
		'virtual', 'void', 'volatile',
		'wchar_t', 'while'
	);
	my @operators = (
		'::',
		'(', ')', '[', ']', '->', '.', '++', '--',
		'!', '~',
		'*', '/', '%',
		'+', '-',
		'<<', '>>',
		'<', '<=', '>', '=>',
		'==', '!=',
		'&', '^', '|',
		'&&' ,'||',
		'?', ':',
		'=',
		','
	);

	# process text line by line
	foreach( @text ) {
		# remove C style comment
		if( m!\/\*.*! ) {
			if( m!\*\/! ) { # comment ends in the same line
				s!\/\*.*?\*\/!!;
			}
			else {
				s!\/\*.*!!;
				$in_comment = 1;
			}
		}
		elsif( $in_comment == 1 ) {
			if( m!\*\/! ) { # end of multi-line comment
				s!.*?\*\/!!;
				$in_comment = 0;
			}
			else {
				s!.*!!;
			}
		}

		# remove C++ style comment
		s!\/\/.*!!;
		
		# remove indent
		s/^[ \t]+//;

		# remove trailing whitespace
		s/[ \t]+$//;

		# ignore preprocessor commands
		if( m/^#/ || $in_pre == 1 ) {
			$in_pre = ( m/\\$/ ) ? 1 : 0;
			s/$/DONOTCOBFUSE/; # workaround, see "concatenate lines" further below
			next;
		}

		# ignore embedded SQL
		if( m/^exec[ \t]+sql/i || $in_esql == 1 ) {
			if( m/begin[ \t]+declare[ \t]+section/i ) {
				$in_esql = $in_esql_decl = 1;
			}
			elsif( m/end[ \t]+declare[ \t]+section/i ) {
				$in_esql = $in_esql_decl = 0;
			}
			elsif( $in_esql_decl != 1 ) {
				$in_esql = ( m/;$/ ) ? 0 : 1;
			}
			next;
		}
		
		# replace strings
		while( m/((?<!\\)".*?(?<!\\)")/ ) {
			my $string = $1;
			if( ! defined( $string_map{ $string } ) ) {
				$string_map{ $string } = new_token();
				$header .=
					'#define ' . $string_map{ $string } . ' ' . $string . "\n";
			}
			s/\Q$string\E/$string_map{$string}/;
		}

		# replace numbers
		while( m/\b(?<!\\)(\d+\.?\d*)\b/ ) {
			my $number = $1;
			if( ! defined( $number_map{ $number } ) ) {
				$number_map{ $number } = new_token();
				$header .=
					'#define ' . $number_map{ $number } . ' ' . $number . "\n";
			}
			s/\b$number\b/$number_map{$number}/;
		}

		# replace C and C++ keywords
		foreach my $keyword( @keywords ) {
			while( m/\b$keyword\b/ ) {
				if( ! defined( $keyword_map{ $keyword } ) ) {
					$keyword_map{ $keyword } = new_token();
					$header .=
						'#define ' . $keyword_map{ $keyword } . ' '. $keyword .  "\n";
				}
				s/\b$keyword\b/$keyword_map{$keyword}/;
			}
		}
	}

	# process text as a whole
	my $text = join( '', @text );
	
	# remove empty lines
	$text =~ s/^\n+//;
	$text =~ s/\n{2,}/\n/g;

	# concatenate lines
	$text =~ s/\{\n(?!#)/\{/g;
	$text =~ s/(?<!DONOTCOBFUSE)\n\}/\}/g;
	$text =~ s/DONOTCOBFUSE//g;

	# remove whitespace around operators
	foreach my $op( @operators ) {
		$text =~ s/[ \t]*\Q$op\E[ \t]*/$op/g;
	}

	# put out
	if( defined( $opts{ 'o' } ) ) {
		if( ! open( OUTPUT, "> $opts{ 'o' }" ) ) {
			die( "$NAME: File could not be written: $opts{ 'o' }\n" );
		}
		print OUTPUT ( $header, $text );
		close( OUTPUT );
	}
	else {
		print STDOUT ( $header, $text );
	}

	exit( 0 );
}

BEGIN
{
	my $index = -1;
	my $xp = 10; # 2^xp = number of tokens to generate
	my @tokens =
		map { substr( $_, -$xp ) }
		map { unpack( "B*", pack( "n", $_ ) ) } # decimal to binary conversion
		0..(2 ** $xp - 1);
	@tokens = sort { ( -1, 1 )[ rand( 2 ) ] } @tokens; # shuffle
	foreach( @tokens ) { tr/0/I/, tr/1/l/ } # obliterate

	sub new_token
	{
		$index++;
		return $tokens[ $index ];
	}
}

