Skip to content

Commit 6c6af03

Browse files
committed
string: strlength() workaround for unicode2native() buffer-length bug in pre-10.2.0 Octave
In Octave 9.x and earlier, and 10.1.0, strlength() would return the wrong length for short strings, because Octave's unicode2native() will return an incorrect result with extra bytes, due to a buffer length calculation bug. This adds a disgusting hack to work around that particular bug and produce correct strlength results. Uses a conservative check to only activate that workaround when it detects that particular buggy strlength() behavior. See #143.
1 parent cef6f58 commit 6c6af03

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

CHANGES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ Tablicious Changelog
44
0.4.6 (unreleased)
55
------------------
66

7-
(nothing here yet)
7+
* Work around `unicode2native` buffer length bug in `string.strlength`. ([#143](https://github.com/apjanke/octave-tablicious/issues/143))
8+
* Internal changes.
9+
* Change some more code to GNU Octave style.
810

911
0.4.5 (2025-07-03)
1012
------------------

inst/@string/string.m

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,13 +555,52 @@ function disp (this)
555555
##
556556
## @end deftypefn
557557
function out = strlength (this)
558+
persistent do_u2n_buflen_bug_workaround
559+
if isempty (do_u2n_buflen_bug_workaround)
560+
# Workaround for unicode2native() buffer-length bug in pre-10.2.0 Octave that
561+
# results in extra bytes in its output for short strings.
562+
# See:
563+
# * https://github.com/apjanke/octave-tablicious/issues/143
564+
# * https://octave.discourse.group/t/wrong-number-of-bytes-returned-from-unicode2native/6418
565+
# There's a bug in Octave that causes unicode2native() conversions to UTF-16 or
566+
# UTF-32 to add extra bogus bytes when converting short strings, which throws off
567+
# the character count here. The bug doesn't seem to affect strings length 4 or
568+
# longer. So as a hacky workaround, in affected Octave versions, we'll just add
569+
# some padding to all the strings passed to unicode2native to get out of the danger
570+
# zone for that bug, and then back them out of the resulting char counts.
571+
# I'm not sure which versions of Octave were affected, or exactly what the scope of
572+
# the bug is, so probe for the specific buggy behavior I'm familiar with, and bail
573+
# if we get something that doesn't closely match that or the good-case behavior.
574+
good_strlens = [0 2 4 6 8 10];
575+
buflenbug_strlens = [0 5 6 7 8 10];
576+
got_strlens = arrayfun(@(n) numel (unicode2native (repmat ('a', [1 n]), 'UTF-16LE')), 0:5);
577+
if (isequal (got_strlens, good_strlens))
578+
do_u2n_buflen_bug_workaround = false;
579+
elseif (isequal (got_strlens, buflenbug_strlens))
580+
do_u2n_buflen_bug_workaround = true;
581+
else
582+
error ('tablicious:BUG', ['Got an unexpected strlen probe value for unicode2native test ' ...
583+
'test in strlength() initialization. This indicates a behavior of core Octave that ' ...
584+
'Tablicious is unprepared for. This is a bug that Tablicious cannot handle. Please ' ...
585+
'report this to the Tablicious maintainers if you are so inclined. Cannot produce ' ...
586+
'reasonable strlength() results in these conditions. Aborting. Sorry.'])
587+
endif
588+
endif
558589
out = NaN (size (this));
559590
for i = 1:numel (out)
560591
if (this.tfMissing(i))
561592
continue
562593
endif
563-
utf16 = unicode2native (this.strs{i}, 'UTF-16LE');
564-
out(i) = numel (utf16) / 2;
594+
if (do_u2n_buflen_bug_workaround)
595+
# unicode2native bug workaround case for pre-10.2.0 Octave; see above.
596+
# Yes, this is a terrible hack. Sorry about that.
597+
utf16 = unicode2native (['aaaa' this.strs{i}], 'UTF-16LE');
598+
out(i) = (numel (utf16) / 2) - 4;
599+
else
600+
# regular case
601+
utf16 = unicode2native (this.strs{i}, 'UTF-16LE');
602+
out(i) = numel (utf16) / 2;
603+
endif
565604
endfor
566605
endfunction
567606

inst/t/t_02_string.m

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Covered by the 3-clause BSD License (see LICENSES/LICENSE-MATPOWER file for details).
2+
#
3+
# Copyright (c) 2025, Andrew Janke
4+
5+
function obj = t_02_string (quiet)
6+
# Tests for string array.
7+
8+
if nargin < 1 || isempty (quiet); quiet = false; endif
9+
10+
verbose = ! quiet;
11+
12+
# Test fixtures
13+
14+
# strlength tests
15+
16+
# scalars: { str, strlen; ...}
17+
fixture_1 = {
18+
'' 0
19+
'a' 1
20+
'aa' 2
21+
'aaa' 3
22+
'aaaa' 4
23+
'aaaaa' 5
24+
};
25+
26+
# nonscalars: { strs, strlens; ...}
27+
fixture_2 = {
28+
{''} [0]
29+
{'a'} [1]
30+
{'a', 'aa', 'aaa', ''} [1, 2, 3, 0]
31+
};
32+
33+
# Run strlength tests
34+
35+
t_begin (size (fixture_1, 1) + size (fixture_2, 1), quiet);
36+
37+
# scalar strlength tests
38+
39+
max_prec = 16;
40+
for i_test = 1:size (fixture_1, 1)
41+
[str_c, exp_len] = fixture_1{i_test,:};
42+
str = string (str_c);
43+
t_is (strlength (str), exp_len, max_prec, sprintf ('strlength ("%s")\n', str_c));
44+
endfor
45+
46+
# nonscalar strlength tests
47+
48+
for i_test = 1:size (fixture_2, 1)
49+
[strs_c, exp_len] = fixture_1{i_test,:};
50+
strs = string (strs_c);
51+
t_is (strlength (strs), exp_len, max_prec, sprintf ('strlength (<item %d>)\n', i_test));
52+
endfor
53+
54+
# Wrap up
55+
56+
if nargout
57+
obj = T;
58+
endif
59+
60+
t_end;
61+
62+
endfunction

0 commit comments

Comments
 (0)