Sunday, March 29, 2009

Powerbuilder Base 64 Encoding/Decoding

Edit: Added of_decode_base64_to_blob at bottom.

There is a PB example out there for Base64 encoding and decoding, but it relies on calling COM objects. (At least the few I found did..) i.e :


string ls_ret
oleobject lo_xml
oleobject lo_node

lo_xml = create oleobject
lo_xml.connecttonewobject("Microsoft.XMLDOM")

lo_node = lo_xml.createElement("b64")
lo_node.dataType = "bin.base64"

//Encode
lo_node.NodeTypedValue = Blob(as_data)
ls_ret = lo_node.Text

//Decode
//lo_node.Text = as_data
//ls_ret = String( lo_node.NodeTypedValue )


Destroy lo_node
Destroy lo_xml

return ls_ret


Well, I came across a situation where I needed to call PB functions from a PBNI interface. One of the functions called used the above function to decode base64. Well for a reason I've yet to figure out, calling PB from PBNI fails on "create oleobject". Fail. So I rewrote the base64 decoding and encoding to do it manually and remove the dependency of the oleobject. The math could probably be simplified in here..but im too lazy to think.

Note: Both only work with unicode strings (Default for PB strings)
Note2: I used code found here http://www.motobit.com/tips/detpg_Base64/ as a base (youll notice more in the decode section as I got lazier and didnt rename the variables..)


Encode:

public function string of_encode_base64 (string as_data);
string ls_data, ls_ret
string ls_base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

string ls_tmp
longlong ll_24bits, ll_tmp, ll_tmp2, ll_tmp3
longlong ll_1, ll_2, ll_3, ll_4, ll_i
long ll_newline_offset

ls_data = as_data
ls_ret = ""

SetNull( ll_tmp3 )

For ll_i = 1 To longlong( Len( ls_data ) + 1 )

//Create 24bit buffer from unicode
If IsNull( ll_tmp3 ) Then

//Reverse first two bytes
ll_tmp = Asc( Mid( ls_data, ll_i, 1 ) )
ll_tmp = (( ll_tmp - ( Int( ll_tmp / 256 ) * 256 ) ) * 256) + ( ll_tmp / 256 )

ll_i++

//Get 3rd byte and store 4th for later
ll_tmp2 = Asc( Mid( ls_data, ll_i, 1) )
ll_tmp3 = Int( ll_tmp2 / 256 )
ll_tmp2 = ( ll_tmp2 - ( ll_tmp3 * 256 ) )

ll_24bits = (256 * ll_tmp) + ll_tmp2
Else
//Reverse next char and add ll_tmp3 to the beginning to create our next 3 byte(24bit) buffer
ll_tmp = Asc( Mid( ls_data, ll_i, 1 ) )
ll_24bits = ( ll_tmp3 * 65536 ) + ( ( ll_tmp - ( Int( ll_tmp / 256 ) * 256 ) ) * 256 ) + ( Int( ll_tmp / 256 ) )
SetNull( ll_tmp3 )
End If

//Get each 6 bit indexes
ll_1 = Int( ll_24bits / 262144 ) //Shift 18 bits right to get first 6bit index
ll_24bits -= ll_1 * 262144 //remove first 6 bits from buffer
ll_2 = Int( ll_24bits / 4096 ) //Shift 12 bits right for 2nd
ll_24bits -= ll_2 * 4096 //Remove it from buffer
ll_3 = Int( ll_24bits / 64 ) //Shift 6 bits for third
ll_4 = ll_24bits - ( ll_3 * 64 ) // remove third and left with last 6 bit index

//Convert To base64
ls_ret += Mid( ls_base64, ll_1 + 1, 1 ) + Mid( ls_base64, ll_2 + 1, 1 ) + Mid( ls_base64, ll_3 + 1, 1 ) + + Mid( ls_base64, ll_4 + 1, 1 )

//Add a new line For Each 72 chars In dest
If Mod( Len( ls_ret ) - ll_newline_offset , 72 ) = 0 Then
ls_ret += "~r~n"
ll_newline_offset += 2
End If

Next

//Pad end
Choose Case Mod( Len( ls_data ) - 1, 3 )
Case 1 //8 bit final
ls_ret = Left( ls_ret, Len( ls_ret ) - 2 ) + "=="
Case 2 //16 bit final
ls_ret = Left( ls_ret, Len( ls_ret ) - 1 ) + "="
End Choose

return ls_ret
end function


Decode:

public function string of_dec_to_hex (long al_number);
// 0 <= n <= 15
// Converts integer from 0 - 15 into character hex representation

string sHexChar
Choose Case al_number
Case 10
sHexChar = 'A'
Case 11
sHexChar = 'B'
Case 12
sHexChar = 'C'
Case 13
sHexChar = 'D'
Case 14
sHexChar = 'E'
Case 15
sHexChar = 'F'
Case Else
sHexChar = String(al_number)
End Choose

Return sHexChar

end function

public function string of_convert_to_hex (long al_number);
// string of_convert_to_hex( long alNumber ), recursive:
// Recursive function to translate number into hex representation
If al_number > 15 Then
Return of_convert_to_hex( al_number / 16 ) + of_dec_to_hex( Mod( al_number, 16 ) )
Else
Return of_dec_to_hex( al_number )
End If

end function

public function long of_hex2long (string as_hex);
string ls_hex
integer i,length
long result = 0

length = len(as_hex)
ls_hex = Upper(as_hex)
FOR i = 1 to length
result += &
(Pos ('123456789ABCDEF', mid(ls_hex, i, 1)) * &
( 16 ^ ( length - i ) ))
NEXT
RETURN result

end function

public function string of_replace (string as_source, string as_replace, string as_with);
int ll_start = 1,ll_len

