Branch data Line data Source code
1 : : /*
2 : : * gnome-keyring
3 : : *
4 : : * Copyright (C) 2011 Collabora Ltd.
5 : : *
6 : : * This program is free software; you can redistribute it and/or modify
7 : : * it under the terms of the GNU Lesser General Public License as
8 : : * published by the Free Software Foundation; either version 2.1 of
9 : : * the License, or (at your option) any later version.
10 : : *
11 : : * This program is distributed in the hope that it will be useful, but
12 : : * WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : : * Lesser General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU Lesser General Public
17 : : * License along with this program; if not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Author: Stef Walter <stefw@collabora.co.uk>
20 : : */
21 : :
22 : : #include "config.h"
23 : :
24 : : #include "gcr-openssh.h"
25 : : #include "gcr-internal.h"
26 : : #include "gcr-types.h"
27 : :
28 : : #include "gcr/gcr-oids.h"
29 : :
30 : : #include "egg/egg-asn1x.h"
31 : : #include "egg/egg-asn1-defs.h"
32 : : #include "egg/egg-buffer.h"
33 : : #include "egg/egg-decimal.h"
34 : :
35 : : #include <p11-kit/pkcs11.h>
36 : :
37 : : #include <string.h>
38 : :
39 : : typedef struct {
40 : : GcrOpensshPubCallback callback;
41 : : gpointer user_data;
42 : : } OpensshPubClosure;
43 : :
44 : : static void
45 : 5627 : skip_spaces (const gchar ** line,
46 : : gsize *n_line)
47 : : {
48 [ + + + + ]: 6119 : while (*n_line > 0 && (*line)[0] == ' ') {
49 : 492 : (*line)++;
50 : 492 : (*n_line)--;
51 : : }
52 : 5627 : }
53 : :
54 : : static gboolean
55 : 3813 : next_word (const gchar **line,
56 : : gsize *n_line,
57 : : const gchar **word,
58 : : gsize *n_word)
59 : : {
60 : : const gchar *beg;
61 : : const gchar *end;
62 : : const gchar *at;
63 : : gboolean quotes;
64 : :
65 : 3813 : skip_spaces (line, n_line);
66 : :
67 [ + + ]: 3813 : if (!*n_line) {
68 : 1552 : *word = NULL;
69 : 1552 : *n_word = 0;
70 : 1552 : return FALSE;
71 : : }
72 : :
73 : 2261 : beg = at = *line;
74 : 2261 : end = beg + *n_line;
75 : 2261 : quotes = FALSE;
76 : :
77 : : do {
78 [ + + + ]: 141436 : switch (*at) {
79 : 269 : case '"':
80 : 269 : quotes = !quotes;
81 : 269 : at++;
82 : 269 : break;
83 : 692 : case ' ':
84 [ + + ]: 692 : if (!quotes)
85 : 604 : end = at;
86 : : else
87 : 88 : at++;
88 : 692 : break;
89 : 140475 : default:
90 : 140475 : at++;
91 : 140475 : break;
92 : : }
93 [ + + ]: 141436 : } while (at < end);
94 : :
95 : 2261 : *word = beg;
96 : 2261 : *n_word = end - beg;
97 : 2261 : (*line) += *n_word;
98 : 2261 : (*n_line) -= *n_word;
99 : 2261 : return TRUE;
100 : : }
101 : :
102 : : static gboolean
103 : 2084 : match_word (const gchar *word,
104 : : gsize n_word,
105 : : const gchar *matches)
106 : : {
107 : 2084 : gsize len = strlen (matches);
108 [ + + ]: 2084 : if (len != n_word)
109 : 2050 : return FALSE;
110 : 34 : return memcmp (word, matches, n_word) == 0;
111 : : }
112 : :
113 : : static gulong
114 : 1048 : keytype_to_algo (const gchar *algo,
115 : : gsize length)
116 : : {
117 [ - + ]: 1048 : if (!algo)
118 : 0 : return G_MAXULONG;
119 [ + + ]: 1048 : else if (match_word (algo, length, "ssh-rsa"))
120 : 12 : return CKK_RSA;
121 [ + + ]: 1036 : else if (match_word (algo, length, "ssh-dss"))
122 : 8 : return CKK_DSA;
123 [ + + + + ]: 1028 : else if (length >= 6 && strncmp (algo, "ecdsa-", 6) == 0)
124 : 4 : return CKK_ECDSA;
125 : 1024 : return G_MAXULONG;
126 : : }
127 : :
128 : : static gboolean
129 : 8 : read_decimal_mpi (const gchar *decimal,
130 : : gsize n_decimal,
131 : : GckBuilder *builder,
132 : : gulong attribute_type)
133 : : {
134 : : gpointer data;
135 : : gsize n_data;
136 : :
137 : 8 : data = egg_decimal_decode (decimal, n_decimal, &n_data);
138 [ - + ]: 8 : if (data == NULL)
139 : 0 : return FALSE;
140 : :
141 : 8 : gck_builder_add_data (builder, attribute_type, data, n_data);
142 : 8 : g_free (data);
143 : 8 : return TRUE;
144 : : }
145 : :
146 : : static gint
147 : 87 : atoin (const char *p, gint digits)
148 : : {
149 : 87 : gint ret = 0, base = 1;
150 [ + + ]: 103 : while(--digits >= 0) {
151 [ + + + + ]: 99 : if (p[digits] < '0' || p[digits] > '9')
152 : 83 : return -1;
153 : 16 : ret += (p[digits] - '0') * base;
154 : 16 : base *= 10;
155 : : }
156 : 4 : return ret;
157 : : }
158 : :
159 : : static GcrDataError
160 : 893 : parse_v1_public_line (const gchar *line,
161 : : gsize length,
162 : : GBytes *backing,
163 : : GcrOpensshPubCallback callback,
164 : : gpointer user_data)
165 : : {
166 : : const gchar *word_bits, *word_exponent, *word_modulus, *word_options, *outer;
167 : : gsize len_bits, len_exponent, len_modulus, len_options, n_outer;
168 : 893 : GckBuilder builder = GCK_BUILDER_INIT;
169 : : GckAttributes *attrs;
170 : : gchar *label, *options;
171 : : GBytes *bytes;
172 : : gint bits;
173 : :
174 [ - + ]: 893 : g_assert (line);
175 : :
176 : 893 : outer = line;
177 : 893 : n_outer = length;
178 : 893 : options = NULL;
179 : 893 : label = NULL;
180 : :
181 : : /* Eat space at the front */
182 : 893 : skip_spaces (&line, &length);
183 : :
184 : : /* Blank line or comment */
185 [ + - + + ]: 893 : if (length == 0 || line[0] == '#')
186 : 6 : return GCR_ERROR_UNRECOGNIZED;
187 : :
188 : : /*
189 : : * If the line starts with a digit, then no options:
190 : : *
191 : : * 2048 35 25213680043....93533757 Label
192 : : *
193 : : * If the line doesn't start with a digit, then have options:
194 : : *
195 : : * option,option 2048 35 25213680043....93533757 Label
196 : : */
197 [ + + ]: 887 : if (g_ascii_isdigit (line[0])) {
198 : 69 : word_options = NULL;
199 : 69 : len_options = 0;
200 : : } else {
201 [ - + ]: 818 : if (!next_word (&line, &length, &word_options, &len_options))
202 : 0 : return GCR_ERROR_UNRECOGNIZED;
203 : : }
204 : :
205 [ + + + + ]: 1088 : if (!next_word (&line, &length, &word_bits, &len_bits) ||
206 [ + + ]: 308 : !next_word (&line, &length, &word_exponent, &len_exponent) ||
207 : 107 : !next_word (&line, &length, &word_modulus, &len_modulus))
208 : 800 : return GCR_ERROR_UNRECOGNIZED;
209 : :
210 : 87 : bits = atoin (word_bits, len_bits);
211 [ + + ]: 87 : if (bits <= 0)
212 : 83 : return GCR_ERROR_UNRECOGNIZED;
213 : :
214 [ + - - + ]: 8 : if (!read_decimal_mpi (word_exponent, len_exponent, &builder, CKA_PUBLIC_EXPONENT) ||
215 : 4 : !read_decimal_mpi (word_modulus, len_modulus, &builder, CKA_MODULUS)) {
216 : 0 : gck_builder_clear (&builder);
217 : 0 : return GCR_ERROR_UNRECOGNIZED;
218 : : }
219 : :
220 : 4 : gck_builder_add_ulong (&builder, CKA_KEY_TYPE, CKK_RSA);
221 : 4 : gck_builder_add_ulong (&builder, CKA_CLASS, CKO_PUBLIC_KEY);
222 : :
223 : 4 : skip_spaces (&line, &length);
224 [ + - ]: 4 : if (length > 0) {
225 : 4 : label = g_strndup (line, length);
226 : 4 : g_strstrip (label);
227 : 4 : gck_builder_add_string (&builder, CKA_LABEL, label);
228 : : }
229 : :
230 [ + + ]: 4 : if (word_options)
231 : 2 : options = g_strndup (word_options, len_options);
232 : :
233 : 4 : attrs = gck_builder_end (&builder);
234 : :
235 [ + + ]: 4 : if (callback != NULL) {
236 : 2 : bytes = g_bytes_new_with_free_func (outer, n_outer,
237 : : (GDestroyNotify)g_bytes_unref,
238 : 2 : g_bytes_ref (backing));
239 : 2 : (callback) (attrs, label, options, bytes, user_data);
240 : 2 : g_bytes_unref (bytes);
241 : : }
242 : :
243 : 4 : gck_attributes_unref (attrs);
244 : 4 : g_free (options);
245 : 4 : g_free (label);
246 : 4 : return GCR_SUCCESS;
247 : : }
248 : :
249 : : static gboolean
250 : 2 : read_buffer_mpi_to_der (EggBuffer *buffer,
251 : : gsize *offset,
252 : : GckBuilder *builder,
253 : : gulong attribute_type)
254 : : {
255 : : const guchar *data, *data_value;
256 : 2 : GBytes *der_data = NULL;
257 : : gsize len, data_len;
258 : 2 : GNode *asn = NULL;
259 : 2 : gboolean rv = FALSE;
260 : :
261 [ - + ]: 2 : if (!egg_buffer_get_byte_array (buffer, *offset, offset, &data, &len))
262 : 0 : return FALSE;
263 : :
264 : 2 : asn = egg_asn1x_create (pk_asn1_tab, "ECPoint");
265 [ - + ]: 2 : if (!asn)
266 : 0 : return FALSE;
267 : :
268 : 2 : egg_asn1x_set_string_as_raw (asn, (guchar *)data, len, NULL);
269 : 2 : der_data = egg_asn1x_encode (asn, g_realloc);
270 [ - + ]: 2 : if (!der_data)
271 : 0 : goto out;
272 : :
273 : 2 : data_value = g_bytes_get_data (der_data, &data_len);
274 : 2 : gck_builder_add_data (builder, attribute_type, data_value, data_len);
275 : 2 : rv = TRUE;
276 : 2 : out:
277 : 2 : g_bytes_unref (der_data);
278 : 2 : egg_asn1x_destroy (asn);
279 : 2 : return rv;
280 : : }
281 : :
282 : : static gboolean
283 : 28 : read_buffer_mpi (EggBuffer *buffer,
284 : : gsize *offset,
285 : : GckBuilder *builder,
286 : : gulong attribute_type)
287 : : {
288 : : const guchar *data;
289 : : gsize len;
290 : :
291 [ - + ]: 28 : if (!egg_buffer_get_byte_array (buffer, *offset, offset, &data, &len))
292 : 0 : return FALSE;
293 : :
294 : 28 : gck_builder_add_data (builder, attribute_type, data, len);
295 : 28 : return TRUE;
296 : : }
297 : :
298 : : static gboolean
299 : 4 : read_v2_public_dsa (EggBuffer *buffer,
300 : : gsize *offset,
301 : : GckBuilder *builder)
302 : : {
303 [ + - + - ]: 8 : if (!read_buffer_mpi (buffer, offset, builder, CKA_PRIME) ||
304 [ + - ]: 8 : !read_buffer_mpi (buffer, offset, builder, CKA_SUBPRIME) ||
305 [ - + ]: 8 : !read_buffer_mpi (buffer, offset, builder, CKA_BASE) ||
306 : 4 : !read_buffer_mpi (buffer, offset, builder, CKA_VALUE)) {
307 : 0 : return FALSE;
308 : : }
309 : :
310 : 4 : gck_builder_add_ulong (builder, CKA_KEY_TYPE, CKK_DSA);
311 : 4 : gck_builder_add_ulong (builder, CKA_CLASS, CKO_PUBLIC_KEY);
312 : :
313 : 4 : return TRUE;
314 : : }
315 : :
316 : : static gboolean
317 : 6 : read_v2_public_rsa (EggBuffer *buffer,
318 : : gsize *offset,
319 : : GckBuilder *builder)
320 : : {
321 [ + - - + ]: 12 : if (!read_buffer_mpi (buffer, offset, builder, CKA_PUBLIC_EXPONENT) ||
322 : 6 : !read_buffer_mpi (buffer, offset, builder, CKA_MODULUS)) {
323 : 0 : return FALSE;
324 : : }
325 : :
326 : 6 : gck_builder_add_ulong (builder, CKA_KEY_TYPE, CKK_RSA);
327 : 6 : gck_builder_add_ulong (builder, CKA_CLASS, CKO_PUBLIC_KEY);
328 : :
329 : 6 : return TRUE;
330 : : }
331 : :
332 : : static gboolean
333 : 2 : read_v2_public_ecdsa (EggBuffer *buffer,
334 : : gsize *offset,
335 : : GckBuilder *builder)
336 : : {
337 : : gconstpointer data;
338 : : GBytes *bytes;
339 : : GNode *asn;
340 : : GNode *node;
341 : : gchar *curve;
342 : : GQuark oid;
343 : : gsize len;
344 : :
345 : : /* The named curve */
346 [ - + ]: 2 : if (!egg_buffer_get_string (buffer, *offset, offset,
347 : : &curve, (EggBufferAllocator)g_realloc))
348 : 0 : return FALSE;
349 : :
350 [ + - ]: 2 : if (g_strcmp0 (curve, "nistp256") == 0) {
351 : 2 : oid = GCR_OID_EC_SECP256R1;
352 [ # # ]: 0 : } else if (g_strcmp0 (curve, "nistp384") == 0) {
353 : 0 : oid = GCR_OID_EC_SECP384R1;
354 [ # # ]: 0 : } else if (g_strcmp0 (curve, "nistp521") == 0) {
355 : 0 : oid = GCR_OID_EC_SECP521R1;
356 : : } else {
357 : 0 : g_free (curve);
358 : 0 : g_message ("unknown or unsupported curve in ssh public key");
359 : 0 : return FALSE;
360 : : }
361 : :
362 : 2 : g_free (curve);
363 : :
364 : 2 : asn = egg_asn1x_create (pk_asn1_tab, "ECParameters");
365 [ - + ]: 2 : g_return_val_if_fail (asn != NULL, FALSE);
366 : :
367 : 2 : node = egg_asn1x_node (asn, "namedCurve", NULL);
368 [ - + ]: 2 : if (!egg_asn1x_set_choice (asn, node))
369 : 0 : g_return_val_if_reached (FALSE);
370 : :
371 [ - + ]: 2 : if (!egg_asn1x_set_oid_as_quark (node, oid))
372 : 0 : g_return_val_if_reached (FALSE);
373 : :
374 : 2 : bytes = egg_asn1x_encode (asn, g_realloc);
375 [ - + ]: 2 : g_return_val_if_fail (bytes != NULL, FALSE);
376 : 2 : egg_asn1x_destroy (asn);
377 : :
378 : 2 : data = g_bytes_get_data (bytes, &len);
379 : 2 : gck_builder_add_data (builder, CKA_EC_PARAMS, data, len);
380 : 2 : g_bytes_unref (bytes);
381 : :
382 : : /* need to convert to DER encoded OCTET STRING */
383 [ - + ]: 2 : if (!read_buffer_mpi_to_der (buffer, offset, builder, CKA_EC_POINT))
384 : 0 : return FALSE;
385 : :
386 : 2 : gck_builder_add_ulong (builder, CKA_KEY_TYPE, CKK_ECDSA);
387 : 2 : gck_builder_add_ulong (builder, CKA_CLASS, CKO_PUBLIC_KEY);
388 : :
389 : 2 : return TRUE;
390 : : }
391 : :
392 : : static gboolean
393 : 12 : read_v2_public_key (gulong algo,
394 : : gconstpointer data,
395 : : gsize n_data,
396 : : GckBuilder *builder)
397 : : {
398 : : EggBuffer buffer;
399 : : gboolean ret;
400 : : gsize offset;
401 : : gchar *stype;
402 : : int alg;
403 : :
404 : 12 : egg_buffer_init_static (&buffer, data, n_data);
405 : 12 : offset = 0;
406 : :
407 : : /* The string algorithm */
408 [ - + ]: 12 : if (!egg_buffer_get_string (&buffer, offset, &offset,
409 : : &stype, (EggBufferAllocator)g_realloc))
410 : 0 : return FALSE;
411 : :
412 [ + - ]: 12 : alg = keytype_to_algo (stype, stype ? strlen (stype) : 0);
413 : 12 : g_free (stype);
414 : :
415 [ - + ]: 12 : if (alg != algo) {
416 : 0 : g_message ("invalid or mis-matched algorithm in ssh public key: %s", stype);
417 : 0 : egg_buffer_uninit (&buffer);
418 : 0 : return FALSE;
419 : : }
420 : :
421 [ + + + - ]: 12 : switch (algo) {
422 : 6 : case CKK_RSA:
423 : 6 : ret = read_v2_public_rsa (&buffer, &offset, builder);
424 : 6 : break;
425 : 4 : case CKK_DSA:
426 : 4 : ret = read_v2_public_dsa (&buffer, &offset, builder);
427 : 4 : break;
428 : 2 : case CKK_ECDSA:
429 : 2 : ret = read_v2_public_ecdsa (&buffer, &offset, builder);
430 : 2 : break;
431 : 0 : default:
432 : 0 : g_assert_not_reached ();
433 : : break;
434 : : }
435 : :
436 : 12 : egg_buffer_uninit (&buffer);
437 : 12 : return ret;
438 : : }
439 : :
440 : : static gboolean
441 : 12 : decode_v2_public_key (gulong algo,
442 : : const gchar *data,
443 : : gsize n_data,
444 : : GckBuilder *builder)
445 : : {
446 : : gpointer decoded;
447 : : gsize n_decoded;
448 : : gboolean ret;
449 : : guint save;
450 : : gint state;
451 : :
452 : : /* Decode the base64 key */
453 : 12 : save = state = 0;
454 : 12 : decoded = g_malloc (n_data * 3 / 4);
455 : 12 : n_decoded = g_base64_decode_step ((gchar*)data, n_data, decoded, &state, &save);
456 : :
457 [ - + ]: 12 : if (!n_decoded) {
458 : 0 : g_free (decoded);
459 : 0 : return FALSE;
460 : : }
461 : :
462 : : /* Parse the actual key */
463 : 12 : ret = read_v2_public_key (algo, decoded, n_decoded, builder);
464 : :
465 : 12 : g_free (decoded);
466 : :
467 : 12 : return ret;
468 : : }
469 : :
470 : : static GcrDataError
471 : 905 : parse_v2_public_line (const gchar *line,
472 : : gsize length,
473 : : GBytes *backing,
474 : : GcrOpensshPubCallback callback,
475 : : gpointer user_data)
476 : : {
477 : : const gchar *word_options, *word_algo, *word_key;
478 : : gsize len_options, len_algo, len_key;
479 : 905 : GckBuilder builder = GCK_BUILDER_INIT;
480 : : GckAttributes *attrs;
481 : : gchar *options;
482 : 905 : gchar *label = NULL;
483 : 905 : const gchar *outer = line;
484 : 905 : gsize n_outer = length;
485 : : GBytes *bytes;
486 : : gulong algo;
487 : :
488 [ - + ]: 905 : g_assert (line);
489 : :
490 : : /* Eat space at the front */
491 : 905 : skip_spaces (&line, &length);
492 : :
493 : : /* Blank line or comment */
494 [ + - + + ]: 905 : if (length == 0 || line[0] == '#')
495 : 6 : return GCR_ERROR_UNRECOGNIZED;
496 : :
497 [ - + ]: 899 : if (!next_word (&line, &length, &word_algo, &len_algo))
498 : 0 : return GCR_ERROR_UNRECOGNIZED;
499 : :
500 : : /*
501 : : * If the first word is not the algorithm, then we have options:
502 : : *
503 : : * option,option ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAI...EAz8Ji= Label here
504 : : *
505 : : * If the first word is the algorithm, then we have no options:
506 : : *
507 : : * ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAI...EAz8Ji= Label here
508 : : */
509 : 899 : algo = keytype_to_algo (word_algo, len_algo);
510 [ + + ]: 899 : if (algo == G_MAXULONG) {
511 : 889 : word_options = word_algo;
512 : 889 : len_options = len_algo;
513 [ + + ]: 889 : if (!next_word (&line, &length, &word_algo, &len_algo))
514 : 752 : return GCR_ERROR_UNRECOGNIZED;
515 : 137 : algo = keytype_to_algo (word_algo, len_algo);
516 [ + + ]: 137 : if (algo == G_MAXULONG)
517 : 135 : return GCR_ERROR_UNRECOGNIZED;
518 : : } else {
519 : 10 : word_options = NULL;
520 : 10 : len_options = 0;
521 : : }
522 : :
523 : : /* Must have at least two words */
524 [ - + ]: 12 : if (!next_word (&line, &length, &word_key, &len_key))
525 : 0 : return GCR_ERROR_FAILURE;
526 : :
527 [ - + ]: 12 : if (!decode_v2_public_key (algo, word_key, len_key, &builder)) {
528 : 0 : gck_builder_clear (&builder);
529 : 0 : return GCR_ERROR_FAILURE;
530 : : }
531 : :
532 [ + + ]: 12 : if (word_options)
533 : 2 : options = g_strndup (word_options, len_options);
534 : : else
535 : 10 : options = NULL;
536 : :
537 : : /* The remainder of the line is the label */
538 : 12 : skip_spaces (&line, &length);
539 [ + - ]: 12 : if (length > 0) {
540 : 12 : label = g_strndup (line, length);
541 : 12 : g_strstrip (label);
542 : 12 : gck_builder_add_string (&builder, CKA_LABEL, label);
543 : : }
544 : :
545 : 12 : attrs = gck_builder_end (&builder);
546 : :
547 [ + + ]: 12 : if (callback != NULL) {
548 : 8 : bytes = g_bytes_new_with_free_func (outer, n_outer,
549 : : (GDestroyNotify)g_bytes_unref,
550 : 8 : g_bytes_ref (backing));
551 : 8 : (callback) (attrs, label, options, bytes, user_data);
552 : 8 : g_bytes_unref (bytes);
553 : : }
554 : :
555 : 12 : gck_attributes_unref (attrs);
556 : 12 : g_free (options);
557 : 12 : g_free (label);
558 : 12 : return GCR_SUCCESS;
559 : : }
560 : :
561 : : guint
562 : 57 : _gcr_openssh_pub_parse (GBytes *data,
563 : : GcrOpensshPubCallback callback,
564 : : gpointer user_data)
565 : : {
566 : : const gchar *line;
567 : : const gchar *end;
568 : : gsize length;
569 : : gboolean last;
570 : : GcrDataError res;
571 : : guint num_parsed;
572 : :
573 [ - + ]: 57 : g_return_val_if_fail (data != NULL, FALSE);
574 : :
575 : 57 : line = g_bytes_get_data (data, NULL);
576 : 57 : length = g_bytes_get_size (data);
577 : 57 : last = FALSE;
578 : 57 : num_parsed = 0;
579 : :
580 : : for (;;) {
581 : 1861 : end = memchr (line, '\n', length);
582 [ + + ]: 959 : if (end == NULL) {
583 : 57 : end = line + length;
584 : 57 : last = TRUE;
585 : : }
586 : :
587 [ + + ]: 959 : if (line != end) {
588 : 905 : res = parse_v2_public_line (line, end - line, data, callback, user_data);
589 [ + + ]: 905 : if (res == GCR_ERROR_UNRECOGNIZED)
590 : 893 : res = parse_v1_public_line (line, end - line, data, callback, user_data);
591 [ + + ]: 905 : if (res == GCR_SUCCESS)
592 : 16 : num_parsed++;
593 : : }
594 : :
595 [ + + ]: 959 : if (last)
596 : 57 : break;
597 : :
598 : 902 : end++;
599 : 902 : length -= (end - line);
600 : 902 : line = end;
601 : : }
602 : :
603 : 57 : return num_parsed;
604 : : }
|