+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($PLACEHOLDER);
+use Benchmark qw(cmpthese timethese);
+use CGI::Ex::Conf;
+use POSIX qw(tmpnam);
+
+$PLACEHOLDER = chr(186).'~'.chr(186);
+
+my $n = -2;
+
+my $cob = CGI::Ex::Conf->new;
+my %files = ();
+
+###----------------------------------------------------------------###
+
+# Rate yaml yaml2 sto pl xml g_conf ini sto2
+#yaml 250/s -- -1% -14% -14% -61% -77% -95% -95%
+#yaml2 254/s 1% -- -13% -13% -60% -77% -95% -95%
+#sto 292/s 17% 15% -- -0% -54% -73% -94% -95%
+#pl 292/s 17% 15% 0% -- -54% -73% -94% -95%
+#xml 636/s 155% 151% 118% 118% -- -42% -88% -88%
+#g_conf 1088/s 335% 329% 273% 272% 71% -- -79% -80%
+#ini 5144/s 1958% 1929% 1662% 1660% 708% 373% -- -3%
+#sto2 5321/s 2029% 1999% 1723% 1721% 736% 389% 3% --
+
+my $str = {
+ foo => {key1 => "bar", key2 => "ralph"},
+ pass => {key1 => "word", key2 => "ralph"},
+ garbage => {key1 => "can", key2 => "ralph"},
+ mighty => {key1 => "ducks", key2 => "ralph"},
+ quack => {key1 => "moo", key2 => "ralph"},
+ one1 => {key1 => "val1", key2 => "ralph"},
+ one2 => {key1 => "val2", key2 => "ralph"},
+ one3 => {key1 => "val3", key2 => "ralph"},
+ one4 => {key1 => "val4", key2 => "ralph"},
+ one5 => {key1 => "val5", key2 => "ralph"},
+ one6 => {key1 => "val6", key2 => "ralph"},
+ one7 => {key1 => "val7", key2 => "ralph"},
+ one8 => {key1 => "val8", key2 => "ralph"},
+};
+
+###----------------------------------------------------------------###
+
+# Rate yaml yaml2 pl sto xml g_conf sto2
+#yaml 736/s -- -3% -20% -21% -62% -72% -89%
+#yaml2 755/s 3% -- -18% -19% -61% -71% -89%
+#pl 923/s 25% 22% -- -1% -53% -65% -86%
+#sto 928/s 26% 23% 1% -- -53% -65% -86%
+#xml 1961/s 166% 160% 113% 111% -- -26% -71%
+#g_conf 2635/s 258% 249% 185% 184% 34% -- -61%
+#sto2 6824/s 827% 803% 639% 635% 248% 159% --
+
+#$str = {
+# foo => "bar",
+# pass => "word",
+# garbage => "can",
+# mighty => "ducks",
+# quack => "moo",
+# one1 => "val1",
+# one2 => "val2",
+# one3 => "val3",
+# one4 => "val4",
+# one5 => "val5",
+# one6 => "val6",
+# one7 => "val7",
+# one8 => "val8",
+#};
+
+###----------------------------------------------------------------###
+
+my $conf = eval $str;
+
+my %TESTS = ();
+
+### do perl
+my $dir = tmpnam;
+mkdir $dir, 0755;
+my $tmpnam = "$dir/bench";
+my $file = $tmpnam. '.pl';
+$TESTS{pl} = sub {
+ $cob->write_ref($file, $str);
+};
+$files{pl} = $file;
+
+### do a generic conf_write
+my $file2 = $tmpnam. '.g_conf';
+local $CGI::Ex::Conf::EXT_WRITERS{g_conf} = \&generic_conf_write;
+$TESTS{g_conf} = sub {
+ $cob->write_ref($file2, $str);
+};
+$files{g_conf} = $file2;
+
+
+### load in the rest of the tests that we support
+if (eval {require JSON}) {
+ my $_file = tmpnam(). '.json';
+ $TESTS{json} = sub {
+ $cob->write_ref($file, $str);
+ };
+ $files{json} = $_file;
+}
+
+if (eval {require Storable}) {
+ my $_file = $tmpnam. '.sto';
+ $TESTS{sto} = sub {
+ $cob->write_ref($file, $str);
+ };
+ $files{sto} = $_file;
+}
+
+if (eval {require Storable}) {
+ my $_file = $tmpnam. '.sto2';
+ $TESTS{sto2} = sub {
+ &Storable::store($str, $_file);
+ };
+ $files{sto2} = $_file;
+}
+
+if (eval {require YAML}) {
+ my $_file = $tmpnam. '.yaml';
+ $TESTS{yaml} = sub {
+ $cob->write_ref($_file, $str);
+ };
+ $files{yaml} = $_file;
+}
+
+if (eval {require YAML}) {
+ my $_file = $tmpnam. '.yaml2';
+ $TESTS{yaml2} = sub {
+ &YAML::DumpFile($_file, $str);
+ };
+ $files{yaml2} = $_file;
+}
+
+if (eval {require Config::IniHash}) {
+ my $_file = $tmpnam. '.ini';
+ $TESTS{ini} = sub {
+ local $^W = 0;
+ $cob->write_ref($_file, $str);
+ };
+ $files{ini} = $_file;
+}
+
+if (eval {require XML::Simple}) {
+ my $_file = $tmpnam. '.xml';
+ $TESTS{xml} = sub {
+ $cob->write_ref($_file, $str);
+ };
+ $files{xml} = $_file;
+}
+
+### tell file locations
+foreach my $key (sort keys %files) {
+ print "$key => $files{$key}\n";
+}
+
+foreach my $key (keys %TESTS) {
+ eval { &{ $TESTS{$key} } };
+ if ($@) {
+ warn "Test for $key failed - skipping";
+ delete $TESTS{$key};
+ }
+}
+
+
+cmpthese timethese ($n, \%TESTS);
+
+### comment out this line to inspect files
+unlink $_ foreach values %files;
+rmdir $dir;
+
+###----------------------------------------------------------------###
+
+sub generic_conf_read {
+ my $_file = shift || die "No filename supplied";
+ my $sep_by_newlines = ($_[0] && lc($_[0]) eq 'sep_by_newlines') ? 1 : 0;
+
+ ### fh will now lose scope and close itself if necessary
+ my $FH = do { local *FH; *FH };
+ open ($FH, $_file) || return {};
+
+ my $x = 0;
+ my $conf = {};
+ my $key = '';
+ my $val;
+ my $line;
+ my ($is_array,$is_hash,$is_multiline);
+ my $order;
+ $order = [] if wantarray;
+
+ while( defined($line = <$FH>) ){
+ last if ! defined $line;
+ last if $x++ > 10000;
+
+ next if index($line,'#') == 0;
+
+ if ($line =~ /^\s/ && ($is_multiline || $line ne "\n")){
+ next if ! length($key);
+ $conf->{$key} .= $line;
+ $is_multiline = 1;
+
+ }else{
+ ### duplicate trim section
+ if( length($key) ){
+ $conf->{$key} =~ s/\s+$//;
+ if( $is_array || $is_hash ){
+ $conf->{$key} =~ s/^\s+//;
+ my $urldec = (index($conf->{$key},'%')>-1 || index($conf->{$key},'+')>-1);
+ my @pieces;
+ if ($sep_by_newlines) {
+ @pieces = split(/\s*\n\s*/,$conf->{$key});
+ @pieces = map {split(/\s+/,$_,2)} @pieces if $is_hash;
+ } else {
+ @pieces = split(/\s+/,$conf->{$key});
+ }
+ if( $urldec ){
+ foreach my $_val (@pieces){
+ $_val =~ y/+/ / if ! $sep_by_newlines;
+ $_val =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+ }
+ }
+ if( $is_array ){
+ foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+ $conf->{$key} = \@pieces;
+ }elsif( $is_hash ){
+ foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+ shift(@pieces) if scalar(@pieces) % 2;
+ $conf->{$key} = {@pieces};
+ }
+ }elsif( ! $is_multiline ){
+ $conf->{$key} =~ y/+/ / if ! $sep_by_newlines;
+ $conf->{$key} =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+ }
+ }
+
+ ($key,$val) = split(/\s+/,$line,2);
+ $is_array = 0;
+ $is_hash = 0;
+ $is_multiline = 0;
+ if (! length($key)) {
+ next;
+ } elsif (index($key,'array:') == 0) {
+ $is_array = $key =~ s/^array://i;
+ } elsif (index($key,'hash:') == 0) {
+ $is_hash = $key =~ s/^hash://i;
+ }
+ $key =~ y/+/ / if ! $sep_by_newlines;
+ $key =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+ $conf->{$key} = $val;
+ push @$order, $key if $order;
+ }
+ }
+
+ ### duplicate trim section
+ if( length($key) && defined($conf->{$key}) ){
+ $conf->{$key} =~ s/\s+$//;
+ if( $is_array || $is_hash ){
+ $conf->{$key} =~ s/^\s+//;
+ my $urldec = (index($conf->{$key},'%')>-1 || index($conf->{$key},'+')>-1);
+ my @pieces;
+ if ($sep_by_newlines) {
+ @pieces = split(/\s*\n\s*/,$conf->{$key});
+ @pieces = map {split(/\s+/,$_,2)} @pieces if $is_hash;
+ } else {
+ @pieces = split(/\s+/,$conf->{$key});
+ }
+ if( $urldec ){
+ foreach my $_val (@pieces){
+ $_val =~ y/+/ / if ! $sep_by_newlines;
+ $_val =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+ }
+ }
+ if( $is_array ){
+ foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+ $conf->{$key} = \@pieces;
+ }elsif( $is_hash ){
+ foreach (@pieces){ $_="" if index($_,$PLACEHOLDER)>-1 }
+ shift(@pieces) if scalar(@pieces) % 2;
+ $conf->{$key} = {@pieces};
+ }
+ }elsif( ! $is_multiline ){
+ $conf->{$key} =~ y/+/ / if ! $sep_by_newlines;
+ $conf->{$key} =~ s/%([a-f0-9]{2})/chr(hex($1))/egi;
+ }
+ }
+
+
+ close($FH);
+ return $order ? ($conf,$order) : $conf;
+}
+
+
+sub generic_conf_write{
+ my $_file = shift || die "No filename supplied";
+
+ if (! @_) {
+ return;
+ }
+
+ my $new_conf = shift || die "Missing update hashref";
+ return if ! keys %$new_conf;
+
+
+ ### do we allow writing out hashes in a nice way
+ my $sep_by_newlines = ($_[0] && lc($_[0]) eq 'sep_by_newlines') ? 1 : 0;
+
+ ### touch the file if necessary
+ if( ! -e $_file ){
+ open(TOUCH,">$_file") || die "Conf file \"$_file\" could not be opened for writing: $!";
+ close(TOUCH);
+ }
+
+ ### read old values
+ my $conf = &generic_conf_read($_file) || {};
+ my $key;
+ my $val;
+
+ ### remove duplicates and undefs
+ while (($key,$val) = each %$new_conf){
+ $conf->{$key} = $new_conf->{$key};
+ }
+
+ ### prepare output
+ my $output = '';
+ my $qr = qr/([^\ \!\"\$\&-\*\,-\~])/;
+ foreach $key (sort keys %$conf){
+ next if ! defined $conf->{$key};
+ $val = delete $conf->{$key};
+ $key =~ s/([^\ \!\"\$\&-\*\,-9\;-\~\/])/sprintf("%%%02X",ord($1))/eg;
+ $key =~ tr/\ /+/;
+ my $ref = ref($val);
+ if( $ref ){
+ if( $ref eq 'HASH' ){
+ $output .= "hash:$key\n";
+ foreach my $_key (sort keys %$val){
+ my $_val = $val->{$_key};
+ next if ! defined $_val;
+ $_val =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+ $_key =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+ if ($sep_by_newlines) {
+ $_val =~ s/^(\s)/sprintf("%%%02X",ord($1))/ego;
+ $_val =~ s/(\s)$/sprintf("%%%02X",ord($1))/ego;
+ $_key =~ s/\ /%20/g;
+ } else {
+ $_val =~ tr/\ /+/;
+ $_key =~ tr/\ /+/;
+ }
+ $_val = $PLACEHOLDER if ! length($_val);
+ $output .= "\t$_key\t$_val\n";
+ }
+ }elsif( $ref eq 'ARRAY' ){
+ $output .= "array:$key\n";
+ foreach (@$val){
+ my $_val = $_;
+ $_val =~ s/$qr/sprintf("%%%02X",ord($1))/ego;
+ if ($sep_by_newlines) {
+ $_val =~ s/^(\s)/sprintf("%%%02X",ord($1))/ego;
+ $_val =~ s/(\s)$/sprintf("%%%02X",ord($1))/ego;
+ } else {
+ $_val =~ tr/\ /+/;
+ }
+ $_val = $PLACEHOLDER if ! length($_val);
+ $output .= "\t$_val\n";
+ }
+ }else{
+ $output .= "$key\tbless('$val','$ref')\n"; # stringify the ref
+ }
+ }else{
+ if( $val =~ /\n/ ){ # multiline values that are indented properly don't need encoding
+ if( $val =~ /^\s/ || $val =~ /\s$/ || $val =~ /\n\n/ || $val =~ /\n([^\ \t])/ ){
+ if ($sep_by_newlines) {
+ $val =~ s/([^\!\"\$\&-\~])/sprintf("%%%02X",ord($1))/eg;
+ } else {
+ $val =~ s/([^\ \!\"\$\&-\*\,-\~])/sprintf("%%%02X",ord($1))/eg;
+ $val =~ y/ /+/;
+ }
+ }
+ }else{
+ $val =~ s/([^\ \t\!\"\$\&-\*\,-\~])/sprintf("%%%02X",ord($1))/eg;
+ $val =~ s/^(\s)/sprintf("%%%02X",ord($1))/eg;
+ $val =~ s/(\s)$/sprintf("%%%02X",ord($1))/eg;
+ }
+ $output .= "$key\t$val\n";
+ }
+ }
+
+ open (CONF,"+<$_file") || die "Could not open the file for writing ($_file) -- [$!]";
+ print CONF $output;
+ truncate CONF, length($output);
+ close CONF;
+
+ return 1;
+}
+
+1;
+