Skip to content

Commit 70e3540

Browse files
committed
string: improve base64 standard compliance and performance
This commit hardens the base64 implementation by addressing standard compliance issues and optimizing the hot paths for the zero-copy API. Key changes: * Compliance: Replaced non-conforming 'c_loc' and 'c_f_pointer' usage with 'transfer' and 'select rank' blocks to ensure compatibility with non-C-interoperable types. * Correctness: Fixed a Unicode aliasing bug in the decoder by implementing a bitwise-OR 'unicode_accum' check to reject non-ASCII characters (>255). * Performance: Optimized 'base64_encode_into' by moving string blank-filling ('str = ""') off the hot path, preventing unnecessary memory operations during high-throughput encoding. * API Refinement: Restricted the 'base64_encode_into' power-user API to accept only 'integer(int8)' arrays to guarantee zero-copy performance while remaining standard-compliant. * Ergonomics: Added an optional 'error_flag' to 'base64_decode' to improve error handling for the allocating API. * Testing: Expanded unit tests to include coverage for the new subroutine signatures and error states. Signed-off-by: RatanKokal <ratanskokal@gmail.com>
1 parent d6d3103 commit 70e3540

3 files changed

Lines changed: 144 additions & 177 deletions

File tree

src/strings/stdlib_base64_decode.fypp

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ contains
6060
integer(int8), allocatable :: filtered(:)
6161
logical :: clean_mode
6262

63-
integer(int32) :: v0, v1, v2, v3, c
63+
integer(int32) :: v0, v1, v2, v3, c, raw_c
6464
integer :: i, j, slen, n_pad
65-
integer(int32) :: error_accum
65+
integer(int32) :: error_accum, unicode_accum
6666

6767
error_flag = .false.
6868
decoded_len = 0
69+
unicode_accum = 0
6970

7071
if (len(str) == 0) return
7172

@@ -88,17 +89,21 @@ contains
8889
allocate(filtered(len(str)))
8990
j = 0
9091
do i = 1, len(str)
91-
c = iand(int(iachar(str(i:i)), int32), 255_int32)
92+
raw_c = int(iachar(str(i:i)), int32)
93+
unicode_accum = ior(unicode_accum, raw_c)
94+
c = iand(raw_c, 255_int32)
9295

9396
! ALWAYS write the byte to the next available slot
9497
filtered(j + 1) = int(c, int8)
95-
96-
! Add 0 (if space) or 1 (if valid) to the counter
9798
j = j + IS_VAL(c)
9899
end do
99100
slen = j
100101

101-
if (slen == 0 .or. mod(slen, 4) /= 0) then; error_flag = .true.; return; end if
102+
! Catch Unicode overflow before proceeding
103+
if (unicode_accum > 255_int32 .or. slen == 0 .or. mod(slen, 4) /= 0) then
104+
error_flag = .true.
105+
return
106+
end if
102107

103108
n_pad = 0
104109
if (filtered(slen) == 61_int8) then
@@ -115,10 +120,18 @@ contains
115120
! Fortran Scalar Decoder Loop
116121
do i = 1, slen - 7, 4
117122
if (clean_mode) then
118-
v0 = int(DT(iand(int(iachar(str(i:i)), int32), 255_int32)), int32)
119-
v1 = int(DT(iand(int(iachar(str(i+1:i+1)), int32), 255_int32)), int32)
120-
v2 = int(DT(iand(int(iachar(str(i+2:i+2)), int32), 255_int32)), int32)
121-
v3 = int(DT(iand(int(iachar(str(i+3:i+3)), int32), 255_int32)), int32)
123+
raw_c = int(iachar(str(i:i)), int32)
124+
unicode_accum = ior(unicode_accum, raw_c)
125+
v0 = int(DT(iand(raw_c, 255_int32)), int32)
126+
raw_c = int(iachar(str(i+1:i+1)), int32)
127+
unicode_accum = ior(unicode_accum, raw_c)
128+
v1 = int(DT(iand(raw_c, 255_int32)), int32)
129+
raw_c = int(iachar(str(i+2:i+2)), int32)
130+
unicode_accum = ior(unicode_accum, raw_c)
131+
v2 = int(DT(iand(raw_c, 255_int32)), int32)
132+
raw_c = int(iachar(str(i+3:i+3)), int32)
133+
unicode_accum = ior(unicode_accum, raw_c)
134+
v3 = int(DT(iand(raw_c, 255_int32)), int32)
122135
else
123136
v0 = int(DT(iand(int(filtered(i), int32), 255_int32)), int32)
124137
v1 = int(DT(iand(int(filtered(i+1), int32), 255_int32)), int32)
@@ -134,7 +147,7 @@ contains
134147
res(j+2:j+2) = char(ior(ishft(iand(v2, 3_int32), 6), v3))
135148
end do
136149

