AmberDB
view release on metacpan or search on metacpan
lib/AmberDB/Base/Encoder.pm view on Meta::CPAN
}
my $out = "\x00ABR\x05\x00" . pack( "n", scalar @fields );
for my $f (@fields) {
$out .= $self->_abr_encode_node( $f, 0 );
}
return $out;
}
# my @fields = $adb->db_decode($record);
# my $hash_ref = $adb->db_decode($record);
# ------------------------------------------------
sub db_decode {
my ( $self, $record ) = @_;
return unless defined $record && length $record;
# ABR Binary Check (5-byte magic signature \x00ABR\x05 or \x00ABR\x01)
if ( length($record) >= 7 && substr( $record, 0, 4 ) eq "\x00ABR" && ( substr( $record, 4, 1 ) eq "\x05" || substr( $record, 4, 1 ) eq "\x01" ) ) {
my $mode = substr( $record, 5, 1 );
my $pos = 6;
if ( $mode eq "\x01" ) {
# Single root reference
return $self->_abr_decode_node( \$record, \$pos, 0 );
}
elsif ( $mode eq "\x00" ) {
# Multiple fields
my $fcnt = unpack( "n", substr( $record, $pos, 2 ) );
$pos += 2;
my @fields;
for ( 1 .. $fcnt ) {
push @fields, $self->_abr_decode_node( \$record, \$pos, 0 );
}
return wantarray ? @fields : ( @fields == 1 ? $fields[0] : \@fields );
}
}
# TRANSPARENT FALLBACK: Legacy Text Format Decoding
return $self->tsv_decode($record);
}
# ------------------------------------------------
# tsv_decode: Multi-era decoder for historical and text-based records (v1 - v4)
# ------------------------------------------------
sub tsv_decode {
my ( $self, $record, $expected_rid ) = @_;
return () unless defined $record && length($record);
# If already ABR binary, decode directly via db_decode
if ( length($record) >= 7 && substr( $record, 0, 4 ) eq "\x00ABR" ) {
return $self->db_decode($record);
}
if ( $self && ref($self) ) {
$record = $self->utf_decode($record);
}
# drop line endings: chomp
$record =~ s/\R$//;
# FAST-PATH: Plain TSV record without escapes, entities, or nested tags
if ( index($record, "\\") == -1 && index($record, "<TAB") == -1 && index($record, "&#") == -1 && index($record, "ARRAY:") == -1 && index($record, "HASH:") == -1 ) {
my @fields = split( /\t/, $record, -1 );
return wantarray ? @fields : ( @fields == 1 ? $fields[0] : \@fields );
}
# 1. ERA 2019 - 2025: <TAB> Hierarchy (<TAB0>, <TAB1>, <TAB2>, <TAB3>)
if ( $record =~ /<TAB[0-9]+>/ ) {
my $white_decode = sub {
return map {
my $s = $_;
if ( defined $s ) {
$s =~ s/\\(.)/$1 eq "t" ? "\t" : $1 eq "n" ? "\n" : $1 eq "r" ? "\r" : $1 eq "T" ? "\\T" : $1/eg;
}
$s;
} @_;
};
my $rid_prefix;
if ( $record =~ /^([a-zA-Z0-9_\-\.]+)(?:<TAB0>|\t)(.*)$/s ) {
my ( $candidate_rid, $rest ) = ( $1, $2 );
if ( !defined $expected_rid || $candidate_rid eq $expected_rid ) {
$rid_prefix = $candidate_rid;
$record = $rest;
}
}
my @fields = $record =~ /<TAB0>/ ? split( /<TAB0>/, $record, -1 ) : split( /\t/, $record, -1 );
@fields = $white_decode->(@fields);
for my $f1 (@fields) {
if ( defined $f1 && $f1 =~ /<TAB1>/ ) {
my @sub1 = map { $_ eq '-' ? '' : $_ } split( /<TAB1>/, $f1, -1 );
@sub1 = $white_decode->(@sub1);
for my $f2 (@sub1) {
if ( defined $f2 && $f2 =~ /<TAB2>/ ) {
my @sub2 = map { $_ eq '-' ? '' : $_ } split( /<TAB2>/, $f2, -1 );
@sub2 = $white_decode->(@sub2);
for my $f3 (@sub2) {
if ( defined $f3 && $f3 =~ /<TAB3>/ ) {
my @sub3 = map { $_ eq '-' ? '' : $_ } split( /<TAB3>/, $f3, -1 );
$f3 = [ $white_decode->(@sub3) ];
}
}
$f2 = \@sub2;
}
}
$f1 = \@sub1;
}
}
if ( defined $rid_prefix ) {
unshift @fields, $rid_prefix;
}
return wantarray ? @fields : ( @fields == 1 ? $fields[0] : \@fields );
}
# 2. ERA 2026: HTML entities (&, |, =) + ARRAY: / HASH:
if ( $record =~ /(?:ARRAY:|HASH:|&#(?:38|61|124|92|30);)/ ) {
my $unescape_chars = sub {
my ($str) = @_;
return "" unless defined $str;
$str =~ s/\\\\/\\/g;
$str =~ s/\\([nrt])/$1 eq 'n' ? "\n" : $1 eq 'r' ? "\r" : "\t"/eg;
$str =~ s/=/=/g;
$str =~ s/|/|/g;
$str =~ s//\x1e/g;
$str =~ s/\/\\/g;
$str =~ s/&/&/g;
return $str;
};
my $decode_node;
$decode_node = sub {
my ($field) = @_;
return "" unless defined $field;
if ( $field =~ /^ARRAY:(.*)/s ) {
my $payload = $1;
return [] if $payload eq "";
return [ map { $decode_node->( $unescape_chars->($_) ) } split( /\|/, $payload, -1 ) ];
}
elsif ( $field =~ /^HASH:(.*)/s ) {
my $payload = $1;
my %h;
if ( $payload ne "" ) {
for my $pair ( split( /\|/, $payload, -1 ) ) {
my ( $k, $v ) = split( /=/, $pair, 2 );
$h{ $unescape_chars->($k) } = $decode_node->( $unescape_chars->( $v // '' ) );
}
}
return \%h;
}
elsif ( $field =~ /\\T/ ) {
return [ map { $unescape_chars->($_) } split( /\\T/, $field, -1 ) ];
}
else {
return $unescape_chars->($field);
}
};
my @raw = split( /\t/, $record, -1 );
if ( @raw == 1 && $raw[0] =~ /^(?:ARRAY|HASH):/ ) {
my $res = $decode_node->( $raw[0] );
return wantarray ? ($res) : $res;
}
my @res = map { $decode_node->( $unescape_chars->($_) ) } @raw;
return wantarray ? @res : ( @res == 1 ? $res[0] : \@res );
}
# 3. ERA 2004 - 2006 & 2003: \t root, \T array delimiter, standard escapes
my $unescape_basic = sub {
my ($s) = @_;
return "" unless defined $s;
$s =~ s/\\(.)/$1 eq "t" ? "\t" : $1 eq "n" ? "\n" : $1 eq "r" ? "\r" : $1 eq "T" ? "\\T" : $1 eq "\\" ? "\\" : $1/eg;
return $s;
};
my @raw_fields = split( /\t/, $record, -1 );
@raw_fields = map { $unescape_basic->($_) } @raw_fields;
for my $line (@raw_fields) {
if ( defined $line && $line =~ /\\T/ ) {
my @parts = split( /\\T/, $line, -1 );
@parts = map { $unescape_basic->($_) } @parts;
$line = \@parts;
}
}
return wantarray ? @raw_fields : ( @raw_fields == 1 ? $raw_fields[0] : \@raw_fields );
}
# ------------------------------------------------
# tsv_encode: Legacy text encoder for CSV exports and text-mode pipelines
# ------------------------------------------------
sub tsv_encode {
my ( $self, @fields ) = @_;
return unless @fields;
my $encode_node;
$encode_node = sub {
my ($node) = @_;
if ( ref($node) eq "ARRAY" ) {
my @escaped = map { ref($_) ? $encode_node->($_) : $self->char_escape($_) } @$node;
return "ARRAY:" . join( "|", @escaped );
}
elsif ( ref($node) eq "HASH" ) {
my @escaped_pairs;
foreach my $k ( sort keys %$node ) {
my $safe_k = $self->char_escape($k);
my $safe_v = ref( $node->{$k} ) ? $encode_node->( $node->{$k} ) : $self->char_escape( $node->{$k} );
push @escaped_pairs, "$safe_k=$safe_v";
}
return "HASH:" . join( "|", @escaped_pairs );
}
else {
return $self->char_escape($node);
}
};
if ( @fields == 1 && ref( $fields[0] ) ) {
return $encode_node->( $fields[0] );
}
my @encoded = map { ref($_) ? $encode_node->($_) : $self->char_escape($_) } @fields;
return join( "\t", @encoded );
}
# ------------------------------------------------
sub char_escape {
my ( $self, $str ) = @_;
return "" unless defined $str;
# WARNING: Escape ampersand & first! (Double escaping logic)
$str =~ s/&/&/g; # ampersand
$str =~ s/\\/\/g; # backslash
$str =~ s/\|/|/g; # pipe (Array/Hash delimiter)
$str =~ s/=/=/g; # equals (Hash key-value delimiter)
$str =~ s/\x1e//g; # record separator (Transaction journal delimiter)
$str =~ s/\t/\\t/g; # tab
$str =~ s/\n/\\n/g; # newline
$str =~ s/\r/\\r/g; # carriage return
return $str;
}
# ------------------------------------------------
sub char_unescape {
my ( $self, $str ) = @_;
return "" unless defined $str;
$str =~ s{ (\\\\) | \\([nrt]) | &\#(92|61|124|38|30); }{
defined $1 ? "\\" :
defined $2 ? ( $2 eq 'n' ? "\n" : $2 eq 'r' ? "\r" : "\t" ) :
chr($3)
}gex;
return $str;
}
# encode like cgi escape
# my $sifresiz = $adb->uri_encode("sifreli");
# ------------------------------------------------
sub uri_encode {
my ( $self, $str ) = @_;
$str =~ s/([^A-Za-z0-9\-_.~])/sprintf("%%%02X", ord($1))/ge;
return $str;
}
# decode like cgi unescape
# my $sifresiz = $adb->uri_decode("sifresiz");
# ------------------------------------------------
sub uri_decode {
my ( $self, $str ) = @_;
$str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/ge;
return $str;
}
# my $key_escape = $adb->key_encode($key);
# ------------------------------------------------
sub key_encode {
my ( $self, $key ) = @_;
my $key_escape = "$key";
if ( $key_escape =~ /[^\w]/ ) {
if ( $self && ref($self) ) {
$key_escape = $self->to_ascii($key_escape);
}
$key_escape =~ s/[^\w]//g;
}
return $key_escape;
}
# =====================================================================
# FORMAT DETECTION & LEGACY DECODING (2003-2026 formats)
# =====================================================================
sub detect_record_format {
my ( $self, $record ) = @_;
return 'v5' if !defined $record || $record eq '';
if ( length($record) >= 7 && substr( $record, 0, 4 ) eq "\x00ABR" ) {
return 'v5';
}
if ( $record =~ /(?:ARRAY:|HASH:|&#(?:38|61|124|92|30);)/ ) {
return 'v4';
}
if ( $record =~ /<TAB[0-9]+>/ ) {
return 'v3';
}
if ( $record =~ /\\T/ ) {
return 'v2';
}
return 'v1';
}
# =====================================================================
# BINARY INDEX PACKING (64-bit Big-Endian Packed Identifiers)
# =====================================================================
# $adb->bin_encode(\@rids)
# Encodes list of record IDs into 8-byte packed binary format (64-bit uint Q>*).
# ------------------------------------------------
sub bin_encode {
my ( $self, $rids ) = @_;
return '' unless ref($rids) eq 'ARRAY' && @$rids;
return pack( "(Q>)*", @$rids );
}
# $adb->bin_decode($binary_buffer, $offset, $limit, $dir)
# Decodes 8-byte binary buffer (64-bit uint Q>*) with O(1) substr slicing.
# Returns ($total_count, @sliced_ids)
# ------------------------------------------------
sub bin_decode {
my ( $self, $buffer, $offset, $limit, $dir ) = @_;
return ( 0, () ) unless defined $buffer && length($buffer) >= 8;
my $rec_size = 8;
my $total = int( length($buffer) / $rec_size );
return ( 0, () ) unless $total;
$offset ||= 0;
$limit ||= 0;
$dir = ( defined $dir && $dir =~ /^(asc|desc|reverse)$/i ) ? lc($dir) : 'desc';
if ( lc($dir) eq 'desc' ) {
my ( $real_start, $real_limit );
if ($limit) {
( run in 1.549 second using v1.01-cache-2.11-cpan-54e63673c56 )