ll_len = len( as_replace )
ll_start = Pos( as_source, as_replace, ll_start )

Do While ll_start > 0
as_source = Replace( as_source, ll_start, ll_len, as_with )
ll_start = Pos( as_source, as_replace, ll_start + Len( as_with ) )
Loop

return as_source
end function


public function string of_decode_base64 (string as_base64);
string Base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
string ls_working
long dataLength, groupBegin, ll_mod
string sOut, pOut, ls_tmp

//remove white spaces, If any
ls_working = of_replace(as_base64, "~r", "")
ls_working = of_replace(as_base64, "~n", "")
ls_working = of_replace(ls_working, "~t", "")
ls_working = trim( ls_working )

dataLength = Len( ls_working )
If Mod(dataLength, 4) <> 0 Then
return ""
End If

//Now decode each group:
For groupBegin = 1 To dataLength Step 4
long numDataBytes, CharCounter, thisData, nGroup
char thisChar
//Each data group encodes up To 3 actual bytes.
numDataBytes = 3
nGroup = 0

For CharCounter = 0 To 3
/*
Convert each character into 6 bits of data, And add it To
an integer For temporary storage. If a character is a '=', there
is one fewer data byte. (There can only be a maximum of 2 '=' In
the whole string.)
*/

thisChar = Mid(ls_working, groupBegin + CharCounter, 1)

If thisChar = "=" Then
numDataBytes = numDataBytes - 1
thisData = 0
Else
thisData = Pos(Base64, thisChar, 1) - 1
End If
If thisData = -1 Then
return ""
End If

nGroup = 64 * nGroup + thisData
Next

//Hex splits the long To 6 groups with 4 bits
string ls_group
ls_group = of_convert_to_hex( nGroup )


//Add leading zeros
Do While Len(ls_group) < 6
ls_group = "0" + ls_group
Loop

//Converts two characters at a time accounting for Unicode and Little Endian
//Saves the third char for the next run
If ls_tmp <> "" Then
pOut = Char( of_hex2long( Mid( ls_group, 1, 2 ) + ls_tmp) )
pOut = pOut + Char( of_hex2long( Mid( ls_group, 5, 2 ) + Mid( ls_group, 3, 2) ) )
ls_tmp = ""
Else
pOut = Char( of_hex2long( Mid( ls_group, 3, 2 ) + Mid( ls_group, 1, 2) ) )
ls_tmp = Mid( ls_group, 5, 2 )
End If

//add numDataBytes characters To out string
sOut = sOut + Left(pOut, numDataBytes)
Next

return sOut
end function


Edit:
I got through with my Quiznos pretty quick today, so i decided to take a quick look at the comments about decoding base64 to a blob.

NOTE: I didn't spend very much time going over this (Im lazy!). I did however test it succesfully with two jpegs that where converted to base64 strings using C#'s Convert.ToBase64String

NOTE2: Encoding I did not even glance at. Your on your own there.


public function blob of_decode_base64_to_blob (string as_base64);
string ls_base_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
string ls_working
long ll_data_len, ll_start_from, ll_mod
string ls_tmp, ls_hex

long ll_num_bytes, ll_count, ll_this_pos, ll_dec
char lc_char

blob lb, lb_out
long ll_pos
ll_pos = 1

//remove white spaces, If any
ls_working = gnv_app.of_conv(as_base64, "~r", "")
ls_working = gnv_app.of_conv(as_base64, "~n", "")
ls_working = gnv_app.of_conv(ls_working, "~t", "")
ls_working = trim( ls_working )

ll_data_len = Len( ls_working )
If Mod(ll_data_len, 4) <> 0 Then
return lb
End If

lb = Blob(space(ll_data_len))

//Now decode each group:
For ll_start_from = 1 To ll_data_len Step 4

//Each data group encodes up To 3 actual bytes.
ll_num_bytes = 3
ll_dec = 0

For ll_count = 0 To 3
/*
Convert each character into 6 bits of data, And add it To
an integer For temporary storage. If a character is a '=', there
is one fewer data byte. (There can only be a maximum of 2 '=' In
the whole string.)
*/

lc_char = Mid(ls_working, ll_start_from + ll_count, 1)

If lc_char = "=" Then
ll_num_bytes = ll_num_bytes - 1
ll_this_pos = 0
Else
ll_this_pos = Pos(ls_base_chars, lc_char, 1) - 1
End If
If ll_this_pos = -1 Then
return lb
End If

ll_dec = 64 * ll_dec + ll_this_pos
Next

//Hex splits the long To 6 groups with 4 bits
ls_hex = of_convert_to_hex( ll_dec )

//Add leading zeros
Do While Len(ls_hex) < 6
ls_hex = "0" + ls_hex
Loop

//Converts two characters at a time accounting for Little Endian
//Saves the third char for the next run
If ls_tmp <> "" Then
//Add first two byte
BlobEdit(lb, ll_pos, of_hexlong( Mid( ls_hex, 1, 2 ) + ls_tmp))
ll_pos += 2
//Add next two bytes
BlobEdit(lb, ll_pos, of_hexlong( Mid( ls_hex, 5, 2 ) + Mid( ls_hex, 3, 2) ) )
ll_pos += 2

ls_tmp = ""
Else
//Add two bytes
BlobEdit(lb, ll_pos, of_hexlong( Mid( ls_hex, 3, 2 ) + Mid(ls_hex, 1, 2)) )
ll_pos += 2

//Save third
ls_tmp = Mid( ls_hex, 5, 2 )
End If

//Adjust position as needed
ll_pos = ll_pos - (3 - ll_num_bytes)
Next

lb_out = BlobMid(lb, 1, ll_pos)

return lb_out
end function