Merge lp:~paul-lucas/zorba/feature-utf8_streambuf into lp:zorba
- feature-utf8_streambuf
- Merge into trunk
Proposed by
Paul J. Lucas
Status: | Merged |
---|---|
Approved by: | Paul J. Lucas |
Approved revision: | 11177 |
Merged at revision: | 11201 |
Proposed branch: | lp:~paul-lucas/zorba/feature-utf8_streambuf |
Merge into: | lp:zorba |
Diff against target: |
820 lines (+752/-0) 8 files modified
src/unit_tests/CMakeLists.txt (+1/-0) src/unit_tests/test_utf8_streambuf.cpp (+166/-0) src/unit_tests/unit_test_list.h (+1/-0) src/unit_tests/unit_tests.cpp (+1/-0) src/util/CMakeLists.txt (+1/-0) src/util/utf8_streambuf.cpp (+259/-0) src/util/utf8_streambuf.h (+322/-0) test/unit/CMakeLists.txt (+1/-0) |
To merge this branch: | bzr merge lp:~paul-lucas/zorba/feature-utf8_streambuf |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Matthias Brantner | Approve | ||
Sorin Marian Nasoi | Approve | ||
Juan Zacarias | Approve | ||
Paul J. Lucas | Approve | ||
Review via email: mp+142440@code.launchpad.net |
Commit message
Streambuf for validating UTF-8 on-the-fly.
Description of the change
Streambuf for validating UTF-8 on-the-fly.
To post a comment you must log in.
Revision history for this message
Paul J. Lucas (paul-lucas) : | # |
review:
Approve
Revision history for this message
Paul J. Lucas (paul-lucas) wrote : | # |
Revision history for this message
Juan Zacarias (juan457) : | # |
review:
Approve
Revision history for this message
Sorin Marian Nasoi (sorin.marian.nasoi) : | # |
review:
Approve
Revision history for this message
Matthias Brantner (matthias-brantner) : | # |
review:
Approve
Revision history for this message
Zorba Build Bot (zorba-buildbot) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Revision history for this message
Zorba Build Bot (zorba-buildbot) wrote : | # |
Validation queue starting for merge proposal.
Log at: http://
Revision history for this message
Zorba Build Bot (zorba-buildbot) wrote : | # |
Validation queue job feature-
All tests succeeded!
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/unit_tests/CMakeLists.txt' | |||
2 | --- src/unit_tests/CMakeLists.txt 2013-01-24 02:27:45 +0000 | |||
3 | +++ src/unit_tests/CMakeLists.txt 2013-01-25 22:32:27 +0000 | |||
4 | @@ -23,6 +23,7 @@ | |||
5 | 23 | test_uri.cpp | 23 | test_uri.cpp |
6 | 24 | test_uuid.cpp | 24 | test_uuid.cpp |
7 | 25 | unit_tests.cpp | 25 | unit_tests.cpp |
8 | 26 | test_utf8_streambuf.cpp | ||
9 | 26 | ) | 27 | ) |
10 | 27 | 28 | ||
11 | 28 | IF (ZORBA_WITH_FILE_ACCESS) | 29 | IF (ZORBA_WITH_FILE_ACCESS) |
12 | 29 | 30 | ||
13 | === added file 'src/unit_tests/test_utf8_streambuf.cpp' | |||
14 | --- src/unit_tests/test_utf8_streambuf.cpp 1970-01-01 00:00:00 +0000 | |||
15 | +++ src/unit_tests/test_utf8_streambuf.cpp 2013-01-25 22:32:27 +0000 | |||
16 | @@ -0,0 +1,166 @@ | |||
17 | 1 | /* | ||
18 | 2 | * Copyright 2006-2008 The FLWOR Foundation. | ||
19 | 3 | * | ||
20 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | ||
21 | 5 | * you may not use this file except in compliance with the License. | ||
22 | 6 | * You may obtain a copy of the License at | ||
23 | 7 | * | ||
24 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 | ||
25 | 9 | * | ||
26 | 10 | * Unless required by applicable law or agreed to in writing, software | ||
27 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, | ||
28 | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
29 | 13 | * See the License for the specific language governing permissions and | ||
30 | 14 | * limitations under the License. | ||
31 | 15 | */ | ||
32 | 16 | |||
33 | 17 | #include "stdafx.h" | ||
34 | 18 | |||
35 | 19 | #include <fstream> | ||
36 | 20 | #include <iostream> | ||
37 | 21 | #include <sstream> | ||
38 | 22 | #include <string> | ||
39 | 23 | |||
40 | 24 | #include <zorba/zorba_exception.h> | ||
41 | 25 | |||
42 | 26 | #include "util/utf8_streambuf.h" | ||
43 | 27 | |||
44 | 28 | using namespace std; | ||
45 | 29 | using namespace zorba; | ||
46 | 30 | |||
47 | 31 | #define SMILEY_FACE "\xF0\x9F\x98\x8A" | ||
48 | 32 | #define COPYRIGHT_UTF8 "\xC2\xA9" | ||
49 | 33 | #define ONE_THIRD_UTF8 "\xE2\x85\x93" | ||
50 | 34 | |||
51 | 35 | #define BAD_COPYRIGHT_1_UTF8 "\x42\xA9" | ||
52 | 36 | #define BAD_COPYRIGHT_2_UTF8 "\xC2\x79" | ||
53 | 37 | |||
54 | 38 | static char const *const tests_good[] = { | ||
55 | 39 | "Hello, world!", | ||
56 | 40 | "Copyright " COPYRIGHT_UTF8 " 2012", | ||
57 | 41 | ONE_THIRD_UTF8 " cup sugar", | ||
58 | 42 | "Smiley " SMILEY_FACE, | ||
59 | 43 | "Smiley 2 " SMILEY_FACE SMILEY_FACE, | ||
60 | 44 | SMILEY_FACE " Smiley", | ||
61 | 45 | SMILEY_FACE SMILEY_FACE " 2 Smiley", | ||
62 | 46 | 0 | ||
63 | 47 | }; | ||
64 | 48 | |||
65 | 49 | static char const *const tests_bad[] = { | ||
66 | 50 | "Copyright " BAD_COPYRIGHT_1_UTF8 " 2012", | ||
67 | 51 | "Copyright " BAD_COPYRIGHT_2_UTF8 " 2012", | ||
68 | 52 | 0 | ||
69 | 53 | }; | ||
70 | 54 | |||
71 | 55 | /////////////////////////////////////////////////////////////////////////////// | ||
72 | 56 | |||
73 | 57 | static int failures; | ||
74 | 58 | |||
75 | 59 | static bool assert_true( int no, char const *expr, int line, bool result ) { | ||
76 | 60 | if ( !result ) { | ||
77 | 61 | cout << '#' << no << " FAILED, line " << line << ": " << expr << endl; | ||
78 | 62 | ++failures; | ||
79 | 63 | } | ||
80 | 64 | return result; | ||
81 | 65 | } | ||
82 | 66 | |||
83 | 67 | static void print_exception( int no, char const *expr, int line, | ||
84 | 68 | std::exception const &e ) { | ||
85 | 69 | assert_true( no, expr, line, false ); | ||
86 | 70 | cout << "+ exception: " << e.what() << endl; | ||
87 | 71 | } | ||
88 | 72 | |||
89 | 73 | #define ASSERT_TRUE( NO, EXPR ) assert_true( NO, #EXPR, __LINE__, !!(EXPR) ) | ||
90 | 74 | |||
91 | 75 | #define ASSERT_TRUE_AND_NO_EXCEPTION( NO, EXPR ) \ | ||
92 | 76 | try { ASSERT_TRUE( NO, EXPR ); } \ | ||
93 | 77 | catch ( exception const &e ) { print_exception( NO, #EXPR, __LINE__, e ); } \ | ||
94 | 78 | catch ( ... ) { assert_true( NO, #EXPR, __LINE__, false ); } | ||
95 | 79 | |||
96 | 80 | #define ASSERT_EXCEPTION( NO, EXPR ) \ | ||
97 | 81 | try { EXPR; assert_true( NO, #EXPR, __LINE__, false ); } \ | ||
98 | 82 | catch ( ZorbaException const &e ) { } \ | ||
99 | 83 | catch ( ... ) { assert_true( NO, #EXPR, __LINE__, false ); } | ||
100 | 84 | |||
101 | 85 | /////////////////////////////////////////////////////////////////////////////// | ||
102 | 86 | |||
103 | 87 | static bool test_getline( char const *test ) { | ||
104 | 88 | istringstream iss( test ); | ||
105 | 89 | utf8::streambuf utf_buf( iss.rdbuf() ); | ||
106 | 90 | iss.ios::rdbuf( &utf_buf ); | ||
107 | 91 | iss.exceptions( ios::badbit ); | ||
108 | 92 | |||
109 | 93 | char buf[ 1024 ]; | ||
110 | 94 | iss.getline( buf, sizeof buf ); | ||
111 | 95 | if ( iss.gcount() ) { | ||
112 | 96 | string const s( buf, iss.gcount() ); | ||
113 | 97 | return s == test; | ||
114 | 98 | } | ||
115 | 99 | return false; | ||
116 | 100 | } | ||
117 | 101 | |||
118 | 102 | static bool test_read( char const *test ) { | ||
119 | 103 | istringstream iss( test ); | ||
120 | 104 | utf8::streambuf utf_buf( iss.rdbuf() ); | ||
121 | 105 | iss.ios::rdbuf( &utf_buf ); | ||
122 | 106 | iss.exceptions( ios::badbit ); | ||
123 | 107 | |||
124 | 108 | char buf[ 1024 ]; | ||
125 | 109 | iss.read( buf, sizeof buf ); | ||
126 | 110 | if ( iss.gcount() ) { | ||
127 | 111 | string const s( buf, iss.gcount() ); | ||
128 | 112 | return s == test; | ||
129 | 113 | } | ||
130 | 114 | return false; | ||
131 | 115 | } | ||
132 | 116 | |||
133 | 117 | static bool test_insertion( char const *test ) { | ||
134 | 118 | ostringstream oss; | ||
135 | 119 | utf8::streambuf utf_buf( oss.rdbuf(), true ); | ||
136 | 120 | oss.ios::rdbuf( &utf_buf ); | ||
137 | 121 | oss.exceptions( ios::badbit ); | ||
138 | 122 | |||
139 | 123 | oss << test << flush; | ||
140 | 124 | string const s( oss.str() ); | ||
141 | 125 | return s == test; | ||
142 | 126 | } | ||
143 | 127 | |||
144 | 128 | static bool test_put( char const *test ) { | ||
145 | 129 | ostringstream oss; | ||
146 | 130 | utf8::streambuf utf_buf( oss.rdbuf(), true ); | ||
147 | 131 | oss.ios::rdbuf( &utf_buf ); | ||
148 | 132 | oss.exceptions( ios::badbit ); | ||
149 | 133 | |||
150 | 134 | for ( char const *c = test; *c; ++c ) | ||
151 | 135 | oss.put( *c ); | ||
152 | 136 | |||
153 | 137 | string const s( oss.str() ); | ||
154 | 138 | return s == test; | ||
155 | 139 | } | ||
156 | 140 | |||
157 | 141 | /////////////////////////////////////////////////////////////////////////////// | ||
158 | 142 | |||
159 | 143 | namespace zorba { | ||
160 | 144 | namespace UnitTests { | ||
161 | 145 | |||
162 | 146 | int test_utf8_streambuf( int, char*[] ) { | ||
163 | 147 | int test_no = 0; | ||
164 | 148 | for ( char const *const *s = tests_good; *s; ++s, ++test_no ) { | ||
165 | 149 | ASSERT_TRUE_AND_NO_EXCEPTION( test_no, test_getline( *s ) ); | ||
166 | 150 | ASSERT_TRUE_AND_NO_EXCEPTION( test_no, test_read( *s ) ); | ||
167 | 151 | ASSERT_TRUE_AND_NO_EXCEPTION( test_no, test_insertion( *s ) ); | ||
168 | 152 | ASSERT_TRUE_AND_NO_EXCEPTION( test_no, test_put( *s ) ); | ||
169 | 153 | } | ||
170 | 154 | for ( char const *const *s = tests_bad; *s; ++s, ++test_no ) { | ||
171 | 155 | ASSERT_EXCEPTION( test_no, test_getline( *s ) ); | ||
172 | 156 | ASSERT_EXCEPTION( test_no, test_read( *s ) ); | ||
173 | 157 | ASSERT_EXCEPTION( test_no, test_insertion( *s ) ); | ||
174 | 158 | ASSERT_EXCEPTION( test_no, test_put( *s ) ); | ||
175 | 159 | } | ||
176 | 160 | cout << failures << " test(s) failed\n"; | ||
177 | 161 | return failures ? 1 : 0; | ||
178 | 162 | } | ||
179 | 163 | |||
180 | 164 | } // namespace UnitTests | ||
181 | 165 | } // namespace zorba | ||
182 | 166 | /* vim:set et sw=2 ts=2: */ | ||
183 | 0 | 167 | ||
184 | === modified file 'src/unit_tests/unit_test_list.h' | |||
185 | --- src/unit_tests/unit_test_list.h 2013-01-25 21:39:08 +0000 | |||
186 | +++ src/unit_tests/unit_test_list.h 2013-01-25 22:32:27 +0000 | |||
187 | @@ -63,6 +63,7 @@ | |||
188 | 63 | int test_unordered_set( int, char*[] ); | 63 | int test_unordered_set( int, char*[] ); |
189 | 64 | #endif /* ZORBA_HAVE_UNORDERED_SET */ | 64 | #endif /* ZORBA_HAVE_UNORDERED_SET */ |
190 | 65 | 65 | ||
191 | 66 | int test_utf8_streambuf( int, char*[] ); | ||
192 | 66 | int test_uuid( int, char*[] ); | 67 | int test_uuid( int, char*[] ); |
193 | 67 | 68 | ||
194 | 68 | void initializeTestList(); | 69 | void initializeTestList(); |
195 | 69 | 70 | ||
196 | === modified file 'src/unit_tests/unit_tests.cpp' | |||
197 | --- src/unit_tests/unit_tests.cpp 2013-01-25 21:39:08 +0000 | |||
198 | +++ src/unit_tests/unit_tests.cpp 2013-01-25 22:32:27 +0000 | |||
199 | @@ -64,6 +64,7 @@ | |||
200 | 64 | libunittests["unique_ptr"] = test_unique_ptr; | 64 | libunittests["unique_ptr"] = test_unique_ptr; |
201 | 65 | #endif /* ZORBA_HAVE_UNIQUE_PTR */ | 65 | #endif /* ZORBA_HAVE_UNIQUE_PTR */ |
202 | 66 | 66 | ||
203 | 67 | libunittests["utf8_streambuf"] = test_utf8_streambuf; | ||
204 | 67 | libunittests["uuid"] = test_uuid; | 68 | libunittests["uuid"] = test_uuid; |
205 | 68 | 69 | ||
206 | 69 | #ifndef ZORBA_HAVE_UNORDERED_MAP | 70 | #ifndef ZORBA_HAVE_UNORDERED_MAP |
207 | 70 | 71 | ||
208 | === modified file 'src/util/CMakeLists.txt' | |||
209 | --- src/util/CMakeLists.txt 2013-01-15 19:16:16 +0000 | |||
210 | +++ src/util/CMakeLists.txt 2013-01-25 22:32:27 +0000 | |||
211 | @@ -31,6 +31,7 @@ | |||
212 | 31 | unicode_categories.cpp | 31 | unicode_categories.cpp |
213 | 32 | uri_util.cpp | 32 | uri_util.cpp |
214 | 33 | utf8_util.cpp | 33 | utf8_util.cpp |
215 | 34 | utf8_streambuf.cpp | ||
216 | 34 | xml_util.cpp | 35 | xml_util.cpp |
217 | 35 | fx/fxcharheap.cpp | 36 | fx/fxcharheap.cpp |
218 | 36 | string/empty_rep_base.cpp | 37 | string/empty_rep_base.cpp |
219 | 37 | 38 | ||
220 | === added file 'src/util/utf8_streambuf.cpp' | |||
221 | --- src/util/utf8_streambuf.cpp 1970-01-01 00:00:00 +0000 | |||
222 | +++ src/util/utf8_streambuf.cpp 2013-01-25 22:32:27 +0000 | |||
223 | @@ -0,0 +1,259 @@ | |||
224 | 1 | /* | ||
225 | 2 | * Copyright 2006-2008 The FLWOR Foundation. | ||
226 | 3 | * | ||
227 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | ||
228 | 5 | * you may not use this file except in compliance with the License. | ||
229 | 6 | * You may obtain a copy of the License at | ||
230 | 7 | * | ||
231 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 | ||
232 | 9 | * | ||
233 | 10 | * Unless required by applicable law or agreed to in writing, software | ||
234 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, | ||
235 | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
236 | 13 | * See the License for the specific language governing permissions and | ||
237 | 14 | * limitations under the License. | ||
238 | 15 | */ | ||
239 | 16 | |||
240 | 17 | #include "stdafx.h" | ||
241 | 18 | |||
242 | 19 | //#define ZORBA_DEBUG_UTF8_STREAMBUF | ||
243 | 20 | #ifdef ZORBA_DEBUG_UTF8_STREAMBUF | ||
244 | 21 | # include <stdio.h> | ||
245 | 22 | #endif | ||
246 | 23 | |||
247 | 24 | #include <iomanip> | ||
248 | 25 | #include <stdexcept> | ||
249 | 26 | |||
250 | 27 | #include <zorba/config.h> | ||
251 | 28 | #include <zorba/diagnostic_list.h> | ||
252 | 29 | |||
253 | 30 | #include "diagnostics/diagnostic.h" | ||
254 | 31 | #include "diagnostics/zorba_exception.h" | ||
255 | 32 | #include "util/cxx_util.h" | ||
256 | 33 | #include "util/oseparator.h" | ||
257 | 34 | #include "util/string_util.h" | ||
258 | 35 | #include "util/utf8_util.h" | ||
259 | 36 | |||
260 | 37 | #include "utf8_streambuf.h" | ||
261 | 38 | |||
262 | 39 | using namespace std; | ||
263 | 40 | |||
264 | 41 | namespace zorba { | ||
265 | 42 | namespace utf8 { | ||
266 | 43 | |||
267 | 44 | /////////////////////////////////////////////////////////////////////////////// | ||
268 | 45 | |||
269 | 46 | inline void streambuf::buf_type::clear() { | ||
270 | 47 | char_len_ = 0; | ||
271 | 48 | } | ||
272 | 49 | |||
273 | 50 | void streambuf::buf_type::throw_invalid_utf8( storage_type *buf, | ||
274 | 51 | size_type len ) { | ||
275 | 52 | ostringstream oss; | ||
276 | 53 | oss << hex << setfill('0') << setw(2) << uppercase; | ||
277 | 54 | oseparator comma( ',' ); | ||
278 | 55 | |||
279 | 56 | for ( size_type i = 0; i < len; ++i ) | ||
280 | 57 | oss << comma << "0x" << (static_cast<unsigned>( buf[i] ) & 0xFF); | ||
281 | 58 | |||
282 | 59 | clear(); | ||
283 | 60 | throw ZORBA_EXCEPTION( | ||
284 | 61 | zerr::ZXQD0006_INVALID_UTF8_BYTE_SEQUENCE, | ||
285 | 62 | ERROR_PARAMS( oss.str() ) | ||
286 | 63 | ); | ||
287 | 64 | } | ||
288 | 65 | |||
289 | 66 | void streambuf::buf_type::validate( storage_type c, bool bump ) { | ||
290 | 67 | size_type char_len_copy = char_len_, cur_len_copy = cur_len_; | ||
291 | 68 | |||
292 | 69 | if ( !char_len_copy ) { | ||
293 | 70 | // | ||
294 | 71 | // This means we're (hopefully) at the first byte of a UTF-8 byte sequence | ||
295 | 72 | // comprising a character. | ||
296 | 73 | // | ||
297 | 74 | if ( !(char_len_copy = char_length( c )) ) | ||
298 | 75 | throw_invalid_utf8( &c, 1 ); | ||
299 | 76 | cur_len_copy = 0; | ||
300 | 77 | } | ||
301 | 78 | |||
302 | 79 | storage_type *const cur_byte_ptr = utf8_char_ + cur_len_copy; | ||
303 | 80 | storage_type const old_byte = *cur_byte_ptr; | ||
304 | 81 | *cur_byte_ptr = c; | ||
305 | 82 | |||
306 | 83 | if ( cur_len_copy++ && !is_continuation_byte( c ) ) | ||
307 | 84 | throw_invalid_utf8( utf8_char_, cur_len_copy ); | ||
308 | 85 | |||
309 | 86 | if ( bump ) { | ||
310 | 87 | char_len_ = (cur_len_copy == char_len_copy ? 0 : char_len_copy); | ||
311 | 88 | cur_len_ = cur_len_copy; | ||
312 | 89 | } else { | ||
313 | 90 | *cur_byte_ptr = old_byte; | ||
314 | 91 | } | ||
315 | 92 | } | ||
316 | 93 | |||
317 | 94 | /////////////////////////////////////////////////////////////////////////////// | ||
318 | 95 | |||
319 | 96 | inline void streambuf::clear() { | ||
320 | 97 | gbuf_.clear(); | ||
321 | 98 | pbuf_.clear(); | ||
322 | 99 | } | ||
323 | 100 | |||
324 | 101 | streambuf::streambuf( std::streambuf *orig, bool validate_put ) : | ||
325 | 102 | internal::proxy_streambuf( orig ), | ||
326 | 103 | validate_put_( validate_put ) | ||
327 | 104 | { | ||
328 | 105 | if ( !orig ) | ||
329 | 106 | throw invalid_argument( "null streambuf" ); | ||
330 | 107 | clear(); | ||
331 | 108 | } | ||
332 | 109 | |||
333 | 110 | void streambuf::imbue( std::locale const &loc ) { | ||
334 | 111 | original()->pubimbue( loc ); | ||
335 | 112 | } | ||
336 | 113 | |||
337 | 114 | void streambuf::resync() { | ||
338 | 115 | int_type c = original()->sgetc(); | ||
339 | 116 | while ( !traits_type::eq_int_type( c, traits_type::eof() ) ) { | ||
340 | 117 | if ( is_start_byte( traits_type::to_char_type( c ) ) ) | ||
341 | 118 | break; | ||
342 | 119 | c = original()->sbumpc(); | ||
343 | 120 | } | ||
344 | 121 | } | ||
345 | 122 | |||
346 | 123 | streambuf::pos_type streambuf::seekoff( off_type o, ios_base::seekdir d, | ||
347 | 124 | ios_base::openmode m ) { | ||
348 | 125 | clear(); | ||
349 | 126 | return original()->pubseekoff( o, d, m ); | ||
350 | 127 | } | ||
351 | 128 | |||
352 | 129 | streambuf::pos_type streambuf::seekpos( pos_type p, ios_base::openmode m ) { | ||
353 | 130 | clear(); | ||
354 | 131 | return original()->pubseekpos( p, m ); | ||
355 | 132 | } | ||
356 | 133 | |||
357 | 134 | std::streambuf* streambuf::setbuf( char_type *p, streamsize s ) { | ||
358 | 135 | original()->pubsetbuf( p, s ); | ||
359 | 136 | return this; | ||
360 | 137 | } | ||
361 | 138 | |||
362 | 139 | streamsize streambuf::showmanyc() { | ||
363 | 140 | return original()->in_avail(); | ||
364 | 141 | } | ||
365 | 142 | |||
366 | 143 | int streambuf::sync() { | ||
367 | 144 | return original()->pubsync(); | ||
368 | 145 | } | ||
369 | 146 | |||
370 | 147 | streambuf::int_type streambuf::overflow( int_type c ) { | ||
371 | 148 | #ifdef ZORBA_DEBUG_UTF8_STREAMBUF | ||
372 | 149 | printf( "overflow()\n" ); | ||
373 | 150 | #endif | ||
374 | 151 | if ( traits_type::eq_int_type( c, traits_type::eof() ) ) | ||
375 | 152 | return traits_type::eof(); | ||
376 | 153 | if ( validate_put_ ) | ||
377 | 154 | pbuf_.validate( traits_type::to_char_type( c ), true ); | ||
378 | 155 | original()->sputc( c ); | ||
379 | 156 | return c; | ||
380 | 157 | } | ||
381 | 158 | |||
382 | 159 | streambuf::int_type streambuf::pbackfail( int_type c ) { | ||
383 | 160 | if ( !traits_type::eq_int_type( c, traits_type::eof() ) && | ||
384 | 161 | gbuf_.cur_len_ && | ||
385 | 162 | original()->sputbackc( traits_type::to_char_type( c ) ) ) { | ||
386 | 163 | --gbuf_.cur_len_; | ||
387 | 164 | return c; | ||
388 | 165 | } | ||
389 | 166 | return traits_type::eof(); | ||
390 | 167 | } | ||
391 | 168 | |||
392 | 169 | streambuf::int_type streambuf::uflow() { | ||
393 | 170 | #ifdef ZORBA_DEBUG_UTF8_STREAMBUF | ||
394 | 171 | printf( "uflow()\n" ); | ||
395 | 172 | #endif | ||
396 | 173 | int_type const c = original()->sbumpc(); | ||
397 | 174 | if ( traits_type::eq_int_type( c, traits_type::eof() ) ) | ||
398 | 175 | return traits_type::eof(); | ||
399 | 176 | gbuf_.validate( traits_type::to_char_type( c ) ); | ||
400 | 177 | return c; | ||
401 | 178 | } | ||
402 | 179 | |||
403 | 180 | streambuf::int_type streambuf::underflow() { | ||
404 | 181 | #ifdef ZORBA_DEBUG_UTF8_STREAMBUF | ||
405 | 182 | printf( "underflow()\n" ); | ||
406 | 183 | #endif | ||
407 | 184 | int_type const c = original()->sgetc(); | ||
408 | 185 | if ( traits_type::eq_int_type( c, traits_type::eof() ) ) | ||
409 | 186 | return traits_type::eof(); | ||
410 | 187 | gbuf_.validate( traits_type::to_char_type( c ), false ); | ||
411 | 188 | return c; | ||
412 | 189 | } | ||
413 | 190 | |||
414 | 191 | streamsize streambuf::xsgetn( char_type *to, streamsize size ) { | ||
415 | 192 | #ifdef ZORBA_DEBUG_UTF8_STREAMBUF | ||
416 | 193 | printf( "xsgetn()\n" ); | ||
417 | 194 | #endif | ||
418 | 195 | streamsize return_size = 0; | ||
419 | 196 | |||
420 | 197 | if ( gbuf_.char_len_ ) { | ||
421 | 198 | streamsize const want = gbuf_.char_len_ - gbuf_.cur_len_; | ||
422 | 199 | streamsize const get = min( want, size ); | ||
423 | 200 | streamsize const got = original()->sgetn( to, get ); | ||
424 | 201 | for ( streamsize i = 0; i < got; ++i ) | ||
425 | 202 | gbuf_.validate( to[i] ); | ||
426 | 203 | to += got; | ||
427 | 204 | size -= got, return_size += got; | ||
428 | 205 | } | ||
429 | 206 | |||
430 | 207 | while ( size > 0 ) { | ||
431 | 208 | if ( streamsize const got = original()->sgetn( to, size ) ) { | ||
432 | 209 | for ( streamsize i = 0; i < got; ++i ) | ||
433 | 210 | gbuf_.validate( to[i] ); | ||
434 | 211 | to += got; | ||
435 | 212 | size -= got, return_size += got; | ||
436 | 213 | } else | ||
437 | 214 | break; | ||
438 | 215 | } | ||
439 | 216 | return return_size; | ||
440 | 217 | } | ||
441 | 218 | |||
442 | 219 | streamsize streambuf::xsputn( char_type const *from, streamsize size ) { | ||
443 | 220 | #ifdef ZORBA_DEBUG_UTF8_STREAMBUF | ||
444 | 221 | printf( "xsputn()\n" ); | ||
445 | 222 | #endif | ||
446 | 223 | if ( validate_put_ ) | ||
447 | 224 | for ( streamsize i = 0; i < size; ++i ) | ||
448 | 225 | pbuf_.validate( from[i] ); | ||
449 | 226 | return original()->sputn( from, size ); | ||
450 | 227 | } | ||
451 | 228 | |||
452 | 229 | /////////////////////////////////////////////////////////////////////////////// | ||
453 | 230 | |||
454 | 231 | // Both new & delete are done inside Zorba rather than in the header to | ||
455 | 232 | // guarantee that they're cross-DLL-boundary safe on Windows. | ||
456 | 233 | |||
457 | 234 | std::streambuf* alloc_streambuf( std::streambuf *orig ) { | ||
458 | 235 | return new utf8::streambuf( orig ); | ||
459 | 236 | } | ||
460 | 237 | |||
461 | 238 | int get_streambuf_index() { | ||
462 | 239 | // | ||
463 | 240 | // This function is out-of-line because it has a static constant within it. | ||
464 | 241 | // It has a static constant within it to guarantee (1) initialization before | ||
465 | 242 | // use and (2) initialization happens exactly once. | ||
466 | 243 | // | ||
467 | 244 | // See: "Standard C++ IOStreams and Locales: Advanced Programmer's Guide and | ||
468 | 245 | // Reference," Angelika Langer and Klaus Kreft, Addison-Wesley, 2000, section | ||
469 | 246 | // 3.3.1.1: "Initializing and Maintaining the iword/pword Index." | ||
470 | 247 | // | ||
471 | 248 | // See: "The C++ Programming Language," Bjarne Stroustrup, Addison-Wesley, | ||
472 | 249 | // 2000, section 10.4.8: "Local Static Store." | ||
473 | 250 | // | ||
474 | 251 | static int const index = ios_base::xalloc(); | ||
475 | 252 | return index; | ||
476 | 253 | } | ||
477 | 254 | |||
478 | 255 | /////////////////////////////////////////////////////////////////////////////// | ||
479 | 256 | |||
480 | 257 | } // namespace utf8 | ||
481 | 258 | } // namespace zorba | ||
482 | 259 | /* vim:set et sw=2 ts=2: */ | ||
483 | 0 | 260 | ||
484 | === added file 'src/util/utf8_streambuf.h' | |||
485 | --- src/util/utf8_streambuf.h 1970-01-01 00:00:00 +0000 | |||
486 | +++ src/util/utf8_streambuf.h 2013-01-25 22:32:27 +0000 | |||
487 | @@ -0,0 +1,322 @@ | |||
488 | 1 | /* | ||
489 | 2 | * Copyright 2006-2008 The FLWOR Foundation. | ||
490 | 3 | * | ||
491 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | ||
492 | 5 | * you may not use this file except in compliance with the License. | ||
493 | 6 | * You may obtain a copy of the License at | ||
494 | 7 | * | ||
495 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 | ||
496 | 9 | * | ||
497 | 10 | * Unless required by applicable law or agreed to in writing, software | ||
498 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, | ||
499 | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
500 | 13 | * See the License for the specific language governing permissions and | ||
501 | 14 | * limitations under the License. | ||
502 | 15 | */ | ||
503 | 16 | |||
504 | 17 | #ifndef ZORBA_UTF8_STREAMBUF_H | ||
505 | 18 | #define ZORBA_UTF8_STREAMBUF_H | ||
506 | 19 | |||
507 | 20 | #include <zorba/internal/streambuf.h> | ||
508 | 21 | |||
509 | 22 | #include "util/utf8_util.h" | ||
510 | 23 | |||
511 | 24 | namespace zorba { | ||
512 | 25 | namespace utf8 { | ||
513 | 26 | |||
514 | 27 | /////////////////////////////////////////////////////////////////////////////// | ||
515 | 28 | |||
516 | 29 | /** | ||
517 | 30 | * A %utf8::streambuf is-a std::streambuf for validating UTF-8 on-the-fly. | ||
518 | 31 | * To use it, replace a stream's streambuf: | ||
519 | 32 | * \code | ||
520 | 33 | * istream is; | ||
521 | 34 | * // ... | ||
522 | 35 | * utf8::streambuf xbuf( is.rdbuf() ); | ||
523 | 36 | * is.ios::rdbuf( &xbuf ); | ||
524 | 37 | * \endcode | ||
525 | 38 | * Note that the %utf8::streambuf must exist for as long as it's being used by | ||
526 | 39 | * the stream. If you are replacing the streambuf for a stream you did not | ||
527 | 40 | * create, you should set it back to the original streambuf: | ||
528 | 41 | * \code | ||
529 | 42 | * void f( ostream &os ) { | ||
530 | 43 | * utf8::streambuf xbuf( os.rdbuf() ); | ||
531 | 44 | * try { | ||
532 | 45 | * os.ios::rdbuf( &xbuf ); | ||
533 | 46 | * // ... | ||
534 | 47 | * os.ios::rdbuf( xbuf.original() ); | ||
535 | 48 | * } | ||
536 | 49 | * catch ( ... ) { | ||
537 | 50 | * os.ios::rdbuf( xbuf.original() ); | ||
538 | 51 | * throw; | ||
539 | 52 | * } | ||
540 | 53 | * } | ||
541 | 54 | * \endcode | ||
542 | 55 | * | ||
543 | 56 | * If an invalid UTF-8 byte sequence is read, then the stream's \c badbit is | ||
544 | 57 | * set. Hence using a %utf8::streambuf requires rigorous error-checking. | ||
545 | 58 | * | ||
546 | 59 | * However, if exceptions are enabled for the stream, then | ||
547 | 60 | * \c ZXQD0006_INVALID_UTF8_BYTE_SEQUENCE is thrown. (When enabling exceptions | ||
548 | 61 | * for a stream you didn't create, you should set the exception mask back to | ||
549 | 62 | * the original mask.) | ||
550 | 63 | * \code | ||
551 | 64 | * istream is; | ||
552 | 65 | * std::ios::iostate const orig_exceptions = is.exceptions(); | ||
553 | 66 | * try { | ||
554 | 67 | * is.exceptions( orig_exceptions | ios::badbit ); | ||
555 | 68 | * // ... | ||
556 | 69 | * is.exceptions( orig_exceptions ); | ||
557 | 70 | * } | ||
558 | 71 | * catch ( ... ) { | ||
559 | 72 | * is.exceptions( orig_exceptions ); | ||
560 | 73 | * throw; | ||
561 | 74 | * } | ||
562 | 75 | * \endcode | ||
563 | 76 | * | ||
564 | 77 | * While %utf8::streambuf does support seeking, the positions must always be on | ||
565 | 78 | * the first byte of a UTF-8 character. | ||
566 | 79 | */ | ||
567 | 80 | class streambuf : public internal::proxy_streambuf { | ||
568 | 81 | public: | ||
569 | 82 | /** | ||
570 | 83 | * Constructs a %streambuf. | ||
571 | 84 | * | ||
572 | 85 | * @param orig The original streambuf to read/write from/to. | ||
573 | 86 | * @param validate_put If \c true, characters written are validated; | ||
574 | 87 | * if \c false, characters are written without validation, i.e., it's assumed | ||
575 | 88 | * that you're writing valid UTF-8. | ||
576 | 89 | * @throws std::invalid_argument if \a orig is \c null. | ||
577 | 90 | */ | ||
578 | 91 | streambuf( std::streambuf *orig, bool validate_put = false ); | ||
579 | 92 | |||
580 | 93 | /** | ||
581 | 94 | * If an invalid UTF-8 byte sequence was read, resynchronizes by skipping | ||
582 | 95 | * bytes until a new UTF-8 start byte is encountered. | ||
583 | 96 | */ | ||
584 | 97 | void resync(); | ||
585 | 98 | |||
586 | 99 | protected: | ||
587 | 100 | void imbue( std::locale const& ); | ||
588 | 101 | pos_type seekoff( off_type, std::ios_base::seekdir, std::ios_base::openmode ); | ||
589 | 102 | pos_type seekpos( pos_type, std::ios_base::openmode ); | ||
590 | 103 | std::streambuf* setbuf( char_type*, std::streamsize ); | ||
591 | 104 | std::streamsize showmanyc(); | ||
592 | 105 | int sync(); | ||
593 | 106 | int_type overflow( int_type ); | ||
594 | 107 | int_type pbackfail( int_type ); | ||
595 | 108 | int_type uflow(); | ||
596 | 109 | int_type underflow(); | ||
597 | 110 | std::streamsize xsgetn( char_type*, std::streamsize ); | ||
598 | 111 | std::streamsize xsputn( char_type const*, std::streamsize ); | ||
599 | 112 | |||
600 | 113 | private: | ||
601 | 114 | struct buf_type { | ||
602 | 115 | encoded_char_type utf8_char_; | ||
603 | 116 | size_type char_len_; | ||
604 | 117 | size_type cur_len_; | ||
605 | 118 | |||
606 | 119 | void clear(); | ||
607 | 120 | void throw_invalid_utf8( storage_type *buf, size_type len ); | ||
608 | 121 | void validate( storage_type, bool bump = true ); | ||
609 | 122 | }; | ||
610 | 123 | |||
611 | 124 | buf_type gbuf_, pbuf_; | ||
612 | 125 | bool const validate_put_; | ||
613 | 126 | |||
614 | 127 | void clear(); | ||
615 | 128 | |||
616 | 129 | // forbid | ||
617 | 130 | streambuf( streambuf const& ); | ||
618 | 131 | streambuf& operator=( streambuf const& ); | ||
619 | 132 | }; | ||
620 | 133 | |||
621 | 134 | /////////////////////////////////////////////////////////////////////////////// | ||
622 | 135 | |||
623 | 136 | std::streambuf* alloc_streambuf( std::streambuf *orig ); | ||
624 | 137 | |||
625 | 138 | int get_streambuf_index(); | ||
626 | 139 | |||
627 | 140 | /////////////////////////////////////////////////////////////////////////////// | ||
628 | 141 | |||
629 | 142 | /** | ||
630 | 143 | * Attaches a utf8::streambuf to a stream. Unlike using a | ||
631 | 144 | * utf8::streambuf directly, this function will create the streambuf, | ||
632 | 145 | * attach it to the stream, and manage it for the lifetime of the stream | ||
633 | 146 | * automatically. | ||
634 | 147 | * | ||
635 | 148 | * @param ios The stream to attach the utf8::streambuf to. If the stream | ||
636 | 149 | * already has a utf8::streambuf attached to it, this function does | ||
637 | 150 | * nothing. | ||
638 | 151 | */ | ||
639 | 152 | template<typename charT,typename Traits> inline | ||
640 | 153 | void attach( std::basic_ios<charT,Traits> &ios ) { | ||
641 | 154 | int const index = get_streambuf_index(); | ||
642 | 155 | void *&pword = ios.pword( index ); | ||
643 | 156 | if ( !pword ) { | ||
644 | 157 | std::streambuf *const buf = alloc_streambuf( ios.rdbuf() ); | ||
645 | 158 | ios.rdbuf( buf ); | ||
646 | 159 | pword = buf; | ||
647 | 160 | ios.register_callback( internal::stream_callback, index ); | ||
648 | 161 | } | ||
649 | 162 | } | ||
650 | 163 | |||
651 | 164 | /** | ||
652 | 165 | * Detaches a previously attached utf8::streambuf from a stream. The streambuf | ||
653 | 166 | * is destroyed and the stream's original streambuf is restored. | ||
654 | 167 | * | ||
655 | 168 | * @param ios The stream to detach the utf8::streambuf from. If the stream | ||
656 | 169 | * doesn't have a utf8::streambuf attached to it, this function does nothing. | ||
657 | 170 | */ | ||
658 | 171 | template<typename charT,typename Traits> inline | ||
659 | 172 | void detach( std::basic_ios<charT,Traits> &ios ) { | ||
660 | 173 | int const index = get_streambuf_index(); | ||
661 | 174 | if ( streambuf *const buf = static_cast<streambuf*>( ios.pword( index ) ) ) { | ||
662 | 175 | ios.pword( index ) = 0; | ||
663 | 176 | ios.rdbuf( buf->original() ); | ||
664 | 177 | internal::dealloc_streambuf( buf ); | ||
665 | 178 | } | ||
666 | 179 | } | ||
667 | 180 | |||
668 | 181 | /** | ||
669 | 182 | * Checks whether the given stream has a utf8::streambuf attached. | ||
670 | 183 | * | ||
671 | 184 | * @param ios The stream to check. | ||
672 | 185 | * @return \c true only if a utf8::streambuf is attached. | ||
673 | 186 | */ | ||
674 | 187 | template<typename charT,typename Traits> inline | ||
675 | 188 | bool is_attached( std::basic_ios<charT,Traits> &ios ) { | ||
676 | 189 | return !!ios.pword( get_streambuf_index() ); | ||
677 | 190 | } | ||
678 | 191 | |||
679 | 192 | /** | ||
680 | 193 | * A %utf8::auto_attach is a class that attaches a utf8::streambuf to a stream | ||
681 | 194 | * and automatically detaches it when the %auto_attach object is destroyed. | ||
682 | 195 | * \code | ||
683 | 196 | * void f( ostream &os ) { | ||
684 | 197 | * utf8::auto_attach<ostream> const raii( os, "ISO-8859-1" ); | ||
685 | 198 | * // ... | ||
686 | 199 | * } | ||
687 | 200 | * \endcode | ||
688 | 201 | * A %utf8::auto_attach is useful for streams not created by you. | ||
689 | 202 | * | ||
690 | 203 | * @see http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization | ||
691 | 204 | */ | ||
692 | 205 | template<class StreamType> | ||
693 | 206 | class auto_attach { | ||
694 | 207 | public: | ||
695 | 208 | /** | ||
696 | 209 | * Constructs an %auto_attach object calling attach() on the given stream. | ||
697 | 210 | * | ||
698 | 211 | * @param stream The stream to attach the utf8::streambuf to. If the stream | ||
699 | 212 | * already has a utf8::streambuf attached to it, this contructor does | ||
700 | 213 | * nothing. | ||
701 | 214 | */ | ||
702 | 215 | auto_attach( StreamType &stream ) : stream_( stream ) { | ||
703 | 216 | attach( stream ); | ||
704 | 217 | } | ||
705 | 218 | |||
706 | 219 | /** | ||
707 | 220 | * Destroys this %auto_attach object calling detach() on the previously | ||
708 | 221 | * attached stream. | ||
709 | 222 | */ | ||
710 | 223 | ~auto_attach() { | ||
711 | 224 | detach( stream_ ); | ||
712 | 225 | } | ||
713 | 226 | |||
714 | 227 | private: | ||
715 | 228 | StreamType &stream_; | ||
716 | 229 | }; | ||
717 | 230 | |||
718 | 231 | /////////////////////////////////////////////////////////////////////////////// | ||
719 | 232 | |||
720 | 233 | /** | ||
721 | 234 | * A %utf8::stream is used to wrap a C++ standard I/O stream with a | ||
722 | 235 | * utf8::streambuf so that encoding/decoding and the management of the | ||
723 | 236 | * streambuf happens automatically. | ||
724 | 237 | * | ||
725 | 238 | * A %utf8::stream is useful for streams created by you. | ||
726 | 239 | * | ||
727 | 240 | * @tparam StreamType The I/O stream class type to wrap. It must be a concrete | ||
728 | 241 | * stream class. | ||
729 | 242 | */ | ||
730 | 243 | template<class StreamType> | ||
731 | 244 | class stream : public StreamType { | ||
732 | 245 | public: | ||
733 | 246 | /** | ||
734 | 247 | * Constructs a %utf8::stream. | ||
735 | 248 | */ | ||
736 | 249 | stream() : | ||
737 | 250 | #ifdef WIN32 | ||
738 | 251 | # pragma warning( push ) | ||
739 | 252 | # pragma warning( disable : 4355 ) | ||
740 | 253 | #endif /* WIN32 */ | ||
741 | 254 | utf8_buf_( this->rdbuf() ) | ||
742 | 255 | #ifdef WIN32 | ||
743 | 256 | # pragma warning( pop ) | ||
744 | 257 | #endif /* WIN32 */ | ||
745 | 258 | { | ||
746 | 259 | init(); | ||
747 | 260 | } | ||
748 | 261 | |||
749 | 262 | /** | ||
750 | 263 | * Constructs a %stream. | ||
751 | 264 | * | ||
752 | 265 | * @tparam StreamArgType The type of the first argument of \a StreamType's | ||
753 | 266 | * constructor. | ||
754 | 267 | * @param stream_arg The argument to pass as the first argument to | ||
755 | 268 | * \a StreamType's constructor. | ||
756 | 269 | */ | ||
757 | 270 | template<typename StreamArgType> | ||
758 | 271 | stream( StreamArgType stream_arg ) : | ||
759 | 272 | StreamType( stream_arg ), | ||
760 | 273 | #ifdef WIN32 | ||
761 | 274 | # pragma warning( push ) | ||
762 | 275 | # pragma warning( disable : 4355 ) | ||
763 | 276 | #endif /* WIN32 */ | ||
764 | 277 | utf8_buf_( this->rdbuf() ) | ||
765 | 278 | #ifdef WIN32 | ||
766 | 279 | # pragma warning( pop ) | ||
767 | 280 | #endif /* WIN32 */ | ||
768 | 281 | { | ||
769 | 282 | init(); | ||
770 | 283 | } | ||
771 | 284 | |||
772 | 285 | /** | ||
773 | 286 | * Constructs a %utf8::stream. | ||
774 | 287 | * | ||
775 | 288 | * @tparam StreamArgType The type of the first argument of \a StreamType's | ||
776 | 289 | * constructor. | ||
777 | 290 | * @param stream_arg The argument to pass as the first argument to | ||
778 | 291 | * \a StreamType's constructor. | ||
779 | 292 | * @param mode The open-mode to pass to \a StreamType's constructor. | ||
780 | 293 | */ | ||
781 | 294 | template<typename StreamArgType> | ||
782 | 295 | stream( StreamArgType stream_arg, std::ios_base::openmode mode ) : | ||
783 | 296 | StreamType( stream_arg, mode ), | ||
784 | 297 | #ifdef WIN32 | ||
785 | 298 | # pragma warning( push ) | ||
786 | 299 | # pragma warning( disable : 4355 ) | ||
787 | 300 | #endif /* WIN32 */ | ||
788 | 301 | utf8_buf_( this->rdbuf() ) | ||
789 | 302 | #ifdef WIN32 | ||
790 | 303 | # pragma warning( pop ) | ||
791 | 304 | #endif /* WIN32 */ | ||
792 | 305 | { | ||
793 | 306 | init(); | ||
794 | 307 | } | ||
795 | 308 | |||
796 | 309 | private: | ||
797 | 310 | streambuf utf8_buf_; | ||
798 | 311 | |||
799 | 312 | void init() { | ||
800 | 313 | this->std::ios::rdbuf( &utf8_buf_ ); | ||
801 | 314 | } | ||
802 | 315 | }; | ||
803 | 316 | |||
804 | 317 | /////////////////////////////////////////////////////////////////////////////// | ||
805 | 318 | |||
806 | 319 | } // namespace utf8 | ||
807 | 320 | } // namespace zorba | ||
808 | 321 | #endif /* ZORBA_UTF8_STREAMBUF_H */ | ||
809 | 322 | /* vim:set et sw=2 ts=2: */ | ||
810 | 0 | 323 | ||
811 | === modified file 'test/unit/CMakeLists.txt' | |||
812 | --- test/unit/CMakeLists.txt 2013-01-11 01:34:56 +0000 | |||
813 | +++ test/unit/CMakeLists.txt 2013-01-25 22:32:27 +0000 | |||
814 | @@ -165,5 +165,6 @@ | |||
815 | 165 | IF (NOT ZORBA_HAVE_UNORDERED_SET) | 165 | IF (NOT ZORBA_HAVE_UNORDERED_SET) |
816 | 166 | ZORBA_ADD_TEST("test/libunit/unordered_set" LibUnitTest unordered_set) | 166 | ZORBA_ADD_TEST("test/libunit/unordered_set" LibUnitTest unordered_set) |
817 | 167 | ENDIF (NOT ZORBA_HAVE_UNORDERED_SET) | 167 | ENDIF (NOT ZORBA_HAVE_UNORDERED_SET) |
818 | 168 | ZORBA_ADD_TEST("test/libunit/utf8_streambuf" LibUnitTest utf8_streambuf) | ||
819 | 168 | 169 | ||
820 | 169 | # vim:set et sw=2 ts=2: | 170 | # vim:set et sw=2 ts=2: |
Do you guys want this??
Please not that it does require some discipline to use due to the way exceptions are handled in streams.