137-
if (error_accum < 0) then
150+
if (error_accum < 0 .or. unicode_accum > 255_int32) then
138151
error_flag = .true.
139152
decoded_len = 0
140153
return
@@ -145,13 +158,21 @@ contains
145158
j = (i - 1) / 4 * 3 + 1
146159

147160
if (clean_mode) then
148-
c = iand(int(iachar(str(i:i)), int32), 255_int32)
161+
raw_c = int(iachar(str(i:i)), int32)
162+
unicode_accum = ior(unicode_accum, raw_c)
163+
c = iand(raw_c, 255_int32)
149164
v0 = merge(-1_int32, int(DT(min(c,127)), int32), c > 127_int32)
150-
c = iand(int(iachar(str(i+1:i+1)), int32), 255_int32)
165+
raw_c = int(iachar(str(i+1:i+1)), int32)
166+
unicode_accum = ior(unicode_accum, raw_c)
167+
c = iand(raw_c, 255_int32)
151168
v1 = merge(-1_int32, int(DT(min(c,127)), int32), c > 127_int32)
152-
c = iand(int(iachar(str(i+2:i+2)), int32), 255_int32)
169+
raw_c = int(iachar(str(i+2:i+2)), int32)
170+
unicode_accum = ior(unicode_accum, raw_c)
171+
c = iand(raw_c, 255_int32)
153172
v2 = merge(-1_int32, int(DT(min(c,127)), int32), c > 127_int32)
154-
c = iand(int(iachar(str(i+3:i+3)), int32), 255_int32)
173+
raw_c = int(iachar(str(i+3:i+3)), int32)
174+
unicode_accum = ior(unicode_accum, raw_c)
175+
c = iand(raw_c, 255_int32)
155176
v3 = merge(-1_int32, int(DT(min(c,127)), int32), c > 127_int32)
156177
else
157178
c = iand(int(filtered(i), int32), 255_int32)
@@ -164,7 +185,7 @@ contains
164185
v3 = merge(-1_int32, int(DT(min(c,127)), int32), c > 127_int32)
165186
end if
166187

167-
if (v0 < 0 .or. v1 < 0) then; error_flag = .true.; decoded_len = 0; return; end if
188+
if (v0 < 0 .or. v1 < 0 .or. unicode_accum > 255_int32) then; error_flag = .true.; decoded_len = 0; return; end if
168189
res(j:j) = char(ior(ishft(v0, 2), ishft(v1, -4)))
169190

170191
if (n_pad < 2) then
@@ -179,29 +200,33 @@ contains
179200
end subroutine base64_decode_into
180201

181202
! ERGONOMIC API (Returns allocatable strings)
182-
function base64_decode(str) result(res)
203+
function base64_decode(str, error_flag) result(res)
183204
character(len=*), intent(in) :: str
205+
logical, intent(out), optional :: error_flag
184206
character(len=:), allocatable :: res
185207

186208
integer :: decoded_len
187-
logical :: error_flag
209+
logical :: err
188210

189211
if (len(str) == 0) then
190212
allocate(character(len=0) :: res)
213+
if (present(error_flag)) error_flag = .false.
191214
return
192215
end if
193216

194217
! Pre-allocate maximum possible size needed
195218
allocate(character(len=(len(str) / 4) * 3 + 3) :: res)
196219

197-
call base64_decode_into(str, res, decoded_len, error_flag)
220+
call base64_decode_into(str, res, decoded_len, err)
198221

199-
if (error_flag) then
222+
if (err) then
200223
deallocate(res)
201224
allocate(character(len=0) :: res)
202225
else
203226
res = res(1:decoded_len)
204227
end if
228+
229+
if (present(error_flag)) error_flag = err
205230
end function base64_decode
206231

207232
end module stdlib_base64_decode

0 commit comments

Comments
 (0)